diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ed145b5e..7cb50f95 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,4 +2,4 @@ If you discover a security vulnerability within PocketBase, please send an e-mail to **support at pocketbase.io**. -All reports will be promptly addressed, and you'll be credited accordingly. +All reports will be promptly addressed and you'll be credited in the fix release notes. diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7581d99b..b3a290d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,7 +7,14 @@ on: jobs: goreleaser: runs-on: ubuntu-latest + env: + flags: "" steps: + # re-enable auto-snapshot from goreleaser-action@v3 + # (https://github.com/goreleaser/goreleaser-action-v4-auto-snapshot-example) + - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + run: echo "flags=--snapshot" >> $GITHUB_ENV + - name: Checkout uses: actions/checkout@v4 with: @@ -16,12 +23,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.11.0 + node-version: 20.17.0 - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '>=1.22.5' + go-version: '>=1.23.0' # This step usually is not needed because the /ui/dist is pregenerated locally # but its here to ensure that each release embeds the latest admin ui artifacts. @@ -36,19 +43,14 @@ jobs: # - name: Generate jsvm types # run: go run ./plugins/jsvm/internal/types/types.go - # The prebuilt golangci-lint doesn't support go 1.18+ yet - # https://github.com/golangci/golangci-lint/issues/2649 - # - name: Run linter - # uses: golangci/golangci-lint-action@v3 - - name: Run tests run: go test ./... - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: latest - args: release --clean + version: '~> v2' + args: release --clean ${{ env.flags }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4d6c49f9..967b7ab7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,5 @@ +version: 2 + project_name: pocketbase dist: .builds @@ -58,7 +60,7 @@ checksum: name_template: 'checksums.txt' snapshot: - name_template: '{{ incpatch .Version }}-next' + version_template: '{{ incpatch .Version }}-next' changelog: sort: asc diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbf8161..70dd0cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.23.0-RC (WIP) + +... + + ## 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)). diff --git a/README.md b/README.md index 3b7c8bbf..c8d1c804 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Go package documentation

-[PocketBase](https://pocketbase.io) is an open source Go backend, consisting of: +[PocketBase](https://pocketbase.io) is an open source Go backend that includes: - embedded database (_SQLite_) with **realtime subscriptions** - built-in **files and users management** @@ -46,7 +46,7 @@ your own custom app specific business logic and still have a single portable exe Here is a minimal example: -0. [Install Go 1.21+](https://go.dev/doc/install) (_if you haven't already_) +0. [Install Go 1.23+](https://go.dev/doc/install) (_if you haven't already_) 1. Create a new project directory with the following `main.go` file inside it: ```go @@ -56,29 +56,20 @@ Here is a minimal example: "log" "net/http" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" ) func main() { app := pocketbase.New() - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - // add new "GET /hello" route to the app router (echo) - e.Router.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/hello", - Handler: func(c echo.Context) error { - return c.String(200, "Hello world!") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.ActivityLogger(app), - }, + app.OnServe().BindFunc(func(se *core.ServeEvent) error { + // registers new "GET /hello" route + se.Router.Get("/hello", func(re *core.RequestEvent) error { + return re.String(200, "Hello world!") }) - return nil + return se.Next() }) if err := app.Start(); err != nil { @@ -145,7 +136,7 @@ Check also the [Testing guide](http://pocketbase.io/docs/testing) to learn how t If you discover a security vulnerability within PocketBase, please send an e-mail to **support at pocketbase.io**. -All reports will be promptly addressed, and you'll be credited accordingly. +All reports will be promptly addressed and you'll be credited in the fix release notes. ## Contributing diff --git a/apis/admin.go b/apis/admin.go deleted file mode 100644 index 629b2162..00000000 --- a/apis/admin.go +++ /dev/null @@ -1,353 +0,0 @@ -package apis - -import ( - "net/http" - - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tokens" - "github.com/pocketbase/pocketbase/tools/routine" - "github.com/pocketbase/pocketbase/tools/search" -) - -// bindAdminApi registers the admin api endpoints and the corresponding handlers. -func bindAdminApi(app core.App, rg *echo.Group) { - api := adminApi{app: app} - - subGroup := rg.Group("/admins", ActivityLogger(app)) - subGroup.POST("/auth-with-password", api.authWithPassword) - subGroup.POST("/request-password-reset", api.requestPasswordReset) - subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) - subGroup.POST("/auth-refresh", api.authRefresh, RequireAdminAuth()) - subGroup.GET("", api.list, RequireAdminAuth()) - subGroup.POST("", api.create, RequireAdminAuthOnlyIfAny(app)) - subGroup.GET("/:id", api.view, RequireAdminAuth()) - subGroup.PATCH("/:id", api.update, RequireAdminAuth()) - subGroup.DELETE("/:id", api.delete, RequireAdminAuth()) -} - -type adminApi struct { - app core.App -} - -func (api *adminApi) authResponse(c echo.Context, admin *models.Admin, finalizers ...func(token string) error) error { - token, tokenErr := tokens.NewAdminAuthToken(api.app, admin) - if tokenErr != nil { - return NewBadRequestError("Failed to create auth token.", tokenErr) - } - - for _, f := range finalizers { - if err := f(token); err != nil { - return err - } - } - - event := new(core.AdminAuthEvent) - event.HttpContext = c - event.Admin = admin - event.Token = token - - return api.app.OnAdminAuthRequest().Trigger(event, func(e *core.AdminAuthEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(200, map[string]any{ - "token": e.Token, - "admin": e.Admin, - }) - }) -} - -func (api *adminApi) authRefresh(c echo.Context) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin == nil { - return NewNotFoundError("Missing auth admin context.", nil) - } - - event := new(core.AdminAuthRefreshEvent) - event.HttpContext = c - event.Admin = admin - - return api.app.OnAdminBeforeAuthRefreshRequest().Trigger(event, func(e *core.AdminAuthRefreshEvent) error { - return api.app.OnAdminAfterAuthRefreshRequest().Trigger(event, func(e *core.AdminAuthRefreshEvent) error { - return api.authResponse(e.HttpContext, e.Admin) - }) - }) -} - -func (api *adminApi) authWithPassword(c echo.Context) error { - form := forms.NewAdminLogin(api.app) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - event := new(core.AdminAuthWithPasswordEvent) - event.HttpContext = c - event.Password = form.Password - event.Identity = form.Identity - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - event.Admin = admin - - return api.app.OnAdminBeforeAuthWithPasswordRequest().Trigger(event, func(e *core.AdminAuthWithPasswordEvent) error { - if err := next(e.Admin); err != nil { - return NewBadRequestError("Failed to authenticate.", err) - } - - return api.app.OnAdminAfterAuthWithPasswordRequest().Trigger(event, func(e *core.AdminAuthWithPasswordEvent) error { - return api.authResponse(e.HttpContext, e.Admin) - }) - }) - } - }) - - return submitErr -} - -func (api *adminApi) requestPasswordReset(c echo.Context) error { - form := forms.NewAdminPasswordResetRequest(api.app) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - if err := form.Validate(); err != nil { - return NewBadRequestError("An error occurred while validating the form.", err) - } - - event := new(core.AdminRequestPasswordResetEvent) - event.HttpContext = c - - submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(Admin *models.Admin) error { - event.Admin = Admin - - return api.app.OnAdminBeforeRequestPasswordResetRequest().Trigger(event, func(e *core.AdminRequestPasswordResetEvent) error { - // run in background because we don't need to show the result to the client - routine.FireAndForget(func() { - if err := next(e.Admin); err != nil { - api.app.Logger().Error("Failed to send admin password reset request.", "error", err) - } - }) - - return api.app.OnAdminAfterRequestPasswordResetRequest().Trigger(event, func(e *core.AdminRequestPasswordResetEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - // eagerly write 204 response and skip submit errors - // as a measure against admins enumeration - if !c.Response().Committed { - c.NoContent(http.StatusNoContent) - } - - return submitErr -} - -func (api *adminApi) confirmPasswordReset(c echo.Context) error { - form := forms.NewAdminPasswordResetConfirm(api.app) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.AdminConfirmPasswordResetEvent) - event.HttpContext = c - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - event.Admin = admin - - return api.app.OnAdminBeforeConfirmPasswordResetRequest().Trigger(event, func(e *core.AdminConfirmPasswordResetEvent) error { - if err := next(e.Admin); err != nil { - return NewBadRequestError("Failed to set new password.", err) - } - - return api.app.OnAdminAfterConfirmPasswordResetRequest().Trigger(event, func(e *core.AdminConfirmPasswordResetEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - return submitErr -} - -func (api *adminApi) list(c echo.Context) error { - fieldResolver := search.NewSimpleFieldResolver( - "id", "created", "updated", "name", "email", - ) - - admins := []*models.Admin{} - - result, err := search.NewProvider(fieldResolver). - Query(api.app.Dao().AdminQuery()). - ParseAndExec(c.QueryParams().Encode(), &admins) - - if err != nil { - return NewBadRequestError("", err) - } - - event := new(core.AdminsListEvent) - event.HttpContext = c - event.Admins = admins - event.Result = result - - return api.app.OnAdminsListRequest().Trigger(event, func(e *core.AdminsListEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Result) - }) -} - -func (api *adminApi) view(c echo.Context) error { - id := c.PathParam("id") - if id == "" { - return NewNotFoundError("", nil) - } - - admin, err := api.app.Dao().FindAdminById(id) - if err != nil || admin == nil { - return NewNotFoundError("", err) - } - - event := new(core.AdminViewEvent) - event.HttpContext = c - event.Admin = admin - - return api.app.OnAdminViewRequest().Trigger(event, func(e *core.AdminViewEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Admin) - }) -} - -func (api *adminApi) create(c echo.Context) error { - admin := &models.Admin{} - - form := forms.NewAdminUpsert(api.app, admin) - - // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) - } - - event := new(core.AdminCreateEvent) - event.HttpContext = c - event.Admin = admin - - // create the admin - submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - event.Admin = m - - return api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error { - if err := next(e.Admin); err != nil { - return NewBadRequestError("Failed to create admin.", err) - } - - return api.app.OnAdminAfterCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Admin) - }) - }) - } - }) - - return submitErr -} - -func (api *adminApi) update(c echo.Context) error { - id := c.PathParam("id") - if id == "" { - return NewNotFoundError("", nil) - } - - admin, err := api.app.Dao().FindAdminById(id) - if err != nil || admin == nil { - return NewNotFoundError("", err) - } - - form := forms.NewAdminUpsert(api.app, admin) - - // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) - } - - event := new(core.AdminUpdateEvent) - event.HttpContext = c - event.Admin = admin - - // update the admin - submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - event.Admin = m - - return api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error { - if err := next(e.Admin); err != nil { - return NewBadRequestError("Failed to update admin.", err) - } - - return api.app.OnAdminAfterUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Admin) - }) - }) - } - }) - - return submitErr -} - -func (api *adminApi) delete(c echo.Context) error { - id := c.PathParam("id") - if id == "" { - return NewNotFoundError("", nil) - } - - admin, err := api.app.Dao().FindAdminById(id) - if err != nil || admin == nil { - return NewNotFoundError("", err) - } - - event := new(core.AdminDeleteEvent) - event.HttpContext = c - event.Admin = admin - - return api.app.OnAdminBeforeDeleteRequest().Trigger(event, func(e *core.AdminDeleteEvent) error { - if err := api.app.Dao().DeleteAdmin(e.Admin); err != nil { - return NewBadRequestError("Failed to delete admin.", err) - } - - return api.app.OnAdminAfterDeleteRequest().Trigger(event, func(e *core.AdminDeleteEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) -} diff --git a/apis/admin_test.go b/apis/admin_test.go deleted file mode 100644 index 02f14bd6..00000000 --- a/apis/admin_test.go +++ /dev/null @@ -1,925 +0,0 @@ -package apis_test - -import ( - "errors" - "net/http" - "strings" - "testing" - "time" - - "github.com/labstack/echo/v5" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestAdminAuthWithPassword(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"identity":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "wrong email", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{"identity":"missing@example.com","password":"1234567890"}`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "wrong password", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{"identity":"test@example.com","password":"invalid"}`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid email/password (guest)", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{"identity":"test@example.com","password":"1234567890"}`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"admin":{"id":"sywbhecnh46rhm0"`, - `"token":`, - }, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthWithPasswordRequest": 1, - "OnAdminAfterAuthWithPasswordRequest": 1, - "OnAdminAuthRequest": 1, - }, - }, - { - Name: "valid email/password (already authorized)", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{"identity":"test@example.com","password":"1234567890"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"admin":{"id":"sywbhecnh46rhm0"`, - `"token":`, - }, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthWithPasswordRequest": 1, - "OnAdminAfterAuthWithPasswordRequest": 1, - "OnAdminAuthRequest": 1, - }, - }, - { - Name: "OnAdminAfterAuthWithPasswordRequest error response", - Method: http.MethodPost, - Url: "/api/admins/auth-with-password", - Body: strings.NewReader(`{"identity":"test@example.com","password":"1234567890"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterAuthWithPasswordRequest().Add(func(e *core.AdminAuthWithPasswordEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthWithPasswordRequest": 1, - "OnAdminAfterAuthWithPasswordRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminRequestPasswordReset(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/admins/request-password-reset", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/admins/request-password-reset", - Body: strings.NewReader(`{"email`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "missing admin", - Method: http.MethodPost, - Url: "/api/admins/request-password-reset", - Body: strings.NewReader(`{"email":"missing@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - }, - { - Name: "existing admin", - Method: http.MethodPost, - Url: "/api/admins/request-password-reset", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnMailerBeforeAdminResetPasswordSend": 1, - "OnMailerAfterAdminResetPasswordSend": 1, - "OnAdminBeforeRequestPasswordResetRequest": 1, - "OnAdminAfterRequestPasswordResetRequest": 1, - }, - }, - { - Name: "existing admin (after already sent)", - Method: http.MethodPost, - Url: "/api/admins/request-password-reset", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // simulate recent password request - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - admin.LastResetSentAt = types.NowDateTime() - dao := daos.New(app.Dao().DB()) // new dao to ignore hooks - if err := dao.Save(admin); err != nil { - t.Fatal(err) - } - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminConfirmPasswordReset(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"password":{"code":"validation_required","message":"Cannot be blank."},"passwordConfirm":{"code":"validation_required","message":"Cannot be blank."},"token":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(`{"password`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "expired token", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTY0MDk5MTY2MX0.GLwCOsgWTTEKXTK-AyGW838de1OeZGIjfHH0FoRLqZg", - "password":"1234567890", - "passwordConfirm":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"token":{"code":"validation_invalid_token","message":"Invalid or expired token."}}}`}, - }, - { - Name: "valid token + invalid password", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc", - "password":"123456", - "passwordConfirm":"123456" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"password":{"code":"validation_length_out_of_range"`}, - }, - { - Name: "valid token + valid password", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc", - "password":"1234567891", - "passwordConfirm":"1234567891" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnAdminBeforeConfirmPasswordResetRequest": 1, - "OnAdminAfterConfirmPasswordResetRequest": 1, - }, - }, - { - Name: "OnAdminAfterConfirmPasswordResetRequest error response", - Method: http.MethodPost, - Url: "/api/admins/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc", - "password":"1234567891", - "passwordConfirm":"1234567891" - }`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterConfirmPasswordResetRequest().Add(func(e *core.AdminConfirmPasswordResetEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnAdminBeforeConfirmPasswordResetRequest": 1, - "OnAdminAfterConfirmPasswordResetRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminRefresh(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodPost, - Url: "/api/admins/auth-refresh", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as user", - Method: http.MethodPost, - Url: "/api/admins/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin (expired token)", - Method: http.MethodPost, - Url: "/api/admins/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MDk5MTY2MX0.I7w8iktkleQvC7_UIRpD7rNzcU4OnF7i7SFIUu6lD_4", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin (valid token)", - Method: http.MethodPost, - Url: "/api/admins/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"admin":{"id":"sywbhecnh46rhm0"`, - `"token":`, - }, - ExpectedEvents: map[string]int{ - "OnAdminAuthRequest": 1, - "OnAdminBeforeAuthRefreshRequest": 1, - "OnAdminAfterAuthRefreshRequest": 1, - }, - }, - { - Name: "OnAdminAfterAuthRefreshRequest error response", - Method: http.MethodPost, - Url: "/api/admins/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterAuthRefreshRequest().Add(func(e *core.AdminAuthRefreshEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeAuthRefreshRequest": 1, - "OnAdminAfterAuthRefreshRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminsList(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodGet, - Url: "/api/admins", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as user", - Method: http.MethodGet, - Url: "/api/admins", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin", - Method: http.MethodGet, - Url: "/api/admins", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"page":1`, - `"perPage":30`, - `"totalItems":3`, - `"items":[{`, - `"id":"sywbhecnh46rhm0"`, - `"id":"sbmbsdb40jyxf7h"`, - `"id":"9q2trqumvlyr3bd"`, - }, - ExpectedEvents: map[string]int{ - "OnAdminsListRequest": 1, - }, - }, - { - Name: "authorized as admin + paging and sorting", - Method: http.MethodGet, - Url: "/api/admins?page=2&perPage=1&sort=-created", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"page":2`, - `"perPage":1`, - `"totalItems":3`, - `"items":[{`, - `"id":"sbmbsdb40jyxf7h"`, - }, - NotExpectedContent: []string{ - `"tokenKey"`, - `"passwordHash"`, - }, - ExpectedEvents: map[string]int{ - "OnAdminsListRequest": 1, - }, - }, - { - Name: "authorized as admin + invalid filter", - Method: http.MethodGet, - Url: "/api/admins?filter=invalidfield~'test2'", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + valid filter", - Method: http.MethodGet, - Url: "/api/admins?filter=email~'test3'", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"page":1`, - `"perPage":30`, - `"totalItems":1`, - `"items":[{`, - `"id":"9q2trqumvlyr3bd"`, - }, - NotExpectedContent: []string{ - `"tokenKey"`, - `"passwordHash"`, - }, - ExpectedEvents: map[string]int{ - "OnAdminsListRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminView(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodGet, - Url: "/api/admins/sbmbsdb40jyxf7h", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as user", - Method: http.MethodGet, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + nonexisting admin id", - Method: http.MethodGet, - Url: "/api/admins/nonexisting", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + existing admin id", - Method: http.MethodGet, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":"sbmbsdb40jyxf7h"`, - }, - NotExpectedContent: []string{ - `"tokenKey"`, - `"passwordHash"`, - }, - ExpectedEvents: map[string]int{ - "OnAdminViewRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminDelete(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodDelete, - Url: "/api/admins/sbmbsdb40jyxf7h", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as user", - Method: http.MethodDelete, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + missing admin id", - Method: http.MethodDelete, - Url: "/api/admins/missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + existing admin id", - Method: http.MethodDelete, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnAdminBeforeDeleteRequest": 1, - "OnAdminAfterDeleteRequest": 1, - }, - }, - { - Name: "authorized as admin - try to delete the only remaining admin", - Method: http.MethodDelete, - Url: "/api/admins/sywbhecnh46rhm0", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // delete all admins except the authorized one - adminModel := &models.Admin{} - _, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{ - "id": "sywbhecnh46rhm0", - })).Execute() - if err != nil { - t.Fatal(err) - } - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeDeleteRequest": 1, - }, - }, - { - Name: "OnAdminAfterDeleteRequest error response", - Method: http.MethodDelete, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterDeleteRequest().Add(func(e *core.AdminDeleteEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnAdminBeforeDeleteRequest": 1, - "OnAdminAfterDeleteRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminCreate(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized (while having at least 1 existing admin)", - Method: http.MethodPost, - Url: "/api/admins", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "unauthorized (while having 0 existing admins)", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":3}`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // delete all admins - _, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute() - if err != nil { - t.Fatal(err) - } - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":`, - `"email":"testnew@example.com"`, - `"avatar":3`, - }, - ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnAdminBeforeCreateRequest": 1, - "OnAdminAfterCreateRequest": 1, - }, - }, - { - Name: "authorized as user", - Method: http.MethodPost, - Url: "/api/admins", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + empty data", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(``), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "authorized as admin + invalid data format", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(`{`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + invalid data", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(`{ - "email":"test@example.com", - "password":"1234", - "passwordConfirm":"4321", - "avatar":99 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"avatar":{"code":"validation_max_less_equal_than_required"`, - `"email":{"code":"validation_admin_email_exists"`, - `"password":{"code":"validation_length_out_of_range"`, - `"passwordConfirm":{"code":"validation_values_mismatch"`, - }, - }, - { - Name: "authorized as admin + valid data", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(`{ - "email":"testnew@example.com", - "password":"1234567890", - "passwordConfirm":"1234567890", - "avatar":3 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":`, - `"email":"testnew@example.com"`, - `"avatar":3`, - }, - NotExpectedContent: []string{ - `"password"`, - `"passwordConfirm"`, - `"tokenKey"`, - `"passwordHash"`, - }, - ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnAdminBeforeCreateRequest": 1, - "OnAdminAfterCreateRequest": 1, - }, - }, - { - Name: "OnAdminAfterCreateRequest error response", - Method: http.MethodPost, - Url: "/api/admins", - Body: strings.NewReader(`{ - "email":"testnew@example.com", - "password":"1234567890", - "passwordConfirm":"1234567890", - "avatar":3 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterCreateRequest().Add(func(e *core.AdminCreateEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnAdminBeforeCreateRequest": 1, - "OnAdminAfterCreateRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestAdminUpdate(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as user", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + missing admin", - Method: http.MethodPatch, - Url: "/api/admins/missing", - Body: strings.NewReader(``), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + empty data", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - Body: strings.NewReader(``), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":"sbmbsdb40jyxf7h"`, - `"email":"test2@example.com"`, - `"avatar":2`, - }, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnAdminBeforeUpdateRequest": 1, - "OnAdminAfterUpdateRequest": 1, - }, - }, - { - Name: "authorized as admin + invalid formatted data", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - Body: strings.NewReader(`{`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "authorized as admin + invalid data", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - Body: strings.NewReader(`{ - "email":"test@example.com", - "password":"1234", - "passwordConfirm":"4321", - "avatar":99 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"avatar":{"code":"validation_max_less_equal_than_required"`, - `"email":{"code":"validation_admin_email_exists"`, - `"password":{"code":"validation_length_out_of_range"`, - `"passwordConfirm":{"code":"validation_values_mismatch"`, - }, - }, - { - Name: "authorized as admin + valid data", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - Body: strings.NewReader(`{ - "email":"testnew@example.com", - "password":"1234567891", - "passwordConfirm":"1234567891", - "avatar":5 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":"sbmbsdb40jyxf7h"`, - `"email":"testnew@example.com"`, - `"avatar":5`, - }, - NotExpectedContent: []string{ - `"password"`, - `"passwordConfirm"`, - `"tokenKey"`, - `"passwordHash"`, - }, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnAdminBeforeUpdateRequest": 1, - "OnAdminAfterUpdateRequest": 1, - }, - }, - { - Name: "OnAdminAfterUpdateRequest error response", - Method: http.MethodPatch, - Url: "/api/admins/sbmbsdb40jyxf7h", - Body: strings.NewReader(`{ - "email":"testnew@example.com", - "password":"1234567891", - "passwordConfirm":"1234567891", - "avatar":5 - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnAdminAfterUpdateRequest().Add(func(e *core.AdminUpdateEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnAdminBeforeUpdateRequest": 1, - "OnAdminAfterUpdateRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} diff --git a/apis/api_error.go b/apis/api_error.go deleted file mode 100644 index 8658ac8f..00000000 --- a/apis/api_error.go +++ /dev/null @@ -1,132 +0,0 @@ -package apis - -import ( - "net/http" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/tools/inflector" -) - -// ApiError defines the struct for a basic api error response. -type ApiError struct { - Code int `json:"code"` - Message string `json:"message"` - Data map[string]any `json:"data"` - - // stores unformatted error data (could be an internal error, text, etc.) - rawData any -} - -// Error makes it compatible with the `error` interface. -func (e *ApiError) Error() string { - return e.Message -} - -// RawData returns the unformatted error data (could be an internal error, text, etc.) -func (e *ApiError) RawData() any { - return e.rawData -} - -// NewNotFoundError creates and returns 404 `ApiError`. -func NewNotFoundError(message string, data any) *ApiError { - if message == "" { - message = "The requested resource wasn't found." - } - - return NewApiError(http.StatusNotFound, message, data) -} - -// NewBadRequestError creates and returns 400 `ApiError`. -func NewBadRequestError(message string, data any) *ApiError { - if message == "" { - message = "Something went wrong while processing your request." - } - - return NewApiError(http.StatusBadRequest, message, data) -} - -// NewForbiddenError creates and returns 403 `ApiError`. -func NewForbiddenError(message string, data any) *ApiError { - if message == "" { - message = "You are not allowed to perform this request." - } - - return NewApiError(http.StatusForbidden, message, data) -} - -// NewUnauthorizedError creates and returns 401 `ApiError`. -func NewUnauthorizedError(message string, data any) *ApiError { - if message == "" { - message = "Missing or invalid authentication token." - } - - return NewApiError(http.StatusUnauthorized, message, data) -} - -// NewApiError creates and returns new normalized `ApiError` instance. -func NewApiError(status int, message string, data any) *ApiError { - return &ApiError{ - rawData: data, - Data: safeErrorsData(data), - Code: status, - Message: strings.TrimSpace(inflector.Sentenize(message)), - } -} - -func safeErrorsData(data any) map[string]any { - switch v := data.(type) { - case validation.Errors: - return resolveSafeErrorsData[error](v) - case map[string]validation.Error: - return resolveSafeErrorsData[validation.Error](v) - case map[string]error: - return resolveSafeErrorsData[error](v) - case map[string]any: - return resolveSafeErrorsData[any](v) - default: - return map[string]any{} // not nil to ensure that is json serialized as object - } -} - -func resolveSafeErrorsData[T any](data map[string]T) map[string]any { - result := map[string]any{} - - for name, err := range data { - if isNestedError(err) { - result[name] = safeErrorsData(err) - continue - } - result[name] = resolveSafeErrorItem(err) - } - - return result -} - -func isNestedError(err any) bool { - switch err.(type) { - case validation.Errors, map[string]validation.Error, map[string]error, map[string]any: - return true - } - - return false -} - -// resolveSafeErrorItem extracts from each validation error its -// public safe error code and message. -func resolveSafeErrorItem(err any) map[string]string { - // default public safe error values - code := "validation_invalid_value" - msg := "Invalid value." - - // only validation errors are public safe - if obj, ok := err.(validation.Error); ok { - code = obj.Code() - msg = inflector.Sentenize(obj.Error()) - } - - return map[string]string{ - "code": code, - "message": msg, - } -} diff --git a/apis/api_error_aliases.go b/apis/api_error_aliases.go new file mode 100644 index 00000000..f6fdf4d9 --- /dev/null +++ b/apis/api_error_aliases.go @@ -0,0 +1,42 @@ +package apis + +import "github.com/pocketbase/pocketbase/tools/router" + +// ApiError aliases to minimize the breaking changes with earlier versions +// and for consistency with the JSVM binds. +// ------------------------------------------------------------------- + +// NewApiError is an alias for [router.NewApiError]. +func NewApiError(status int, message string, errData any) *router.ApiError { + return router.NewApiError(status, message, errData) +} + +// NewBadRequestError is an alias for [router.NewBadRequestError]. +func NewBadRequestError(message string, errData any) *router.ApiError { + return router.NewBadRequestError(message, errData) +} + +// NewNotFoundError is an alias for [router.NewNotFoundError]. +func NewNotFoundError(message string, errData any) *router.ApiError { + return router.NewNotFoundError(message, errData) +} + +// NewForbiddenError is an alias for [router.NewForbiddenError]. +func NewForbiddenError(message string, errData any) *router.ApiError { + return router.NewForbiddenError(message, errData) +} + +// NewUnauthorizedError is an alias for [router.NewUnauthorizedError]. +func NewUnauthorizedError(message string, errData any) *router.ApiError { + return router.NewUnauthorizedError(message, errData) +} + +// NewTooManyRequestsError is an alias for [router.NewTooManyRequestsError]. +func NewTooManyRequestsError(message string, errData any) *router.ApiError { + return router.NewTooManyRequestsError(message, errData) +} + +// NewInternalServerError is an alias for [router.NewInternalServerError]. +func NewInternalServerError(message string, errData any) *router.ApiError { + return router.NewInternalServerError(message, errData) +} diff --git a/apis/api_error_test.go b/apis/api_error_test.go deleted file mode 100644 index d6e77b67..00000000 --- a/apis/api_error_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package apis_test - -import ( - "encoding/json" - "errors" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/apis" -) - -func TestNewApiErrorWithRawData(t *testing.T) { - t.Parallel() - - e := apis.NewApiError( - 300, - "message_test", - "rawData_test", - ) - - result, _ := json.Marshal(e) - expected := `{"code":300,"message":"Message_test.","data":{}}` - - if string(result) != expected { - t.Errorf("Expected %v, got %v", expected, string(result)) - } - - if e.Error() != "Message_test." { - t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) - } - - if e.RawData() != "rawData_test" { - t.Errorf("Expected rawData %v, got %v", "rawData_test", e.RawData()) - } -} - -func TestNewApiErrorWithValidationData(t *testing.T) { - t.Parallel() - - e := apis.NewApiError( - 300, - "message_test", - validation.Errors{ - "err1": errors.New("test error"), // should be normalized - "err2": validation.ErrRequired, - "err3": validation.Errors{ - "sub1": errors.New("test error"), // should be normalized - "sub2": validation.ErrRequired, - "sub3": validation.Errors{ - "sub11": validation.ErrRequired, - }, - }, - }, - ) - - result, _ := json.Marshal(e) - expected := `{"code":300,"message":"Message_test.","data":{"err1":{"code":"validation_invalid_value","message":"Invalid value."},"err2":{"code":"validation_required","message":"Cannot be blank."},"err3":{"sub1":{"code":"validation_invalid_value","message":"Invalid value."},"sub2":{"code":"validation_required","message":"Cannot be blank."},"sub3":{"sub11":{"code":"validation_required","message":"Cannot be blank."}}}}}` - - if string(result) != expected { - t.Errorf("Expected \n%v, \ngot \n%v", expected, string(result)) - } - - if e.Error() != "Message_test." { - t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) - } - - if e.RawData() == nil { - t.Error("Expected non-nil rawData") - } -} - -func TestNewNotFoundError(t *testing.T) { - t.Parallel() - - scenarios := []struct { - message string - data any - expected string - }{ - {"", nil, `{"code":404,"message":"The requested resource wasn't found.","data":{}}`}, - {"demo", "rawData_test", `{"code":404,"message":"Demo.","data":{}}`}, - {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"code":404,"message":"Demo.","data":{"err1":{"code":"test_code","message":"Test_message."}}}`}, - } - - for i, scenario := range scenarios { - e := apis.NewNotFoundError(scenario.message, scenario.data) - result, _ := json.Marshal(e) - - if string(result) != scenario.expected { - t.Errorf("(%d) Expected \n%v, \ngot \n%v", i, scenario.expected, string(result)) - } - } -} - -func TestNewBadRequestError(t *testing.T) { - t.Parallel() - - scenarios := []struct { - message string - data any - expected string - }{ - {"", nil, `{"code":400,"message":"Something went wrong while processing your request.","data":{}}`}, - {"demo", "rawData_test", `{"code":400,"message":"Demo.","data":{}}`}, - {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"code":400,"message":"Demo.","data":{"err1":{"code":"test_code","message":"Test_message."}}}`}, - } - - for i, scenario := range scenarios { - e := apis.NewBadRequestError(scenario.message, scenario.data) - result, _ := json.Marshal(e) - - if string(result) != scenario.expected { - t.Errorf("(%d) Expected \n%v, \ngot \n%v", i, scenario.expected, string(result)) - } - } -} - -func TestNewForbiddenError(t *testing.T) { - t.Parallel() - - scenarios := []struct { - message string - data any - expected string - }{ - {"", nil, `{"code":403,"message":"You are not allowed to perform this request.","data":{}}`}, - {"demo", "rawData_test", `{"code":403,"message":"Demo.","data":{}}`}, - {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"code":403,"message":"Demo.","data":{"err1":{"code":"test_code","message":"Test_message."}}}`}, - } - - for i, scenario := range scenarios { - e := apis.NewForbiddenError(scenario.message, scenario.data) - result, _ := json.Marshal(e) - - if string(result) != scenario.expected { - t.Errorf("(%d) Expected \n%v, \ngot \n%v", i, scenario.expected, string(result)) - } - } -} - -func TestNewUnauthorizedError(t *testing.T) { - t.Parallel() - - scenarios := []struct { - message string - data any - expected string - }{ - {"", nil, `{"code":401,"message":"Missing or invalid authentication token.","data":{}}`}, - {"demo", "rawData_test", `{"code":401,"message":"Demo.","data":{}}`}, - {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"code":401,"message":"Demo.","data":{"err1":{"code":"test_code","message":"Test_message."}}}`}, - } - - for i, scenario := range scenarios { - e := apis.NewUnauthorizedError(scenario.message, scenario.data) - result, _ := json.Marshal(e) - - if string(result) != scenario.expected { - t.Errorf("(%d) Expected \n%v, \ngot \n%v", i, scenario.expected, string(result)) - } - } -} diff --git a/apis/backup.go b/apis/backup.go index a100e56b..b54fc392 100644 --- a/apis/backup.go +++ b/apis/backup.go @@ -6,42 +6,37 @@ import ( "path/filepath" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/router" + "github.com/pocketbase/pocketbase/tools/routine" "github.com/pocketbase/pocketbase/tools/types" "github.com/spf13/cast" ) // bindBackupApi registers the file api endpoints and the corresponding handlers. -// -// @todo add hooks once the app hooks api restructuring is finalized -func bindBackupApi(app core.App, rg *echo.Group) { - api := backupApi{app: app} - - subGroup := rg.Group("/backups", ActivityLogger(app)) - subGroup.GET("", api.list, RequireAdminAuth()) - subGroup.POST("", api.create, RequireAdminAuth()) - subGroup.POST("/upload", api.upload, RequireAdminAuth()) - subGroup.GET("/:key", api.download) - subGroup.DELETE("/:key", api.delete, RequireAdminAuth()) - subGroup.POST("/:key/restore", api.restore, RequireAdminAuth()) +func bindBackupApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + sub := rg.Group("/backups") + sub.GET("", backupsList).Bind(RequireSuperuserAuth()) + sub.POST("", backupCreate).Bind(RequireSuperuserAuth()) + sub.POST("/upload", backupUpload).Bind(RequireSuperuserAuthOnlyIfAny()) + sub.GET("/{key}", backupDownload) // relies on superuser file token + sub.DELETE("/{key}", backupDelete).Bind(RequireSuperuserAuth()) + sub.POST("/{key}/restore", backupRestore).Bind(RequireSuperuserAuthOnlyIfAny()) } -type backupApi struct { - app core.App +type backupFileInfo struct { + Modified types.DateTime `json:"modified"` + Key string `json:"key"` + Size int64 `json:"size"` } -func (api *backupApi) list(c echo.Context) error { +func backupsList(e *core.RequestEvent) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - fsys, err := api.app.NewBackupsFilesystem() + fsys, err := e.App.NewBackupsFilesystem() if err != nil { - return NewBadRequestError("Failed to load backups filesystem.", err) + return e.BadRequestError("Failed to load backups filesystem.", err) } defer fsys.Close() @@ -49,166 +44,112 @@ func (api *backupApi) list(c echo.Context) error { backups, err := fsys.List("") if err != nil { - return NewBadRequestError("Failed to retrieve backup items. Raw error: \n"+err.Error(), nil) + return e.BadRequestError("Failed to retrieve backup items. Raw error: \n"+err.Error(), nil) } - result := make([]models.BackupFileInfo, len(backups)) + result := make([]backupFileInfo, len(backups)) for i, obj := range backups { modified, _ := types.ParseDateTime(obj.ModTime) - result[i] = models.BackupFileInfo{ + result[i] = backupFileInfo{ Key: obj.Key, Size: obj.Size, Modified: modified, } } - return c.JSON(http.StatusOK, result) + return e.JSON(http.StatusOK, result) } -func (api *backupApi) create(c echo.Context) error { - if api.app.Store().Has(core.StoreKeyActiveBackup) { - return NewBadRequestError("Try again later - another backup/restore process has already been started", nil) - } +func backupDownload(e *core.RequestEvent) error { + fileToken := e.Request.URL.Query().Get("token") - form := forms.NewBackupCreate(api.app) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - return form.Submit(func(next forms.InterceptorNextFunc[string]) forms.InterceptorNextFunc[string] { - return func(name string) error { - if err := next(name); err != nil { - return NewBadRequestError("Failed to create backup.", err) - } - - // we don't retrieve the generated backup file because it may not be - // available yet due to the eventually consistent nature of some S3 providers - return c.NoContent(http.StatusNoContent) - } - }) -} - -func (api *backupApi) upload(c echo.Context) error { - files, err := rest.FindUploadedFiles(c.Request(), "file") - if err != nil { - return NewBadRequestError("Missing or invalid uploaded file.", err) - } - - form := forms.NewBackupUpload(api.app) - form.File = files[0] - - return form.Submit(func(next forms.InterceptorNextFunc[*filesystem.File]) forms.InterceptorNextFunc[*filesystem.File] { - return func(file *filesystem.File) error { - if err := next(file); err != nil { - return NewBadRequestError("Failed to upload backup.", err) - } - - // we don't retrieve the generated backup file because it may not be - // available yet due to the eventually consistent nature of some S3 providers - return c.NoContent(http.StatusNoContent) - } - }) -} - -func (api *backupApi) download(c echo.Context) error { - fileToken := c.QueryParam("token") - - _, err := api.app.Dao().FindAdminByToken( - fileToken, - api.app.Settings().AdminFileToken.Secret, - ) - if err != nil { - return NewForbiddenError("Insufficient permissions to access the resource.", err) + authRecord, err := e.App.FindAuthRecordByToken(fileToken, core.TokenTypeFile) + if err != nil || !authRecord.IsSuperuser() { + return e.ForbiddenError("Insufficient permissions to access the resource.", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - fsys, err := api.app.NewBackupsFilesystem() + fsys, err := e.App.NewBackupsFilesystem() if err != nil { - return NewBadRequestError("Failed to load backups filesystem.", err) + return e.InternalServerError("Failed to load backups filesystem.", err) } defer fsys.Close() fsys.SetContext(ctx) - key := c.PathParam("key") - - br, err := fsys.GetFile(key) - if err != nil { - return NewBadRequestError("Failed to retrieve backup item. Raw error: \n"+err.Error(), nil) - } - defer br.Close() + key := e.Request.PathValue("key") return fsys.Serve( - c.Response(), - c.Request(), + e.Response, + e.Request, key, filepath.Base(key), // without the path prefix (if any) ) } -func (api *backupApi) restore(c echo.Context) error { - if api.app.Store().Has(core.StoreKeyActiveBackup) { - return NewBadRequestError("Try again later - another backup/restore process has already been started.", nil) +func backupDelete(e *core.RequestEvent) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + fsys, err := e.App.NewBackupsFilesystem() + if err != nil { + return e.InternalServerError("Failed to load backups filesystem.", err) + } + defer fsys.Close() + + fsys.SetContext(ctx) + + key := e.Request.PathValue("key") + + if key != "" && cast.ToString(e.App.Store().Get(core.StoreKeyActiveBackup)) == key { + return e.BadRequestError("The backup is currently being used and cannot be deleted.", nil) } - key := c.PathParam("key") + if err := fsys.Delete(key); err != nil { + return e.BadRequestError("Invalid or already deleted backup file. Raw error: \n"+err.Error(), nil) + } + + return e.NoContent(http.StatusNoContent) +} + +func backupRestore(e *core.RequestEvent) error { + if e.App.Store().Has(core.StoreKeyActiveBackup) { + return e.BadRequestError("Try again later - another backup/restore process has already been started.", nil) + } + + key := e.Request.PathValue("key") existsCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - fsys, err := api.app.NewBackupsFilesystem() + fsys, err := e.App.NewBackupsFilesystem() if err != nil { - return NewBadRequestError("Failed to load backups filesystem.", err) + return e.InternalServerError("Failed to load backups filesystem.", err) } defer fsys.Close() fsys.SetContext(existsCtx) if exists, err := fsys.Exists(key); !exists { - return NewBadRequestError("Missing or invalid backup file.", err) + return e.BadRequestError("Missing or invalid backup file.", err) } - go func() { - // wait max 15 minutes to fetch the backup - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) - defer cancel() - - // give some optimistic time to write the response + routine.FireAndForget(func() { + // give some optimistic time to write the response before restarting the app time.Sleep(1 * time.Second) - if err := api.app.RestoreBackup(ctx, key); err != nil { - api.app.Logger().Error("Failed to restore backup", "key", key, "error", err.Error()) + // wait max 10 minutes to fetch the backup + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + if err := e.App.RestoreBackup(ctx, key); err != nil { + e.App.Logger().Error("Failed to restore backup", "key", key, "error", err.Error()) } - }() + }) - return c.NoContent(http.StatusNoContent) -} - -func (api *backupApi) delete(c echo.Context) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - fsys, err := api.app.NewBackupsFilesystem() - if err != nil { - return NewBadRequestError("Failed to load backups filesystem.", err) - } - defer fsys.Close() - - fsys.SetContext(ctx) - - key := c.PathParam("key") - - if key != "" && cast.ToString(api.app.Store().Get(core.StoreKeyActiveBackup)) == key { - return NewBadRequestError("The backup is currently being used and cannot be deleted.", nil) - } - - if err := fsys.Delete(key); err != nil { - return NewBadRequestError("Invalid or already deleted backup file. Raw error: \n"+err.Error(), nil) - } - - return c.NoContent(http.StatusNoContent) + return e.NoContent(http.StatusNoContent) } diff --git a/apis/backup_create.go b/apis/backup_create.go new file mode 100644 index 00000000..d2ebe8dd --- /dev/null +++ b/apis/backup_create.go @@ -0,0 +1,78 @@ +package apis + +import ( + "context" + "net/http" + "regexp" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" +) + +func backupCreate(e *core.RequestEvent) error { + if e.App.Store().Has(core.StoreKeyActiveBackup) { + return e.BadRequestError("Try again later - another backup/restore process has already been started", nil) + } + + form := new(backupCreateForm) + form.app = e.App + + err := e.BindBody(form) + if err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) + } + + err = form.validate() + if err != nil { + return e.BadRequestError("An error occurred while validating the submitted data.", err) + } + + err = e.App.CreateBackup(context.Background(), form.Name) + if err != nil { + return e.BadRequestError("Failed to create backup.", err) + } + + // we don't retrieve the generated backup file because it may not be + // available yet due to the eventually consistent nature of some S3 providers + return e.NoContent(http.StatusNoContent) +} + +// ------------------------------------------------------------------- + +var backupNameRegex = regexp.MustCompile(`^[a-z0-9_-]+\.zip$`) + +type backupCreateForm struct { + app core.App + + Name string `form:"name" json:"name"` +} + +func (form *backupCreateForm) validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Name, + validation.Length(1, 150), + validation.Match(backupNameRegex), + validation.By(form.checkUniqueName), + ), + ) +} + +func (form *backupCreateForm) checkUniqueName(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + fsys, err := form.app.NewBackupsFilesystem() + if err != nil { + return err + } + defer fsys.Close() + + if exists, err := fsys.Exists(v); err != nil || exists { + return validation.NewError("validation_backup_name_exists", "The backup file name is invalid or already exists.") + } + + return nil +} diff --git a/apis/backup_test.go b/apis/backup_test.go index 3927cfec..faa5892a 100644 --- a/apis/backup_test.go +++ b/apis/backup_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "gocloud.dev/blob" @@ -23,50 +22,51 @@ func TestBackupsList(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/backups", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (empty list)", + Name: "authorized as superuser (empty list)", Method: http.MethodGet, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `[]`, + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, + ExpectedStatus: 200, + ExpectedContent: []string{`[]`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin", + Name: "authorized as superuser", Method: http.MethodGet, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } @@ -77,6 +77,7 @@ func TestBackupsList(t *testing.T) { `"test2.zip"`, `"test3.zip"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -92,50 +93,53 @@ func TestBackupsCreate(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/backups", - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + URL: "/api/backups", + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (pending backup)", + Name: "authorized as superuser (pending backup)", Method: http.MethodPost, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { app.Store().Set(core.StoreKeyActiveBackup, "") }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (autogenerated name)", + Name: "authorized as superuser (autogenerated name)", Method: http.MethodPost, - Url: "/api/backups", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) @@ -151,16 +155,20 @@ func TestBackupsCreate(t *testing.T) { } }, ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBackupCreate": 1, + }, }, { - Name: "authorized as admin (invalid name)", + Name: "authorized as superuser (invalid name)", Method: http.MethodPost, - Url: "/api/backups", + URL: "/api/backups", Body: strings.NewReader(`{"name":"!test.zip"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, ExpectedStatus: 400, @@ -168,16 +176,17 @@ func TestBackupsCreate(t *testing.T) { `"data":{`, `"name":{"code":"validation_match_invalid"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (valid name)", + Name: "authorized as superuser (valid name)", Method: http.MethodPost, - Url: "/api/backups", + URL: "/api/backups", Body: strings.NewReader(`{"name":"test.zip"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) @@ -193,6 +202,10 @@ func TestBackupsCreate(t *testing.T) { } }, ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBackupCreate": 1, + }, }, } @@ -201,7 +214,7 @@ func TestBackupsCreate(t *testing.T) { } } -func TestBackupsUpload(t *testing.T) { +func TestBackupUpload(t *testing.T) { t.Parallel() // create dummy form data bodies @@ -243,55 +256,58 @@ func TestBackupsUpload(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/backups/upload", + URL: "/api/backups/upload", Body: bodies[0].buffer, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": bodies[0].contentType, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/backups/upload", + URL: "/api/backups/upload", Body: bodies[1].buffer, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": bodies[1].contentType, - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (missing file)", + Name: "authorized as superuser (missing file)", Method: http.MethodPost, - Url: "/api/backups/upload", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/upload", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureNoBackups(t, app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (existing backup name)", + Name: "authorized as superuser (existing backup name)", Method: http.MethodPost, - Url: "/api/backups/upload", + URL: "/api/backups/upload", Body: bodies[3].buffer, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": bodies[3].contentType, - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { fsys, err := app.NewBackupsFilesystem() if err != nil { t.Fatal(err) @@ -302,7 +318,7 @@ func TestBackupsUpload(t *testing.T) { t.Fatal(err) } }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, _ := getBackupFiles(app) if total := len(files); total != 1 { t.Fatalf("Expected %d backup file, got %d", 1, total) @@ -310,23 +326,49 @@ func TestBackupsUpload(t *testing.T) { }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{"file":{`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (valid file)", + Name: "authorized as superuser (valid file)", Method: http.MethodPost, - Url: "/api/backups/upload", + URL: "/api/backups/upload", Body: bodies[4].buffer, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": bodies[4].contentType, - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, _ := getBackupFiles(app) if total := len(files); total != 1 { t.Fatalf("Expected %d backup file, got %d", 1, total) } }, ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "unauthorized with 0 superusers (valid file)", + Method: http.MethodPost, + URL: "/api/backups/upload", + Body: bodies[5].buffer, + Headers: map[string]string{ + "Content-Type": bodies[5].contentType, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // delete all superusers + _, err := app.DB().NewQuery("DELETE FROM {{" + core.CollectionNameSuperusers + "}}").Execute() + if err != nil { + t.Fatal(err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + files, _ := getBackupFiles(app) + if total := len(files); total != 1 { + t.Fatalf("Expected %d backup file, got %d", 1, total) + } + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -342,148 +384,159 @@ func TestBackupsDownload(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/backups/test1.zip", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "with record auth header", Method: http.MethodGet, - Url: "/api/backups/test1.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/backups/test1.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with admin auth header", + Name: "with superuser auth header", Method: http.MethodGet, - Url: "/api/backups/test1.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/test1.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "with empty or invalid token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "with valid record auth token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "with valid record file token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8ifQ.nSTLuCPcGpWn2K2l-BFkC3Vlzc-ZTDPByYq8dN1oPSo", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with valid admin auth token", + Name: "with valid superuser auth token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with expired admin file token", + Name: "with expired superuser file token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6ImFkbWluIn0.g7Q_3UX6H--JWJ7yt1Hoe-1ugTX1KpbKzdt0zjGSe-E", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.hTNDzikwJdcoWrLnRnp7xbaifZ2vuYZ0oOYRHtJfnk4", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with valid admin file token but missing backup name", + Name: "with valid superuser file token but missing backup name", Method: http.MethodGet, - Url: "/api/backups/missing?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/missing?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.C8m3aRZNOxUDhMiuZuDTRIIjRl7wsOyzoxs8EjvKNgY", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with valid admin file token", + Name: "with valid superuser file token", Method: http.MethodGet, - Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.C8m3aRZNOxUDhMiuZuDTRIIjRl7wsOyzoxs8EjvKNgY", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 200, ExpectedContent: []string{ - `storage/`, - `data.db`, - `logs.db`, + "storage/", + "data.db", + "aux.db", }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "with valid admin file token and backup name with escaped char", + Name: "with valid superuser file token and backup name with escaped char", Method: http.MethodGet, - Url: "/api/backups/%40test4.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/%40test4.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.C8m3aRZNOxUDhMiuZuDTRIIjRl7wsOyzoxs8EjvKNgY", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 200, ExpectedContent: []string{ - `storage/`, - `data.db`, - `logs.db`, + "storage/", + "data.db", + "aux.db", }, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -495,7 +548,7 @@ func TestBackupsDownload(t *testing.T) { func TestBackupsDelete(t *testing.T) { t.Parallel() - noTestBackupFilesChanges := func(t *testing.T, app *tests.TestApp) { + noTestBackupFilesChanges := func(t testing.TB, app *tests.TestApp) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) @@ -511,62 +564,65 @@ func TestBackupsDelete(t *testing.T) { { Name: "unauthorized", Method: http.MethodDelete, - Url: "/api/backups/test1.zip", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { noTestBackupFilesChanges(t, app) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodDelete, - Url: "/api/backups/test1.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/backups/test1.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { noTestBackupFilesChanges(t, app) }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (missing file)", + Name: "authorized as superuser (missing file)", Method: http.MethodDelete, - Url: "/api/backups/missing.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/missing.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { noTestBackupFilesChanges(t, app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (existing file with matching active backup)", + Name: "authorized as superuser (existing file with matching active backup)", Method: http.MethodDelete, - Url: "/api/backups/test1.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/test1.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } @@ -574,20 +630,21 @@ func TestBackupsDelete(t *testing.T) { // mock active backup with the same name to delete app.Store().Set(core.StoreKeyActiveBackup, "test1.zip") }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { noTestBackupFilesChanges(t, app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (existing file and no matching active backup)", + Name: "authorized as superuser (existing file and no matching active backup)", Method: http.MethodDelete, - Url: "/api/backups/test1.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/test1.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } @@ -595,7 +652,7 @@ func TestBackupsDelete(t *testing.T) { // mock active backup with different name app.Store().Set(core.StoreKeyActiveBackup, "new.zip") }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) @@ -614,20 +671,21 @@ func TestBackupsDelete(t *testing.T) { } }, ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (backup with escaped character)", + Name: "authorized as superuser (backup with escaped character)", Method: http.MethodDelete, - Url: "/api/backups/%40test4.zip", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/%40test4.zip", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) @@ -646,6 +704,7 @@ func TestBackupsDelete(t *testing.T) { } }, ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -661,53 +720,56 @@ func TestBackupsRestore(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/backups/test1.zip/restore", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + URL: "/api/backups/test1.zip/restore", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/backups/test1.zip/restore", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/backups/test1.zip/restore", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (missing file)", + Name: "authorized as superuser (missing file)", Method: http.MethodPost, - Url: "/api/backups/missing.zip/restore", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/missing.zip/restore", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (active backup process)", + Name: "authorized as superuser (active backup process)", Method: http.MethodPost, - Url: "/api/backups/test1.zip/restore", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/backups/test1.zip/restore", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { if err := createTestBackups(app); err != nil { t.Fatal(err) } @@ -716,6 +778,26 @@ func TestBackupsRestore(t *testing.T) { }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "unauthorized with no superusers (checks only access)", + Method: http.MethodPost, + URL: "/api/backups/missing.zip/restore", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // delete all superusers + _, err := app.DB().NewQuery("DELETE FROM {{" + core.CollectionNameSuperusers + "}}").Execute() + if err != nil { + t.Fatal(err) + } + + if err := createTestBackups(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -758,7 +840,7 @@ func getBackupFiles(app core.App) ([]*blob.ListObject, error) { return fsys.List("") } -func ensureNoBackups(t *testing.T, app *tests.TestApp) { +func ensureNoBackups(t testing.TB, app *tests.TestApp) { files, err := getBackupFiles(app) if err != nil { t.Fatal(err) diff --git a/apis/backup_upload.go b/apis/backup_upload.go new file mode 100644 index 00000000..3117abcf --- /dev/null +++ b/apis/backup_upload.go @@ -0,0 +1,72 @@ +package apis + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/filesystem" +) + +func backupUpload(e *core.RequestEvent) error { + fsys, err := e.App.NewBackupsFilesystem() + if err != nil { + return err + } + defer fsys.Close() + + form := new(backupUploadForm) + form.fsys = fsys + files, _ := FindUploadedFiles(e.Request, "file") + if len(files) > 0 { + form.File = files[0] + } + + err = form.validate() + if err != nil { + return e.BadRequestError("An error occurred while validating the submitted data.", err) + } + + err = fsys.UploadFile(form.File, form.File.OriginalName) + if err != nil { + return e.BadRequestError("Failed to upload backup.", err) + } + + // we don't retrieve the generated backup file because it may not be + // available yet due to the eventually consistent nature of some S3 providers + return e.NoContent(http.StatusNoContent) +} + +// ------------------------------------------------------------------- + +type backupUploadForm struct { + fsys *filesystem.System + + File *filesystem.File `json:"file"` +} + +func (form *backupUploadForm) validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.File, + validation.Required, + validation.By(validators.UploadedFileMimeType([]string{"application/zip"})), + validation.By(form.checkUniqueName), + ), + ) +} + +func (form *backupUploadForm) checkUniqueName(value any) error { + v, _ := value.(*filesystem.File) + if v == nil { + return nil // nothing to check + } + + // note: we use the original name because that is what we upload + if exists, err := form.fsys.Exists(v.OriginalName); err != nil || exists { + return validation.NewError("validation_backup_name_exists", "Backup file with the specified name already exists.") + } + + return nil +} diff --git a/apis/base.go b/apis/base.go index 404c806a..42003474 100644 --- a/apis/base.go +++ b/apis/base.go @@ -1,266 +1,202 @@ -// Package apis implements the default PocketBase api services and middlewares. package apis import ( - "database/sql" "errors" "fmt" "io/fs" - "log/slog" "net/http" - "net/url" "path/filepath" "strings" - "time" - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/tools/rest" - "github.com/pocketbase/pocketbase/ui" - "github.com/spf13/cast" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" ) -const trailedAdminPath = "/_/" +// StaticWildcardParam is the name of Static handler wildcard parameter. +const StaticWildcardParam = "path" -// InitApi creates a configured echo instance with registered -// system and app specific routes and middlewares. -func InitApi(app core.App) (*echo.Echo, error) { - e := echo.New() - e.Debug = false - e.Binder = &rest.MultiBinder{} - e.JSONSerializer = &rest.Serializer{ - FieldsParam: fieldsQueryParam, - } +// NewRouter returns a new router instance loaded with the default app middlewares and api routes. +func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) { + pbRouter := router.NewRouter(func(w http.ResponseWriter, r *http.Request) (*core.RequestEvent, router.EventCleanupFunc) { + event := new(core.RequestEvent) + event.Response = w + event.Request = r + event.App = app - // configure a custom router - e.ResetRouterCreator(func(ec *echo.Echo) echo.Router { - return echo.NewRouter(echo.RouterConfig{ - UnescapePathParamValues: true, - AllowOverwritingRoute: true, - }) + return event, nil }) - // default middlewares - e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{ - Skipper: func(c echo.Context) bool { - // enable by default only for the API routes - return !strings.HasPrefix(c.Request().URL.Path, "/api/") - }, - })) - e.Pre(LoadAuthContext(app)) - e.Use(middleware.Recover()) - e.Use(middleware.Secure()) - e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Set(ContextExecStartKey, time.Now()) + // register default middlewares + pbRouter.Bind(activityLogger()) + pbRouter.Bind(loadAuthToken()) + pbRouter.Bind(securityHeaders()) + pbRouter.Bind(rateLimit()) + pbRouter.Bind(BodyLimit(DefaultMaxBodySize)) - return next(c) - } - }) + apiGroup := pbRouter.Group("/api") + bindSettingsApi(app, apiGroup) + bindCollectionApi(app, apiGroup) + bindRecordCrudApi(app, apiGroup) + bindRecordAuthApi(app, apiGroup) + bindLogsApi(app, apiGroup) + bindBackupApi(app, apiGroup) + bindFileApi(app, apiGroup) + bindBatchApi(app, apiGroup) + bindRealtimeApi(app, apiGroup) + bindHealthApi(app, apiGroup) - // custom error handler - e.HTTPErrorHandler = func(c echo.Context, err error) { - if err == nil { - return // no error - } - - var apiErr *ApiError - - if errors.As(err, &apiErr) { - // already an api error... - } else if v := new(echo.HTTPError); errors.As(err, &v) { - msg := fmt.Sprintf("%v", v.Message) - apiErr = NewApiError(v.Code, msg, v) - } else { - if errors.Is(err, sql.ErrNoRows) { - apiErr = NewNotFoundError("", err) - } else { - apiErr = NewBadRequestError("", err) - } - } - - logRequest(app, c, apiErr) - - if c.Response().Committed { - return // already committed - } - - event := new(core.ApiErrorEvent) - event.HttpContext = c - event.Error = apiErr - - // send error response - hookErr := app.OnBeforeApiError().Trigger(event, func(e *core.ApiErrorEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - // @see https://github.com/labstack/echo/issues/608 - if e.HttpContext.Request().Method == http.MethodHead { - return e.HttpContext.NoContent(apiErr.Code) - } - - return e.HttpContext.JSON(apiErr.Code, apiErr) - }) - - if hookErr == nil { - if err := app.OnAfterApiError().Trigger(event); err != nil { - app.Logger().Debug("OnAfterApiError failure", slog.String("error", err.Error())) - } - } else { - app.Logger().Debug("OnBeforeApiError error (truly rare case, eg. client already disconnected)", slog.String("error", hookErr.Error())) - } - } - - // admin ui routes - bindStaticAdminUI(app, e) - - // default routes - api := e.Group("/api", eagerRequestInfoCache(app)) - bindSettingsApi(app, api) - bindAdminApi(app, api) - bindCollectionApi(app, api) - bindRecordCrudApi(app, api) - bindRecordAuthApi(app, api) - bindFileApi(app, api) - bindRealtimeApi(app, api) - bindLogsApi(app, api) - bindHealthApi(app, api) - bindBackupApi(app, api) - - // catch all any route - api.Any("/*", func(c echo.Context) error { - return echo.ErrNotFound - }, ActivityLogger(app)) - - return e, nil + return pbRouter, nil } -// StaticDirectoryHandler is similar to `echo.StaticDirectoryHandler` -// but without the directory redirect which conflicts with RemoveTrailingSlash middleware. +// WrapStdHandler wraps Go [http.Handler] into a PocketBase handler func. +func WrapStdHandler(h http.Handler) hook.HandlerFunc[*core.RequestEvent] { + return func(e *core.RequestEvent) error { + h.ServeHTTP(e.Response, e.Request) + return nil + } +} + +// WrapStdMiddleware wraps Go [func(http.Handler) http.Handle] into a PocketBase middleware func. +func WrapStdMiddleware(m func(http.Handler) http.Handler) hook.HandlerFunc[*core.RequestEvent] { + return func(e *core.RequestEvent) (err error) { + m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + e.Response = w + e.Request = r + err = e.Next() + })).ServeHTTP(e.Response, e.Request) + return err + } +} + +// MustSubFS returns an [fs.FS] corresponding to the subtree rooted at fsys's dir. +// +// This is similar to [fs.Sub] but panics on failure. +func MustSubFS(fsys fs.FS, dir string) fs.FS { + dir = filepath.ToSlash(filepath.Clean(dir)) // ToSlash in case of Windows path + + sub, err := fs.Sub(fsys, dir) + if err != nil { + panic(fmt.Errorf("failed to create sub FS: %w", err)) + } + + return sub +} + +// Static is a handler function to serve static directory content from fsys. // // If a file resource is missing and indexFallback is set, the request -// will be forwarded to the base index.html (useful also for SPA). +// will be forwarded to the base index.html (useful for SPA with pretty urls). // -// @see https://github.com/labstack/echo/issues/2211 -func StaticDirectoryHandler(fileSystem fs.FS, indexFallback bool) echo.HandlerFunc { - return func(c echo.Context) error { - p := c.PathParam("*") +// NB! Expects the route to have a "{path...}" wildcard parameter. +// +// Special redirects: +// - if "path" is a file that ends in index.html, it is redirected to its non-index.html version (eg. /test/index.html -> /test/) +// - if "path" is a directory that has index.html, the index.html file is rendered, +// otherwise if missing - returns 404 or fallback to the root index.html if indexFallback is set +// +// Example: +// +// fsys := os.DirFS("./pb_public") +// router.GET("/files/{path...}", apis.Static(fsys, false)) +func Static(fsys fs.FS, indexFallback bool) hook.HandlerFunc[*core.RequestEvent] { + if fsys == nil { + panic("Static: the provided fs.FS argument is nil") + } - // escape url path - tmpPath, err := url.PathUnescape(p) - if err != nil { - return fmt.Errorf("failed to unescape path variable: %w", err) + return func(e *core.RequestEvent) error { + // disable the activity logger to avoid flooding with messages + // + // note: errors are still logged + if e.Get(requestEventKeySkipSuccessActivityLog) == nil { + e.Set(requestEventKeySkipSuccessActivityLog, true) } - p = tmpPath - // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid - name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/"))) + filename := e.Request.PathValue(StaticWildcardParam) + filename = filepath.ToSlash(filepath.Clean(strings.TrimPrefix(filename, "/"))) - fileErr := c.FileFS(name, fileSystem) + // eagerly check for directory traversal + // + // note: this is just out of an abundance of caution because the fs.FS implementation could be non-std, + // but usually shouldn't be necessary since os.DirFS.Open is expected to fail if the filename starts with dots + if len(filename) > 2 && filename[0] == '.' && filename[1] == '.' && (filename[2] == '/' || filename[2] == '\\') { + if indexFallback && filename != router.IndexPage { + return e.FileFS(fsys, router.IndexPage) + } + return router.ErrFileNotFound + } - if fileErr != nil && indexFallback && errors.Is(fileErr, echo.ErrNotFound) { - return c.FileFS("index.html", fileSystem) + fi, err := fs.Stat(fsys, filename) + if err != nil { + if indexFallback && filename != router.IndexPage { + return e.FileFS(fsys, router.IndexPage) + } + return router.ErrFileNotFound + } + + if fi.IsDir() { + // redirect to a canonical dir url, aka. with trailing slash + if !strings.HasSuffix(e.Request.URL.Path, "/") { + return e.Redirect(http.StatusMovedPermanently, safeRedirectPath(e.Request.URL.Path+"/")) + } + } else { + urlPath := e.Request.URL.Path + if strings.HasSuffix(urlPath, "/") { + // redirect to a non-trailing slash file route + urlPath = strings.TrimRight(urlPath, "/") + if len(urlPath) > 0 { + return e.Redirect(http.StatusMovedPermanently, safeRedirectPath(urlPath)) + } + } else if stripped, ok := strings.CutSuffix(urlPath, router.IndexPage); ok { + // redirect without the index.html + return e.Redirect(http.StatusMovedPermanently, safeRedirectPath(stripped)) + } + } + + fileErr := e.FileFS(fsys, filename) + + if fileErr != nil && indexFallback && filename != router.IndexPage && errors.Is(fileErr, router.ErrFileNotFound) { + return e.FileFS(fsys, router.IndexPage) } return fileErr } } -// bindStaticAdminUI registers the endpoints that serves the static admin UI. -func bindStaticAdminUI(app core.App, e *echo.Echo) error { - // redirect to trailing slash to ensure that relative urls will still work properly - e.GET( - strings.TrimRight(trailedAdminPath, "/"), - func(c echo.Context) error { - return c.Redirect(http.StatusTemporaryRedirect, strings.TrimLeft(trailedAdminPath, "/")) - }, - ) - - // serves static files from the /ui/dist directory - // (similar to echo.StaticFS but with gzip middleware enabled) - e.GET( - trailedAdminPath+"*", - echo.StaticDirectoryHandler(ui.DistDirFS, false), - installerRedirect(app), - uiCacheControl(), - middleware.Gzip(), - ) - - return nil +// safeRedirectPath normalizes the path string by replacing all beginning slashes +// (`\\`, `//`, `\/`) with a single forward slash to prevent open redirect attacks +func safeRedirectPath(path string) string { + if len(path) > 1 && (path[0] == '\\' || path[0] == '/') && (path[1] == '\\' || path[1] == '/') { + path = "/" + strings.TrimLeft(path, `/\`) + } + return path } -func uiCacheControl() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // add default Cache-Control header for all Admin UI resources - // (ignoring the root admin path) - if c.Request().URL.Path != trailedAdminPath { - c.Response().Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400") - } - - return next(c) +// FindUploadedFiles extracts all form files of "key" from a http request +// and returns a slice with filesystem.File instances (if any). +func FindUploadedFiles(r *http.Request, key string) ([]*filesystem.File, error) { + if r.MultipartForm == nil { + err := r.ParseMultipartForm(router.DefaultMaxMemory) + if err != nil { + return nil, err } } -} -const hasAdminsCacheKey = "@hasAdmins" - -func updateHasAdminsCache(app core.App) error { - total, err := app.Dao().TotalAdmins() - if err != nil { - return err + if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File[key]) == 0 { + return nil, http.ErrMissingFile } - app.Store().Set(hasAdminsCacheKey, total > 0) + result := make([]*filesystem.File, 0, len(r.MultipartForm.File[key])) - return nil -} - -// installerRedirect redirects the user to the installer admin UI page -// when the application needs some preliminary configurations to be done. -func installerRedirect(app core.App) echo.MiddlewareFunc { - // keep hasAdminsCacheKey value up-to-date - app.OnAdminAfterCreateRequest().Add(func(data *core.AdminCreateEvent) error { - return updateHasAdminsCache(app) - }) - - app.OnAdminAfterDeleteRequest().Add(func(data *core.AdminDeleteEvent) error { - return updateHasAdminsCache(app) - }) - - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // skip redirect checks for non-root level index.html requests - path := c.Request().URL.Path - if path != trailedAdminPath && path != trailedAdminPath+"index.html" { - return next(c) - } - - hasAdmins := cast.ToBool(app.Store().Get(hasAdminsCacheKey)) - - if !hasAdmins { - // update the cache to make sure that the admin wasn't created by another process - if err := updateHasAdminsCache(app); err != nil { - return err - } - hasAdmins = cast.ToBool(app.Store().Get(hasAdminsCacheKey)) - } - - _, hasInstallerParam := c.Request().URL.Query()["installer"] - - if !hasAdmins && !hasInstallerParam { - // redirect to the installer page - return c.Redirect(http.StatusTemporaryRedirect, "?installer#") - } - - if hasAdmins && hasInstallerParam { - // clear the installer param - return c.Redirect(http.StatusTemporaryRedirect, "?") - } - - return next(c) + for _, fh := range r.MultipartForm.File[key] { + file, err := filesystem.NewFileFromMultipart(fh) + if err != nil { + return nil, err } + + result = append(result, file) } + + return result, nil } diff --git a/apis/base_test.go b/apis/base_test.go index ea4a3ace..79d71c1d 100644 --- a/apis/base_test.go +++ b/apis/base_test.go @@ -1,422 +1,386 @@ package apis_test import ( - "database/sql" - "errors" + "bytes" "fmt" + "mime/multipart" "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" "strings" "testing" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/rest" - "github.com/spf13/cast" + "github.com/pocketbase/pocketbase/tools/router" ) -func Test404(t *testing.T) { +func TestWrapStdHandler(t *testing.T) { t.Parallel() - scenarios := []tests.ApiScenario{ - { - Method: http.MethodGet, - Url: "/api/missing", - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Method: http.MethodPost, - Url: "/api/missing", - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Method: http.MethodPatch, - Url: "/api/missing", - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Method: http.MethodDelete, - Url: "/api/missing", - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Method: http.MethodHead, - Url: "/api/missing", - ExpectedStatus: 404, - }, - } + app, _ := tests.NewTestApp() + defer app.Cleanup() - for _, scenario := range scenarios { - scenario.Test(t) - } -} + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() -func TestCustomRoutesAndErrorsHandling(t *testing.T) { - t.Parallel() + e := new(core.RequestEvent) + e.App = app + e.Request = req + e.Response = rec - scenarios := []tests.ApiScenario{ - { - Name: "custom route", - Method: http.MethodGet, - Url: "/custom", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "custom route with url encoded parameter", - Method: http.MethodGet, - Url: "/a%2Bb%2Bc", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/:param", - Handler: func(c echo.Context) error { - return c.String(200, c.PathParam("param")) - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"a+b+c"}, - }, - { - Name: "route with HTTPError", - Method: http.MethodGet, - Url: "/http-error", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/http-error", - Handler: func(c echo.Context) error { - return echo.ErrBadRequest - }, - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`{"code":400,"message":"Bad Request.","data":{}}`}, - }, - { - Name: "route with api error", - Method: http.MethodGet, - Url: "/api-error", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/api-error", - Handler: func(c echo.Context) error { - return apis.NewApiError(500, "test message", errors.New("internal_test")) - }, - }) - }, - ExpectedStatus: 500, - ExpectedContent: []string{`{"code":500,"message":"Test message.","data":{}}`}, - }, - { - Name: "route with plain error", - Method: http.MethodGet, - Url: "/plain-error", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/plain-error", - Handler: func(c echo.Context) error { - return errors.New("Test error") - }, - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`{"code":400,"message":"Something went wrong while processing your request.","data":{}}`}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRemoveTrailingSlashMiddleware(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "non /api/* route (exact match)", - Method: http.MethodGet, - Url: "/custom", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "non /api/* route (with trailing slash)", - Method: http.MethodGet, - Url: "/custom/", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - }) - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "/api/* route (exact match)", - Method: http.MethodGet, - Url: "/api/custom", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/api/custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "/api/* route (with trailing slash)", - Method: http.MethodGet, - Url: "/api/custom/", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/api/custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestMultiBinder(t *testing.T) { - t.Parallel() - - rawJson := `{"name":"test123"}` - - formData, mp, err := tests.MockMultipartData(map[string]string{ - rest.MultipartJsonKey: rawJson, - }) + err := apis.WrapStdHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("test")) + }))(e) if err != nil { t.Fatal(err) } - scenarios := []tests.ApiScenario{ - { - Name: "non-api group route", - Method: "POST", - Url: "/custom", - Body: strings.NewReader(rawJson), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: "POST", - Path: "/custom", - Handler: func(c echo.Context) error { - data := &struct { - Name string `json:"name"` - }{} - - if err := c.Bind(data); err != nil { - return err - } - - // try to read the body again - r := apis.RequestInfo(c) - if v := cast.ToString(r.Data["name"]); v != "test123" { - t.Fatalf("Expected request data with name %q, got, %q", "test123", v) - } - - return c.NoContent(200) - }, - }) - }, - ExpectedStatus: 200, - }, - { - Name: "api group route", - Method: "GET", - Url: "/api/admins", - Body: strings.NewReader(rawJson), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // it is not important whether the route handler return an error since - // we just need to ensure that the eagerRequestInfoCache was registered - next(c) - - // ensure that the body was read at least once - data := &struct { - Name string `json:"name"` - }{} - c.Bind(data) - - // try to read the body again - r := apis.RequestInfo(c) - if v := cast.ToString(r.Data["name"]); v != "test123" { - t.Fatalf("Expected request data with name %q, got, %q", "test123", v) - } - - return nil - } - }) - }, - ExpectedStatus: 200, - }, - { - Name: "custom route with @jsonPayload as multipart body", - Method: "POST", - Url: "/custom", - Body: formData, - RequestHeaders: map[string]string{ - "Content-Type": mp.FormDataContentType(), - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: "POST", - Path: "/custom", - Handler: func(c echo.Context) error { - data := &struct { - Name string `json:"name"` - }{} - - if err := c.Bind(data); err != nil { - return err - } - - // try to read the body again - r := apis.RequestInfo(c) - if v := cast.ToString(r.Data["name"]); v != "test123" { - t.Fatalf("Expected request data with name %q, got, %q", "test123", v) - } - - return c.NoContent(200) - }, - }) - }, - ExpectedStatus: 200, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) + if body := rec.Body.String(); body != "test" { + t.Fatalf("Expected body %q, got %q", "test", body) } } -func TestErrorHandler(t *testing.T) { +func TestWrapStdMiddleware(t *testing.T) { t.Parallel() - scenarios := []tests.ApiScenario{ + app, _ := tests.NewTestApp() + defer app.Cleanup() + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + e := new(core.RequestEvent) + e.App = app + e.Request = req + e.Response = rec + + err := apis.WrapStdMiddleware(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("test")) + }) + })(e) + if err != nil { + t.Fatal(err) + } + + if body := rec.Body.String(); body != "test" { + t.Fatalf("Expected body %q, got %q", "test", body) + } +} + +func TestStatic(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fsys := os.DirFS(filepath.Join(dir, "sub")) + + type staticScenario struct { + path string + indexFallback bool + expectedStatus int + expectBody string + expectError bool + } + + scenarios := []staticScenario{ { - Name: "apis.ApiError", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return apis.NewApiError(418, "test", nil) - }) - }, - ExpectedStatus: 418, - ExpectedContent: []string{`"message":"Test."`}, + path: "", + indexFallback: false, + expectedStatus: 200, + expectBody: "sub index.html", + expectError: false, }, { - Name: "wrapped apis.ApiError", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return fmt.Errorf("example 123: %w", apis.NewApiError(418, "test", nil)) - }) - }, - ExpectedStatus: 418, - ExpectedContent: []string{`"message":"Test."`}, - NotExpectedContent: []string{"example", "123"}, + path: "missing/a/b/c", + indexFallback: false, + expectedStatus: 404, + expectBody: "", + expectError: true, }, { - Name: "echo.HTTPError", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return echo.NewHTTPError(418, "test") - }) - }, - ExpectedStatus: 418, - ExpectedContent: []string{`"message":"Test."`}, + path: "missing/a/b/c", + indexFallback: true, + expectedStatus: 200, + expectBody: "sub index.html", + expectError: false, }, { - Name: "wrapped echo.HTTPError", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return fmt.Errorf("example 123: %w", echo.NewHTTPError(418, "test")) - }) - }, - ExpectedStatus: 418, - ExpectedContent: []string{`"message":"Test."`}, - NotExpectedContent: []string{"example", "123"}, + path: "testroot", // parent directory file + indexFallback: false, + expectedStatus: 404, + expectBody: "", + expectError: true, }, { - Name: "wrapped sql.ErrNoRows", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return fmt.Errorf("example 123: %w", sql.ErrNoRows) - }) - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - NotExpectedContent: []string{"example", "123"}, + path: "test", + indexFallback: false, + expectedStatus: 200, + expectBody: "sub test", + expectError: false, }, { - Name: "custom error", - Method: http.MethodGet, - Url: "/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.GET("/test", func(c echo.Context) error { - return fmt.Errorf("example 123") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - NotExpectedContent: []string{"example", "123"}, + path: "sub2", + indexFallback: false, + expectedStatus: 301, + expectBody: "", + expectError: false, + }, + { + path: "sub2/", + indexFallback: false, + expectedStatus: 200, + expectBody: "sub2 index.html", + expectError: false, + }, + { + path: "sub2/test", + indexFallback: false, + expectedStatus: 200, + expectBody: "sub2 test", + expectError: false, + }, + { + path: "sub2/test/", + indexFallback: false, + expectedStatus: 301, + expectBody: "", + expectError: false, }, } - for _, scenario := range scenarios { - scenario.Test(t) + // extra directory traversal checks + dtp := []string{ + "/../", + "\\../", + "../", + "../../", + "..\\", + "..\\..\\", + "../..\\", + "..\\..//", + `%2e%2e%2f`, + `%2e%2e%2f%2e%2e%2f`, + `%2e%2e/`, + `%2e%2e/%2e%2e/`, + `..%2f`, + `..%2f..%2f`, + `%2e%2e%5c`, + `%2e%2e%5c%2e%2e%5c`, + `%2e%2e\`, + `%2e%2e\%2e%2e\`, + `..%5c`, + `..%5c..%5c`, + `%252e%252e%255c`, + `%252e%252e%255c%252e%252e%255c`, + `..%255c`, + `..%255c..%255c`, + } + for _, p := range dtp { + scenarios = append(scenarios, + staticScenario{ + path: p + "testroot", + indexFallback: false, + expectedStatus: 404, + expectBody: "", + expectError: true, + }, + staticScenario{ + path: p + "testroot", + indexFallback: true, + expectedStatus: 200, + expectBody: "sub index.html", + expectError: false, + }, + ) + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%v", i, s.path, s.indexFallback), func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/"+s.path, nil) + req.SetPathValue(apis.StaticWildcardParam, s.path) + + rec := httptest.NewRecorder() + + e := new(core.RequestEvent) + e.App = app + e.Request = req + e.Response = rec + + err := apis.Static(fsys, s.indexFallback)(e) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + body := rec.Body.String() + if body != s.expectBody { + t.Fatalf("Expected body %q, got %q", s.expectBody, body) + } + + if hasErr { + apiErr := router.ToApiError(err) + if apiErr.Status != s.expectedStatus { + t.Fatalf("Expected status code %d, got %d", s.expectedStatus, apiErr.Status) + } + } + }) } } + +func TestFindUploadedFiles(t *testing.T) { + scenarios := []struct { + filename string + expectedPattern string + }{ + {"ab.png", `^ab\w{10}_\w{10}\.png$`}, + {"test", `^test_\w{10}\.txt$`}, + {"a b c d!@$.j!@$pg", `^a_b_c_d_\w{10}\.jpg$`}, + {strings.Repeat("a", 150), `^a{100}_\w{10}\.txt$`}, + } + + for _, s := range scenarios { + t.Run(s.filename, func(t *testing.T) { + // create multipart form file body + body := new(bytes.Buffer) + mp := multipart.NewWriter(body) + w, err := mp.CreateFormFile("test", s.filename) + if err != nil { + t.Fatal(err) + } + w.Write([]byte("test")) + mp.Close() + // --- + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + result, err := apis.FindUploadedFiles(req, "test") + if err != nil { + t.Fatal(err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 file, got %d", len(result)) + } + + if result[0].Size != 4 { + t.Fatalf("Expected the file size to be 4 bytes, got %d", result[0].Size) + } + + pattern, err := regexp.Compile(s.expectedPattern) + if err != nil { + t.Fatalf("Invalid filename pattern %q: %v", s.expectedPattern, err) + } + if !pattern.MatchString(result[0].Name) { + t.Fatalf("Expected filename to match %s, got filename %s", s.expectedPattern, result[0].Name) + } + }) + } +} + +func TestFindUploadedFilesMissing(t *testing.T) { + body := new(bytes.Buffer) + mp := multipart.NewWriter(body) + mp.Close() + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + result, err := apis.FindUploadedFiles(req, "test") + if err == nil { + t.Error("Expected error, got nil") + } + + if result != nil { + t.Errorf("Expected result to be nil, got %v", result) + } +} + +func TestMustSubFS(t *testing.T) { + t.Parallel() + + dir := createTestDir(t) + defer os.RemoveAll(dir) + + // invalid path (no beginning and ending slashes) + if !hasPanicked(func() { + apis.MustSubFS(os.DirFS(dir), "/test/") + }) { + t.Fatalf("Expected to panic") + } + + // valid path + if hasPanicked(func() { + apis.MustSubFS(os.DirFS(dir), "./////a/b/c") // checks if ToSlash was called + }) { + t.Fatalf("Didn't expect to panic") + } + + // check sub content + sub := apis.MustSubFS(os.DirFS(dir), "sub") + + _, err := sub.Open("test") + if err != nil { + t.Fatalf("Missing expected file sub/test") + } +} + +// ------------------------------------------------------------------- + +func hasPanicked(f func()) (didPanic bool) { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + f() + return +} + +// note: make sure to call os.RemoveAll(dir) after you are done +// working with the created test dir. +func createTestDir(t *testing.T) string { + dir, err := os.MkdirTemp(os.TempDir(), "test_dir") + if err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("root index.html"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "testroot"), []byte("root test"), 0644); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(filepath.Join(dir, "sub"), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "sub/index.html"), []byte("sub index.html"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "sub/test"), []byte("sub test"), 0644); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(filepath.Join(dir, "sub", "sub2"), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "sub/sub2/index.html"), []byte("sub2 index.html"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "sub/sub2/test"), []byte("sub2 test"), 0644); err != nil { + t.Fatal(err) + } + + return dir +} diff --git a/apis/batch.go b/apis/batch.go new file mode 100644 index 00000000..bd1727f1 --- /dev/null +++ b/apis/batch.go @@ -0,0 +1,542 @@ +package apis + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "regexp" + "slices" + "strconv" + "strings" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/router" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +func bindBatchApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + sub := rg.Group("/batch") + sub.POST("", batchTransaction).Unbind(DefaultBodyLimitMiddlewareId) // the body limit is inlined +} + +type HandleFunc func(e *core.RequestEvent) error + +type BatchActionHandlerFunc func(app core.App, ir *core.InternalRequest, params map[string]string, next func() 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[^\/\?]+)/records(?P\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc { + var id string + if len(ir.Body) > 0 && ir.Body["id"] != "" { + id = cast.ToString(ir.Body["id"]) + } + if id != "" { + _, err := app.FindRecordById(params["collection"], id) + if err == nil { + // update + // --- + params["id"] = id // required for the path value + ir.Method = "PATCH" + ir.URL = "/api/collections/" + params["collection"] + "/records/" + id + params["query"] + return recordUpdate(next) + } + } + + // create + // --- + ir.Method = "POST" + ir.URL = "/api/collections/" + params["collection"] + "/records" + params["query"] + return recordCreate(next) + }, + regexp.MustCompile(`^POST /api/collections/(?P[^\/\?]+)/records(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc { + return recordCreate(next) + }, + regexp.MustCompile(`^PATCH /api/collections/(?P[^\/\?]+)/records/(?P[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc { + return recordUpdate(next) + }, + regexp.MustCompile(`^DELETE /api/collections/(?P[^\/\?]+)/records/(?P[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc { + return recordDelete(next) + }, +} + +type BatchRequestResult struct { + Body any `json:"body"` + Status int `json:"status"` +} + +type batchRequestsForm struct { + Requests []*core.InternalRequest `form:"requests" json:"requests"` + + max int +} + +func (brs batchRequestsForm) validate() error { + return validation.ValidateStruct(&brs, + validation.Field(&brs.Requests, validation.Required, validation.Length(0, brs.max)), + ) +} + +// NB! When the request is submitted as multipart/form-data, +// the regular fields data is expected to be submitted as serailized +// json under the @jsonPayload field and file keys need to follow the +// pattern "requests.N.fileField" or requests[N].fileField. +func batchTransaction(e *core.RequestEvent) error { + maxRequests := e.App.Settings().Batch.MaxRequests + if !e.App.Settings().Batch.Enabled || maxRequests <= 0 { + return e.ForbiddenError("Batch requests are not allowed.", nil) + } + + txTimeout := time.Duration(e.App.Settings().Batch.Timeout) * time.Second + if txTimeout <= 0 { + txTimeout = 3 * time.Second // for now always limit + } + + maxBodySize := e.App.Settings().Batch.MaxBodySize + if maxBodySize <= 0 { + maxBodySize = 128 << 20 + } + + err := applyBodyLimit(e, maxBodySize) + if err != nil { + return err + } + + form := &batchRequestsForm{max: maxRequests} + + // load base requests data + err = e.BindBody(form) + if err != nil { + return e.BadRequestError("Failed to read the submitted batch data.", err) + } + + // load uploaded files into each request item + // note: expects the files to be under "requests.N.fileField" or "requests[N].fileField" format + // (the other regular fields must be put under `@jsonPayload` as serialized json) + if strings.HasPrefix(e.Request.Header.Get("Content-Type"), "multipart/form-data") { + for i, ir := range form.Requests { + iStr := strconv.Itoa(i) + + files, err := extractPrefixedFiles(e.Request, "requests."+iStr+".", "requests["+iStr+"].") + if err != nil { + return e.BadRequestError("Failed to read the submitted batch files data.", err) + } + + for key, files := range files { + if ir.Body == nil { + ir.Body = map[string]any{} + } + ir.Body[key] = files + } + } + } + + // validate batch request form + err = form.validate() + if err != nil { + return e.BadRequestError("Invalid batch request data.", err) + } + + event := new(core.BatchRequestEvent) + event.RequestEvent = e + event.Batch = form.Requests + + return e.App.OnBatchRequest().Trigger(event, func(e *core.BatchRequestEvent) error { + bp := batchProcessor{ + app: e.App, + baseEvent: e.RequestEvent, + infoContext: core.RequestInfoContextBatch, + } + + if err := bp.Process(e.Batch, txTimeout); err != nil { + return firstApiError(err, e.BadRequestError("Batch transaction failed.", err)) + } + + return e.JSON(http.StatusOK, bp.results) + }) +} + +type batchProcessor struct { + app core.App + baseEvent *core.RequestEvent + infoContext string + results []*BatchRequestResult + failedIndex int + errCh chan error + stopCh chan struct{} +} + +func (p *batchProcessor) Process(batch []*core.InternalRequest, timeout time.Duration) error { + p.results = make([]*BatchRequestResult, 0, len(batch)) + + if p.stopCh != nil { + close(p.stopCh) + } + p.stopCh = make(chan struct{}, 1) + + if p.errCh != nil { + close(p.errCh) + } + p.errCh = make(chan error, 1) + + return p.app.RunInTransaction(func(txApp core.App) error { + // used to interupts the recursive processing calls in case of a timeout or connection close + defer func() { + p.stopCh <- struct{}{} + }() + + go func() { + err := p.process(txApp, batch, 0) + + if err != nil { + err = validation.Errors{ + "requests": validation.Errors{ + strconv.Itoa(p.failedIndex): &BatchResponseError{ + code: "batch_request_failed", + message: "Batch request failed.", + err: router.ToApiError(err), + }, + }, + } + } + + // note: to avoid copying and due to the process recursion the final results order is reversed + if err == nil { + slices.Reverse(p.results) + } + + p.errCh <- err + }() + + select { + case responseErr := <-p.errCh: + return responseErr + case <-time.After(timeout): + // note: we don't return 408 Reques Timeout error because + // some browsers perform automatic retry behind the scenes + // which are hard to debug and unnecessary + return errors.New("batch transaction timeout") + case <-p.baseEvent.Request.Context().Done(): + return errors.New("batch request interrupted") + } + }) +} + +func (p *batchProcessor) process(activeApp core.App, batch []*core.InternalRequest, i int) error { + select { + case <-p.stopCh: + return nil + default: + if len(batch) == 0 { + return nil + } + + result, err := processInternalRequest( + activeApp, + p.baseEvent, + batch[0], + p.infoContext, + func() error { + if len(batch) == 1 { + return nil + } + + err := p.process(activeApp, batch[1:], i+1) + + // update the failed batch index (if not already) + if err != nil && p.failedIndex == 0 { + p.failedIndex = i + 1 + } + + return err + }, + ) + + if err != nil { + return err + } + + p.results = append(p.results, result) + + return nil + } +} + +func processInternalRequest( + activeApp core.App, + baseEvent *core.RequestEvent, + ir *core.InternalRequest, + infoContext string, + optNext func() error, +) (*BatchRequestResult, error) { + handle, params, ok := prepareInternalAction(activeApp, ir, optNext) + if !ok { + return nil, errors.New("unknown batch request action") + } + + // construct a new http.Request + // --------------------------------------------------------------- + buf, mw, err := multipartDataFromInternalRequest(ir) + if err != nil { + return nil, err + } + + r, err := http.NewRequest(strings.ToUpper(ir.Method), ir.URL, buf) + if err != nil { + return nil, err + } + + // cleanup multipart temp files + defer func() { + if r.MultipartForm != nil { + if err := r.MultipartForm.RemoveAll(); err != nil { + activeApp.Logger().Warn("failed to cleanup temp batch files", "error", err) + } + } + }() + + // load batch request path params + // --- + for k, v := range params { + r.SetPathValue(k, v) + } + + // clone original request + // --- + r.RequestURI = r.URL.RequestURI() + r.Proto = baseEvent.Request.Proto + r.ProtoMajor = baseEvent.Request.ProtoMajor + r.ProtoMinor = baseEvent.Request.ProtoMinor + r.Host = baseEvent.Request.Host + r.RemoteAddr = baseEvent.Request.RemoteAddr + r.TLS = baseEvent.Request.TLS + + if s := baseEvent.Request.TransferEncoding; s != nil { + s2 := make([]string, len(s)) + copy(s2, s) + r.TransferEncoding = s2 + } + + if baseEvent.Request.Trailer != nil { + r.Trailer = baseEvent.Request.Trailer.Clone() + } + + if baseEvent.Request.Header != nil { + r.Header = baseEvent.Request.Header.Clone() + } + + // apply batch request specific headers + // --- + for k, v := range ir.Headers { + r.Header.Set(k, v) + } + r.Header.Set("Content-Type", mw.FormDataContentType()) + + // construct a new RequestEvent + // --------------------------------------------------------------- + event := &core.RequestEvent{} + event.App = activeApp + event.Auth = baseEvent.Auth + event.SetAll(baseEvent.GetAll()) + + // load RequestInfo context + if infoContext == "" { + infoContext = core.RequestInfoContextDefault + } + event.Set(core.RequestEventKeyInfoContext, infoContext) + + // assign request + event.Request = r + event.Request.Body = &router.RereadableReadCloser{ReadCloser: r.Body} // enables multiple reads + + // assign response + rec := httptest.NewRecorder() + event.Response = &router.ResponseWriter{ResponseWriter: rec} // enables status and write tracking + + // execute + // --------------------------------------------------------------- + if err := handle(event); err != nil { + return nil, err + } + + result := rec.Result() + defer result.Body.Close() + + body, _ := types.ParseJSONRaw(rec.Body.Bytes()) + + return &BatchRequestResult{ + Status: result.StatusCode, + Body: body, + }, nil +} + +func multipartDataFromInternalRequest(ir *core.InternalRequest) (*bytes.Buffer, *multipart.Writer, error) { + buf := &bytes.Buffer{} + + mw := multipart.NewWriter(buf) + + regularFields := map[string]any{} + fileFields := map[string][]*filesystem.File{} + + // separate regular fields from files + // --- + for k, rawV := range ir.Body { + switch v := rawV.(type) { + case *filesystem.File: + fileFields[k] = append(fileFields[k], v) + case []*filesystem.File: + fileFields[k] = append(fileFields[k], v...) + default: + regularFields[k] = v + } + } + + // submit regularFields as @jsonPayload + // --- + rawBody, err := json.Marshal(regularFields) + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + + jsonPayload, err := mw.CreateFormField("@jsonPayload") + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + _, err = jsonPayload.Write(rawBody) + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + + // submit fileFields as multipart files + // --- + for key, files := range fileFields { + for _, file := range files { + part, err := mw.CreateFormFile(key, file.Name) + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + + fr, err := file.Reader.Open() + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + + _, err = io.Copy(part, fr) + if err != nil { + return nil, nil, errors.Join(err, fr.Close(), mw.Close()) + } + + err = fr.Close() + if err != nil { + return nil, nil, errors.Join(err, mw.Close()) + } + } + } + + return buf, mw, mw.Close() +} + +func extractPrefixedFiles(request *http.Request, prefixes ...string) (map[string][]*filesystem.File, error) { + if request.MultipartForm == nil { + if err := request.ParseMultipartForm(router.DefaultMaxMemory); err != nil { + return nil, err + } + } + + result := make(map[string][]*filesystem.File) + + for k, fhs := range request.MultipartForm.File { + for _, p := range prefixes { + if strings.HasPrefix(k, p) { + resultKey := strings.TrimPrefix(k, p) + + for _, fh := range fhs { + file, err := filesystem.NewFileFromMultipart(fh) + if err != nil { + return nil, err + } + + result[resultKey] = append(result[resultKey], file) + } + } + } + } + + return result, nil +} + +func prepareInternalAction(activeApp core.App, ir *core.InternalRequest, optNext func() error) (HandleFunc, map[string]string, bool) { + full := strings.ToUpper(ir.Method) + " " + ir.URL + + for re, actionFactory := range ValidBatchActions { + params, ok := findNamedMatches(re, full) + if ok { + return actionFactory(activeApp, ir, params, optNext), params, true + } + } + + return nil, nil, false +} + +func findNamedMatches(re *regexp.Regexp, str string) (map[string]string, bool) { + match := re.FindStringSubmatch(str) + if match == nil { + return nil, false + } + + result := map[string]string{} + + names := re.SubexpNames() + + for i, m := range match { + if names[i] != "" { + result[names[i]] = m + } + } + + return result, true +} + +// ------------------------------------------------------------------- + +var ( + _ router.SafeErrorItem = (*BatchResponseError)(nil) + _ router.SafeErrorResolver = (*BatchResponseError)(nil) +) + +type BatchResponseError struct { + err *router.ApiError + code string + message string +} + +func (e *BatchResponseError) Error() string { + return e.message +} + +func (e *BatchResponseError) Code() string { + return e.code +} + +func (e *BatchResponseError) Resolve(errData map[string]any) any { + errData["response"] = e.err + return errData +} + +func (e BatchResponseError) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "message": e.message, + "code": e.code, + "response": e.err, + }) +} diff --git a/apis/batch_test.go b/apis/batch_test.go new file mode 100644 index 00000000..544719a7 --- /dev/null +++ b/apis/batch_test.go @@ -0,0 +1,691 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/router" +) + +func TestBatchRequest(t *testing.T) { + t.Parallel() + + formData, mp, err := tests.MockMultipartData( + map[string]string{ + router.JSONPayloadKey: `{ + "requests":[ + {"method":"POST", "url":"/api/collections/demo3/records", "body": {"title": "batch1"}}, + {"method":"POST", "url":"/api/collections/demo3/records", "body": {"title": "batch2"}}, + {"method":"POST", "url":"/api/collections/demo3/records", "body": {"title": "batch3"}}, + {"method":"PATCH", "url":"/api/collections/demo3/records/lcl9d87w22ml6jy", "body": {"files-": "test_FLurQTgrY8.txt"}} + ] + }`, + }, + "requests.0.files", + "requests.0.files", + "requests.0.files", + "requests[2].files", + ) + if err != nil { + t.Fatal(err) + } + + scenarios := []tests.ApiScenario{ + { + Name: "disabled batch requets", + Method: http.MethodPost, + URL: "/api/batch", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().Batch.Enabled = false + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "max request limits reached", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"GET", "url":"/test1"}, + {"method":"GET", "url":"/test2"}, + {"method":"GET", "url":"/test3"} + ] + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().Batch.Enabled = true + app.Settings().Batch.MaxRequests = 2 + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"requests":{"code":"validation_length_too_long"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "trigger requests validations", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {}, + {"method":"GET", "url":"/valid"}, + {"method":"invalid", "url":"/valid"}, + {"method":"POST", "url":"` + strings.Repeat("a", 2001) + `"} + ] + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().Batch.Enabled = true + app.Settings().Batch.MaxRequests = 100 + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"requests":{`, + `"0":{"method":{"code":"validation_required"`, + `"2":{"method":{"code":"validation_in_invalid"`, + `"3":{"url":{"code":"validation_length_too_long"`, + }, + NotExpectedContent: []string{ + `"1":`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "unknown batch request action", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"GET", "url":"/api/health"} + ] + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"requests":{`, + `0":{"code":"batch_request_failed"`, + `"response":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + }, + }, + { + Name: "base 2 successful and 1 failed (public collection)", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch1"}}, + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch2"}}, + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": ""}} + ] + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"response":{`, + `"2":{"code":"batch_request_failed"`, + `"response":{"data":{"title":{"code":"validation_required"`, + }, + NotExpectedContent: []string{ + `"0":`, + `"1":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + "OnRecordCreateRequest": 3, + "OnModelCreate": 3, + "OnModelCreateExecute": 2, + "OnModelAfterCreateError": 3, + "OnModelValidate": 3, + "OnRecordCreate": 3, + "OnRecordCreateExecute": 2, + "OnRecordAfterCreateError": 3, + "OnRecordValidate": 3, + "OnRecordEnrich": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + records, err := app.FindRecordsByFilter("demo2", `title~"batch"`, "", 0, 0) + if err != nil { + t.Fatal(err) + } + + if len(records) != 0 { + t.Fatalf("Expected no batch records to be persisted, got %d", len(records)) + } + }, + }, + { + Name: "base 4 successful (public collection)", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch1"}}, + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch2"}}, + {"method":"PUT", "url":"/api/collections/demo2/records", "body": {"title": "batch3"}}, + {"method":"PUT", "url":"/api/collections/demo2/records?fields=*,id:excerpt(4,true)", "body": {"id":"achvryl401bhse3","title": "batch4"}} + ] + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"title":"batch1"`, + `"title":"batch2"`, + `"title":"batch3"`, + `"title":"batch4"`, + `"id":"achv..."`, + `"active":false`, + `"active":true`, + `"status":200`, + `"body":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + "OnModelValidate": 4, + "OnRecordValidate": 4, + "OnRecordEnrich": 4, + + "OnRecordCreateRequest": 3, + "OnModelCreate": 3, + "OnModelCreateExecute": 3, + "OnModelAfterCreateSuccess": 3, + "OnRecordCreate": 3, + "OnRecordCreateExecute": 3, + "OnRecordAfterCreateSuccess": 3, + + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + records, err := app.FindRecordsByFilter("demo2", `title~"batch"`, "", 0, 0) + if err != nil { + t.Fatal(err) + } + + if len(records) != 4 { + t.Fatalf("Expected %d batch records to be persisted, got %d", 3, len(records)) + } + }, + }, + { + Name: "mixed create/update/delete (rules failure)", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch_create"}}, + {"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3"}, + {"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}} + ] + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"requests":{`, + `"2":{"code":"batch_request_failed"`, + `"response":{`, + }, + NotExpectedContent: []string{ + // only demo3 requires authentication + `"0":`, + `"1":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateError": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteError": 1, + "OnModelValidate": 1, + "OnRecordCreateRequest": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateError": 1, + "OnRecordDeleteRequest": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteError": 1, + "OnRecordEnrich": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindFirstRecordByFilter("demo2", `title="batch_create"`) + if err == nil { + t.Fatal("Expected record to not be created") + } + + _, err = app.FindFirstRecordByFilter("demo3", `title="batch_update"`) + if err == nil { + t.Fatal("Expected record to not be updated") + } + + _, err = app.FindRecordById("demo2", "achvryl401bhse3") + if err != nil { + t.Fatal("Expected record to not be deleted") + } + }, + }, + { + Name: "mixed create/update/delete (rules success)", + Method: http.MethodPost, + URL: "/api/batch", + Headers: map[string]string{ + // test@example.com, clients + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch_create"}}, + {"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3"}, + {"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}} + ] + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"title":"batch_create"`, + `"title":"batch_update"`, + `"status":200`, + `"status":204`, + `"body":{`, + `"body":null`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 2, + // --- + "OnRecordCreateRequest": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordDeleteRequest": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + "OnRecordUpdateRequest": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 2, + "OnRecordEnrich": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindFirstRecordByFilter("demo2", `title="batch_create"`) + if err != nil { + t.Fatal(err) + } + + _, err = app.FindFirstRecordByFilter("demo3", `title="batch_update"`) + if err != nil { + t.Fatal(err) + } + + _, err = app.FindRecordById("demo2", "achvryl401bhse3") + if err == nil { + t.Fatal("Expected record to be deleted") + } + }, + }, + { + Name: "mixed create/update/delete (superuser auth)", + Method: http.MethodPost, + URL: "/api/batch", + Headers: map[string]string{ + // test@example.com, superusers + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch_create"}}, + {"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3"}, + {"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}} + ] + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"title":"batch_create"`, + `"title":"batch_update"`, + `"status":200`, + `"status":204`, + `"body":{`, + `"body":null`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 2, + // --- + "OnRecordCreateRequest": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordDeleteRequest": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + "OnRecordUpdateRequest": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 2, + "OnRecordEnrich": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindFirstRecordByFilter("demo2", `title="batch_create"`) + if err != nil { + t.Fatal(err) + } + + _, err = app.FindFirstRecordByFilter("demo3", `title="batch_update"`) + if err != nil { + t.Fatal(err) + } + + _, err = app.FindRecordById("demo2", "achvryl401bhse3") + if err == nil { + t.Fatal("Expected record to be deleted") + } + }, + }, + { + Name: "cascade delete/update", + Method: http.MethodPost, + URL: "/api/batch", + Headers: map[string]string{ + // test@example.com, superusers + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{ + "requests": [ + {"method":"DELETE", "url":"/api/collections/demo3/records/1tmknxy2868d869"}, + {"method":"DELETE", "url":"/api/collections/demo3/records/mk5fmymtx4wsprk"} + ] + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"status":204`, + `"body":null`, + }, + NotExpectedContent: []string{ + `"status":200`, + `"body":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + // --- + "OnModelDelete": 3, // 2 batch + 1 cascade delete + "OnModelDeleteExecute": 3, + "OnModelAfterDeleteSuccess": 3, + "OnModelUpdate": 5, // 5 cascade update + "OnModelUpdateExecute": 5, + "OnModelAfterUpdateSuccess": 5, + // --- + "OnRecordDeleteRequest": 2, + "OnRecordDelete": 3, + "OnRecordDeleteExecute": 3, + "OnRecordAfterDeleteSuccess": 3, + "OnRecordUpdate": 5, + "OnRecordUpdateExecute": 5, + "OnRecordAfterUpdateSuccess": 5, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + ids := []string{ + "1tmknxy2868d869", + "mk5fmymtx4wsprk", + "qzaqccwrmva4o1n", + } + + for _, id := range ids { + _, err := app.FindRecordById("demo2", id) + if err == nil { + t.Fatalf("Expected record %q to be deleted", id) + } + } + }, + }, + { + Name: "transaction timeout", + Method: http.MethodPost, + URL: "/api/batch", + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch1"}}, + {"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch2"}} + ] + }`), + Headers: map[string]string{ + // test@example.com, superusers + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().Batch.Timeout = 1 + app.OnRecordCreateRequest("demo2").BindFunc(func(e *core.RecordRequestEvent) error { + time.Sleep(600 * time.Millisecond) // < 1s so that the first request can succeed + return e.Next() + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{}`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + "OnRecordCreateRequest": 2, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateError": 1, + "OnRecordEnrich": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + records, err := app.FindRecordsByFilter("demo2", `title~"batch"`, "", 0, 0) + if err != nil { + t.Fatal(err) + } + + if len(records) != 0 { + t.Fatalf("Expected %d batch records to be persisted, got %d", 0, len(records)) + } + }, + }, + { + Name: "multipart/form-data + file upload", + Method: http.MethodPost, + URL: "/api/batch", + Body: formData, + Headers: map[string]string{ + // test@example.com, clients + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + "Content-Type": mp.FormDataContentType(), + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"title":"batch1"`, + `"title":"batch2"`, + `"title":"batch3"`, + `"id":"lcl9d87w22ml6jy"`, + `"files":["300_UhLKX91HVb.png"]`, + `"tmpfile_`, + `"status":200`, + `"body":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + // --- + "OnModelCreate": 3, + "OnModelCreateExecute": 3, + "OnModelAfterCreateSuccess": 3, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 4, + // --- + "OnRecordCreateRequest": 3, + "OnRecordUpdateRequest": 1, + "OnRecordCreate": 3, + "OnRecordCreateExecute": 3, + "OnRecordAfterCreateSuccess": 3, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 4, + "OnRecordEnrich": 4, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + batch1, err := app.FindFirstRecordByFilter("demo3", `title="batch1"`) + if err != nil { + t.Fatalf("missing batch1: %v", err) + } + batch1Files := batch1.GetStringSlice("files") + if len(batch1Files) != 3 { + t.Fatalf("Expected %d batch1 file(s), got %d", 3, len(batch1Files)) + } + + batch2, err := app.FindFirstRecordByFilter("demo3", `title="batch2"`) + if err != nil { + t.Fatalf("missing batch2: %v", err) + } + batch2Files := batch2.GetStringSlice("files") + if len(batch2Files) != 0 { + t.Fatalf("Expected %d batch2 file(s), got %d", 0, len(batch2Files)) + } + + batch3, err := app.FindFirstRecordByFilter("demo3", `title="batch3"`) + if err != nil { + t.Fatalf("missing batch3: %v", err) + } + batch3Files := batch3.GetStringSlice("files") + if len(batch3Files) != 1 { + t.Fatalf("Expected %d batch3 file(s), got %d", 1, len(batch3Files)) + } + + batch4, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy") + if err != nil { + t.Fatalf("missing batch4: %v", err) + } + batch4Files := batch4.GetStringSlice("files") + if len(batch4Files) != 1 { + t.Fatalf("Expected %d batch4 file(s), got %d", 1, len(batch4Files)) + } + }, + }, + { + Name: "create/update with expand query params", + Method: http.MethodPost, + URL: "/api/batch", + Headers: map[string]string{ + // test@example.com, superusers + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo5/records?expand=rel_one", "body": {"total": 9, "rel_one":"qzaqccwrmva4o1n"}}, + {"method":"PATCH", "url":"/api/collections/demo5/records/qjeql998mtp1azp?expand=rel_many", "body": {"total": 10}} + ] + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"body":{`, + `"id":"qjeql998mtp1azp"`, + `"id":"qzaqccwrmva4o1n"`, + `"id":"i9naidtvr6qsgb4"`, + `"expand":{"rel_one"`, + `"expand":{"rel_many"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnBatchRequest": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 2, + // --- + "OnRecordCreateRequest": 1, + "OnRecordUpdateRequest": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 2, + "OnRecordEnrich": 5, + }, + }, + { + Name: "check body limit middleware", + Method: http.MethodPost, + URL: "/api/batch", + Headers: map[string]string{ + // test@example.com, superusers + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{ + "requests": [ + {"method":"POST", "url":"/api/collections/demo5/records?expand=rel_one", "body": {"total": 9, "rel_one":"qzaqccwrmva4o1n"}}, + {"method":"PATCH", "url":"/api/collections/demo5/records/qjeql998mtp1azp?expand=rel_many", "body": {"total": 10}} + ] + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().Batch.MaxBodySize = 10 + }, + ExpectedStatus: 413, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/collection.go b/apis/collection.go index 05fc154b..e49da210 100644 --- a/apis/collection.go +++ b/apis/collection.go @@ -1,210 +1,186 @@ package apis import ( + "errors" "net/http" + "strings" - "github.com/labstack/echo/v5" + validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/search" ) // bindCollectionApi registers the collection api endpoints and the corresponding handlers. -func bindCollectionApi(app core.App, rg *echo.Group) { - api := collectionApi{app: app} - - subGroup := rg.Group("/collections", ActivityLogger(app), RequireAdminAuth()) - subGroup.GET("", api.list) - subGroup.POST("", api.create) - subGroup.GET("/:collection", api.view) - subGroup.PATCH("/:collection", api.update) - subGroup.DELETE("/:collection", api.delete) - subGroup.PUT("/import", api.bulkImport) +func bindCollectionApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + subGroup := rg.Group("/collections").Bind(RequireSuperuserAuth()) + subGroup.GET("", collectionsList) + subGroup.POST("", collectionCreate) + subGroup.GET("/{collection}", collectionView) + subGroup.PATCH("/{collection}", collectionUpdate) + subGroup.DELETE("/{collection}", collectionDelete) + subGroup.DELETE("/{collection}/truncate", collectionTruncate) + subGroup.PUT("/import", collectionsImport) + subGroup.GET("/meta/scaffolds", collectionScaffolds) } -type collectionApi struct { - app core.App -} - -func (api *collectionApi) list(c echo.Context) error { +func collectionsList(e *core.RequestEvent) error { fieldResolver := search.NewSimpleFieldResolver( "id", "created", "updated", "name", "system", "type", ) - collections := []*models.Collection{} + collections := []*core.Collection{} result, err := search.NewProvider(fieldResolver). - Query(api.app.Dao().CollectionQuery()). - ParseAndExec(c.QueryParams().Encode(), &collections) + Query(e.App.CollectionQuery()). + ParseAndExec(e.Request.URL.Query().Encode(), &collections) if err != nil { - return NewBadRequestError("", err) + return e.BadRequestError("", err) } - event := new(core.CollectionsListEvent) - event.HttpContext = c + event := new(core.CollectionsListRequestEvent) + event.RequestEvent = e event.Collections = collections event.Result = result - return api.app.OnCollectionsListRequest().Trigger(event, func(e *core.CollectionsListEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Result) + return event.App.OnCollectionsListRequest().Trigger(event, func(e *core.CollectionsListRequestEvent) error { + return e.JSON(http.StatusOK, e.Result) }) } -func (api *collectionApi) view(c echo.Context) error { - collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) +func collectionView(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) if err != nil || collection == nil { - return NewNotFoundError("", err) + return e.NotFoundError("", err) } - event := new(core.CollectionViewEvent) - event.HttpContext = c + event := new(core.CollectionRequestEvent) + event.RequestEvent = e event.Collection = collection - return api.app.OnCollectionViewRequest().Trigger(event, func(e *core.CollectionViewEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Collection) + return e.App.OnCollectionViewRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + return e.JSON(http.StatusOK, e.Collection) }) } -func (api *collectionApi) create(c echo.Context) error { - collection := &models.Collection{} - - form := forms.NewCollectionUpsert(api.app, collection) - - // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) +func collectionCreate(e *core.RequestEvent) error { + // populate the minimal required factory collection data (if any) + factoryExtract := struct { + Type string `form:"type" json:"type"` + Name string `form:"name" json:"name"` + }{} + if err := e.BindBody(&factoryExtract); err != nil { + return e.BadRequestError("Failed to load the collection type data due to invalid formatting.", err) } - event := new(core.CollectionCreateEvent) - event.HttpContext = c + // create scaffold + collection := core.NewCollection(factoryExtract.Type, factoryExtract.Name) + + // merge the scaffold with the submitted request data + if err := e.BindBody(collection); err != nil { + return e.BadRequestError("Failed to load the submitted data due to invalid formatting.", err) + } + + event := new(core.CollectionRequestEvent) + event.RequestEvent = e event.Collection = collection - // create the collection - return form.Submit(func(next forms.InterceptorNextFunc[*models.Collection]) forms.InterceptorNextFunc[*models.Collection] { - return func(m *models.Collection) error { - event.Collection = m - - return api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error { - if err := next(e.Collection); err != nil { - return NewBadRequestError("Failed to create the collection.", err) - } - - return api.app.OnCollectionAfterCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Collection) - }) - }) - } - }) -} - -func (api *collectionApi) update(c echo.Context) error { - collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) - if err != nil || collection == nil { - return NewNotFoundError("", err) - } - - form := forms.NewCollectionUpsert(api.app, collection) - - // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) - } - - event := new(core.CollectionUpdateEvent) - event.HttpContext = c - event.Collection = collection - - // update the collection - return form.Submit(func(next forms.InterceptorNextFunc[*models.Collection]) forms.InterceptorNextFunc[*models.Collection] { - return func(m *models.Collection) error { - event.Collection = m - - return api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error { - if err := next(e.Collection); err != nil { - return NewBadRequestError("Failed to update the collection.", err) - } - - return api.app.OnCollectionAfterUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Collection) - }) - }) - } - }) -} - -func (api *collectionApi) delete(c echo.Context) error { - collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) - if err != nil || collection == nil { - return NewNotFoundError("", err) - } - - event := new(core.CollectionDeleteEvent) - event.HttpContext = c - event.Collection = collection - - return api.app.OnCollectionBeforeDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error { - if err := api.app.Dao().DeleteCollection(e.Collection); err != nil { - return NewBadRequestError("Failed to delete collection due to existing dependency.", err) - } - - return api.app.OnCollectionAfterDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error { - if e.HttpContext.Response().Committed { - return nil + return e.App.OnCollectionCreateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + if err := e.App.Save(e.Collection); err != nil { + // validation failure + var validationErrors validation.Errors + if errors.As(err, &validationErrors) { + return e.BadRequestError("Failed to create collection.", validationErrors) } - return e.HttpContext.NoContent(http.StatusNoContent) - }) + // other generic db error + return e.BadRequestError("Failed to create collection. Raw error: \n"+err.Error(), nil) + } + + return e.JSON(http.StatusOK, e.Collection) }) } -func (api *collectionApi) bulkImport(c echo.Context) error { - form := forms.NewCollectionsImport(api.app) - - // load request data - if err := c.Bind(form); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) +func collectionUpdate(e *core.RequestEvent) error { + collection, err := e.App.FindCollectionByNameOrId(e.Request.PathValue("collection")) + if err != nil || collection == nil { + return e.NotFoundError("", err) } - event := new(core.CollectionsImportEvent) - event.HttpContext = c - event.Collections = form.Collections + if err := e.BindBody(collection); err != nil { + return e.BadRequestError("Failed to load the submitted data due to invalid formatting.", err) + } - // import collections - return form.Submit(func(next forms.InterceptorNextFunc[[]*models.Collection]) forms.InterceptorNextFunc[[]*models.Collection] { - return func(imports []*models.Collection) error { - event.Collections = imports + event := new(core.CollectionRequestEvent) + event.RequestEvent = e + event.Collection = collection - return api.app.OnCollectionsBeforeImportRequest().Trigger(event, func(e *core.CollectionsImportEvent) error { - if err := next(e.Collections); err != nil { - return NewBadRequestError("Failed to import the submitted collections.", err) - } + return event.App.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + if err := e.App.Save(e.Collection); err != nil { + // validation failure + var validationErrors validation.Errors + if errors.As(err, &validationErrors) { + return e.BadRequestError("Failed to update collection.", validationErrors) + } - return api.app.OnCollectionsAfterImportRequest().Trigger(event, func(e *core.CollectionsImportEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) + // other generic db error + return e.BadRequestError("Failed to update collection. Raw error: \n"+err.Error(), nil) } + + return e.JSON(http.StatusOK, e.Collection) + }) +} + +func collectionDelete(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) + if err != nil || collection == nil { + return e.NotFoundError("", err) + } + + event := new(core.CollectionRequestEvent) + event.RequestEvent = e + event.Collection = collection + + return e.App.OnCollectionDeleteRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + if err := e.App.Delete(e.Collection); err != nil { + msg := "Failed to delete collection" + + // check fo references + refs, _ := e.App.FindCollectionReferences(e.Collection, e.Collection.Id) + if len(refs) > 0 { + names := make([]string, 0, len(refs)) + for ref := range refs { + names = append(names, ref.Name) + } + msg += " probably due to existing reference in " + strings.Join(names, ", ") + } + + return e.BadRequestError(msg, err) + } + + return e.NoContent(http.StatusNoContent) + }) +} + +func collectionTruncate(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) + if err != nil || collection == nil { + return e.NotFoundError("", err) + } + + err = e.App.TruncateCollection(collection) + if err != nil { + return e.BadRequestError("Failed to truncate collection (most likely due to required cascade delete record references).", err) + } + + return e.NoContent(http.StatusNoContent) +} + +func collectionScaffolds(e *core.RequestEvent) error { + return e.JSON(http.StatusOK, map[string]*core.Collection{ + core.CollectionTypeBase: core.NewBaseCollection(""), + core.CollectionTypeAuth: core.NewAuthCollection(""), + core.CollectionTypeView: core.NewViewCollection(""), }) } diff --git a/apis/collection_import.go b/apis/collection_import.go new file mode 100644 index 00000000..040270c1 --- /dev/null +++ b/apis/collection_import.go @@ -0,0 +1,60 @@ +package apis + +import ( + "errors" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" +) + +func collectionsImport(e *core.RequestEvent) error { + form := new(collectionsImportForm) + + err := e.BindBody(form) + if err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + + err = form.validate() + if err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + event := new(core.CollectionsImportRequestEvent) + event.RequestEvent = e + event.CollectionsData = form.Collections + event.DeleteMissing = form.DeleteMissing + + return event.App.OnCollectionsImportRequest().Trigger(event, func(e *core.CollectionsImportRequestEvent) error { + importErr := e.App.ImportCollections(e.CollectionsData, form.DeleteMissing) + if importErr == nil { + return e.NoContent(http.StatusNoContent) + } + + // validation failure + var validationErrors validation.Errors + if errors.As(err, &validationErrors) { + return e.BadRequestError("Failed to import collections.", validationErrors) + } + + // generic/db failure + return e.BadRequestError("Failed to import collections.", validation.Errors{"collections": validation.NewError( + "validation_collections_import_failure", + "Failed to import the collections configuration. Raw error:\n"+importErr.Error(), + )}) + }) +} + +// ------------------------------------------------------------------- + +type collectionsImportForm struct { + Collections []map[string]any `form:"collections" json:"collections"` + DeleteMissing bool `form:"deleteMissing" json:"deleteMissing"` +} + +func (form *collectionsImportForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Collections, validation.Required), + ) +} diff --git a/apis/collection_import_test.go b/apis/collection_import_test.go new file mode 100644 index 00000000..1216345a --- /dev/null +++ b/apis/collection_import_test.go @@ -0,0 +1,257 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestCollectionsImport(t *testing.T) { + t.Parallel() + + totalCollections := 16 + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPut, + URL: "/api/collections/import", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as regular user", + Method: http.MethodPut, + URL: "/api/collections/import", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as superuser + empty collections", + Method: http.MethodPut, + URL: "/api/collections/import", + Body: strings.NewReader(`{"collections":[]}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"collections":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + collections := []*core.Collection{} + if err := app.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + expected := totalCollections + if len(collections) != expected { + t.Fatalf("Expected %d collections, got %d", expected, len(collections)) + } + }, + }, + { + Name: "authorized as superuser + collections validator failure", + Method: http.MethodPut, + URL: "/api/collections/import", + Body: strings.NewReader(`{ + "collections":[ + {"name": "import1"}, + { + "name": "import2", + "fields": [ + { + "id": "koih1lqx", + "name": "expand", + "type": "text" + } + ] + } + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"collections":{"code":"validation_collections_import_failure"`, + `import2`, + `fields`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionsImportRequest": 1, + "OnCollectionCreate": 2, + "OnCollectionCreateExecute": 2, + "OnCollectionAfterCreateError": 2, + "OnModelCreate": 2, + "OnModelCreateExecute": 2, + "OnModelAfterCreateError": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + collections := []*core.Collection{} + if err := app.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + expected := totalCollections + if len(collections) != expected { + t.Fatalf("Expected %d collections, got %d", expected, len(collections)) + } + }, + }, + { + Name: "authorized as superuser + successful collections create", + Method: http.MethodPut, + URL: "/api/collections/import", + Body: strings.NewReader(`{ + "collections":[ + { + "name": "import1", + "fields": [ + { + "id": "koih1lqx", + "name": "test", + "type": "text" + } + ] + }, + { + "name": "import2", + "fields": [ + { + "id": "koih1lqx", + "name": "test", + "type": "text" + } + ], + "indexes": [ + "create index idx_test on import2 (test)" + ] + }, + { + "name": "auth_without_fields", + "type": "auth" + } + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionsImportRequest": 1, + "OnCollectionCreate": 3, + "OnCollectionCreateExecute": 3, + "OnCollectionAfterCreateSuccess": 3, + "OnModelCreate": 3, + "OnModelCreateExecute": 3, + "OnModelAfterCreateSuccess": 3, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + collections := []*core.Collection{} + if err := app.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + + expected := totalCollections + 3 + if len(collections) != expected { + t.Fatalf("Expected %d collections, got %d", expected, len(collections)) + } + + indexes, err := app.TableIndexes("import2") + if err != nil || indexes["idx_test"] == "" { + t.Fatalf("Missing index %s (%v)", "idx_test", err) + } + }, + }, + { + Name: "authorized as superuser + create/update/delete", + Method: http.MethodPut, + URL: "/api/collections/import", + Body: strings.NewReader(`{ + "deleteMissing": true, + "collections":[ + {"name": "test123"}, + { + "id":"wsmn24bux7wo113", + "name":"demo1", + "fields":[ + { + "id":"_2hlxbmp", + "name":"title", + "type":"text", + "required":true + } + ], + "indexes": [] + } + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionsImportRequest": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + // --- + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnCollectionUpdate": 1, + "OnCollectionUpdateExecute": 1, + "OnCollectionAfterUpdateSuccess": 1, + // --- + "OnModelDelete": 14, + "OnModelAfterDeleteSuccess": 14, + "OnModelDeleteExecute": 14, + "OnCollectionDelete": 9, + "OnCollectionDeleteExecute": 9, + "OnCollectionAfterDeleteSuccess": 9, + "OnRecordAfterDeleteSuccess": 5, + "OnRecordDelete": 5, + "OnRecordDeleteExecute": 5, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + collections := []*core.Collection{} + if err := app.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + + systemCollections := 0 + for _, c := range collections { + if c.System { + systemCollections++ + } + } + + expected := systemCollections + 2 + if len(collections) != expected { + t.Fatalf("Expected %d collections, got %d", expected, len(collections)) + } + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/collection_test.go b/apis/collection_test.go index 9f66ddc5..d1224a73 100644 --- a/apis/collection_test.go +++ b/apis/collection_test.go @@ -9,9 +9,7 @@ import ( "testing" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" ) @@ -23,85 +21,97 @@ func TestCollectionsList(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/collections", + URL: "/api/collections", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/collections", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin", + Name: "authorized as superuser", Method: http.MethodGet, - Url: "/api/collections", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, `"perPage":30`, - `"totalItems":11`, + `"totalItems":16`, `"items":[{`, - `"id":"_pb_users_auth_"`, - `"id":"v851q4r790rhknl"`, - `"id":"kpv709sk2lqbqk8"`, - `"id":"wsmn24bux7wo113"`, - `"id":"sz5l5z67tg7gku0"`, - `"id":"wzlqyes4orhoygb"`, - `"id":"4d1blo5cuycfaca"`, - `"id":"9n89pl5vkct6330"`, - `"id":"ib3m2700k5hlsjz"`, + `"name":"` + core.CollectionNameSuperusers + `"`, + `"name":"` + core.CollectionNameAuthOrigins + `"`, + `"name":"` + core.CollectionNameExternalAuths + `"`, + `"name":"` + core.CollectionNameMFAs + `"`, + `"name":"` + core.CollectionNameOTPs + `"`, + `"name":"users"`, + `"name":"nologin"`, + `"name":"clients"`, + `"name":"demo1"`, + `"name":"demo2"`, + `"name":"demo3"`, + `"name":"demo4"`, + `"name":"demo5"`, + `"name":"numeric_id_view"`, + `"name":"view1"`, + `"name":"view2"`, `"type":"auth"`, `"type":"base"`, + `"type":"view"`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnCollectionsListRequest": 1, }, }, { - Name: "authorized as admin + paging and sorting", + Name: "authorized as superuser + paging and sorting", Method: http.MethodGet, - Url: "/api/collections?page=2&perPage=2&sort=-created", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections?page=2&perPage=2&sort=-created", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"page":2`, `"perPage":2`, - `"totalItems":11`, + `"totalItems":16`, `"items":[{`, - `"id":"v9gwnfh02gjq1q0"`, - `"id":"9n89pl5vkct6330"`, + `"name":"` + core.CollectionNameMFAs + `"`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnCollectionsListRequest": 1, }, }, { - Name: "authorized as admin + invalid filter", + Name: "authorized as superuser + invalid filter", Method: http.MethodGet, - Url: "/api/collections?filter=invalidfield~'demo2'", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections?filter=invalidfield~'demo2'", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + valid filter", + Name: "authorized as superuser + valid filter", Method: http.MethodGet, - Url: "/api/collections?filter=name~'demo'", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections?filter=name~'demo'", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -109,13 +119,14 @@ func TestCollectionsList(t *testing.T) { `"perPage":30`, `"totalItems":5`, `"items":[{`, - `"id":"wsmn24bux7wo113"`, - `"id":"sz5l5z67tg7gku0"`, - `"id":"wzlqyes4orhoygb"`, - `"id":"4d1blo5cuycfaca"`, - `"id":"9n89pl5vkct6330"`, + `"name":"demo1"`, + `"name":"demo2"`, + `"name":"demo3"`, + `"name":"demo4"`, + `"name":"demo5"`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnCollectionsListRequest": 1, }, }, @@ -133,36 +144,39 @@ func TestCollectionView(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/collections/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections/demo1", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + nonexisting collection identifier", + Name: "authorized as superuser + nonexisting collection identifier", Method: http.MethodGet, - Url: "/api/collections/missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/missing", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + using the collection name", + Name: "authorized as superuser + using the collection name", Method: http.MethodGet, - Url: "/api/collections/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -170,15 +184,16 @@ func TestCollectionView(t *testing.T) { `"name":"demo1"`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnCollectionViewRequest": 1, }, }, { - Name: "authorized as admin + using the collection id", + Name: "authorized as superuser + using the collection id", Method: http.MethodGet, - Url: "/api/collections/wsmn24bux7wo113", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/wsmn24bux7wo113", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -186,6 +201,7 @@ func TestCollectionView(t *testing.T) { `"name":"demo1"`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnCollectionViewRequest": 1, }, }, @@ -212,128 +228,155 @@ func TestCollectionDelete(t *testing.T) { { Name: "unauthorized", Method: http.MethodDelete, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", + Name: "authorized as regular user", Method: http.MethodDelete, - Url: "/api/collections/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections/demo1", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + nonexisting collection identifier", + Name: "authorized as superuser + nonexisting collection identifier", Method: http.MethodDelete, - Url: "/api/collections/missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/missing", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + using the collection name", + Name: "authorized as superuser + using the collection name", Method: http.MethodDelete, - Url: "/api/collections/demo5", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo5", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, Delay: 100 * time.Millisecond, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnCollectionBeforeDeleteRequest": 1, - "OnCollectionAfterDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, + "OnCollectionDelete": 1, + "OnCollectionDeleteExecute": 1, + "OnCollectionAfterDeleteSuccess": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureDeletedFiles(app, "9n89pl5vkct6330") }, }, { - Name: "authorized as admin + using the collection id", + Name: "authorized as superuser + using the collection id", Method: http.MethodDelete, - Url: "/api/collections/9n89pl5vkct6330", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/9n89pl5vkct6330", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, Delay: 100 * time.Millisecond, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnCollectionBeforeDeleteRequest": 1, - "OnCollectionAfterDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, + "OnCollectionDelete": 1, + "OnCollectionDeleteExecute": 1, + "OnCollectionAfterDeleteSuccess": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureDeletedFiles(app, "9n89pl5vkct6330") }, }, { - Name: "authorized as admin + trying to delete a system collection", + Name: "authorized as superuser + trying to delete a system collection", Method: http.MethodDelete, - Url: "/api/collections/nologin", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/" + core.CollectionNameMFAs, + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnCollectionBeforeDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, + "OnCollectionDelete": 1, + "OnCollectionDeleteExecute": 1, + "OnCollectionAfterDeleteError": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteError": 1, }, }, { - Name: "authorized as admin + trying to delete a referenced collection", + Name: "authorized as superuser + trying to delete a referenced collection", Method: http.MethodDelete, - Url: "/api/collections/demo2", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo2", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnCollectionBeforeDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, + "OnCollectionDelete": 1, + "OnCollectionDeleteExecute": 1, + "OnCollectionAfterDeleteError": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteError": 1, }, }, { - Name: "authorized as admin + deleting a view", + Name: "authorized as superuser + deleting a view", Method: http.MethodDelete, - Url: "/api/collections/view2", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/view2", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnCollectionBeforeDeleteRequest": 1, - "OnCollectionAfterDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, + "OnCollectionDelete": 1, + "OnCollectionDeleteExecute": 1, + "OnCollectionAfterDeleteSuccess": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, }, }, { - Name: "OnCollectionAfterDeleteRequest error response", + Name: "OnCollectionAfterDeleteSuccessRequest error response", Method: http.MethodDelete, - Url: "/api/collections/view2", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/view2", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnCollectionAfterDeleteRequest().Add(func(e *core.CollectionDeleteEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnCollectionDeleteRequest().BindFunc(func(e *core.CollectionRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnCollectionBeforeDeleteRequest": 1, - "OnCollectionAfterDeleteRequest": 1, + "*": 0, + "OnCollectionDeleteRequest": 1, }, }, } @@ -350,57 +393,83 @@ func TestCollectionCreate(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/collections", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections", + Body: strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","name":"test"}]}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + empty data", + Name: "authorized as superuser + empty data", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(``), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"name":{"code":"validation_required"`, - `"schema":{"code":"validation_required"`, + }, + NotExpectedContent: []string{ + `"fields":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { - Name: "authorized as admin + invalid data (eg. existing name)", + Name: "authorized as superuser + invalid data (eg. existing name)", Method: http.MethodPost, - Url: "/api/collections", - Body: strings.NewReader(`{"name":"demo1","type":"base","schema":[{"type":"text","name":""}]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections", + Body: strings.NewReader(`{"name":"demo1","type":"base","fields":[{"type":"text","name":""}]}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, + `"fields":{`, `"name":{"code":"validation_collection_name_exists"`, - `"schema":{"0":{"name":{"code":"validation_required"`, + `"name":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { - Name: "authorized as admin + valid data", + Name: "authorized as superuser + valid data", Method: http.MethodPost, - Url: "/api/collections", - Body: strings.NewReader(`{"name":"new","type":"base","schema":[{"type":"text","id":"12345789","name":"test"}]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections", + Body: strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","id":"12345789","name":"test"}]}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -408,23 +477,37 @@ func TestCollectionCreate(t *testing.T) { `"name":"new"`, `"type":"base"`, `"system":false`, - `"schema":[{"system":false,"id":"12345789","name":"test","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`, - `"options":{}`, + // ensures that id field was prepended + `"fields":[{"autogeneratePattern":"[a-z0-9]{15}","hidden":false,"id":"text3208210256","max":15,"min":15,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"autogeneratePattern":"","hidden":false,"id":"12345789","max":0,"min":0,"name":"test","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"}]`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, }, }, { - Name: "creating auth collection without specified options", + Name: "creating auth collection (default settings merge test)", Method: http.MethodPost, - Url: "/api/collections", - Body: strings.NewReader(`{"name":"new","type":"auth","schema":[{"type":"text","id":"12345789","name":"test"}]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections", + Body: strings.NewReader(`{ + "name":"new", + "type":"auth", + "emailChangeToken":{"duration":123}, + "fields":[ + {"type":"text","id":"12345789","name":"test"}, + {"type":"text","name":"tokenKey","required":false,"min":10} + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -432,62 +515,44 @@ func TestCollectionCreate(t *testing.T) { `"name":"new"`, `"type":"auth"`, `"system":false`, - `"schema":[{"system":false,"id":"12345789","name":"test","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`, - `"options":{"allowEmailAuth":false,"allowOAuth2Auth":false,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":0,"onlyEmailDomains":null,"onlyVerified":false,"requireEmail":false}`, + `"passwordAuth":{"enabled":true,"identityFields":["email"]}`, + `"authRule":""`, + `"manageRule":null`, + `"name":"test"`, + `"name":"id"`, + `"name":"tokenKey"`, + `"name":"password"`, + `"name":"email"`, + `"name":"emailVisibility"`, + `"name":"verified"`, + `"duration":123`, + // should overwrite the user required option but keep the min value + `{"autogeneratePattern":"","hidden":true,"id":"text2504183744","max":0,"min":10,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"}`, + }, + NotExpectedContent: []string{ + `"secret":"`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, - }, - }, - { - Name: "trying to create auth collection with reserved auth fields", - Method: http.MethodPost, - Url: "/api/collections", - Body: strings.NewReader(`{ - "name":"new", - "type":"auth", - "schema":[ - {"type":"text","name":"email"}, - {"type":"text","name":"username"}, - {"type":"text","name":"verified"}, - {"type":"text","name":"emailVisibility"}, - {"type":"text","name":"lastResetSentAt"}, - {"type":"text","name":"lastVerificationSentAt"}, - {"type":"text","name":"tokenKey"}, - {"type":"text","name":"passwordHash"}, - {"type":"text","name":"password"}, - {"type":"text","name":"passwordConfirm"}, - {"type":"text","name":"oldPassword"} - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{"schema":{`, - `"0":{"name":{"code":"validation_reserved_auth_field_name"`, - `"1":{"name":{"code":"validation_reserved_auth_field_name"`, - `"2":{"name":{"code":"validation_reserved_auth_field_name"`, - `"3":{"name":{"code":"validation_reserved_auth_field_name"`, - `"4":{"name":{"code":"validation_reserved_auth_field_name"`, - `"5":{"name":{"code":"validation_reserved_auth_field_name"`, - `"6":{"name":{"code":"validation_reserved_auth_field_name"`, - `"7":{"name":{"code":"validation_reserved_auth_field_name"`, - `"8":{"name":{"code":"validation_reserved_auth_field_name"`, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, }, }, { Name: "creating base collection with reserved auth fields", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"base", - "schema":[ + "fields":[ {"type":"text","name":"email"}, {"type":"text","name":"username"}, {"type":"text","name":"verified"}, @@ -501,91 +566,113 @@ func TestCollectionCreate(t *testing.T) { {"type":"text","name":"oldPassword"} ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"name":"new"`, `"type":"base"`, - `"schema":[{`, + `"fields":[{`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, }, }, { - Name: "trying to create base collection with reserved base fields", + Name: "trying to create base collection with reserved system fields", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"base", - "schema":[ + "fields":[ {"type":"text","name":"id"}, - {"type":"text","name":"created"}, - {"type":"text","name":"updated"}, {"type":"text","name":"expand"}, {"type":"text","name":"collectionId"}, {"type":"text","name":"collectionName"} ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ - `"data":{"schema":{`, - `"0":{"name":{"code":"validation_not_in_invalid`, + `"data":{"fields":{`, `"1":{"name":{"code":"validation_not_in_invalid`, `"2":{"name":{"code":"validation_not_in_invalid`, `"3":{"name":{"code":"validation_not_in_invalid`, - `"4":{"name":{"code":"validation_not_in_invalid`, - `"5":{"name":{"code":"validation_not_in_invalid`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { - Name: "trying to create auth collection with invalid options", + Name: "trying to create auth collection with reserved auth fields", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"auth", - "schema":[{"type":"text","id":"12345789","name":"test"}], - "options":{"allowUsernameAuth": true} + "fields":[ + {"type":"text","name":"oldPassword"}, + {"type":"text","name":"passwordConfirm"} + ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ - `"data":{`, - `"options":{"minPasswordLength":{"code":"validation_required"`, + `"data":{"fields":{`, + `"1":{"name":{"code":"validation_reserved_field_name`, + `"2":{"name":{"code":"validation_reserved_field_name`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { - Name: "OnCollectionAfterCreateRequest error response", + Name: "OnCollectionCreateRequest error response", Method: http.MethodPost, - Url: "/api/collections", - Body: strings.NewReader(`{"name":"new","type":"base","schema":[{"type":"text","id":"12345789","name":"test"}]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections", + Body: strings.NewReader(`{"name":"new","type":"base"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnCollectionAfterCreateRequest().Add(func(e *core.CollectionCreateEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnCollectionCreateRequest().BindFunc(func(e *core.CollectionRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, + "*": 0, + "OnCollectionCreateRequest": 1, }, }, @@ -594,48 +681,62 @@ func TestCollectionCreate(t *testing.T) { { Name: "trying to create view collection with invalid options", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"view", - "schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}], - "options":{"query": "invalid"} + "fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}], + "viewQuery":"invalid" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"options":{"query":{"code":"validation_invalid_view_query`, + `"viewQuery":{"code":"validation_invalid_view_query`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { Name: "creating view collection", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"view", - "schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}], - "options": { - "query": "select 1 as id from _admins" - } + "fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}], + "viewQuery": "select 1 as id from ` + core.CollectionNameSuperusers + `" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"name":"new"`, `"type":"view"`, - `"schema":[]`, + `"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, }, }, @@ -644,11 +745,11 @@ func TestCollectionCreate(t *testing.T) { { Name: "creating base collection with invalid indexes", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"base", - "schema":[ + "fields":[ {"type":"text","name":"test"} ], "indexes": [ @@ -656,8 +757,8 @@ func TestCollectionCreate(t *testing.T) { "create index idx_test2 on new (missing)" ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ @@ -665,18 +766,102 @@ func TestCollectionCreate(t *testing.T) { `"indexes":{"1":{"code":"`, }, ExpectedEvents: map[string]int{ - "OnCollectionBeforeCreateRequest": 1, - "OnModelBeforeCreate": 1, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, + }, + }, + { + Name: "creating base collection with index name from another collection", + Method: http.MethodPost, + URL: "/api/collections", + Body: strings.NewReader(`{ + "name":"new", + "type":"base", + "fields":[ + {"type":"text","name":"test"} + ], + "indexes": [ + "create index exist_test on new (test)" + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + demo1.AddIndex("exist_test", false, "created", "") + if err = app.Save(demo1); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"indexes":{`, + `"0":{"code":"validation_existing_index_name"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, + }, + }, + { + Name: "creating base collection with 2 indexes using the same name", + Method: http.MethodPost, + URL: "/api/collections", + Body: strings.NewReader(`{ + "name":"new", + "type":"base", + "indexes": [ + "create index duplicate_idx on new (created)", + "create index duplicate_idx on new (updated)" + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"indexes":{`, + `"1":{"code":"validation_duplicated_index_name"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionAfterCreateError": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnModelValidate": 1, }, }, { Name: "creating base collection with valid indexes (+ random table name)", Method: http.MethodPost, - Url: "/api/collections", + URL: "/api/collections", Body: strings.NewReader(`{ "name":"new", "type":"base", - "schema":[ + "fields":[ {"type":"text","name":"test"} ], "indexes": [ @@ -684,8 +869,8 @@ func TestCollectionCreate(t *testing.T) { "create index idx_test2 on anything (id, test)" ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -696,18 +881,27 @@ func TestCollectionCreate(t *testing.T) { `idx_test2`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnCollectionBeforeCreateRequest": 1, - "OnCollectionAfterCreateRequest": 1, + "*": 0, + "OnCollectionCreateRequest": 1, + "OnCollectionCreate": 1, + "OnCollectionCreateExecute": 1, + "OnCollectionAfterCreateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - indexes, err := app.Dao().TableIndexes("new") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + indexes, err := app.TableIndexes("new") if err != nil { t.Fatal(err) } expected := []string{"idx_test1", "idx_test2"} + if len(indexes) != len(expected) { + t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes) + } for name := range indexes { if !list.ExistInSlice(name, expected) { t.Fatalf("Missing index %q", name) @@ -729,38 +923,41 @@ func TestCollectionUpdate(t *testing.T) { { Name: "unauthorized", Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", + Name: "authorized as regular user", Method: http.MethodPatch, - Url: "/api/collections/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections/demo1", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + missing collection", + Name: "authorized as superuser + missing collection", Method: http.MethodPatch, - Url: "/api/collections/missing", + URL: "/api/collections/missing", Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + empty body", + Name: "authorized as superuser + empty body", Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -768,44 +965,48 @@ func TestCollectionUpdate(t *testing.T) { `"name":"demo1"`, }, ExpectedEvents: map[string]int{ - "OnCollectionAfterUpdateRequest": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionUpdateExecute": 1, + "OnCollectionAfterUpdateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, }, }, { - Name: "OnCollectionAfterUpdateRequest error response", + Name: "OnCollectionAfterUpdateSuccessRequest error response", Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnCollectionAfterUpdateRequest().Add(func(e *core.CollectionUpdateEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnCollectionUpdateRequest().BindFunc(func(e *core.CollectionRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnCollectionAfterUpdateRequest": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, + "*": 0, + "OnCollectionUpdateRequest": 1, }, }, { - Name: "authorized as admin + invalid data (eg. existing name)", + Name: "authorized as superuser + invalid data (eg. existing name)", Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", Body: strings.NewReader(`{ "name":"demo2", "type":"auth" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ @@ -813,14 +1014,24 @@ func TestCollectionUpdate(t *testing.T) { `"name":{"code":"validation_collection_name_exists"`, `"type":{"code":"validation_collection_type_change"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, }, { - Name: "authorized as admin + valid data", + Name: "authorized as superuser + valid data", Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", Body: strings.NewReader(`{"name":"new"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -828,110 +1039,32 @@ func TestCollectionUpdate(t *testing.T) { `"name":"new"`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnCollectionAfterUpdateRequest": 1, + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionUpdateExecute": 1, + "OnCollectionAfterUpdateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { // check if the record table was renamed - if !app.Dao().HasTable("new") { + if !app.HasTable("new") { t.Fatal("Couldn't find record table 'new'.") } }, }, { - Name: "trying to update auth collection with reserved auth fields", + Name: "trying to update collection with reserved fields", Method: http.MethodPatch, - Url: "/api/collections/users", - Body: strings.NewReader(`{ - "schema":[ - {"type":"text","name":"email"}, - {"type":"text","name":"username"}, - {"type":"text","name":"verified"}, - {"type":"text","name":"emailVisibility"}, - {"type":"text","name":"lastResetSentAt"}, - {"type":"text","name":"lastVerificationSentAt"}, - {"type":"text","name":"tokenKey"}, - {"type":"text","name":"passwordHash"}, - {"type":"text","name":"password"}, - {"type":"text","name":"passwordConfirm"}, - {"type":"text","name":"oldPassword"} - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{"schema":{`, - `"0":{"name":{"code":"validation_reserved_auth_field_name"`, - `"1":{"name":{"code":"validation_reserved_auth_field_name"`, - `"2":{"name":{"code":"validation_reserved_auth_field_name"`, - `"3":{"name":{"code":"validation_reserved_auth_field_name"`, - `"4":{"name":{"code":"validation_reserved_auth_field_name"`, - `"5":{"name":{"code":"validation_reserved_auth_field_name"`, - `"6":{"name":{"code":"validation_reserved_auth_field_name"`, - `"7":{"name":{"code":"validation_reserved_auth_field_name"`, - `"8":{"name":{"code":"validation_reserved_auth_field_name"`, - }, - }, - { - Name: "updating base collection with reserved auth fields", - Method: http.MethodPatch, - Url: "/api/collections/demo4", - Body: strings.NewReader(`{ - "schema":[ - {"type":"text","name":"email"}, - {"type":"text","name":"username"}, - {"type":"text","name":"verified"}, - {"type":"text","name":"emailVisibility"}, - {"type":"text","name":"lastResetSentAt"}, - {"type":"text","name":"lastVerificationSentAt"}, - {"type":"text","name":"tokenKey"}, - {"type":"text","name":"passwordHash"}, - {"type":"text","name":"password"}, - {"type":"text","name":"passwordConfirm"}, - {"type":"text","name":"oldPassword"} - ], - "indexes": [] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"name":"demo4"`, - `"type":"base"`, - `"schema":[{`, - `"email"`, - `"username"`, - `"verified"`, - `"emailVisibility"`, - `"lastResetSentAt"`, - `"lastVerificationSentAt"`, - `"tokenKey"`, - `"passwordHash"`, - `"password"`, - `"passwordConfirm"`, - `"oldPassword"`, - }, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnCollectionAfterUpdateRequest": 1, - }, - }, - { - Name: "trying to update base collection with reserved base fields", - Method: http.MethodPatch, - Url: "/api/collections/demo1", + URL: "/api/collections/demo1", Body: strings.NewReader(`{ "name":"new", - "type":"base", - "schema":[ - {"type":"text","name":"id"}, + "fields":[ + {"type":"text","name":"id","id":"_pbf_text_id_"}, {"type":"text","name":"created"}, {"type":"text","name":"updated"}, {"type":"text","name":"expand"}, @@ -939,34 +1072,85 @@ func TestCollectionUpdate(t *testing.T) { {"type":"text","name":"collectionName"} ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ - `"data":{"schema":{`, - `"0":{"name":{"code":"validation_not_in_invalid`, - `"1":{"name":{"code":"validation_not_in_invalid`, - `"2":{"name":{"code":"validation_not_in_invalid`, + `"data":{"fields":{`, `"3":{"name":{"code":"validation_not_in_invalid`, `"4":{"name":{"code":"validation_not_in_invalid`, `"5":{"name":{"code":"validation_not_in_invalid`, }, + NotExpectedContent: []string{ + `"0":`, + `"1":`, + `"2":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, + }, + { + Name: "trying to update collection with changed/removed system fields", + Method: http.MethodPatch, + URL: "/api/collections/demo1", + Body: strings.NewReader(`{ + "name":"new", + "fields":[ + {"type":"text","name":"created"} + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{"fields":{`, + `"code":"validation_system_field_change"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, }, { Name: "trying to update auth collection with invalid options", Method: http.MethodPatch, - Url: "/api/collections/users", + URL: "/api/collections/users", Body: strings.NewReader(`{ - "options":{"minPasswordLength": 4} + "passwordAuth":{"identityFields": ["missing"]} }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"options":{"minPasswordLength":{"code":"validation_min_greater_equal_than_required"`, + `"passwordAuth":{"identityFields":{"code":"validation_missing_field"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, }, }, @@ -975,52 +1159,63 @@ func TestCollectionUpdate(t *testing.T) { { Name: "trying to update view collection with invalid options", Method: http.MethodPatch, - Url: "/api/collections/view1", + URL: "/api/collections/view1", Body: strings.NewReader(`{ - "schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}], - "options":{"query": "invalid"} + "fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}], + "viewQuery":"invalid" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"options":{"query":{"code":"validation_invalid_view_query`, + `"viewQuery":{"code":"validation_invalid_view_query"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, }, }, { Name: "updating view collection", Method: http.MethodPatch, - Url: "/api/collections/view2", + URL: "/api/collections/view2", Body: strings.NewReader(`{ "name":"view2_update", - "schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}], - "options": { - "query": "select 2 as id, created, updated, email from _admins" - } + "fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}], + "viewQuery": "select 2 as id, created, updated, email from ` + core.CollectionNameSuperusers + `" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"name":"view2_update"`, `"type":"view"`, - `"schema":[{`, + `"fields":[{`, `"name":"email"`, - }, - NotExpectedContent: []string{ - // base model fields are not part of the schema `"name":"id"`, `"name":"created"`, `"name":"updated"`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnCollectionAfterUpdateRequest": 1, + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionUpdateExecute": 1, + "OnCollectionAfterUpdateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, }, }, @@ -1029,55 +1224,145 @@ func TestCollectionUpdate(t *testing.T) { { Name: "updating base collection with invalid indexes", Method: http.MethodPatch, - Url: "/api/collections/demo2", + URL: "/api/collections/demo2", Body: strings.NewReader(`{ "indexes": [ "create unique idx_test1 on demo1 (text)", "create index idx_test2 on demo2 (id, title)" ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"indexes":{"0":{"code":"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, }, + + { + Name: "updating base collection with index name from another collection", + Method: http.MethodPatch, + URL: "/api/collections/demo2", + Body: strings.NewReader(`{ + "indexes": [ + "create index exist_test on new (test)" + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + demo1.AddIndex("exist_test", false, "created", "") + if err = app.Save(demo1); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"indexes":{`, + `"0":{"code":"validation_existing_index_name"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, + }, + { + Name: "updating base collection with 2 indexes using the same name", + Method: http.MethodPatch, + URL: "/api/collections/demo2", + Body: strings.NewReader(`{ + "indexes": [ + "create index duplicate_idx on new (created)", + "create index duplicate_idx on new (updated)" + ] + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"indexes":{`, + `"1":{"code":"validation_duplicated_index_name"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionAfterUpdateError": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + }, + }, + { Name: "updating base collection with valid indexes (+ random table name)", Method: http.MethodPatch, - Url: "/api/collections/demo2", + URL: "/api/collections/demo2", Body: strings.NewReader(`{ "indexes": [ "create unique index idx_test1 on demo2 (title)", "create index idx_test2 on anything (active)" ] }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"name":"demo2"`, `"indexes":[`, - "CREATE UNIQUE INDEX `idx_test1`", - "CREATE INDEX `idx_test2`", + `idx_test1`, + `idx_test2`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnCollectionBeforeUpdateRequest": 1, - "OnCollectionAfterUpdateRequest": 1, + "*": 0, + "OnCollectionUpdateRequest": 1, + "OnCollectionUpdate": 1, + "OnCollectionUpdateExecute": 1, + "OnCollectionAfterUpdateSuccess": 1, + "OnCollectionValidate": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - indexes, err := app.Dao().TableIndexes("new") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + indexes, err := app.TableIndexes("demo2") if err != nil { t.Fatal(err) } expected := []string{"idx_test1", "idx_test2"} + if len(indexes) != len(expected) { + t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes) + } for name := range indexes { if !list.ExistInSlice(name, expected) { t.Fatalf("Missing index %q", name) @@ -1092,334 +1377,120 @@ func TestCollectionUpdate(t *testing.T) { } } -func TestCollectionsImport(t *testing.T) { +func TestCollectionScaffolds(t *testing.T) { t.Parallel() - totalCollections := 11 - scenarios := []tests.ApiScenario{ { Name: "unauthorized", - Method: http.MethodPut, - Url: "/api/collections/import", + Method: http.MethodGet, + URL: "/api/collections/meta/scaffolds", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as user", - Method: http.MethodPut, - Url: "/api/collections/import", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + Name: "authorized as regular user", + Method: http.MethodGet, + URL: "/api/collections/meta/scaffolds", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + empty collections", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{"collections":[]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Name: "authorized as superuser", + Method: http.MethodGet, + URL: "/api/collections/meta/scaffolds", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - ExpectedStatus: 400, + ExpectedStatus: 200, ExpectedContent: []string{ - `"data":{`, - `"collections":{"code":"validation_required"`, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - expected := totalCollections - if len(collections) != expected { - t.Fatalf("Expected %d collections, got %d", expected, len(collections)) - } - }, - }, - { - Name: "authorized as admin + trying to delete system collections", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{"deleteMissing": true, "collections":[{"name": "test123"}]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"collections":{"code":"collections_import_failure"`, - }, - ExpectedEvents: map[string]int{ - "OnCollectionsBeforeImportRequest": 1, - "OnModelBeforeDelete": 1, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - expected := totalCollections - if len(collections) != expected { - t.Fatalf("Expected %d collections, got %d", expected, len(collections)) - } - }, - }, - { - Name: "authorized as admin + collections validator failure", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{ - "collections":[ - { - "name": "import1", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ] - }, - {"name": "import2"} - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"collections":{"code":"collections_import_validate_failure"`, - }, - ExpectedEvents: map[string]int{ - "OnCollectionsBeforeImportRequest": 1, - "OnModelBeforeCreate": 2, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - expected := totalCollections - if len(collections) != expected { - t.Fatalf("Expected %d collections, got %d", expected, len(collections)) - } - }, - }, - { - Name: "authorized as admin + successful collections save", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{ - "collections":[ - { - "name": "import1", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ] - }, - { - "name": "import2", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ], - "indexes": [ - "create index idx_test on import2 (test)" - ] - }, - { - "name": "auth_without_schema", - "type": "auth" - } - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnCollectionsBeforeImportRequest": 1, - "OnCollectionsAfterImportRequest": 1, - "OnModelBeforeCreate": 3, - "OnModelAfterCreate": 3, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - - expected := totalCollections + 3 - if len(collections) != expected { - t.Fatalf("Expected %d collections, got %d", expected, len(collections)) - } - - indexes, err := app.Dao().TableIndexes("import2") - if err != nil || indexes["idx_test"] == "" { - t.Fatalf("Missing index %s (%v)", "idx_test", err) - } - }, - }, - { - Name: "authorized as admin + successful collections save and old non-system collections deletion", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{ - "deleteMissing": true, - "collections":[ - { - "name": "new_import", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ] - }, - { - "id": "kpv709sk2lqbqk8", - "system": true, - "name": "nologin", - "type": "auth", - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": [], - "manageRule": "@request.auth.collectionName = 'users'", - "minPasswordLength": 8, - "onlyEmailDomains": [], - "requireEmail": true - }, - "listRule": "", - "viewRule": "", - "createRule": "", - "updateRule": "", - "deleteRule": "", - "schema": [ - { - "id": "x8zzktwe", - "name": "name", - "type": "text", - "system": false, - "required": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": "" - } - } - ] - }, - { - "id":"wsmn24bux7wo113", - "name":"demo1", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - } - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnCollectionsAfterImportRequest": 1, - "OnCollectionsBeforeImportRequest": 1, - "OnModelBeforeDelete": 9, - "OnModelAfterDelete": 9, - "OnModelBeforeUpdate": 2, - "OnModelAfterUpdate": 2, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - expected := 3 - if len(collections) != expected { - t.Fatalf("Expected %d collections, got %d", expected, len(collections)) - } - }, - }, - { - Name: "authorized as admin + successful collections save", - Method: http.MethodPut, - Url: "/api/collections/import", - Body: strings.NewReader(`{ - "collections":[ - { - "name": "import1", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ] - }, - { - "name": "import2", - "schema": [ - { - "id": "koih1lqx", - "name": "test", - "type": "text" - } - ], - "indexes": [ - "create index idx_test on import2 (test)" - ] - }, - { - "name": "auth_without_schema", - "type": "auth" - } - ] - }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnCollectionsAfterImportRequest().Add(func(e *core.CollectionsImportEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnCollectionsBeforeImportRequest": 1, - "OnCollectionsAfterImportRequest": 1, - "OnModelBeforeCreate": 3, - "OnModelAfterCreate": 3, + `"auth":{`, + `"base":{`, + `"view":{`, + `"type":"auth"`, + `"type":"base"`, + `"type":"view"`, + `"fields":[{`, + `"fields":[{`, + `"id":"text3208210256"`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCollectionTruncate(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodDelete, + URL: "/api/collections/demo5/truncate", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as regular user", + Method: http.MethodDelete, + URL: "/api/collections/demo5/truncate", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as superuser", + Method: http.MethodDelete, + URL: "/api/collections/demo5/truncate", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnModelDelete": 2, + "OnModelDeleteExecute": 2, + "OnModelAfterDeleteSuccess": 2, + "OnRecordDelete": 2, + "OnRecordDeleteExecute": 2, + "OnRecordAfterDeleteSuccess": 2, + }, + }, + { + Name: "authorized as superuser but collection with required cascade delete references", + Method: http.MethodDelete, + URL: "/api/collections/demo3/truncate", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnModelDelete": 2, + "OnModelDeleteExecute": 2, + "OnModelAfterDeleteError": 2, + "OnModelUpdate": 2, + "OnModelUpdateExecute": 2, + "OnModelAfterUpdateError": 2, + "OnRecordDelete": 2, + "OnRecordDeleteExecute": 2, + "OnRecordAfterDeleteError": 2, + "OnRecordUpdate": 2, + "OnRecordUpdateExecute": 2, + "OnRecordAfterUpdateError": 2, }, }, } diff --git a/apis/dashboard.go b/apis/dashboard.go new file mode 100644 index 00000000..91f5ea8f --- /dev/null +++ b/apis/dashboard.go @@ -0,0 +1,138 @@ +package apis + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" +) + +const installerParam = "pbinstal" + +var wildcardPlaceholderRegex = regexp.MustCompile(`/{.+\.\.\.}$`) + +func stripWildcard(pattern string) string { + return wildcardPlaceholderRegex.ReplaceAllString(pattern, "") +} + +// installerRedirect redirects the user to the installer dashboard UI page +// when the application needs some preliminary configurations to be done. +func installerRedirect(app core.App, cpPath string) hook.HandlerFunc[*core.RequestEvent] { + // note: to avoid locks contention it is not concurrent safe but it + // is expected to be updated only once during initialization + var hasSuperuser bool + + // strip named wildcard + cpPath = stripWildcard(cpPath) + + updateHasSuperuser := func(app core.App) error { + total, err := app.CountRecords(core.CollectionNameSuperusers) + if err != nil { + return err + } + + hasSuperuser = total > 0 + + return nil + } + + // load initial state on app init + app.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error { + err := e.Next() + if err != nil { + return err + } + + err = updateHasSuperuser(e.App) + if err != nil { + return fmt.Errorf("failed to check for existing superuser: %w", err) + } + + return nil + }) + + // update on superuser create + app.OnRecordCreateRequest(core.CollectionNameSuperusers).BindFunc(func(e *core.RecordRequestEvent) error { + err := e.Next() + if err != nil { + return err + } + + if !hasSuperuser { + hasSuperuser = true + } + + return nil + }) + + return func(e *core.RequestEvent) error { + if hasSuperuser { + return e.Next() + } + + isAPI := strings.HasPrefix(e.Request.URL.Path, "/api/") + isControlPanel := strings.HasPrefix(e.Request.URL.Path, cpPath) + wildcard := e.Request.PathValue(StaticWildcardParam) + + // skip redirect checks for API and non-root level dashboard index.html requests (css, images, etc.) + if isAPI || (isControlPanel && wildcard != "" && wildcard != router.IndexPage) { + return e.Next() + } + + // check again in case the superuser was created by some other process + if err := updateHasSuperuser(e.App); err != nil { + return err + } + + if hasSuperuser { + return e.Next() + } + + _, hasInstallerParam := e.Request.URL.Query()[installerParam] + + // redirect to the installer page + if !hasInstallerParam { + return e.Redirect(http.StatusTemporaryRedirect, cpPath+"?"+installerParam+"#") + } + + return e.Next() + } +} + +// dashboardRemoveInstallerParam redirects to a non-installer +// query param in case there is already a superuser created. +// +// Note: intended to be registered only for the dashboard route +// to prevent excessive checks for every other route in installerRedirect. +func dashboardRemoveInstallerParam() hook.HandlerFunc[*core.RequestEvent] { + return func(e *core.RequestEvent) error { + _, hasInstallerParam := e.Request.URL.Query()[installerParam] + if !hasInstallerParam { + return e.Next() // nothing to remove + } + + // clear installer param + total, _ := e.App.CountRecords(core.CollectionNameSuperusers) + if total > 0 { + return e.Redirect(http.StatusTemporaryRedirect, "?") + } + + return e.Next() + } +} + +// dashboardCacheControl adds default Cache-Control header for all +// dashboard UI resources (ignoring the root index.html path) +func dashboardCacheControl() hook.HandlerFunc[*core.RequestEvent] { + return func(e *core.RequestEvent) error { + if e.Request.PathValue(StaticWildcardParam) != "" { + e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400") + } + + return e.Next() + } +} diff --git a/apis/file.go b/apis/file.go index 610fa25b..aaa87976 100644 --- a/apis/file.go +++ b/apis/file.go @@ -7,18 +7,12 @@ import ( "log/slog" "net/http" "runtime" - "strings" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/spf13/cast" + "github.com/pocketbase/pocketbase/tools/router" "golang.org/x/sync/semaphore" "golang.org/x/sync/singleflight" ) @@ -27,23 +21,19 @@ var imageContentTypes = []string{"image/png", "image/jpg", "image/jpeg", "image/ var defaultThumbSizes = []string{"100x100"} // bindFileApi registers the file api endpoints and the corresponding handlers. -func bindFileApi(app core.App, rg *echo.Group) { +func bindFileApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { api := fileApi{ - app: app, thumbGenSem: semaphore.NewWeighted(int64(runtime.NumCPU() + 2)), // the value is arbitrary chosen and may change in the future thumbGenPending: new(singleflight.Group), thumbGenMaxWait: 60 * time.Second, } - subGroup := rg.Group("/files", ActivityLogger(app)) - subGroup.POST("/token", api.fileToken) - subGroup.HEAD("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app)) - subGroup.GET("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app)) + sub := rg.Group("/files") + sub.POST("/token", api.fileToken).Bind(RequireAuth()) + sub.GET("/{collection}/{recordId}/{filename}", api.download).Bind(collectionPathRateLimit("", "file")) } type fileApi struct { - app core.App - // thumbGenSem is a semaphore to prevent too much concurrent // requests generating new thumbs at the same time. thumbGenSem *semaphore.Weighted @@ -57,84 +47,67 @@ type fileApi struct { thumbGenMaxWait time.Duration } -func (api *fileApi) fileToken(c echo.Context) error { - event := new(core.FileTokenEvent) - event.HttpContext = c - - if admin, _ := c.Get(ContextAdminKey).(*models.Admin); admin != nil { - event.Model = admin - event.Token, _ = tokens.NewAdminFileToken(api.app, admin) - } else if record, _ := c.Get(ContextAuthRecordKey).(*models.Record); record != nil { - event.Model = record - event.Token, _ = tokens.NewRecordFileToken(api.app, record) +func (api *fileApi) fileToken(e *core.RequestEvent) error { + if e.Auth == nil { + return e.UnauthorizedError("Missing auth context.", nil) } - return api.app.OnFileBeforeTokenRequest().Trigger(event, func(e *core.FileTokenEvent) error { - if e.Model == nil || e.Token == "" { - return NewBadRequestError("Failed to generate file token.", nil) - } + token, err := e.Auth.NewFileToken() + if err != nil { + return e.InternalServerError("Failed to generate file token", err) + } - return api.app.OnFileAfterTokenRequest().Trigger(event, func(e *core.FileTokenEvent) error { - if e.HttpContext.Response().Committed { - return nil - } + event := new(core.FileTokenRequestEvent) + event.RequestEvent = e + event.Token = token - return e.HttpContext.JSON(http.StatusOK, map[string]string{ - "token": e.Token, - }) + return e.App.OnFileTokenRequest().Trigger(event, func(e *core.FileTokenRequestEvent) error { + return e.JSON(http.StatusOK, map[string]string{ + "token": e.Token, }) }) } -func (api *fileApi) download(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", nil) - } - - recordId := c.PathParam("recordId") - if recordId == "" { - return NewNotFoundError("", nil) - } - - record, err := api.app.Dao().FindRecordById(collection.Id, recordId) +func (api *fileApi) download(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) if err != nil { - return NewNotFoundError("", err) + return e.NotFoundError("", nil) } - filename := c.PathParam("filename") + recordId := e.Request.PathValue("recordId") + if recordId == "" { + return e.NotFoundError("", nil) + } + + record, err := e.App.FindRecordById(collection, recordId) + if err != nil { + return e.NotFoundError("", err) + } + + filename := e.Request.PathValue("filename") fileField := record.FindFileFieldByFile(filename) if fileField == nil { - return NewNotFoundError("", nil) - } - - options, ok := fileField.Options.(*schema.FileOptions) - if !ok { - return NewBadRequestError("", errors.New("failed to load file options")) + return e.NotFoundError("", nil) } // check whether the request is authorized to view the protected file - if options.Protected { - token := c.QueryParam("token") - - adminOrAuthRecord, _ := api.findAdminOrAuthRecordByFileToken(token) - - // create a copy of the cached request data and adjust it for the current auth model - requestInfo := *RequestInfo(c) - requestInfo.Context = models.RequestInfoContextProtectedFile - requestInfo.Admin = nil - requestInfo.AuthRecord = nil - if adminOrAuthRecord != nil { - if admin, _ := adminOrAuthRecord.(*models.Admin); admin != nil { - requestInfo.Admin = admin - } else if record, _ := adminOrAuthRecord.(*models.Record); record != nil { - requestInfo.AuthRecord = record - } + if fileField.Protected { + originalRequestInfo, err := e.RequestInfo() + if err != nil { + return e.InternalServerError("Failed to load request info", err) } - if ok, _ := api.app.Dao().CanAccessRecord(record, &requestInfo, record.Collection().ViewRule); !ok { - return NewForbiddenError("Insufficient permissions to access the file resource.", nil) + token := e.Request.URL.Query().Get("token") + authRecord, _ := e.App.FindAuthRecordByToken(token, core.TokenTypeFile) + + // create a shallow copy of the cached request data and adjust it to the current auth record (if any) + requestInfo := *originalRequestInfo + requestInfo.Context = core.RequestInfoContextProtectedFile + requestInfo.Auth = authRecord + + if ok, _ := e.App.CanAccessRecord(record, &requestInfo, record.Collection().ViewRule); !ok { + return e.NotFoundError("", errors.New("insufficient permissions to access the file resource")) } } @@ -142,16 +115,16 @@ func (api *fileApi) download(c echo.Context) error { // fetch the original view file field related record if collection.IsView() { - fileRecord, err := api.app.Dao().FindRecordByViewFile(collection.Id, fileField.Name, filename) + fileRecord, err := e.App.FindRecordByViewFile(collection.Id, fileField.Name, filename) if err != nil { - return NewNotFoundError("", fmt.Errorf("Failed to fetch view file field record: %w", err)) + return e.NotFoundError("", fmt.Errorf("failed to fetch view file field record: %w", err)) } baseFilesPath = fileRecord.BaseFilesPath() } - fsys, err := api.app.NewFilesystem() + fsys, err := e.App.NewFilesystem() if err != nil { - return NewBadRequestError("Filesystem initialization failure.", err) + return e.InternalServerError("Filesystem initialization failure.", err) } defer fsys.Close() @@ -160,12 +133,12 @@ func (api *fileApi) download(c echo.Context) error { servedName := filename // check for valid thumb size param - thumbSize := c.QueryParam("thumb") - if thumbSize != "" && (list.ExistInSlice(thumbSize, defaultThumbSizes) || list.ExistInSlice(thumbSize, options.Thumbs)) { + thumbSize := e.Request.URL.Query().Get("thumb") + if thumbSize != "" && (list.ExistInSlice(thumbSize, defaultThumbSizes) || list.ExistInSlice(thumbSize, fileField.Thumbs)) { // extract the original file meta attributes and check it existence oAttrs, oAttrsErr := fsys.Attributes(originalPath) if oAttrsErr != nil { - return NewNotFoundError("", err) + return e.NotFoundError("", err) } // check if it is an image @@ -176,8 +149,8 @@ func (api *fileApi) download(c echo.Context) error { // create a new thumb if it doesn't exist if exists, _ := fsys.Exists(servedPath); !exists { - if err := api.createThumb(c, fsys, originalPath, servedPath, thumbSize); err != nil { - api.app.Logger().Warn( + if err := api.createThumb(e, fsys, originalPath, servedPath, thumbSize); err != nil { + e.App.Logger().Warn( "Fallback to original - failed to create thumb "+servedName, slog.Any("error", err), slog.String("original", originalPath), @@ -192,8 +165,8 @@ func (api *fileApi) download(c echo.Context) error { } } - event := new(core.FileDownloadEvent) - event.HttpContext = c + event := new(core.FileDownloadRequestEvent) + event.RequestEvent = e event.Collection = collection event.Record = record event.FileField = fileField @@ -203,61 +176,26 @@ func (api *fileApi) download(c echo.Context) error { // clickjacking shouldn't be a concern when serving uploaded files, // so it safe to unset the global X-Frame-Options to allow files embedding // (note: it is out of the hook to allow users to customize the behavior) - c.Response().Header().Del("X-Frame-Options") + e.Response.Header().Del("X-Frame-Options") - return api.app.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - if err := fsys.Serve(e.HttpContext.Response(), e.HttpContext.Request(), e.ServedPath, e.ServedName); err != nil { - return NewNotFoundError("", err) + return e.App.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadRequestEvent) error { + if err := fsys.Serve(e.Response, e.Request, e.ServedPath, e.ServedName); err != nil { + return e.NotFoundError("", err) } return nil }) } -func (api *fileApi) findAdminOrAuthRecordByFileToken(fileToken string) (models.Model, error) { - fileToken = strings.TrimSpace(fileToken) - if fileToken == "" { - return nil, errors.New("missing file token") - } - - claims, _ := security.ParseUnverifiedJWT(strings.TrimSpace(fileToken)) - tokenType := cast.ToString(claims["type"]) - - switch tokenType { - case tokens.TypeAdmin: - admin, err := api.app.Dao().FindAdminByToken( - fileToken, - api.app.Settings().AdminFileToken.Secret, - ) - if err == nil && admin != nil { - return admin, nil - } - case tokens.TypeAuthRecord: - record, err := api.app.Dao().FindAuthRecordByToken( - fileToken, - api.app.Settings().RecordFileToken.Secret, - ) - if err == nil && record != nil { - return record, nil - } - } - - return nil, errors.New("missing or invalid file token") -} - func (api *fileApi) createThumb( - c echo.Context, + e *core.RequestEvent, fsys *filesystem.System, originalPath string, thumbPath string, thumbSize string, ) error { ch := api.thumbGenPending.DoChan(thumbPath, func() (any, error) { - ctx, cancel := context.WithTimeout(c.Request().Context(), api.thumbGenMaxWait) + ctx, cancel := context.WithTimeout(e.Request.Context(), api.thumbGenMaxWait) defer cancel() if err := api.thumbGenSem.Acquire(ctx, 1); err != nil { diff --git a/apis/file_test.go b/apis/file_test.go index 36b353de..7193934a 100644 --- a/apis/file_test.go +++ b/apis/file_test.go @@ -10,11 +10,8 @@ import ( "sync" "testing" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/types" ) @@ -26,23 +23,54 @@ func TestFileToken(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/files/token", - ExpectedStatus: 400, + URL: "/api/files/token", + ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "regular user", + Method: http.MethodPost, + URL: "/api/files/token", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":"`, + }, ExpectedEvents: map[string]int{ - "OnFileBeforeTokenRequest": 1, + "*": 0, + "OnFileTokenRequest": 1, }, }, { - Name: "unauthorized with model and token via hook", + Name: "superuser", Method: http.MethodPost, - Url: "/api/files/token", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnFileBeforeTokenRequest().Add(func(e *core.FileTokenEvent) error { - record, _ := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - e.Model = record + URL: "/api/files/token", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnFileTokenRequest": 1, + }, + }, + { + Name: "hook token overwrite", + Method: http.MethodPost, + URL: "/api/files/token", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileTokenRequest().BindFunc(func(e *core.FileTokenRequestEvent) error { e.Token = "test" - return nil + return e.Next() }) }, ExpectedStatus: 200, @@ -50,40 +78,8 @@ func TestFileToken(t *testing.T) { `"token":"test"`, }, ExpectedEvents: map[string]int{ - "OnFileBeforeTokenRequest": 1, - "OnFileAfterTokenRequest": 1, - }, - }, - { - Name: "auth record", - Method: http.MethodPost, - Url: "/api/files/token", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"token":"`, - }, - ExpectedEvents: map[string]int{ - "OnFileBeforeTokenRequest": 1, - "OnFileAfterTokenRequest": 1, - }, - }, - { - Name: "admin", - Method: http.MethodPost, - Url: "/api/files/token", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"token":"`, - }, - ExpectedEvents: map[string]int{ - "OnFileBeforeTokenRequest": 1, - "OnFileAfterTokenRequest": 1, + "*": 0, + "OnFileTokenRequest": 1, }, }, } @@ -152,233 +148,271 @@ func TestFileDownload(t *testing.T) { { Name: "missing collection", Method: http.MethodGet, - Url: "/api/files/missing/4q1xlclmfloku33/300_1SEi6Q6U72.png", + URL: "/api/files/missing/4q1xlclmfloku33/300_1SEi6Q6U72.png", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "missing record", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/missing/300_1SEi6Q6U72.png", + URL: "/api/files/_pb_users_auth_/missing/300_1SEi6Q6U72.png", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "missing file", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/missing.png", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/missing.png", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "existing image", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png", ExpectedStatus: 200, ExpectedContent: []string{string(testImg)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - missing thumb (should fallback to the original)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=999x999", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=999x999", ExpectedStatus: 200, ExpectedContent: []string{string(testImg)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (crop center)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropCenter)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (crop top)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50t", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50t", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropTop)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (crop bottom)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50b", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50b", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropBottom)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (fit)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50f", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50f", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbFit)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (zero width)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=0x50", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=0x50", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbZeroWidth)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing image - existing thumb (zero height)", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x0", + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x0", ExpectedStatus: 200, ExpectedContent: []string{string(testThumbZeroHeight)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "existing non image file - thumb parameter should be ignored", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt?thumb=100x100", + URL: "/api/files/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt?thumb=100x100", ExpectedStatus: 200, ExpectedContent: []string{string(testFile)}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, // protected file access checks { - Name: "protected file - expired token", + Name: "protected file - superuser with expired file token", Method: http.MethodGet, - Url: "/api/files/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt?thumb=100x100", - ExpectedStatus: 200, - ExpectedContent: []string{string(testFile)}, - ExpectedEvents: map[string]int{ - "OnFileDownloadRequest": 1, - }, - }, - { - Name: "protected file - admin with expired file token", - Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6ImFkbWluIn0.g7Q_3UX6H--JWJ7yt1Hoe-1ugTX1KpbKzdt0zjGSe-E", - ExpectedStatus: 403, + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.hTNDzikwJdcoWrLnRnp7xbaifZ2vuYZ0oOYRHtJfnk4", + ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "protected file - admin with valid file token", + Name: "protected file - superuser with valid file token", Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU", + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.C8m3aRZNOxUDhMiuZuDTRIIjRl7wsOyzoxs8EjvKNgY", ExpectedStatus: 200, ExpectedContent: []string{"PNG"}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "protected file - guest without view access", Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png", - ExpectedStatus: 403, + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png", + ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "protected file - guest with view access", Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - dao := daos.New(app.Dao().DB()) - + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { // mock public view access - c, err := dao.FindCollectionByNameOrId("demo1") + c, err := app.FindCachedCollectionByNameOrId("demo1") if err != nil { t.Fatalf("Failed to fetch mock collection: %v", err) } c.ViewRule = types.Pointer("") - if err := dao.SaveCollection(c); err != nil { + if err := app.UnsafeWithoutHooks().Save(c); err != nil { t.Fatalf("Failed to update mock collection: %v", err) } }, ExpectedStatus: 200, ExpectedContent: []string{"PNG"}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "protected file - auth record without view access", Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - dao := daos.New(app.Dao().DB()) - + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8ifQ.nSTLuCPcGpWn2K2l-BFkC3Vlzc-ZTDPByYq8dN1oPSo", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { // mock restricted user view access - c, err := dao.FindCollectionByNameOrId("demo1") + c, err := app.FindCachedCollectionByNameOrId("demo1") if err != nil { t.Fatalf("Failed to fetch mock collection: %v", err) } c.ViewRule = types.Pointer("@request.auth.verified = true") - if err := dao.SaveCollection(c); err != nil { + if err := app.UnsafeWithoutHooks().Save(c); err != nil { t.Fatalf("Failed to update mock collection: %v", err) } }, - ExpectedStatus: 403, + ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "protected file - auth record with view access", Method: http.MethodGet, - Url: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - dao := daos.New(app.Dao().DB()) - + URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8ifQ.nSTLuCPcGpWn2K2l-BFkC3Vlzc-ZTDPByYq8dN1oPSo", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { // mock user view access - c, err := dao.FindCollectionByNameOrId("demo1") + c, err := app.FindCachedCollectionByNameOrId("demo1") if err != nil { t.Fatalf("Failed to fetch mock collection: %v", err) } c.ViewRule = types.Pointer("@request.auth.verified = false") - if err := dao.SaveCollection(c); err != nil { + if err := app.UnsafeWithoutHooks().Save(c); err != nil { t.Fatalf("Failed to update mock collection: %v", err) } }, ExpectedStatus: 200, ExpectedContent: []string{"PNG"}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, { Name: "protected file in view (view's View API rule failure)", Method: http.MethodGet, - Url: "/api/files/view1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk", - ExpectedStatus: 403, + URL: "/api/files/view1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8ifQ.nSTLuCPcGpWn2K2l-BFkC3Vlzc-ZTDPByYq8dN1oPSo", + ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "protected file in view (view's View API rule success)", Method: http.MethodGet, - Url: "/api/files/view1/84nmscqy84lsi1t/test_d61b33QdDU.txt?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk", + URL: "/api/files/view1/84nmscqy84lsi1t/test_d61b33QdDU.txt?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8ifQ.nSTLuCPcGpWn2K2l-BFkC3Vlzc-ZTDPByYq8dN1oPSo", ExpectedStatus: 200, ExpectedContent: []string{"test"}, ExpectedEvents: map[string]int{ + "*": 0, "OnFileDownloadRequest": 1, }, }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:file", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:file"}, + {MaxRequests: 0, Label: "users:file"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:file", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:file"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, } for _, scenario := range scenarios { @@ -410,30 +444,23 @@ func TestConcurrentThumbsGeneration(t *testing.T) { defer fsys.Close() // create a dummy file field collection - demo1, err := app.Dao().FindCollectionByNameOrId("demo1") + demo1, err := app.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - fileField := demo1.Schema.GetFieldByName("file_one") - fileField.Options = &schema.FileOptions{ - Protected: false, - MaxSelect: 1, - MaxSize: 999999, - // new thumbs - Thumbs: []string{"111x111", "111x222", "111x333"}, - } - demo1.Schema.AddField(fileField) - if err := app.Dao().SaveCollection(demo1); err != nil { + fileField := demo1.Fields.GetByName("file_one").(*core.FileField) + fileField.Protected = false + fileField.MaxSelect = 1 + fileField.MaxSize = 999999 + // new thumbs + fileField.Thumbs = []string{"111x111", "111x222", "111x333"} + demo1.Fields.Add(fileField) + if err = app.Save(demo1); err != nil { t.Fatal(err) } fileKey := "wsmn24bux7wo113/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png" - e, err := apis.InitApi(app) - if err != nil { - t.Fatal(err) - } - urls := []string{ "/api/files/" + fileKey + "?thumb=111x111", "/api/files/" + fileKey + "?thumb=111x111", // should still result in single thumb @@ -446,7 +473,6 @@ func TestConcurrentThumbsGeneration(t *testing.T) { wg.Add(len(urls)) for _, url := range urls { - url := url go func() { defer wg.Done() @@ -454,7 +480,11 @@ func TestConcurrentThumbsGeneration(t *testing.T) { req := httptest.NewRequest("GET", url, nil) - e.ServeHTTP(recorder, req) + pbRouter, _ := apis.NewRouter(app) + mux, _ := pbRouter.BuildMux() + if mux != nil { + mux.ServeHTTP(recorder, req) + } }() } diff --git a/apis/health.go b/apis/health.go index eb108ddb..5b13b429 100644 --- a/apis/health.go +++ b/apis/health.go @@ -2,42 +2,52 @@ package apis import ( "net/http" + "slices" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" ) // bindHealthApi registers the health api endpoint. -func bindHealthApi(app core.App, rg *echo.Group) { - api := healthApi{app: app} - +func bindHealthApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { subGroup := rg.Group("/health") - subGroup.HEAD("", api.healthCheck) - subGroup.GET("", api.healthCheck) -} - -type healthApi struct { - app core.App -} - -type healthCheckResponse struct { - Message string `json:"message"` - Code int `json:"code"` - Data struct { - CanBackup bool `json:"canBackup"` - } `json:"data"` + subGroup.GET("", healthCheck) } // healthCheck returns a 200 OK response if the server is healthy. -func (api *healthApi) healthCheck(c echo.Context) error { - if c.Request().Method == http.MethodHead { - return c.NoContent(http.StatusOK) +func healthCheck(e *core.RequestEvent) error { + resp := struct { + Message string `json:"message"` + Code int `json:"code"` + Data map[string]any `json:"data"` + }{ + Code: http.StatusOK, + Message: "API is healthy.", } - resp := new(healthCheckResponse) - resp.Code = http.StatusOK - resp.Message = "API is healthy." - resp.Data.CanBackup = !api.app.Store().Has(core.StoreKeyActiveBackup) + if e.HasSuperuserAuth() { + resp.Data = make(map[string]any, 3) + resp.Data["canBackup"] = !e.App.Store().Has(core.StoreKeyActiveBackup) + resp.Data["realIP"] = e.RealIP() - return c.JSON(http.StatusOK, resp) + // loosely check if behind a reverse proxy + // (usually used in the dashboard to remind superusers in case deployed behind reverse-proxy) + possibleProxyHeader := "" + headersToCheck := append( + slices.Clone(e.App.Settings().TrustedProxy.Headers), + // common proxy headers + "CF-Connecting-IP", "Fly-Client-IP", "X‑Forwarded-For", + ) + for _, header := range headersToCheck { + if e.Request.Header.Get(header) != "" { + possibleProxyHeader = header + break + } + } + resp.Data["possibleProxyHeader"] = possibleProxyHeader + } else { + resp.Data = map[string]any{} // ensure that it is returned as object + } + + return e.JSON(http.StatusOK, resp) } diff --git a/apis/health_test.go b/apis/health_test.go index 6afed6bf..bff9fed9 100644 --- a/apis/health_test.go +++ b/apis/health_test.go @@ -12,21 +12,56 @@ func TestHealthAPI(t *testing.T) { scenarios := []tests.ApiScenario{ { - Name: "HEAD health status", - Method: http.MethodHead, - Url: "/api/health", + Name: "GET health status (guest)", + Method: http.MethodGet, // automatically matches also HEAD as a side-effect of the Go std mux + URL: "/api/health", ExpectedStatus: 200, + ExpectedContent: []string{ + `"code":200`, + `"data":{}`, + }, + NotExpectedContent: []string{ + "canBackup", + "realIP", + "possibleProxyHeader", + }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "GET health status", - Method: http.MethodGet, - Url: "/api/health", + Name: "GET health status (regular user)", + Method: http.MethodGet, + URL: "/api/health", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"code":200`, + `"data":{}`, + }, + NotExpectedContent: []string{ + "canBackup", + "realIP", + "possibleProxyHeader", + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "GET health status (superuser)", + Method: http.MethodGet, + URL: "/api/health", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, ExpectedStatus: 200, ExpectedContent: []string{ `"code":200`, `"data":{`, `"canBackup":true`, + `"realIP"`, + `"possibleProxyHeader"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } diff --git a/apis/logs.go b/apis/logs.go index 34b7aaee..9f924921 100644 --- a/apis/logs.go +++ b/apis/logs.go @@ -3,79 +3,71 @@ package apis import ( "net/http" - "github.com/labstack/echo/v5" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/search" ) // bindLogsApi registers the request logs api endpoints. -func bindLogsApi(app core.App, rg *echo.Group) { - api := logsApi{app: app} - - subGroup := rg.Group("/logs", RequireAdminAuth()) - subGroup.GET("", api.list) - subGroup.GET("/stats", api.stats) - subGroup.GET("/:id", api.view) -} - -type logsApi struct { - app core.App +func bindLogsApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + sub := rg.Group("/logs").Bind(RequireSuperuserAuth(), SkipSuccessActivityLog()) + sub.GET("", logsList) + sub.GET("/stats", logsStats) + sub.GET("/{id}", logsView) } var logFilterFields = []string{ - "rowid", "id", "created", "updated", - "level", "message", "data", + "id", "created", "level", "message", "data", `^data\.[\w\.\:]*\w+$`, } -func (api *logsApi) list(c echo.Context) error { +func logsList(e *core.RequestEvent) error { fieldResolver := search.NewSimpleFieldResolver(logFilterFields...) result, err := search.NewProvider(fieldResolver). - Query(api.app.LogsDao().LogQuery()). - ParseAndExec(c.QueryParams().Encode(), &[]*models.Log{}) + Query(e.App.AuxModelQuery(&core.Log{})). + ParseAndExec(e.Request.URL.Query().Encode(), &[]*core.Log{}) if err != nil { - return NewBadRequestError("", err) + return e.BadRequestError("", err) } - return c.JSON(http.StatusOK, result) + return e.JSON(http.StatusOK, result) } -func (api *logsApi) stats(c echo.Context) error { +func logsStats(e *core.RequestEvent) error { fieldResolver := search.NewSimpleFieldResolver(logFilterFields...) - filter := c.QueryParam(search.FilterQueryParam) + filter := e.Request.URL.Query().Get(search.FilterQueryParam) var expr dbx.Expression if filter != "" { var err error expr, err = search.FilterData(filter).BuildExpr(fieldResolver) if err != nil { - return NewBadRequestError("Invalid filter format.", err) + return e.BadRequestError("Invalid filter format.", err) } } - stats, err := api.app.LogsDao().LogsStats(expr) + stats, err := e.App.LogsStats(expr) if err != nil { - return NewBadRequestError("Failed to generate logs stats.", err) + return e.BadRequestError("Failed to generate logs stats.", err) } - return c.JSON(http.StatusOK, stats) + return e.JSON(http.StatusOK, stats) } -func (api *logsApi) view(c echo.Context) error { - id := c.PathParam("id") +func logsView(e *core.RequestEvent) error { + id := e.Request.PathValue("id") if id == "" { - return NewNotFoundError("", nil) + return e.NotFoundError("", nil) } - log, err := api.app.LogsDao().FindLogById(id) + log, err := e.App.FindLogById(id) if err != nil || log == nil { - return NewNotFoundError("", err) + return e.NotFoundError("", err) } - return c.JSON(http.StatusOK, log) + return e.JSON(http.StatusOK, log) } diff --git a/apis/logs_test.go b/apis/logs_test.go index 74d46bdc..18b9d4d5 100644 --- a/apis/logs_test.go +++ b/apis/logs_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" ) @@ -15,29 +15,31 @@ func TestLogsList(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/logs", + URL: "/api/logs", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/logs", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/logs", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin", + Name: "authorized as superuser", Method: http.MethodGet, - Url: "/api/logs", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, @@ -50,16 +52,17 @@ func TestLogsList(t *testing.T) { `"id":"873f2133-9f38-44fb-bf82-c8f53b310d91"`, `"id":"f2133873-44fb-9f38-bf82-c918f53b310d"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin + filter", + Name: "authorized as superuser + filter", Method: http.MethodGet, - Url: "/api/logs?filter=data.status>200", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs?filter=data.status>200", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, @@ -71,6 +74,7 @@ func TestLogsList(t *testing.T) { `"items":[{`, `"id":"f2133873-44fb-9f38-bf82-c918f53b310d"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -86,44 +90,47 @@ func TestLogView(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", + URL: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (nonexisting request log)", + Name: "authorized as superuser (nonexisting request log)", Method: http.MethodGet, - Url: "/api/logs/missing1-9f38-44fb-bf82-c8f53b310d91", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs/missing1-9f38-44fb-bf82-c8f53b310d91", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (existing request log)", + Name: "authorized as superuser (existing request log)", Method: http.MethodGet, - Url: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs/873f2133-9f38-44fb-bf82-c8f53b310d91", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, @@ -131,6 +138,7 @@ func TestLogView(t *testing.T) { ExpectedContent: []string{ `"id":"873f2133-9f38-44fb-bf82-c8f53b310d91"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -146,52 +154,54 @@ func TestLogsStats(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/logs/stats", + URL: "/api/logs/stats", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/logs/stats", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/logs/stats", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin", + Name: "authorized as superuser", Method: http.MethodGet, - Url: "/api/logs/stats", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs/stats", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 200, ExpectedContent: []string{ - `[{"total":1,"date":"2022-05-01 10:00:00.000Z"},{"total":1,"date":"2022-05-02 10:00:00.000Z"}]`, + `[{"date":"2022-05-01 10:00:00.000Z","total":1},{"date":"2022-05-02 10:00:00.000Z","total":1}]`, }, }, { - Name: "authorized as admin + filter", + Name: "authorized as superuser + filter", Method: http.MethodGet, - Url: "/api/logs/stats?filter=data.status>200", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/logs/stats?filter=data.status>200", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - if err := tests.MockLogsData(app); err != nil { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubLogsData(app); err != nil { t.Fatal(err) } }, ExpectedStatus: 200, ExpectedContent: []string{ - `[{"total":1,"date":"2022-05-02 10:00:00.000Z"}]`, + `[{"date":"2022-05-02 10:00:00.000Z","total":1}]`, }, }, } diff --git a/apis/middlewares.go b/apis/middlewares.go index f5798ab3..e4adbfa9 100644 --- a/apis/middlewares.go +++ b/apis/middlewares.go @@ -3,303 +3,321 @@ package apis import ( "fmt" "log/slog" - "net" "net/http" "net/url" + "slices" "strings" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tokens" + "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/routine" - "github.com/pocketbase/pocketbase/tools/security" "github.com/spf13/cast" ) -// Common request context keys used by the middlewares and api handlers. +// Common request event store keys used by the middlewares and api handlers. const ( - ContextAdminKey string = "admin" - ContextAuthRecordKey string = "authRecord" - ContextCollectionKey string = "collection" - ContextExecStartKey string = "execStart" + RequestEventKeyLogMeta = "pbLogMeta" // extra data to store with the request activity log + + requestEventKeyExecStart = "__execStart" // the value must be time.Time + requestEventKeySkipSuccessActivityLog = "__skipSuccessActivityLogger" // the value must be bool +) + +const ( + DefaultWWWRedirectMiddlewarePriority = -99999 + DefaultWWWRedirectMiddlewareId = "pbWWWRedirect" + + DefaultActivityLoggerMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 30 + DefaultActivityLoggerMiddlewareId = "pbActivityLogger" + DefaultSkipSuccessActivityLogMiddlewareId = "pbSkipSuccessActivityLog" + DefaultEnableAuthIdActivityLog = "pbEnableAuthIdActivityLog" + + DefaultLoadAuthTokenMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 20 + DefaultLoadAuthTokenMiddlewareId = "pbLoadAuthToken" + + DefaultSecurityHeadersMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 10 + DefaultSecurityHeadersMiddlewareId = "pbSecurityHeaders" + + DefaultRequireGuestOnlyMiddlewareId = "pbRequireGuestOnly" + DefaultRequireAuthMiddlewareId = "pbRequireAuth" + DefaultRequireSuperuserAuthMiddlewareId = "pbRequireSuperuserAuth" + DefaultRequireSuperuserAuthOnlyIfAnyMiddlewareId = "pbRequireSuperuserAuthOnlyIfAny" + DefaultRequireSuperuserOrOwnerAuthMiddlewareId = "pbRequireSuperuserOrOwnerAuth" + DefaultRequireSameCollectionContextAuthMiddlewareId = "pbRequireSameCollectionContextAuth" ) // RequireGuestOnly middleware requires a request to NOT have a valid // Authorization header. // -// This middleware is the opposite of [apis.RequireAdminOrRecordAuth()]. -func RequireGuestOnly() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - err := NewBadRequestError("The request can be accessed only by guests.", nil) - - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record != nil { - return err +// This middleware is the opposite of [apis.RequireAuth()]. +func RequireGuestOnly() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireGuestOnlyMiddlewareId, + Func: func(e *core.RequestEvent) error { + if e.Auth != nil { + return router.NewBadRequestError("The request can be accessed only by guests.", nil) } - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin != nil { - return err - } - - return next(c) - } + return e.Next() + }, } } -// RequireRecordAuth middleware requires a request to have -// a valid record auth Authorization header. +// RequireAuth middleware requires a request to have a valid record Authorization header. // // The auth record could be from any collection. -// -// You can further filter the allowed record auth collections by -// specifying their names. +// You can further filter the allowed record auth collections by specifying their names. // // Example: // -// apis.RequireRecordAuth() -// -// Or: -// -// apis.RequireRecordAuth("users", "supervisors") -// -// To restrict the auth record only to the loaded context collection, -// use [apis.RequireSameContextRecordAuth()] instead. -func RequireRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record == nil { - return NewUnauthorizedError("The request requires valid record authorization token to be set.", nil) - } - - // check record collection name - if len(optCollectionNames) > 0 && !list.ExistInSlice(record.Collection().Name, optCollectionNames) { - return NewForbiddenError("The authorized record model is not allowed to perform this action.", nil) - } - - return next(c) - } +// apis.RequireAuth() // any auth collection +// apis.RequireAuth("_superusers", "users") // only the listed auth collections +func RequireAuth(optCollectionNames ...string) *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireAuthMiddlewareId, + Func: requireAuth(optCollectionNames...), } } -// RequireSameContextRecordAuth middleware requires a request to have -// a valid record Authorization header. -// -// The auth record must be from the same collection already loaded in the context. -func RequireSameContextRecordAuth() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record == nil { - return NewUnauthorizedError("The request requires valid record authorization token to be set.", nil) - } - - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil || record.Collection().Id != collection.Id { - return NewForbiddenError(fmt.Sprintf("The request requires auth record from %s collection.", record.Collection().Name), nil) - } - - return next(c) +func requireAuth(optCollectionNames ...string) hook.HandlerFunc[*core.RequestEvent] { + return func(e *core.RequestEvent) error { + if e.Auth == nil { + return e.UnauthorizedError("The request requires valid record authorization token.", nil) } + + // check record collection name + if len(optCollectionNames) > 0 && !slices.Contains(optCollectionNames, e.Auth.Collection().Name) { + return e.ForbiddenError("The authorized record is not allowed to perform this action.", nil) + } + + return e.Next() } } -// RequireAdminAuth middleware requires a request to have -// a valid admin Authorization header. -func RequireAdminAuth() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin == nil { - return NewUnauthorizedError("The request requires valid admin authorization token to be set.", nil) - } - - return next(c) - } +// RequireSuperuserAuth middleware requires a request to have +// a valid superuser Authorization header. +func RequireSuperuserAuth() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireSuperuserAuthMiddlewareId, + Func: requireAuth(core.CollectionNameSuperusers), } } -// RequireAdminAuthOnlyIfAny middleware requires a request to have -// a valid admin Authorization header ONLY if the application has -// at least 1 existing Admin model. -func RequireAdminAuthOnlyIfAny(app core.App) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin != nil { - return next(c) +// RequireSuperuserAuthOnlyIfAny middleware requires a request to have +// a valid superuser Authorization header ONLY if the application has +// at least 1 existing superuser. +func RequireSuperuserAuthOnlyIfAny() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireSuperuserAuthOnlyIfAnyMiddlewareId, + Func: func(e *core.RequestEvent) error { + if e.HasSuperuserAuth() { + return e.Next() } - totalAdmins, err := app.Dao().TotalAdmins() + totalSuperusers, err := e.App.CountRecords(core.CollectionNameSuperusers) if err != nil { - return NewBadRequestError("Failed to fetch admins info.", err) + return e.InternalServerError("Failed to fetch superusers info.", err) } - if totalAdmins == 0 { - return next(c) + if totalSuperusers == 0 { + return e.Next() } - return NewUnauthorizedError("The request requires valid admin authorization token to be set.", nil) - } + return requireAuth(core.CollectionNameSuperusers)(e) + }, } } -// RequireAdminOrRecordAuth middleware requires a request to have -// a valid admin or record Authorization header set. +// RequireSuperuserOrOwnerAuth middleware requires a request to have +// a valid superuser or regular record owner Authorization header set. // -// You can further filter the allowed auth record collections by providing their names. -// -// This middleware is the opposite of [apis.RequireGuestOnly()]. -func RequireAdminOrRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - - if admin == nil && record == nil { - return NewUnauthorizedError("The request requires admin or record authorization token to be set.", nil) - } - - if record != nil && len(optCollectionNames) > 0 && !list.ExistInSlice(record.Collection().Name, optCollectionNames) { - return NewForbiddenError("The authorized record model is not allowed to perform this action.", nil) - } - - return next(c) - } - } -} - -// RequireAdminOrOwnerAuth middleware requires a request to have -// a valid admin or auth record owner Authorization header set. -// -// This middleware is similar to [apis.RequireAdminOrRecordAuth()] but +// This middleware is similar to [apis.RequireAuth()] but // for the auth record token expects to have the same id as the path -// parameter ownerIdParam (default to "id" if empty). -func RequireAdminOrOwnerAuth(ownerIdParam string) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin != nil { - return next(c) +// parameter ownerIdPathParam (default to "id" if empty). +func RequireSuperuserOrOwnerAuth(ownerIdPathParam string) *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireSuperuserOrOwnerAuthMiddlewareId, + Func: func(e *core.RequestEvent) error { + if e.Auth == nil { + return e.UnauthorizedError("The request requires superuser or record authorization token.", nil) } - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record == nil { - return NewUnauthorizedError("The request requires admin or record authorization token to be set.", nil) + if e.Auth.IsSuperuser() { + return e.Next() } - if ownerIdParam == "" { - ownerIdParam = "id" + if ownerIdPathParam == "" { + ownerIdPathParam = "id" } - ownerId := c.PathParam(ownerIdParam) + ownerId := e.Request.PathValue(ownerIdPathParam) - // note: it is "safe" to compare only the record id since the auth - // record ids are treated as unique across all auth collections - if record.Id != ownerId { - return NewForbiddenError("You are not allowed to perform this request.", nil) + // note: it is considered "safe" to compare only the record id + // since the auth record ids are treated as unique across all auth collections + if e.Auth.Id != ownerId { + return e.ForbiddenError("You are not allowed to perform this request.", nil) } - return next(c) - } + return e.Next() + }, } } -// LoadAuthContext middleware reads the Authorization request header -// and loads the token related record or admin instance into the -// request's context. +// RequireSameCollectionContextAuth middleware requires a request to have +// a valid record Authorization header and the auth record's collection to +// match the one from the route path parameter (default to "collection" if collectionParam is empty). +func RequireSameCollectionContextAuth(collectionPathParam string) *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRequireSameCollectionContextAuthMiddlewareId, + Func: func(e *core.RequestEvent) error { + if e.Auth == nil { + return e.UnauthorizedError("The request requires valid record authorization token.", nil) + } + + if collectionPathParam == "" { + collectionPathParam = "collection" + } + + collection, _ := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue(collectionPathParam)) + if collection == nil || e.Auth.Collection().Id != collection.Id { + return e.ForbiddenError(fmt.Sprintf("The request requires auth record from %s collection.", e.Auth.Collection().Name), nil) + } + + return e.Next() + }, + } +} + +// loadAuthToken attempts to load the auth context based on the "Authorization: TOKEN" header value. // -// This middleware is expected to be already registered by default for all routes. -func LoadAuthContext(app core.App) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - token := c.Request().Header.Get("Authorization") +// This middleware does nothing in case of missing, invalid or expired token. +// +// This middleware is registered by default for all routes. +// +// Note: We don't throw an error on invalid or expired token to allow +// users to extend with their own custom handling in external middleware(s). +func loadAuthToken() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultLoadAuthTokenMiddlewareId, + Priority: DefaultLoadAuthTokenMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + token := getAuthTokenFromRequest(e) if token == "" { - return next(c) + return e.Next() } - // the schema is not required and it is only for - // compatibility with the defaults of some HTTP clients - token = strings.TrimPrefix(token, "Bearer ") - - claims, _ := security.ParseUnverifiedJWT(token) - tokenType := cast.ToString(claims["type"]) - - switch tokenType { - case tokens.TypeAdmin: - admin, err := app.Dao().FindAdminByToken( - token, - app.Settings().AdminAuthToken.Secret, - ) - if err == nil && admin != nil { - c.Set(ContextAdminKey, admin) - } - case tokens.TypeAuthRecord: - record, err := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordAuthToken.Secret, - ) - if err == nil && record != nil { - c.Set(ContextAuthRecordKey, record) - } + record, err := e.App.FindAuthRecordByToken(token, core.TokenTypeAuth) + if err != nil { + e.App.Logger().Debug("loadAuthToken failure", "error", err) + } else if record != nil { + e.Auth = record } - return next(c) - } + return e.Next() + }, } } -// LoadCollectionContext middleware finds the collection with related -// path identifier and loads it into the request context. +func getAuthTokenFromRequest(e *core.RequestEvent) string { + token := e.Request.Header.Get("Authorization") + if token != "" { + // the schema prefix is not required and it is only for + // compatibility with the defaults of some HTTP clients + token = strings.TrimPrefix(token, "Bearer ") + } + return token +} + +// wwwRedirect performs www->non-www redirect(s) if the request host +// matches with one of the values in redirectHosts. // -// Set optCollectionTypes to further filter the found collection by its type. -func LoadCollectionContext(app core.App, optCollectionTypes ...string) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if param := c.PathParam("collection"); param != "" { - collection, err := core.FindCachedCollectionByNameOrId(app, param) - if err != nil || collection == nil { - return NewNotFoundError("", err) - } +// This middleware is registered by default on Serve for all routes. +func wwwRedirect(redirectHosts []string) *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultWWWRedirectMiddlewareId, + Priority: DefaultWWWRedirectMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + host := e.Request.Host - if len(optCollectionTypes) > 0 && !list.ExistInSlice(collection.Type, optCollectionTypes) { - return NewBadRequestError("Unsupported collection type.", nil) - } - - c.Set(ContextCollectionKey, collection) + if strings.HasPrefix(host, "www.") && list.ExistInSlice(host, redirectHosts) { + return e.Redirect( + http.StatusTemporaryRedirect, + (e.Request.URL.Scheme + "://" + host[4:] + e.Request.RequestURI), + ) } - return next(c) - } + return e.Next() + }, } } -// ActivityLogger middleware takes care to save the request information +// securityHeaders middleware adds common security headers to the response. +// +// This middleware is registered by default for all routes. +func securityHeaders() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultSecurityHeadersMiddlewareId, + Priority: DefaultSecurityHeadersMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + e.Response.Header().Set("X-XSS-Protection", "1; mode=block") + e.Response.Header().Set("X-Content-Type-Options", "nosniff") + e.Response.Header().Set("X-Frame-Options", "SAMEORIGIN") + + // @todo consider a default HSTS? + // (see also https://webkit.org/blog/8146/protecting-against-hsts-abuse/) + + return e.Next() + }, + } +} + +// SkipSuccessActivityLog is a helper middleware that instructs the global +// activity logger to log only requests that have failed/returned an error. +func SkipSuccessActivityLog() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultSkipSuccessActivityLogMiddlewareId, + Func: func(e *core.RequestEvent) error { + e.Set(requestEventKeySkipSuccessActivityLog, true) + return e.Next() + }, + } +} + +// activityLogger middleware takes care to save the request information // into the logs database. // +// This middleware is registered by default for all routes. +// // The middleware does nothing if the app logs retention period is zero // (aka. app.Settings().Logs.MaxDays = 0). -func ActivityLogger(app core.App) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if err := next(c); err != nil { - return err - } +// +// Users can attach the [apis.SkipSuccessActivityLog()] middleware if +// you want to log only the failed requests. +func activityLogger() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultActivityLoggerMiddlewareId, + Priority: DefaultActivityLoggerMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + e.Set(requestEventKeyExecStart, time.Now()) - logRequest(app, c, nil) + err := e.Next() - return nil - } + logRequest(e, err) + + return err + }, } } -func logRequest(app core.App, c echo.Context, err *ApiError) { +func logRequest(event *core.RequestEvent, err error) { // no logs retention - if app.Settings().Logs.MaxDays == 0 { + if event.App.Settings().Logs.MaxDays == 0 { + return + } + + // the non-error route has explicitly disabled the activity logger + if err == nil && event.Get(requestEventKeySkipSuccessActivityLog) != nil { return } @@ -307,32 +325,31 @@ func logRequest(app core.App, c echo.Context, err *ApiError) { attrs = append(attrs, slog.String("type", "request")) - started := cast.ToTime(c.Get(ContextExecStartKey)) + started := cast.ToTime(event.Get(requestEventKeyExecStart)) if !started.IsZero() { attrs = append(attrs, slog.Float64("execTime", float64(time.Since(started))/float64(time.Millisecond))) } - httpRequest := c.Request() - httpResponse := c.Response() - method := strings.ToUpper(httpRequest.Method) - status := httpResponse.Status - requestUri := httpRequest.URL.RequestURI() + if meta := event.Get(RequestEventKeyLogMeta); meta != nil { + attrs = append(attrs, slog.Any("meta", meta)) + } + + status := event.Status() + method := cutStr(strings.ToUpper(event.Request.Method), 50) + requestUri := cutStr(event.Request.URL.RequestURI(), 3000) // parse the request error if err != nil { - status = err.Code - attrs = append( - attrs, - slog.String("error", err.Message), - slog.Any("details", err.RawData()), - ) - } - - requestAuth := models.RequestAuthGuest - if c.Get(ContextAuthRecordKey) != nil { - requestAuth = models.RequestAuthRecord - } else if c.Get(ContextAdminKey) != nil { - requestAuth = models.RequestAuthAdmin + if apiErr, ok := err.(*router.ApiError); ok { + status = apiErr.Status + attrs = append( + attrs, + slog.String("error", apiErr.Message), + slog.Any("details", apiErr.RawData()), + ) + } else { + attrs = append(attrs, slog.String("error", err.Error())) + } } attrs = append( @@ -340,17 +357,33 @@ func logRequest(app core.App, c echo.Context, err *ApiError) { slog.String("url", requestUri), slog.String("method", method), slog.Int("status", status), - slog.String("auth", requestAuth), - slog.String("referer", httpRequest.Referer()), - slog.String("userAgent", httpRequest.UserAgent()), + slog.String("referer", cutStr(event.Request.Referer(), 2000)), + slog.String("userAgent", cutStr(event.Request.UserAgent(), 2000)), ) - if app.Settings().Logs.LogIp { - ip, _, _ := net.SplitHostPort(httpRequest.RemoteAddr) + if event.Auth != nil { + attrs = append(attrs, slog.String("auth", event.Auth.Collection().Name)) + + if event.App.Settings().Logs.LogAuthId { + attrs = append(attrs, slog.String("authId", event.Auth.Id)) + } + } else { + attrs = append(attrs, slog.String("auth", "")) + } + + if event.App.Settings().Logs.LogIP { + var userIP string + if len(event.App.Settings().TrustedProxy.Headers) > 0 { + userIP = event.RealIP() + } else { + // fallback to the legacy behavior (it is "safe" since it is only for log purposes) + userIP = cutStr(event.UnsafeRealIP(), 50) + } + attrs = append( attrs, - slog.String("userIp", realUserIp(httpRequest, ip)), - slog.String("remoteIp", ip), + slog.String("userIP", userIP), + slog.String("remoteIP", event.RemoteIP()), ) } @@ -358,64 +391,23 @@ func logRequest(app core.App, c echo.Context, err *ApiError) { routine.FireAndForget(func() { message := method + " " - if escaped, err := url.PathUnescape(requestUri); err == nil { + if escaped, unescapeErr := url.PathUnescape(requestUri); unescapeErr == nil { message += escaped } else { message += requestUri } if err != nil { - app.Logger().Error(message, attrs...) + event.App.Logger().Error(message, attrs...) } else { - app.Logger().Info(message, attrs...) + event.App.Logger().Info(message, attrs...) } }) } -// Returns the "real" user IP from common proxy headers (or fallbackIp if none is found). -// -// The returned IP value shouldn't be trusted if not behind a trusted reverse proxy! -func realUserIp(r *http.Request, fallbackIp string) string { - if ip := r.Header.Get("CF-Connecting-IP"); ip != "" { - return ip - } - - if ip := r.Header.Get("Fly-Client-IP"); ip != "" { - return ip - } - - if ip := r.Header.Get("X-Real-IP"); ip != "" { - return ip - } - - if ipsList := r.Header.Get("X-Forwarded-For"); ipsList != "" { - // extract the first non-empty leftmost-ish ip - ips := strings.Split(ipsList, ",") - for _, ip := range ips { - ip = strings.TrimSpace(ip) - if ip != "" { - return ip - } - } - } - - return fallbackIp -} - -// @todo consider removing as this may no longer be needed due to the custom rest.MultiBinder. -// -// eagerRequestInfoCache ensures that the request data is cached in the request -// context to allow reading for example the json request body data more than once. -func eagerRequestInfoCache(app core.App) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - switch c.Request().Method { - // currently we are eagerly caching only the requests with body - case "POST", "PUT", "PATCH", "DELETE": - RequestInfo(c) - } - - return next(c) - } +func cutStr(str string, max int) string { + if len(str) > max { + return str[:max] + "..." } + return str } diff --git a/apis/middlewares_body_limit.go b/apis/middlewares_body_limit.go new file mode 100644 index 00000000..643ec59d --- /dev/null +++ b/apis/middlewares_body_limit.go @@ -0,0 +1,123 @@ +package apis + +import ( + "io" + "net/http" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" +) + +var ErrRequestEntityTooLarge = router.NewApiError(http.StatusRequestEntityTooLarge, "Request entity too large", nil) + +const DefaultMaxBodySize int64 = 32 << 20 + +const ( + DefaultBodyLimitMiddlewareId = "pbBodyLimit" + DefaultBodyLimitMiddlewarePriority = DefaultRateLimitMiddlewarePriority + 10 +) + +// 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, +// it sends 413 error response. +func BodyLimit(limitBytes int64) *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultBodyLimitMiddlewareId, + Priority: DefaultBodyLimitMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + err := applyBodyLimit(e, limitBytes) + if err != nil { + return err + } + + return e.Next() + }, + } +} + +func dynamicCollectionBodyLimit(collectionPathParam string) *hook.Handler[*core.RequestEvent] { + if collectionPathParam == "" { + collectionPathParam = "collection" + } + + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultBodyLimitMiddlewareId, + Priority: DefaultBodyLimitMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue(collectionPathParam)) + if err != nil { + return e.NotFoundError("Missing or invalid collection context.", err) + } + + limitBytes := DefaultMaxBodySize + if !collection.IsView() { + for _, f := range collection.Fields { + if calc, ok := f.(core.MaxBodySizeCalculator); ok { + limitBytes += calc.CalculateMaxBodySize() + } + } + } + + err = applyBodyLimit(e, limitBytes) + if err != nil { + return err + } + + return e.Next() + }, + } +} + +func applyBodyLimit(e *core.RequestEvent, limitBytes int64) error { + // no limit + if limitBytes <= 0 { + return nil + } + + // optimistically check the submitted request content length + if e.Request.ContentLength > limitBytes { + return ErrRequestEntityTooLarge + } + + // replace the request body + // + // note: we don't use sync.Pool since the size of the elements could vary too much + // and it might not be efficient (see https://github.com/golang/go/issues/23199) + e.Request.Body = &limitedReader{ReadCloser: e.Request.Body, limit: limitBytes} + + return nil +} + +type limitedReader struct { + io.ReadCloser + limit int64 + totalRead int64 +} + +func (r *limitedReader) Read(b []byte) (int, error) { + n, err := r.ReadCloser.Read(b) + if err != nil { + return n, err + } + + r.totalRead += int64(n) + if r.totalRead > r.limit { + return n, ErrRequestEntityTooLarge + } + + return n, nil +} + +func (r *limitedReader) Reread() { + rr, ok := r.ReadCloser.(router.Rereader) + if ok { + rr.Reread() + } +} diff --git a/apis/middlewares_body_limit_test.go b/apis/middlewares_body_limit_test.go new file mode 100644 index 00000000..8e04e724 --- /dev/null +++ b/apis/middlewares_body_limit_test.go @@ -0,0 +1,60 @@ +package apis_test + +import ( + "bytes" + "fmt" + "net/http/httptest" + "testing" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestBodyLimitMiddleware(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + pbRouter, err := apis.NewRouter(app) + if err != nil { + t.Fatal(err) + } + pbRouter.POST("/a", func(e *core.RequestEvent) error { + return e.String(200, "a") + }) // default global BodyLimit check + + pbRouter.POST("/b", func(e *core.RequestEvent) error { + return e.String(200, "b") + }).Bind(apis.BodyLimit(20)) + + mux, err := pbRouter.BuildMux() + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + url string + size int64 + expectedStatus int + }{ + {"/a", 21, 200}, + {"/a", apis.DefaultMaxBodySize + 1, 413}, + {"/b", 20, 200}, + {"/b", 21, 413}, + } + + for _, s := range scenarios { + t.Run(fmt.Sprintf("%s_%d", s.url, s.size), func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest("POST", s.url, bytes.NewReader(make([]byte, s.size))) + mux.ServeHTTP(rec, req) + + result := rec.Result() + defer result.Body.Close() + + if result.StatusCode != s.expectedStatus { + t.Fatalf("Expected response status %d, got %d", s.expectedStatus, result.StatusCode) + } + }) + } +} diff --git a/apis/middlewares_cors.go b/apis/middlewares_cors.go new file mode 100644 index 00000000..635e4b1d --- /dev/null +++ b/apis/middlewares_cors.go @@ -0,0 +1,307 @@ +package apis + +// ------------------------------------------------------------------- +// This middleware is ported from echo/middleware to minimize the breaking +// changes and differences in the API behavior from earlier PocketBase versions +// (https://github.com/labstack/echo/blob/ec5b858dab6105ab4c3ed2627d1ebdfb6ae1ecb8/middleware/cors.go). +// +// I doubt that this would matter for most cases, but the only major difference +// is that for non-supported routes this middleware doesn't return 405 and fallbacks +// to the default catch-all PocketBase route (aka. returns 404) to avoid +// the extra overhead of further hijacking and wrapping the Go default mux +// (https://github.com/golang/go/issues/65648#issuecomment-1955328807). +// ------------------------------------------------------------------- + +import ( + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" +) + +const ( + DefaultCorsMiddlewareId = "pbCors" + DefaultCorsMiddlewarePriority = DefaultActivityLoggerMiddlewarePriority - 1 // before the activity logger and rate limit so that OPTIONS preflight requests are not counted +) + +// CORSConfig defines the config for CORS middleware. +type CORSConfig struct { + // AllowOrigins determines the value of the Access-Control-Allow-Origin + // response header. This header defines a list of origins that may access the + // resource. The wildcard characters '*' and '?' are supported and are + // converted to regex fragments '.*' and '.' accordingly. + // + // Security: use extreme caution when handling the origin, and carefully + // validate any logic. Remember that attackers may register hostile domain names. + // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Optional. Default value []string{"*"}. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + AllowOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the + // origin as an argument and returns true if allowed or false otherwise. If + // an error is returned, it is returned by the handler. If this option is + // set, AllowOrigins is ignored. + // + // Security: use extreme caution when handling the origin, and carefully + // validate any logic. Remember that attackers may register hostile domain names. + // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Optional. + AllowOriginFunc func(origin string) (bool, error) + + // AllowMethods determines the value of the Access-Control-Allow-Methods + // response header. This header specified the list of methods allowed when + // accessing the resource. This is used in response to a preflight request. + // + // Optional. Default value DefaultCORSConfig.AllowMethods. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + AllowMethods []string + + // AllowHeaders determines the value of the Access-Control-Allow-Headers + // response header. This header is used in response to a preflight request to + // indicate which HTTP headers can be used when making the actual request. + // + // Optional. Default value []string{}. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + AllowHeaders []string + + // AllowCredentials determines the value of the + // Access-Control-Allow-Credentials response header. This header indicates + // whether or not the response to the request can be exposed when the + // credentials mode (Request.credentials) is true. When used as part of a + // response to a preflight request, this indicates whether or not the actual + // request can be made using credentials. See also + // [MDN: Access-Control-Allow-Credentials]. + // + // Optional. Default value false, in which case the header is not set. + // + // Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`. + // See "Exploiting CORS misconfigurations for Bitcoins and bounties", + // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + AllowCredentials bool + + // UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials + // flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header. + // + // This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties) + // attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject. + // + // Optional. Default value is false. + UnsafeWildcardOriginWithAllowCredentials bool + + // ExposeHeaders determines the value of Access-Control-Expose-Headers, which + // defines a list of headers that clients are allowed to access. + // + // Optional. Default value []string{}, in which case the header is not set. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header + ExposeHeaders []string + + // MaxAge determines the value of the Access-Control-Max-Age response header. + // This header indicates how long (in seconds) the results of a preflight + // request can be cached. + // The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response. + // + // Optional. Default value 0 - meaning header is not sent. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + MaxAge int +} + +// DefaultCORSConfig is the default CORS middleware config. +var DefaultCORSConfig = CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, +} + +// CORSWithConfig returns a CORS middleware with config. +func CORSWithConfig(config CORSConfig) hook.HandlerFunc[*core.RequestEvent] { + // Defaults + if len(config.AllowOrigins) == 0 { + config.AllowOrigins = DefaultCORSConfig.AllowOrigins + } + if len(config.AllowMethods) == 0 { + config.AllowMethods = DefaultCORSConfig.AllowMethods + } + + allowOriginPatterns := []string{} + for _, origin := range config.AllowOrigins { + pattern := regexp.QuoteMeta(origin) + pattern = strings.ReplaceAll(pattern, "\\*", ".*") + pattern = strings.ReplaceAll(pattern, "\\?", ".") + pattern = "^" + pattern + "$" + allowOriginPatterns = append(allowOriginPatterns, pattern) + } + + allowMethods := strings.Join(config.AllowMethods, ",") + allowHeaders := strings.Join(config.AllowHeaders, ",") + exposeHeaders := strings.Join(config.ExposeHeaders, ",") + + maxAge := "0" + if config.MaxAge > 0 { + maxAge = strconv.Itoa(config.MaxAge) + } + + return func(e *core.RequestEvent) error { + req := e.Request + res := e.Response + origin := req.Header.Get("Origin") + allowOrigin := "" + + 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 + + // 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) + } + + 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 { + 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 { + 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) + } +} + +func matchScheme(domain, pattern string) bool { + didx := strings.Index(domain, ":") + pidx := strings.Index(pattern, ":") + return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx] +} + +// matchSubdomain compares authority with wildcard +func matchSubdomain(domain, pattern string) bool { + if !matchScheme(domain, pattern) { + return false + } + didx := strings.Index(domain, "://") + pidx := strings.Index(pattern, "://") + if didx == -1 || pidx == -1 { + return false + } + domAuth := domain[didx+3:] + // to avoid long loop by invalid long domain + if len(domAuth) > 253 { + return false + } + patAuth := pattern[pidx+3:] + + domComp := strings.Split(domAuth, ".") + patComp := strings.Split(patAuth, ".") + for i := len(domComp)/2 - 1; i >= 0; i-- { + opp := len(domComp) - 1 - i + domComp[i], domComp[opp] = domComp[opp], domComp[i] + } + for i := len(patComp)/2 - 1; i >= 0; i-- { + opp := len(patComp) - 1 - i + patComp[i], patComp[opp] = patComp[opp], patComp[i] + } + + for i, v := range domComp { + if len(patComp) <= i { + return false + } + p := patComp[i] + if p == "*" { + return true + } + if p != v { + return false + } + } + return false +} diff --git a/apis/middlewares_gzip.go b/apis/middlewares_gzip.go new file mode 100644 index 00000000..b778dded --- /dev/null +++ b/apis/middlewares_gzip.go @@ -0,0 +1,237 @@ +package apis + +// ------------------------------------------------------------------- +// This middleware is ported from echo/middleware to minimize the breaking +// changes and differences in the API behavior from earlier PocketBase versions +// (https://github.com/labstack/echo/blob/ec5b858dab6105ab4c3ed2627d1ebdfb6ae1ecb8/middleware/compress.go). +// ------------------------------------------------------------------- + +import ( + "bufio" + "bytes" + "compress/gzip" + "errors" + "io" + "net" + "net/http" + "strings" + "sync" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" +) + +const ( + gzipScheme = "gzip" +) + +// GzipConfig defines the config for Gzip middleware. +type GzipConfig struct { + // Gzip compression level. + // Optional. Default value -1. + Level int + + // Length threshold before gzip compression is applied. + // Optional. Default value 0. + // + // Most of the time you will not need to change the default. Compressing + // a short response might increase the transmitted data because of the + // gzip format overhead. Compressing the response will also consume CPU + // and time on the server and the client (for decompressing). Depending on + // your use case such a threshold might be useful. + // + // See also: + // https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits + MinLength int +} + +// Gzip returns a middleware which compresses HTTP response using gzip compression scheme. +func Gzip() hook.HandlerFunc[*core.RequestEvent] { + return GzipWithConfig(GzipConfig{}) +} + +// GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme. +func GzipWithConfig(config GzipConfig) hook.HandlerFunc[*core.RequestEvent] { + if config.Level < -2 || config.Level > 9 { // these are consts: gzip.HuffmanOnly and gzip.BestCompression + panic(errors.New("invalid gzip level")) + } + if config.Level == 0 { + config.Level = -1 + } + if config.MinLength < 0 { + config.MinLength = 0 + } + + pool := sync.Pool{ + New: func() interface{} { + w, err := gzip.NewWriterLevel(io.Discard, config.Level) + if err != nil { + return err + } + return w + }, + } + + bpool := sync.Pool{ + New: func() interface{} { + b := &bytes.Buffer{} + return b + }, + } + + 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")) + } + + 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() + } +} + +type gzipResponseWriter struct { + http.ResponseWriter + io.Writer + buffer *bytes.Buffer + minLength int + code int + wroteHeader bool + wroteBody bool + minLengthExceeded bool +} + +func (w *gzipResponseWriter) WriteHeader(code int) { + w.Header().Del("Content-Length") // Issue echo#444 + + w.wroteHeader = true + + // Delay writing of the header until we know if we'll actually compress the response + w.code = code +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + if w.Header().Get("Content-Type") == "" { + w.Header().Set("Content-Type", http.DetectContentType(b)) + } + + w.wroteBody = true + + if !w.minLengthExceeded { + n, err := w.buffer.Write(b) + + if w.buffer.Len() >= w.minLength { + w.minLengthExceeded = true + + // The minimum length is exceeded, add Content-Encoding header and write the header + w.Header().Set("Content-Encoding", gzipScheme) + if w.wroteHeader { + w.ResponseWriter.WriteHeader(w.code) + } + + return w.Writer.Write(w.buffer.Bytes()) + } + + return n, err + } + + return w.Writer.Write(b) +} + +func (w *gzipResponseWriter) Flush() { + if !w.minLengthExceeded { + // Enforce compression because we will not know how much more data will come + w.minLengthExceeded = true + w.Header().Set("Content-Encoding", gzipScheme) + if w.wroteHeader { + w.ResponseWriter.WriteHeader(w.code) + } + + _, _ = w.Writer.Write(w.buffer.Bytes()) + } + + _ = w.Writer.(*gzip.Writer).Flush() + + _ = http.NewResponseController(w.ResponseWriter).Flush() +} + +func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return http.NewResponseController(w.ResponseWriter).Hijack() +} + +func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error { + rw := w.ResponseWriter + for { + switch p := rw.(type) { + case http.Pusher: + return p.Push(target, opts) + case router.RWUnwrapper: + rw = p.Unwrap() + default: + return http.ErrNotSupported + } + } +} + +func (w *gzipResponseWriter) ReadFrom(r io.Reader) (n int64, err error) { + if w.wroteHeader { + w.ResponseWriter.WriteHeader(w.code) + } + + rw := w.ResponseWriter + for { + switch rf := rw.(type) { + case io.ReaderFrom: + return rf.ReadFrom(r) + case router.RWUnwrapper: + rw = rf.Unwrap() + default: + return io.Copy(w.ResponseWriter, r) + } + } +} + +func (w *gzipResponseWriter) Unwrap() http.ResponseWriter { + return w.ResponseWriter +} diff --git a/apis/middlewares_rate_limit.go b/apis/middlewares_rate_limit.go new file mode 100644 index 00000000..b0f1b925 --- /dev/null +++ b/apis/middlewares_rate_limit.go @@ -0,0 +1,298 @@ +package apis + +import ( + "sync" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/store" +) + +const ( + DefaultRateLimitMiddlewareId = "pbRateLimit" + DefaultRateLimitMiddlewarePriority = -1000 +) + +const ( + rateLimitersStoreKey = "__pbRateLimiters__" + rateLimitersCronKey = "__pbRateLimitersCleanup__" + rateLimitersSettingsHookId = "__pbRateLimitersSettingsHook__" +) + +// rateLimit defines the global rate limit middleware. +// +// This middleware is registered by default for all routes. +func rateLimit() *hook.Handler[*core.RequestEvent] { + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRateLimitMiddlewareId, + Priority: DefaultRateLimitMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + if skipRateLimit(e) { + return e.Next() + } + + rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(defaultRateLimitLabels(e)) + if ok { + err := checkRateLimit(e, e.Request.Pattern, rule) + if err != nil { + return err + } + } + + return e.Next() + }, + } +} + +// collectionPathRateLimit defines a rate limit middleware for the internal collection handlers. +func collectionPathRateLimit(collectionPathParam string, baseTags ...string) *hook.Handler[*core.RequestEvent] { + if collectionPathParam == "" { + collectionPathParam = "collection" + } + + return &hook.Handler[*core.RequestEvent]{ + Id: DefaultRateLimitMiddlewareId, + Priority: DefaultRateLimitMiddlewarePriority, + Func: func(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue(collectionPathParam)) + if err != nil { + return e.NotFoundError("Missing or invalid collection context.", err) + } + + if err := checkCollectionRateLimit(e, collection, baseTags...); err != nil { + return err + } + + return e.Next() + }, + } +} + +// checkCollectionRateLimit checks whether the current request satisfy the +// rate limit configuration for the specific collection. +// +// Each baseTags entry will be prefixed with the collection name and its wildcard variant. +func checkCollectionRateLimit(e *core.RequestEvent, collection *core.Collection, baseTags ...string) error { + if skipRateLimit(e) { + return nil + } + + labels := make([]string, 0, 2+len(baseTags)*2) + + rtId := collection.Id + e.Request.Pattern + + // add first the primary labels (aka. ["collectionName:action1", "collectionName:action2"]) + for _, baseTag := range baseTags { + rtId += baseTag + labels = append(labels, collection.Name+":"+baseTag) + } + + // add the wildcard labels (aka. [..., "*:action1","*:action2", "*"]) + for _, baseTag := range baseTags { + labels = append(labels, "*:"+baseTag) + } + labels = append(labels, defaultRateLimitLabels(e)...) + + rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(labels) + if ok { + return checkRateLimit(e, rtId, rule) + } + + return nil +} + +// ------------------------------------------------------------------- + +// @todo consider exporting as RateLimit helper? +func checkRateLimit(e *core.RequestEvent, rtId string, rule core.RateLimitRule) error { + rateLimiters := e.App.Store().GetOrSet(rateLimitersStoreKey, func() any { + return initRateLimitersStore(e.App) + }).(*store.Store[*rateLimiter]) + if rateLimiters == nil { + e.App.Logger().Warn("Failed to retrieve app rate limiters store") + return nil + } + + rt := rateLimiters.GetOrSet(rtId, func() *rateLimiter { + return newRateLimiter(rule.MaxRequests, rule.Duration, rule.Duration+1800) + }) + if rt == nil { + e.App.Logger().Warn("Failed to retrieve app rate limiter", "id", rtId) + return nil + } + + key := e.RealIP() + if key == "" { + e.App.Logger().Warn("Empty rate limit client key") + return nil + } + + if !rt.isAllowed(key) { + return e.TooManyRequestsError("", nil) + } + + return nil +} + +func skipRateLimit(e *core.RequestEvent) bool { + return !e.App.Settings().RateLimits.Enabled || e.HasSuperuserAuth() +} + +func defaultRateLimitLabels(e *core.RequestEvent) []string { + return []string{e.Request.Method + " " + e.Request.URL.Path, e.Request.URL.Path} +} + +func destroyRateLimitersStore(app core.App) { + app.OnSettingsReload().Unbind(rateLimitersSettingsHookId) + app.Cron().Remove(rateLimitersCronKey) + app.Store().Remove(rateLimitersStoreKey) +} + +func initRateLimitersStore(app core.App) *store.Store[*rateLimiter] { + app.Cron().Add(rateLimitersCronKey, "2 * * * *", func() { // offset a little since too many cleanup tasks execute at 00 + limitersStore, ok := app.Store().Get(rateLimitersStoreKey).(*store.Store[*rateLimiter]) + if !ok { + return + } + limiters := limitersStore.GetAll() + for _, limiter := range limiters { + limiter.clean() + } + }) + + app.OnSettingsReload().Bind(&hook.Handler[*core.SettingsReloadEvent]{ + Id: rateLimitersSettingsHookId, + Func: func(e *core.SettingsReloadEvent) error { + err := e.Next() + if err != nil { + return err + } + + // reset + destroyRateLimitersStore(e.App) + + return nil + }, + }) + + return store.New[*rateLimiter](nil) +} + +func newRateLimiter(maxAllowed int, intervalInSec int64, minDeleteIntervalInSec int64) *rateLimiter { + return &rateLimiter{ + maxAllowed: maxAllowed, + interval: intervalInSec, + minDeleteInterval: minDeleteIntervalInSec, + clients: map[string]*fixedWindow{}, + } +} + +type rateLimiter struct { + clients map[string]*fixedWindow + + maxAllowed int + interval int64 + minDeleteInterval int64 + totalDeleted int64 + + sync.RWMutex +} + +func (rt *rateLimiter) isAllowed(key string) bool { + // lock only reads to minimize locks contention + rt.RLock() + client, ok := rt.clients[key] + rt.RUnlock() + + if !ok { + rt.Lock() + // check again in case the client was added by another request + client, ok = rt.clients[key] + if !ok { + client = newFixedWindow(rt.maxAllowed, rt.interval) + rt.clients[key] = client + } + rt.Unlock() + } + + return client.consume() +} + +func (rt *rateLimiter) clean() { + rt.Lock() + defer rt.Unlock() + + nowUnix := time.Now().Unix() + + for k, client := range rt.clients { + if client.hasExpired(nowUnix, rt.minDeleteInterval) { + delete(rt.clients, k) + rt.totalDeleted++ + } + } + + // "shrink" the map if too may items were deleted + // + // @todo remove after https://github.com/golang/go/issues/20135 + if rt.totalDeleted >= 300 { + shrunk := make(map[string]*fixedWindow, len(rt.clients)) + for k, v := range rt.clients { + shrunk[k] = v + } + rt.clients = shrunk + rt.totalDeleted = 0 + } +} + +func newFixedWindow(maxAllowed int, intervalInSec int64) *fixedWindow { + return &fixedWindow{ + maxAllowed: maxAllowed, + interval: intervalInSec, + } +} + +type fixedWindow struct { + // use plain Mutex instead of RWMutex since the operations are expected + // to be mostly writes (e.g. consume()) and it should perform better + sync.Mutex + + maxAllowed int // the max allowed tokens per interval + available int // the total available tokens + interval int64 // in seconds + lastConsume int64 // the time of the last consume +} + +// hasExpired checks whether it has been at least minElapsed seconds since the lastConsume time. +// (usually used to perform periodic cleanup of staled instances). +func (l *fixedWindow) hasExpired(relativeNow int64, minElapsed int64) bool { + l.Lock() + defer l.Unlock() + + return relativeNow-l.lastConsume > minElapsed +} + +// consume decrease the current window allowance with 1 (if not exhausted already). +// +// It returns false if the allowance has been already exhausted and the user +// has to wait until it resets back to its maxAllowed value. +func (l *fixedWindow) consume() bool { + l.Lock() + defer l.Unlock() + + nowUnix := time.Now().Unix() + + // reset consumed counter + if nowUnix-l.lastConsume >= l.interval { + l.available = l.maxAllowed + } + + if l.available > 0 { + l.available-- + l.lastConsume = nowUnix + + return true + } + + return false +} diff --git a/apis/middlewares_rate_limit_test.go b/apis/middlewares_rate_limit_test.go new file mode 100644 index 00000000..a86012a4 --- /dev/null +++ b/apis/middlewares_rate_limit_test.go @@ -0,0 +1,103 @@ +package apis_test + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestDefaultRateLimitMiddleware(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + { + Label: "/rate/", + MaxRequests: 2, + Duration: 1, + }, + { + Label: "/rate/b", + MaxRequests: 3, + Duration: 1, + }, + { + Label: "POST /rate/b", + MaxRequests: 1, + Duration: 1, + }, + } + + pbRouter, err := apis.NewRouter(app) + if err != nil { + t.Fatal(err) + } + pbRouter.GET("/norate", func(e *core.RequestEvent) error { + return e.String(200, "norate") + }).BindFunc(func(e *core.RequestEvent) error { + return e.Next() + }) + pbRouter.GET("/rate/a", func(e *core.RequestEvent) error { + return e.String(200, "a") + }) + pbRouter.GET("/rate/b", func(e *core.RequestEvent) error { + return e.String(200, "b") + }) + + mux, err := pbRouter.BuildMux() + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + url string + wait float64 + expectedStatus int + }{ + {"/norate", 0, 200}, + {"/norate", 0, 200}, + {"/norate", 0, 200}, + {"/norate", 0, 200}, + {"/norate", 0, 200}, + + {"/rate/a", 0, 200}, + {"/rate/a", 0, 200}, + {"/rate/a", 0, 429}, + {"/rate/a", 0, 429}, + {"/rate/a", 1.1, 200}, + {"/rate/a", 0, 200}, + {"/rate/a", 0, 429}, + + {"/rate/b", 0, 200}, + {"/rate/b", 0, 200}, + {"/rate/b", 0, 200}, + {"/rate/b", 0, 429}, + {"/rate/b", 1.1, 200}, + {"/rate/b", 0, 200}, + {"/rate/b", 0, 200}, + {"/rate/b", 0, 429}, + } + + for _, s := range scenarios { + t.Run(s.url, func(t *testing.T) { + if s.wait > 0 { + time.Sleep(time.Duration(s.wait) * time.Second) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", s.url, nil) + mux.ServeHTTP(rec, req) + + result := rec.Result() + + if result.StatusCode != s.expectedStatus { + t.Fatalf("Expected response status %d, got %d", s.expectedStatus, result.StatusCode) + } + }) + } +} diff --git a/apis/middlewares_test.go b/apis/middlewares_test.go index 176b4363..a8f44ef6 100644 --- a/apis/middlewares_test.go +++ b/apis/middlewares_test.go @@ -4,99 +4,65 @@ import ( "net/http" "testing" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" ) func TestRequireGuestOnly(t *testing.T) { t.Parallel() + beforeTestFunc := func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireGuestOnly()) + } + scenarios := []tests.ApiScenario{ { - Name: "valid record token", + Name: "valid regular user token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireGuestOnly(), - }, - }) + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, + BeforeTestFunc: beforeTestFunc, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid admin token", + Name: "valid superuser auth token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireGuestOnly(), - }, - }) + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, + BeforeTestFunc: beforeTestFunc, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "expired/invalid token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxfQ.HqvpCpM0RAk3Qu9PfCMuZsk_DKh9UYuzFLwXBMTZd1w", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireGuestOnly(), - }, - }) + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.2D3tmqPn3vc5LoqqCz8V-iCDVXo9soYiH0d32G7FQT4", }, + BeforeTestFunc: beforeTestFunc, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "guest", - Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireGuestOnly(), - }, - }) - }, + Name: "guest", + Method: http.MethodGet, + URL: "/my/test", + BeforeTestFunc: beforeTestFunc, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -105,135 +71,116 @@ func TestRequireGuestOnly(t *testing.T) { } } -func TestRequireRecordAuth(t *testing.T) { +func TestRequireAuth(t *testing.T) { t.Parallel() scenarios := []tests.ApiScenario{ { Name: "guest", Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth(), - }, - }) + 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(200, "test123") + }).Bind(apis.RequireAuth()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "expired/invalid token", + Name: "expired token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxfQ.HqvpCpM0RAk3Qu9PfCMuZsk_DKh9UYuzFLwXBMTZd1w", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.2D3tmqPn3vc5LoqqCz8V-iCDVXo9soYiH0d32G7FQT4", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid admin token", + Name: "invalid token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJfcGJjXzMzMjM4NjYzMzkifQ.C8m3aRZNOxUDhMiuZuDTRIIjRl7wsOyzoxs8EjvKNgY", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token", + Name: "valid record auth token with no collection restrictions", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test", + Headers: map[string]string{ + // regular user + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth()) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, }, { - Name: "valid record token with collection not in the restricted list", + Name: "valid record static auth token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test", + Headers: map[string]string{ + // regular user + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6ZmFsc2V9.4IsO6YMsR19crhwl_YWzvRH8pfq2Ri4Gv2dzGyneLak", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth("demo1", "demo2"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth()) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + { + Name: "valid record auth token with collection not in the restricted list", + Method: http.MethodGet, + URL: "/my/test", + Headers: map[string]string{ + // superuser + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth("users", "demo1")) }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token with collection in the restricted list", + Name: "valid record auth token with collection in the restricted list", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test", + Headers: map[string]string{ + // superuser + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth("demo1", "demo2", "users"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireAuth("users", core.CollectionNameSuperusers)) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, @@ -245,113 +192,66 @@ func TestRequireRecordAuth(t *testing.T) { } } -func TestRequireSameContextRecordAuth(t *testing.T) { +func TestRequireSuperuserAuth(t *testing.T) { t.Parallel() scenarios := []tests.ApiScenario{ { Name: "guest", Method: http.MethodGet, - Url: "/my/users/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireSameContextRecordAuth(), - }, - }) + 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(200, "test123") + }).Bind(apis.RequireSuperuserAuth()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "expired/invalid token", Method: http.MethodGet, - Url: "/my/users/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxfQ.HqvpCpM0RAk3Qu9PfCMuZsk_DKh9UYuzFLwXBMTZd1w", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.a668tes0bS6FU-OOlXMoRrdd57a_oldIPd5b0Gv_RYw", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireSameContextRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuth()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid admin token", + Name: "valid regular user auth token", Method: http.MethodGet, - Url: "/my/users/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireSameContextRecordAuth(), - }, - }) - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "valid record token but from different collection", - Method: http.MethodGet, - Url: "/my/users/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireSameContextRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuth()) }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token", + Name: "valid superuser auth token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuth()) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, @@ -363,146 +263,37 @@ func TestRequireSameContextRecordAuth(t *testing.T) { } } -func TestRequireAdminAuth(t *testing.T) { +func TestRequireSuperuserAuthOnlyIfAny(t *testing.T) { t.Parallel() scenarios := []tests.ApiScenario{ { - Name: "guest", + Name: "guest (while having at least 1 existing superuser)", Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuth(), - }, - }) + 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(200, "test123") + }).Bind(apis.RequireSuperuserAuthOnlyIfAny()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "expired/invalid token", + Name: "guest (while having 0 existing superusers)", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuth(), - }, - }) - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "valid record token", - Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuth(), - }, - }) - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "valid admin token", - Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuth(), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRequireAdminAuthOnlyIfAny(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "guest (while having at least 1 existing admin)", - Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuthOnlyIfAny(app), - }, - }) - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "guest (while having 0 existing admins)", - Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // delete all admins - _, err := app.Dao().DB().NewQuery("DELETE FROM {{_admins}}").Execute() + URL: "/my/test", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // delete all superusers + _, err := app.DB().NewQuery("DELETE FROM {{" + core.CollectionNameSuperusers + "}}").Execute() if err != nil { t.Fatal(err) } - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuthOnlyIfAny(app), - }, - }) + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuthOnlyIfAny()) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, @@ -510,65 +301,46 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) { { Name: "expired/invalid token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.a668tes0bS6FU-OOlXMoRrdd57a_oldIPd5b0Gv_RYw", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuthOnlyIfAny(app), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuthOnlyIfAny()) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token", + Name: "valid regular user token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuthOnlyIfAny(app), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuthOnlyIfAny()) }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid admin token", + Name: "valid superuser auth token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminAuthOnlyIfAny(app), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserAuthOnlyIfAny()) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, @@ -580,157 +352,112 @@ func TestRequireAdminAuthOnlyIfAny(t *testing.T) { } } -func TestRequireAdminOrRecordAuth(t *testing.T) { +func TestRequireSuperuserOrOwnerAuth(t *testing.T) { t.Parallel() scenarios := []tests.ApiScenario{ { Name: "guest", Method: http.MethodGet, - Url: "/my/test", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth(), - }, - }) + URL: "/my/test/4q1xlclmfloku33", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "expired/invalid token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + URL: "/my/test/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.a668tes0bS6FU-OOlXMoRrdd57a_oldIPd5b0Gv_RYw", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token", + Name: "valid record auth token (different user)", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test/oap640cot4yru2s", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth(), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "valid record token with collection not in the restricted list", - Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth("demo1", "demo2", "clients"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token with collection in the restricted list", + Name: "valid record auth token (owner)", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/my/test/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth("demo1", "demo2", "users"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, }, { - Name: "valid admin token", + Name: "valid record auth token (owner + non-matching custom owner param)", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth(), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("test")) + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid record auth token (owner + matching custom owner param)", + Method: http.MethodGet, + URL: "/my/test/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{test}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("test")) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, }, { - Name: "valid admin token + restricted collections list (should be ignored)", + Name: "valid superuser auth token", Method: http.MethodGet, - Url: "/my/test", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrRecordAuth("demo1", "demo2"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, @@ -742,269 +469,117 @@ func TestRequireAdminOrRecordAuth(t *testing.T) { } } -func TestRequireAdminOrOwnerAuth(t *testing.T) { +func TestRequireSameCollectionContextAuth(t *testing.T) { t.Parallel() scenarios := []tests.ApiScenario{ { Name: "guest", Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:id", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth(""), - }, - }) + URL: "/my/test/_pb_users_auth_", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{collection}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSameCollectionContextAuth("")) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "expired/invalid token", Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxfQ.HqvpCpM0RAk3Qu9PfCMuZsk_DKh9UYuzFLwXBMTZd1w", + URL: "/my/test/_pb_users_auth_", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.2D3tmqPn3vc5LoqqCz8V-iCDVXo9soYiH0d32G7FQT4", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:id", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth(""), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{collection}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSameCollectionContextAuth("")) }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token (different user)", + Name: "valid record auth token (different collection)", Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImJnczgyMG4zNjF2ajFxZCIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.tW4NZWZ0mHBgvSZsQ0OOQhWajpUNFPCvNrOF9aCZLZs", + URL: "/my/test/clients", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:id", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth(""), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{collection}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSameCollectionContextAuth("")) }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "valid record token (different collection)", + Name: "valid record auth token (same collection)", Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", + URL: "/my/test/_pb_users_auth_", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:id", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth(""), - }, - }) - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "valid record token (owner)", - Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:id", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth(""), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{collection}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSameCollectionContextAuth("")) }, ExpectedStatus: 200, ExpectedContent: []string{"test123"}, }, { - Name: "valid admin token", + Name: "valid record auth token (non-matching/missing collection param)", Method: http.MethodGet, - Url: "/my/test/4q1xlclmfloku33", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/my/test/_pb_users_auth_", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/test/:custom", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.RequireAdminOrOwnerAuth("custom"), - }, - }) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{id}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("")) }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestLoadCollectionContext(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "missing collection", - Method: http.MethodGet, - Url: "/my/missing", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app), - }, - }) - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "guest", - Method: http.MethodGet, - Url: "/my/demo1", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "valid record token", - Method: http.MethodGet, - Url: "/my/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "valid admin token", - Method: http.MethodGet, - Url: "/my/demo1", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, - }, - { - Name: "mismatched type", - Method: http.MethodGet, - Url: "/my/demo1", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app, "auth"), - }, - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "matched type", - Method: http.MethodGet, - Url: "/my/users", - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - e.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/my/:collection", - Handler: func(c echo.Context) error { - return c.String(200, "test123") - }, - Middlewares: []echo.MiddlewareFunc{ - apis.LoadCollectionContext(app, "auth"), - }, - }) - }, - ExpectedStatus: 200, - ExpectedContent: []string{"test123"}, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid record auth token (matching custom collection param)", + Method: http.MethodGet, + URL: "/my/test/_pb_users_auth_", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{test}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSuperuserOrOwnerAuth("test")) + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superuser no exception check", + Method: http.MethodGet, + URL: "/my/test/_pb_users_auth_", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + e.Router.GET("/my/test/{collection}", func(e *core.RequestEvent) error { + return e.String(200, "test123") + }).Bind(apis.RequireSameCollectionContextAuth("")) + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, } diff --git a/apis/realtime.go b/apis/realtime.go index 426f1703..99a714d5 100644 --- a/apis/realtime.go +++ b/apis/realtime.go @@ -9,198 +9,196 @@ import ( "strings" "time" - "github.com/labstack/echo/v5" + validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/resolvers" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/picker" + "github.com/pocketbase/pocketbase/tools/router" "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" ) +// note: the chunk size is arbitrary chosen and may change in the future +const clientsChunkSize = 150 + +// RealtimeClientAuthKey is the name of the realtime client store key that holds its auth state. +const RealtimeClientAuthKey = "auth" + // bindRealtimeApi registers the realtime api endpoints. -func bindRealtimeApi(app core.App, rg *echo.Group) { - api := realtimeApi{app: app} +func bindRealtimeApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + sub := rg.Group("/realtime") + sub.GET("", realtimeConnect).Bind(SkipSuccessActivityLog()) + sub.POST("", realtimeSetSubscriptions) - subGroup := rg.Group("/realtime") - subGroup.GET("", api.connect) - subGroup.POST("", api.setSubscriptions, ActivityLogger(app)) - - api.bindEvents() + bindRealtimeEvents(app) } -type realtimeApi struct { - app core.App -} +func realtimeConnect(e *core.RequestEvent) error { + // disable global write deadline for the SSE connection + rc := http.NewResponseController(e.Response) + writeDeadlineErr := rc.SetWriteDeadline(time.Time{}) + if writeDeadlineErr != nil { + if !errors.Is(writeDeadlineErr, http.ErrNotSupported) { + return e.InternalServerError("Failed to initialize SSE connection.", writeDeadlineErr) + } -func (api *realtimeApi) connect(c echo.Context) error { - cancelCtx, cancelRequest := context.WithCancel(c.Request().Context()) + // only log since there are valid cases where it may not be implement (e.g. httptest.ResponseRecorder) + e.App.Logger().Warn("SetWriteDeadline is not supported, fallback to the default server WriteTimeout") + } + + // create cancellable request + cancelCtx, cancelRequest := context.WithCancel(e.Request.Context()) defer cancelRequest() - c.SetRequest(c.Request().Clone(cancelCtx)) + e.Request = e.Request.Clone(cancelCtx) - // register new subscription client - client := subscriptions.NewDefaultClient() - api.app.SubscriptionsBroker().Register(client) - defer func() { - disconnectEvent := &core.RealtimeDisconnectEvent{ - HttpContext: c, - Client: client, - } - - if err := api.app.OnRealtimeDisconnectRequest().Trigger(disconnectEvent); err != nil { - api.app.Logger().Debug( - "OnRealtimeDisconnectRequest error", - slog.String("clientId", client.Id()), - slog.String("error", err.Error()), - ) - } - - api.app.SubscriptionsBroker().Unregister(client.Id()) - }() - - c.Response().Header().Set("Content-Type", "text/event-stream") - c.Response().Header().Set("Cache-Control", "no-store") + e.Response.Header().Set("Content-Type", "text/event-stream") + e.Response.Header().Set("Cache-Control", "no-store") // https://github.com/pocketbase/pocketbase/discussions/480#discussioncomment-3657640 // https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering - c.Response().Header().Set("X-Accel-Buffering", "no") + e.Response.Header().Set("X-Accel-Buffering", "no") - connectEvent := &core.RealtimeConnectEvent{ - HttpContext: c, - Client: client, - IdleTimeout: 5 * time.Minute, - } + connectEvent := new(core.RealtimeConnectRequestEvent) + connectEvent.RequestEvent = e + connectEvent.Client = subscriptions.NewDefaultClient() + connectEvent.IdleTimeout = 5 * time.Minute - if err := api.app.OnRealtimeConnectRequest().Trigger(connectEvent); err != nil { - return err - } + return e.App.OnRealtimeConnectRequest().Trigger(connectEvent, func(ce *core.RealtimeConnectRequestEvent) error { + // register new subscription client + ce.App.SubscriptionsBroker().Register(ce.Client) + defer func() { + e.App.SubscriptionsBroker().Unregister(ce.Client.Id()) + }() - api.app.Logger().Debug("Realtime connection established.", slog.String("clientId", client.Id())) + ce.App.Logger().Debug("Realtime connection established.", slog.String("clientId", ce.Client.Id())) - // signalize established connection (aka. fire "connect" message) - connectMsgEvent := &core.RealtimeMessageEvent{ - HttpContext: c, - Client: client, - Message: &subscriptions.Message{ + // signalize established connection (aka. fire "connect" message) + connectMsgEvent := new(core.RealtimeMessageEvent) + connectMsgEvent.RequestEvent = ce.RequestEvent + connectMsgEvent.Client = ce.Client + connectMsgEvent.Message = &subscriptions.Message{ Name: "PB_CONNECT", - Data: []byte(`{"clientId":"` + client.Id() + `"}`), - }, - } - connectMsgErr := api.app.OnRealtimeBeforeMessageSend().Trigger(connectMsgEvent, func(e *core.RealtimeMessageEvent) error { - w := e.HttpContext.Response() - w.Write([]byte("id:" + client.Id() + "\n")) - w.Write([]byte("event:" + e.Message.Name + "\n")) - w.Write([]byte("data:")) - w.Write(e.Message.Data) - w.Write([]byte("\n\n")) - w.Flush() - return api.app.OnRealtimeAfterMessageSend().Trigger(e) - }) - if connectMsgErr != nil { - api.app.Logger().Debug( - "Realtime connection closed (failed to deliver PB_CONNECT)", - slog.String("clientId", client.Id()), - slog.String("error", connectMsgErr.Error()), - ) - return nil - } - - // start an idle timer to keep track of inactive/forgotten connections - idleTimeout := connectEvent.IdleTimeout - idleTimer := time.NewTimer(idleTimeout) - defer idleTimer.Stop() - - for { - select { - case <-idleTimer.C: - cancelRequest() - case msg, ok := <-client.Channel(): - if !ok { - // channel is closed - api.app.Logger().Debug( - "Realtime connection closed (closed channel)", - slog.String("clientId", client.Id()), - ) - return nil - } - - msgEvent := &core.RealtimeMessageEvent{ - HttpContext: c, - Client: client, - Message: &msg, - } - msgErr := api.app.OnRealtimeBeforeMessageSend().Trigger(msgEvent, func(e *core.RealtimeMessageEvent) error { - w := e.HttpContext.Response() - w.Write([]byte("id:" + e.Client.Id() + "\n")) - w.Write([]byte("event:" + e.Message.Name + "\n")) - w.Write([]byte("data:")) - w.Write(e.Message.Data) - w.Write([]byte("\n\n")) - w.Flush() - return api.app.OnRealtimeAfterMessageSend().Trigger(msgEvent) - }) - if msgErr != nil { - api.app.Logger().Debug( - "Realtime connection closed (failed to deliver message)", - slog.String("clientId", client.Id()), - slog.String("error", msgErr.Error()), - ) - return nil - } - - idleTimer.Stop() - idleTimer.Reset(idleTimeout) - case <-c.Request().Context().Done(): - // connection is closed - api.app.Logger().Debug( - "Realtime connection closed (cancelled request)", - slog.String("clientId", client.Id()), + Data: []byte(`{"clientId":"` + ce.Client.Id() + `"}`), + } + connectMsgErr := ce.App.OnRealtimeMessageSend().Trigger(connectMsgEvent, func(me *core.RealtimeMessageEvent) error { + me.Response.Write([]byte("id:" + me.Client.Id() + "\n")) + me.Response.Write([]byte("event:" + me.Message.Name + "\n")) + me.Response.Write([]byte("data:")) + me.Response.Write(me.Message.Data) + me.Response.Write([]byte("\n\n")) + return me.Flush() + }) + if connectMsgErr != nil { + ce.App.Logger().Debug( + "Realtime connection closed (failed to deliver PB_CONNECT)", + slog.String("clientId", ce.Client.Id()), + slog.String("error", connectMsgErr.Error()), ) return nil } - } + + // start an idle timer to keep track of inactive/forgotten connections + idleTimer := time.NewTimer(ce.IdleTimeout) + defer idleTimer.Stop() + + for { + select { + case <-idleTimer.C: + cancelRequest() + case msg, ok := <-ce.Client.Channel(): + if !ok { + // channel is closed + ce.App.Logger().Debug( + "Realtime connection closed (closed channel)", + slog.String("clientId", ce.Client.Id()), + ) + return nil + } + + msgEvent := new(core.RealtimeMessageEvent) + msgEvent.RequestEvent = ce.RequestEvent + msgEvent.Client = ce.Client + msgEvent.Message = &msg + msgErr := ce.App.OnRealtimeMessageSend().Trigger(msgEvent, func(me *core.RealtimeMessageEvent) error { + me.Response.Write([]byte("id:" + me.Client.Id() + "\n")) + me.Response.Write([]byte("event:" + me.Message.Name + "\n")) + me.Response.Write([]byte("data:")) + me.Response.Write(me.Message.Data) + me.Response.Write([]byte("\n\n")) + return me.Flush() + }) + if msgErr != nil { + ce.App.Logger().Debug( + "Realtime connection closed (failed to deliver message)", + slog.String("clientId", ce.Client.Id()), + slog.String("error", msgErr.Error()), + ) + return nil + } + + idleTimer.Stop() + idleTimer.Reset(ce.IdleTimeout) + case <-ce.Request.Context().Done(): + // connection is closed + ce.App.Logger().Debug( + "Realtime connection closed (cancelled request)", + slog.String("clientId", ce.Client.Id()), + ) + return nil + } + } + }) +} + +type realtimeSubscribeForm struct { + ClientId string `form:"clientId" json:"clientId"` + Subscriptions []string `form:"subscriptions" json:"subscriptions"` +} + +func (form *realtimeSubscribeForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)), + ) } // note: in case of reconnect, clients will have to resubmit all subscriptions again -func (api *realtimeApi) setSubscriptions(c echo.Context) error { - form := forms.NewRealtimeSubscribe() +func realtimeSetSubscriptions(e *core.RequestEvent) error { + form := new(realtimeSubscribeForm) - // read request data - if err := c.Bind(form); err != nil { - return NewBadRequestError("", err) + err := e.BindBody(form) + if err != nil { + return e.BadRequestError("", err) } - // validate request data - if err := form.Validate(); err != nil { - return NewBadRequestError("", err) + err = form.validate() + if err != nil { + return e.BadRequestError("", err) } // find subscription client - client, err := api.app.SubscriptionsBroker().ClientById(form.ClientId) + client, err := e.App.SubscriptionsBroker().ClientById(form.ClientId) if err != nil { - return NewNotFoundError("Missing or invalid client id.", err) + return e.NotFoundError("Missing or invalid client id.", err) } // check if the previous request was authorized oldAuthId := extractAuthIdFromGetter(client) - newAuthId := extractAuthIdFromGetter(c) + newAuthId := extractAuthIdFromGetter(e) if oldAuthId != "" && oldAuthId != newAuthId { - return NewForbiddenError("The current and the previous request authorization don't match.", nil) + return e.ForbiddenError("The current and the previous request authorization don't match.", nil) } - event := &core.RealtimeSubscribeEvent{ - HttpContext: c, - Client: client, - Subscriptions: form.Subscriptions, - } + event := new(core.RealtimeSubscribeRequestEvent) + event.RequestEvent = e + event.Client = client + event.Subscriptions = form.Subscriptions - return api.app.OnRealtimeBeforeSubscribeRequest().Trigger(event, func(e *core.RealtimeSubscribeEvent) error { + return e.App.OnRealtimeSubscribeRequest().Trigger(event, func(e *core.RealtimeSubscribeRequestEvent) error { // update auth state - e.Client.Set(ContextAdminKey, e.HttpContext.Get(ContextAdminKey)) - e.Client.Set(ContextAuthRecordKey, e.HttpContext.Get(ContextAuthRecordKey)) + e.Client.Set(RealtimeClientAuthKey, e.Auth) // unsubscribe from any previous existing subscriptions e.Client.Unsubscribe() @@ -208,155 +206,247 @@ func (api *realtimeApi) setSubscriptions(c echo.Context) error { // subscribe to the new subscriptions e.Client.Subscribe(e.Subscriptions...) - api.app.Logger().Debug( + e.App.Logger().Debug( "Realtime subscriptions updated.", slog.String("clientId", e.Client.Id()), slog.Any("subscriptions", e.Subscriptions), ) - return api.app.OnRealtimeAfterSubscribeRequest().Trigger(event, func(e *core.RealtimeSubscribeEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) + return e.NoContent(http.StatusNoContent) }) } -// updateClientsAuthModel updates the existing clients auth model with the new one (matched by ID). -func (api *realtimeApi) updateClientsAuthModel(contextKey string, newModel models.Model) error { - for _, client := range api.app.SubscriptionsBroker().Clients() { - clientModel, _ := client.Get(contextKey).(models.Model) - if clientModel != nil && - clientModel.TableName() == newModel.TableName() && - clientModel.GetId() == newModel.GetId() { - client.Set(contextKey, newModel) - } +// updateClientsAuth updates the existing clients auth record with the new one (matched by ID). +func realtimeUpdateClientsAuth(app core.App, newAuthRecord *core.Record) error { + chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize) + + group := new(errgroup.Group) + + for _, chunk := range chunks { + group.Go(func() error { + for _, client := range chunk { + clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record) + if clientAuth != nil && + clientAuth.Id == newAuthRecord.Id && + clientAuth.Collection().Name == newAuthRecord.Collection().Name { + client.Set(RealtimeClientAuthKey, newAuthRecord) + } + } + + return nil + }) } - return nil + return group.Wait() } // unregisterClientsByAuthModel unregister all clients that has the provided auth model. -func (api *realtimeApi) unregisterClientsByAuthModel(contextKey string, model models.Model) error { - for _, client := range api.app.SubscriptionsBroker().Clients() { - clientModel, _ := client.Get(contextKey).(models.Model) - if clientModel != nil && - clientModel.TableName() == model.TableName() && - clientModel.GetId() == model.GetId() { - api.app.SubscriptionsBroker().Unregister(client.Id()) - } +func realtimeUnregisterClientsByAuth(app core.App, authModel core.Model) error { + chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize) + + group := new(errgroup.Group) + + for _, chunk := range chunks { + group.Go(func() error { + for _, client := range chunk { + clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record) + if clientAuth != nil && + clientAuth.Id == authModel.PK() && + clientAuth.Collection().Name == authModel.TableName() { + app.SubscriptionsBroker().Unregister(client.Id()) + } + } + + return nil + }) } - return nil + return group.Wait() } -func (api *realtimeApi) bindEvents() { - // update the clients that has admin or auth record association - api.app.OnModelAfterUpdate().PreAdd(func(e *core.ModelEvent) error { - if record := api.resolveRecord(e.Model); record != nil && record.Collection().IsAuth() { - return api.updateClientsAuthModel(ContextAuthRecordKey, record) - } - - if admin, ok := e.Model.(*models.Admin); ok && admin != nil { - return api.updateClientsAuthModel(ContextAdminKey, admin) - } - - return nil - }) - - // remove the client(s) associated to the deleted admin or auth record - api.app.OnModelAfterDelete().PreAdd(func(e *core.ModelEvent) error { - if collection := api.resolveRecordCollection(e.Model); collection != nil && collection.IsAuth() { - return api.unregisterClientsByAuthModel(ContextAuthRecordKey, e.Model) - } - - if admin, ok := e.Model.(*models.Admin); ok && admin != nil { - return api.unregisterClientsByAuthModel(ContextAdminKey, admin) - } - - return nil - }) - - api.app.OnModelAfterCreate().PreAdd(func(e *core.ModelEvent) error { - if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("create", record, false); err != nil { - api.app.Logger().Debug( - "Failed to broadcast record create", - slog.String("id", record.Id), - slog.String("collectionName", record.Collection().Name), - slog.String("error", err.Error()), - ) +func bindRealtimeEvents(app core.App) { + // update the clients that has auth record association + app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + authRecord := realtimeResolveRecord(e.App, e.Model, core.CollectionTypeAuth) + if authRecord != nil { + if err := realtimeUpdateClientsAuth(e.App, authRecord); err != nil { + app.Logger().Warn( + "Failed to update client(s) associated to the updated auth record", + slog.Any("id", authRecord.Id), + slog.String("collectionName", authRecord.Collection().Name), + slog.String("error", err.Error()), + ) + } } - } - return nil + + return e.Next() + }, + Priority: -99, }) - api.app.OnModelAfterUpdate().PreAdd(func(e *core.ModelEvent) error { - if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("update", record, false); err != nil { - api.app.Logger().Debug( - "Failed to broadcast record update", - slog.String("id", record.Id), - slog.String("collectionName", record.Collection().Name), - slog.String("error", err.Error()), - ) + // remove the client(s) associated to the deleted auth model + // (note: works also with custom model for backward compatibility) + app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + collection := realtimeResolveRecordCollection(e.App, e.Model) + if collection != nil && collection.IsAuth() { + if err := realtimeUnregisterClientsByAuth(e.App, e.Model); err != nil { + app.Logger().Warn( + "Failed to remove client(s) associated to the deleted auth model", + slog.Any("id", e.Model.PK()), + slog.String("collectionName", e.Model.TableName()), + slog.String("error", err.Error()), + ) + } } - } - return nil + + return e.Next() + }, + Priority: -99, }) - api.app.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error { - if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastRecord("delete", record, true); err != nil { - api.app.Logger().Debug( - "Failed to dry cache record delete", - slog.String("id", record.Id), - slog.String("collectionName", record.Collection().Name), - slog.String("error", err.Error()), - ) + app.OnModelAfterCreateSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + record := realtimeResolveRecord(e.App, e.Model, "") + if record != nil { + err := realtimeBroadcastRecord(e.App, "create", record, false) + if err != nil { + app.Logger().Debug( + "Failed to broadcast record create", + slog.String("id", record.Id), + slog.String("collectionName", record.Collection().Name), + slog.String("error", err.Error()), + ) + } } - } - return nil + + return e.Next() + }, + Priority: -99, }) - api.app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error { - if record := api.resolveRecord(e.Model); record != nil { - if err := api.broadcastDryCachedRecord("delete", record); err != nil { - api.app.Logger().Debug( - "Failed to broadcast record delete", - slog.String("id", record.Id), - slog.String("collectionName", record.Collection().Name), - slog.String("error", err.Error()), - ) + app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + record := realtimeResolveRecord(e.App, e.Model, "") + if record != nil { + err := realtimeBroadcastRecord(e.App, "update", record, false) + if err != nil { + app.Logger().Debug( + "Failed to broadcast record update", + slog.String("id", record.Id), + slog.String("collectionName", record.Collection().Name), + slog.String("error", err.Error()), + ) + } } - } - return nil + + return e.Next() + }, + Priority: -99, + }) + + // delete: dry cache + app.OnModelDelete().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + record := realtimeResolveRecord(e.App, e.Model, "") + if record != nil { + err := realtimeBroadcastRecord(e.App, "delete", record, true) + if err != nil { + app.Logger().Debug( + "Failed to dry cache record delete", + slog.String("id", record.Id), + slog.String("collectionName", record.Collection().Name), + slog.String("error", err.Error()), + ) + } + } + + return e.Next() + }, + Priority: 99, // execute as later as possible + }) + + // delete: broadcast + app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + record := realtimeResolveRecord(e.App, e.Model, "") + if record != nil { + err := realtimeBroadcastDryCachedRecord(e.App, "delete", record) + if err != nil { + app.Logger().Debug( + "Failed to broadcast record delete", + slog.String("id", record.Id), + slog.String("collectionName", record.Collection().Name), + slog.String("error", err.Error()), + ) + } + } + + return e.Next() + }, + Priority: -99, + }) + + // delete: failure + app.OnModelAfterDeleteError().Bind(&hook.Handler[*core.ModelErrorEvent]{ + Func: func(e *core.ModelErrorEvent) error { + record := realtimeResolveRecord(e.App, e.Model, "") + if record != nil { + err := realtimeUnsetDryCachedRecord(e.App, "delete", record) + if err != nil { + app.Logger().Debug( + "Failed to cleanup after broadcast record delete failure", + slog.String("id", record.Id), + slog.String("collectionName", record.Collection().Name), + slog.String("error", err.Error()), + ) + } + } + + return e.Next() + }, + Priority: -99, }) } // resolveRecord converts *if possible* the provided model interface to a Record. // This is usually helpful if the provided model is a custom Record model struct. -func (api *realtimeApi) resolveRecord(model models.Model) (record *models.Record) { - record, _ = model.(*models.Record) +func realtimeResolveRecord(app core.App, model core.Model, optCollectionType string) *core.Record { + record, _ := model.(*core.Record) + if record != nil { + if optCollectionType == "" || record.Collection().Type == optCollectionType { + return record + } + return nil + } - // check if it is custom Record model struct (ignore "private" tables) - if record == nil && !strings.HasPrefix(model.TableName(), "_") { - record, _ = api.app.Dao().FindRecordById(model.TableName(), model.GetId()) + tblName := model.TableName() + + // skip Log model checks + if tblName == core.LogsTableName { + return nil + } + + // check if it is custom Record model struct + collection, _ := app.FindCachedCollectionByNameOrId(tblName) + if collection != nil && (optCollectionType == "" || collection.Type == optCollectionType) { + if id, ok := model.PK().(string); ok { + record, _ = app.FindRecordById(collection, id) + } } return record } -// resolveRecordCollection extracts *if possible* the Collection model from the provided model interface. +// realtimeResolveRecordCollection extracts *if possible* the Collection model from the provided model interface. // This is usually helpful if the provided model is a custom Record model struct. -func (api *realtimeApi) resolveRecordCollection(model models.Model) (collection *models.Collection) { - if record, ok := model.(*models.Record); ok { +func realtimeResolveRecordCollection(app core.App, model core.Model) (collection *core.Collection) { + if record, ok := model.(*core.Record); ok { collection = record.Collection() - } else if !strings.HasPrefix(model.TableName(), "_") { + } else { // check if it is custom Record model struct (ignore "private" tables) - collection, _ = api.app.Dao().FindCollectionByNameOrId(model.TableName()) + collection, _ = app.FindCachedCollectionByNameOrId(model.TableName()) } return collection @@ -364,18 +454,18 @@ func (api *realtimeApi) resolveRecordCollection(model models.Model) (collection // recordData represents the broadcasted record subscrition message data. type recordData struct { - Record any `json:"record"` /* map or models.Record */ + Record any `json:"record"` /* map or core.Record */ Action string `json:"action"` } -func (api *realtimeApi) broadcastRecord(action string, record *models.Record, dryCache bool) error { +func realtimeBroadcastRecord(app core.App, action string, record *core.Record, dryCache bool) error { collection := record.Collection() if collection == nil { return errors.New("[broadcastRecord] Record collection not set") } - clients := api.app.SubscriptionsBroker().Clients() - if len(clients) == 0 { + chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize) + if len(chunks) == 0 { return nil // no subscribers } @@ -384,152 +474,210 @@ func (api *realtimeApi) broadcastRecord(action string, record *models.Record, dr (collection.Id + "/" + record.Id + "?"): collection.ViewRule, (collection.Name + "/*?"): collection.ListRule, (collection.Id + "/*?"): collection.ListRule, - // @deprecated: the same as the wildcard topic but kept for backward compatibility + + // @deprecated: the same as the wildcard topic but kept for backward compatibility (collection.Name + "?"): collection.ListRule, (collection.Id + "?"): collection.ListRule, } dryCacheKey := action + "/" + record.Id - for _, client := range clients { - client := client + group := new(errgroup.Group) - // note: not executed concurrently to avoid races and to ensure - // that the access checks are applied for the current record db state - for prefix, rule := range subscriptionRuleMap { - subs := client.Subscriptions(prefix) - if len(subs) == 0 { - continue - } - - 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 - cleanRecord := record.CleanCopy() - - // mock request data - requestInfo := &models.RequestInfo{ - Context: models.RequestInfoContextRealtime, - Method: "GET", - Query: options.Query, - Headers: options.Headers, - } - requestInfo.Admin, _ = client.Get(ContextAdminKey).(*models.Admin) - requestInfo.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record) - - if !api.canAccessRecord(cleanRecord, requestInfo, rule) { - continue - } - - rawExpand := cast.ToString(options.Query[expandQueryParam]) - if rawExpand != "" { - expandErrs := api.app.Dao().ExpandRecord(cleanRecord, strings.Split(rawExpand, ","), expandFetch(api.app.Dao(), requestInfo)) - if len(expandErrs) > 0 { - api.app.Logger().Debug( - "[broadcastRecord] expand errors", - slog.String("id", cleanRecord.Id), - slog.String("collectionName", cleanRecord.Collection().Name), - slog.String("sub", sub), - slog.String("expand", rawExpand), - slog.Any("errors", expandErrs), - ) + for _, chunk := range chunks { + group.Go(func() error { + 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 + for prefix, rule := range subscriptionRuleMap { + subs := client.Subscriptions(prefix) + if len(subs) == 0 { + continue } - } - // ignore the auth record email visibility checks - // for auth owner, admin or manager - if collection.IsAuth() { - authId := extractAuthIdFromGetter(client) - if authId == cleanRecord.Id { - if api.canAccessRecord(cleanRecord, requestInfo, collection.AuthOptions().ManageRule) { - cleanRecord.IgnoreEmailVisibility(true) + 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 + cleanRecord := record.Fresh() + + // mock request data + requestInfo := &core.RequestInfo{ + Context: core.RequestInfoContextRealtime, + Method: "GET", + Query: options.Query, + Headers: options.Headers, + } + requestInfo.Auth, _ = client.Get(RealtimeClientAuthKey).(*core.Record) + + if !realtimeCanAccessRecord(app, cleanRecord, requestInfo, rule) { + continue + } + + // trigger the enrich hooks + enrichErr := triggerRecordEnrichHooks(app, requestInfo, []*core.Record{cleanRecord}, func() error { + // apply expand + rawExpand := options.Query[expandQueryParam] + if rawExpand != "" { + expandErrs := app.ExpandRecord(cleanRecord, strings.Split(rawExpand, ","), expandFetch(app, requestInfo)) + if len(expandErrs) > 0 { + app.Logger().Debug( + "[broadcastRecord] expand errors", + slog.String("id", cleanRecord.Id), + slog.String("collectionName", cleanRecord.Collection().Name), + slog.String("sub", sub), + slog.String("expand", rawExpand), + slog.Any("errors", expandErrs), + ) + } + } + + // ignore the auth record email visibility checks + // for auth owner, superuser or manager + if collection.IsAuth() { + authId := extractAuthIdFromGetter(client) + if authId == cleanRecord.Id || + realtimeCanAccessRecord(app, cleanRecord, requestInfo, collection.ManageRule) { + cleanRecord.IgnoreEmailVisibility(true) + } + } + + return nil + }) + if enrichErr != nil { + app.Logger().Debug( + "[broadcastRecord] record enrich error", + slog.String("id", cleanRecord.Id), + slog.String("collectionName", cleanRecord.Collection().Name), + slog.String("sub", sub), + slog.Any("error", enrichErr), + ) + continue + } + + data := &recordData{ + Action: action, + Record: cleanRecord, + } + + // check fields + rawFields := options.Query[fieldsQueryParam] + if rawFields != "" { + decoded, err := picker.Pick(cleanRecord, rawFields) + if err == nil { + data.Record = decoded + } else { + app.Logger().Debug( + "[broadcastRecord] pick fields error", + slog.String("id", cleanRecord.Id), + slog.String("collectionName", cleanRecord.Collection().Name), + slog.String("sub", sub), + slog.String("fields", rawFields), + slog.String("error", err.Error()), + ) + } + } + + dataBytes, err := json.Marshal(data) + if err != nil { + app.Logger().Debug( + "[broadcastRecord] data marshal error", + slog.String("id", cleanRecord.Id), + slog.String("collectionName", cleanRecord.Collection().Name), + slog.String("error", err.Error()), + ) + continue + } + + msg := subscriptions.Message{ + Name: sub, + Data: dataBytes, + } + + if dryCache { + messages, ok := client.Get(dryCacheKey).([]subscriptions.Message) + if !ok { + messages = []subscriptions.Message{msg} + } else { + messages = append(messages, msg) + } + client.Set(dryCacheKey, messages) + } else { + routine.FireAndForget(func() { + client.Send(msg) + }) } } } - - data := &recordData{ - Action: action, - Record: cleanRecord, - } - - // check fields - rawFields := cast.ToString(options.Query[fieldsQueryParam]) - if rawFields != "" { - decoded, err := rest.PickFields(cleanRecord, rawFields) - if err == nil { - data.Record = decoded - } else { - api.app.Logger().Debug( - "[broadcastRecord] pick fields error", - slog.String("id", cleanRecord.Id), - slog.String("collectionName", cleanRecord.Collection().Name), - slog.String("sub", sub), - slog.String("fields", rawFields), - slog.String("error", err.Error()), - ) - } - } - - dataBytes, err := json.Marshal(data) - if err != nil { - api.app.Logger().Debug( - "[broadcastRecord] data marshal error", - slog.String("id", cleanRecord.Id), - slog.String("collectionName", cleanRecord.Collection().Name), - slog.String("error", err.Error()), - ) - continue - } - - msg := subscriptions.Message{ - Name: sub, - Data: dataBytes, - } - - if dryCache { - messages, ok := client.Get(dryCacheKey).([]subscriptions.Message) - if !ok { - messages = []subscriptions.Message{msg} - } else { - messages = append(messages, msg) - } - client.Set(dryCacheKey, messages) - } else { - routine.FireAndForget(func() { - client.Send(msg) - }) - } } - } - } - return nil -} - -// broadcastDryCachedRecord broadcasts all cached record related messages. -func (api *realtimeApi) broadcastDryCachedRecord(action string, record *models.Record) error { - key := action + "/" + record.Id - - clients := api.app.SubscriptionsBroker().Clients() - - for _, client := range clients { - messages, ok := client.Get(key).([]subscriptions.Message) - if !ok { - continue - } - - client.Unset(key) - - client := client - - routine.FireAndForget(func() { - for _, msg := range messages { - client.Send(msg) - } + return nil }) } - return nil + return group.Wait() +} + +// realtimeBroadcastDryCachedRecord broadcasts all cached record related messages. +func realtimeBroadcastDryCachedRecord(app core.App, action string, record *core.Record) error { + chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize) + if len(chunks) == 0 { + return nil // no subscribers + } + + key := action + "/" + record.Id + + group := new(errgroup.Group) + + for _, chunk := range chunks { + group.Go(func() error { + for _, client := range chunk { + messages, ok := client.Get(key).([]subscriptions.Message) + if !ok { + continue + } + + client.Unset(key) + + client := client + + routine.FireAndForget(func() { + for _, msg := range messages { + client.Send(msg) + } + }) + } + + return nil + }) + } + + return group.Wait() +} + +// realtimeUnsetDryCachedRecord removes the dry cached record related messages. +func realtimeUnsetDryCachedRecord(app core.App, action string, record *core.Record) error { + chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize) + if len(chunks) == 0 { + return nil // no subscribers + } + + key := action + "/" + record.Id + + group := new(errgroup.Group) + + for _, chunk := range chunks { + group.Go(func() error { + for _, client := range chunk { + if client.Get(key) != nil { + client.Unset(key) + } + } + + return nil + }) + } + + return group.Wait() } type getter interface { @@ -537,28 +685,24 @@ type getter interface { } func extractAuthIdFromGetter(val getter) string { - record, _ := val.Get(ContextAuthRecordKey).(*models.Record) + record, _ := val.Get(RealtimeClientAuthKey).(*core.Record) if record != nil { return record.Id } - admin, _ := val.Get(ContextAdminKey).(*models.Admin) - if admin != nil { - return admin.Id - } - return "" } -// canAccessRecord checks if the subscription client has access to the specified record model. -func (api *realtimeApi) canAccessRecord( - record *models.Record, - requestInfo *models.RequestInfo, +// realtimeCanAccessRecord checks if the subscription client has access to the specified record model. +func realtimeCanAccessRecord( + app core.App, + record *core.Record, + requestInfo *core.RequestInfo, accessRule *string, ) bool { // check the access rule // --- - if ok, _ := api.app.Dao().CanAccessRecord(record, requestInfo, accessRule); !ok { + if ok, _ := app.CanAccessRecord(record, requestInfo, accessRule); !ok { return false } @@ -569,25 +713,27 @@ func (api *realtimeApi) canAccessRecord( return true // no further checks needed } - if err := checkForAdminOnlyRuleFields(requestInfo); err != nil { + err := checkForSuperuserOnlyRuleFields(requestInfo) + if err != nil { return false } - ruleFunc := func(q *dbx.SelectQuery) error { - resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestInfo, false) + var exists bool - expr, err := search.FilterData(filter).BuildExpr(resolver) - if err != nil { - return err - } - q.AndWhere(expr) + q := app.DB().Select("(1)"). + From(record.Collection().Name). + AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id}) - resolver.UpdateQuery(q) - - return nil + resolver := core.NewRecordFieldResolver(app, record.Collection(), requestInfo, false) + expr, err := search.FilterData(filter).BuildExpr(resolver) + if err != nil { + return false } - _, err := api.app.Dao().FindRecordById(record.Collection().Id, record.Id, ruleFunc) + q.AndWhere(expr) + resolver.UpdateQuery(q) - return err == nil + err = q.Limit(1).Row(&exists) + + return err == nil && exists } diff --git a/apis/realtime_test.go b/apis/realtime_test.go index 38cd070e..de854b8d 100644 --- a/apis/realtime_test.go +++ b/apis/realtime_test.go @@ -1,20 +1,17 @@ package apis_test import ( + "context" "errors" "net/http" "strings" "testing" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/subscriptions" ) @@ -22,7 +19,7 @@ func TestRealtimeConnect(t *testing.T) { scenarios := []tests.ApiScenario{ { Method: http.MethodGet, - Url: "/api/realtime", + URL: "/api/realtime", Timeout: 100 * time.Millisecond, ExpectedStatus: 200, ExpectedContent: []string{ @@ -31,12 +28,11 @@ func TestRealtimeConnect(t *testing.T) { `data:{"clientId":`, }, ExpectedEvents: map[string]int{ - "OnRealtimeConnectRequest": 1, - "OnRealtimeBeforeMessageSend": 1, - "OnRealtimeAfterMessageSend": 1, - "OnRealtimeDisconnectRequest": 1, + "*": 0, + "OnRealtimeConnectRequest": 1, + "OnRealtimeMessageSend": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { if len(app.SubscriptionsBroker().Clients()) != 0 { t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients())) } @@ -45,23 +41,23 @@ func TestRealtimeConnect(t *testing.T) { { Name: "PB_CONNECT interrupt", Method: http.MethodGet, - Url: "/api/realtime", + URL: "/api/realtime", Timeout: 100 * time.Millisecond, ExpectedStatus: 200, ExpectedEvents: map[string]int{ - "OnRealtimeConnectRequest": 1, - "OnRealtimeBeforeMessageSend": 1, - "OnRealtimeDisconnectRequest": 1, + "*": 0, + "OnRealtimeConnectRequest": 1, + "OnRealtimeMessageSend": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRealtimeBeforeMessageSend().Add(func(e *core.RealtimeMessageEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRealtimeMessageSend().BindFunc(func(e *core.RealtimeMessageEvent) error { if e.Message.Name == "PB_CONNECT" { return errors.New("PB_CONNECT error") } - return nil + return e.Next() }) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { if len(app.SubscriptionsBroker().Clients()) != 0 { t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients())) } @@ -70,20 +66,20 @@ func TestRealtimeConnect(t *testing.T) { { Name: "Skipping/ignoring messages", Method: http.MethodGet, - Url: "/api/realtime", + URL: "/api/realtime", Timeout: 100 * time.Millisecond, ExpectedStatus: 200, ExpectedEvents: map[string]int{ - "OnRealtimeConnectRequest": 1, - "OnRealtimeBeforeMessageSend": 1, - "OnRealtimeDisconnectRequest": 1, + "*": 0, + "OnRealtimeConnectRequest": 1, + "OnRealtimeMessageSend": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRealtimeBeforeMessageSend().Add(func(e *core.RealtimeMessageEvent) error { - return hook.StopPropagation + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRealtimeMessageSend().BindFunc(func(e *core.RealtimeMessageEvent) error { + return nil }) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { if len(app.SubscriptionsBroker().Clients()) != 0 { t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients())) } @@ -101,34 +97,34 @@ func TestRealtimeSubscribe(t *testing.T) { resetClient := func() { client.Unsubscribe() - client.Set(apis.ContextAdminKey, nil) - client.Set(apis.ContextAuthRecordKey, nil) + client.Set(apis.RealtimeClientAuthKey, nil) } scenarios := []tests.ApiScenario{ { Name: "missing client", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"missing","subscriptions":["test1", "test2"]}`), ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "existing client - empty subscriptions", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":[]}`), ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnRealtimeBeforeSubscribeRequest": 1, - "OnRealtimeAfterSubscribeRequest": 1, + "*": 0, + "OnRealtimeSubscribeRequest": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { client.Subscribe("test0") app.SubscriptionsBroker().Register(client) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { if len(client.Subscriptions()) != 0 { t.Errorf("Expected no subscriptions, got %v", client.Subscriptions()) } @@ -138,18 +134,18 @@ func TestRealtimeSubscribe(t *testing.T) { { Name: "existing client - 2 new subscriptions", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnRealtimeBeforeSubscribeRequest": 1, - "OnRealtimeAfterSubscribeRequest": 1, + "*": 0, + "OnRealtimeSubscribeRequest": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { client.Subscribe("test0") app.SubscriptionsBroker().Register(client) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { expectedSubs := []string{"test1", "test2"} if len(expectedSubs) != len(client.Subscriptions()) { t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions()) @@ -164,49 +160,49 @@ func TestRealtimeSubscribe(t *testing.T) { }, }, { - Name: "existing client - authorized admin", + Name: "existing client - authorized superuser", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnRealtimeBeforeSubscribeRequest": 1, - "OnRealtimeAfterSubscribeRequest": 1, + "*": 0, + "OnRealtimeSubscribeRequest": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { app.SubscriptionsBroker().Register(client) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - admin, _ := client.Get(apis.ContextAdminKey).(*models.Admin) - if admin == nil { - t.Errorf("Expected admin auth model, got nil") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + authRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record) + if authRecord == nil || !authRecord.IsSuperuser() { + t.Errorf("Expected superuser auth record, got %v", authRecord) } resetClient() }, }, { - Name: "existing client - authorized record", + Name: "existing client - authorized regular record", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnRealtimeBeforeSubscribeRequest": 1, - "OnRealtimeAfterSubscribeRequest": 1, + "*": 0, + "OnRealtimeSubscribeRequest": 1, }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { app.SubscriptionsBroker().Register(client) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - authRecord, _ := client.Get(apis.ContextAuthRecordKey).(*models.Record) + 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") + t.Errorf("Expected regular user auth record, got %v", authRecord) } resetClient() }, @@ -214,22 +210,50 @@ func TestRealtimeSubscribe(t *testing.T) { { Name: "existing client - mismatched auth", Method: http.MethodPost, - Url: "/api/realtime", + URL: "/api/realtime", Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - initialAuth := &models.Record{} - initialAuth.RefreshId() - client.Set(apis.ContextAuthRecordKey, initialAuth) + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test2@example.com") + if err != nil { + t.Fatal(err) + } + + client.Set(apis.RealtimeClientAuthKey, user) app.SubscriptionsBroker().Register(client) }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - authRecord, _ := client.Get(apis.ContextAuthRecordKey).(*models.Record) + 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 - unauthorized client", + Method: http.MethodPost, + URL: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test2@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") } @@ -247,24 +271,29 @@ func TestRealtimeAuthRecordDeleteEvent(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - apis.InitApi(testApp) + // init realtime handlers + apis.NewRouter(testApp) - authRecord, err := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + authRecord, err := testApp.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAuthRecordKey, authRecord) + client.Set(apis.RealtimeClientAuthKey, authRecord) testApp.SubscriptionsBroker().Register(client) + // mock delete event e := new(core.ModelEvent) - e.Dao = testApp.Dao() + e.App = testApp + e.Type = core.ModelEventTypeDelete + e.Context = context.Background() e.Model = authRecord - testApp.OnModelAfterDelete().Trigger(e) - if len(testApp.SubscriptionsBroker().Clients()) != 0 { - t.Fatalf("Expected no subscription clients, found %d", len(testApp.SubscriptionsBroker().Clients())) + testApp.OnModelAfterDeleteSuccess().Trigger(e) + + if total := len(testApp.SubscriptionsBroker().Clients()); total != 0 { + t.Fatalf("Expected no subscription clients, found %d", total) } } @@ -272,111 +301,58 @@ func TestRealtimeAuthRecordUpdateEvent(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - apis.InitApi(testApp) + // init realtime handlers + apis.NewRouter(testApp) - authRecord1, err := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + authRecord1, err := testApp.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAuthRecordKey, authRecord1) + client.Set(apis.RealtimeClientAuthKey, authRecord1) testApp.SubscriptionsBroker().Register(client) // refetch the authRecord and change its email - authRecord2, err := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + authRecord2, err := testApp.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } authRecord2.SetEmail("new@example.com") + // mock update event e := new(core.ModelEvent) - e.Dao = testApp.Dao() + e.App = testApp + e.Type = core.ModelEventTypeUpdate + e.Context = context.Background() e.Model = authRecord2 - testApp.OnModelAfterUpdate().Trigger(e) - clientAuthRecord, _ := client.Get(apis.ContextAuthRecordKey).(*models.Record) + testApp.OnModelAfterUpdateSuccess().Trigger(e) + + clientAuthRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record) if clientAuthRecord.Email() != authRecord2.Email() { t.Fatalf("Expected authRecord with email %q, got %q", authRecord2.Email(), clientAuthRecord.Email()) } } -func TestRealtimeAdminDeleteEvent(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - apis.InitApi(testApp) - - admin, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAdminKey, admin) - testApp.SubscriptionsBroker().Register(client) - - e := new(core.ModelEvent) - e.Dao = testApp.Dao() - e.Model = admin - testApp.OnModelAfterDelete().Trigger(e) - - if len(testApp.SubscriptionsBroker().Clients()) != 0 { - t.Fatalf("Expected no subscription clients, found %d", len(testApp.SubscriptionsBroker().Clients())) - } -} - -func TestRealtimeAdminUpdateEvent(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - apis.InitApi(testApp) - - admin1, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAdminKey, admin1) - testApp.SubscriptionsBroker().Register(client) - - // refetch the authRecord and change its email - admin2, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - admin2.Email = "new@example.com" - - e := new(core.ModelEvent) - e.Dao = testApp.Dao() - e.Model = admin2 - testApp.OnModelAfterUpdate().Trigger(e) - - clientAdmin, _ := client.Get(apis.ContextAdminKey).(*models.Admin) - if clientAdmin.Email != admin2.Email { - t.Fatalf("Expected authRecord with email %q, got %q", admin2.Email, clientAdmin.Email) - } -} - // Custom auth record model struct // ------------------------------------------------------------------- -var _ models.Model = (*CustomUser)(nil) +var _ core.Model = (*CustomUser)(nil) type CustomUser struct { - models.BaseModel + core.BaseModel Email string `db:"email" json:"email"` } func (m *CustomUser) TableName() string { - return "users" // the name of your collection + return "users" } -func findCustomUserByEmail(dao *daos.Dao, email string) (*CustomUser, error) { +func findCustomUserByEmail(app core.App, email string) (*CustomUser, error) { model := &CustomUser{} - err := dao.ModelQuery(model). + err := app.ModelQuery(model). AndWhere(dbx.HashExp{"email": email}). Limit(1). One(model) @@ -392,30 +368,31 @@ func TestRealtimeCustomAuthModelDeleteEvent(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - apis.InitApi(testApp) + // init realtime handlers + apis.NewRouter(testApp) - authRecord, err := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + authRecord, err := testApp.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAuthRecordKey, authRecord) + client.Set(apis.RealtimeClientAuthKey, authRecord) testApp.SubscriptionsBroker().Register(client) // refetch the authRecord as CustomUser - customUser, err := findCustomUserByEmail(testApp.Dao(), "test@example.com") + customUser, err := findCustomUserByEmail(testApp, "test@example.com") if err != nil { t.Fatal(err) } // delete the custom user (should unset the client auth record) - if err := testApp.Dao().Delete(customUser); err != nil { + if err := testApp.Delete(customUser); err != nil { t.Fatal(err) } - if len(testApp.SubscriptionsBroker().Clients()) != 0 { - t.Fatalf("Expected no subscription clients, found %d", len(testApp.SubscriptionsBroker().Clients())) + if total := len(testApp.SubscriptionsBroker().Clients()); total != 0 { + t.Fatalf("Expected no subscription clients, found %d", total) } } @@ -423,30 +400,31 @@ func TestRealtimeCustomAuthModelUpdateEvent(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - apis.InitApi(testApp) + // init realtime handlers + apis.NewRouter(testApp) - authRecord, err := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + authRecord, err := testApp.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } client := subscriptions.NewDefaultClient() - client.Set(apis.ContextAuthRecordKey, authRecord) + client.Set(apis.RealtimeClientAuthKey, authRecord) testApp.SubscriptionsBroker().Register(client) // refetch the authRecord as CustomUser - customUser, err := findCustomUserByEmail(testApp.Dao(), "test@example.com") + customUser, err := findCustomUserByEmail(testApp, "test@example.com") if err != nil { t.Fatal(err) } // change its email customUser.Email = "new@example.com" - if err := testApp.Dao().Save(customUser); err != nil { + if err := testApp.Save(customUser); err != nil { t.Fatal(err) } - clientAuthRecord, _ := client.Get(apis.ContextAuthRecordKey).(*models.Record) + clientAuthRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record) if clientAuthRecord.Email() != customUser.Email { t.Fatalf("Expected authRecord with email %q, got %q", customUser.Email, clientAuthRecord.Email()) } diff --git a/apis/record_auth.go b/apis/record_auth.go index 75f37847..a0f7770e 100644 --- a/apis/record_auth.go +++ b/apis/record_auth.go @@ -1,765 +1,75 @@ package apis import ( - "encoding/json" - "errors" - "fmt" - "log/slog" - "net/http" - "sort" - "time" - - "github.com/labstack/echo/v5" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/resolvers" - "github.com/pocketbase/pocketbase/tools/auth" - "github.com/pocketbase/pocketbase/tools/routine" - "github.com/pocketbase/pocketbase/tools/search" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/subscriptions" - "github.com/pocketbase/pocketbase/tools/types" - "golang.org/x/oauth2" + "github.com/pocketbase/pocketbase/tools/router" ) // bindRecordAuthApi registers the auth record api endpoints and // the corresponding handlers. -func bindRecordAuthApi(app core.App, rg *echo.Group) { - api := recordAuthApi{app: app} - +func bindRecordAuthApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { // global oauth2 subscription redirect handler - rg.GET("/oauth2-redirect", api.oauth2SubscriptionRedirect) - rg.POST("/oauth2-redirect", api.oauth2SubscriptionRedirect) // needed in case of response_mode=form_post + rg.GET("/oauth2-redirect", oauth2SubscriptionRedirect) + // add again as POST in case of response_mode=form_post + rg.POST("/oauth2-redirect", oauth2SubscriptionRedirect) - // common collection record related routes - subGroup := rg.Group( - "/collections/:collection", - ActivityLogger(app), - LoadCollectionContext(app, models.CollectionTypeAuth), + sub := rg.Group("/collections/{collection}") + + sub.GET("/auth-methods", recordAuthMethods).Bind( + collectionPathRateLimit("", "listAuthMethods"), ) - subGroup.GET("/auth-methods", api.authMethods) - subGroup.POST("/auth-refresh", api.authRefresh, RequireSameContextRecordAuth()) - subGroup.POST("/auth-with-oauth2", api.authWithOAuth2) - subGroup.POST("/auth-with-password", api.authWithPassword) - subGroup.POST("/request-password-reset", api.requestPasswordReset) - subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) - subGroup.POST("/request-verification", api.requestVerification) - subGroup.POST("/confirm-verification", api.confirmVerification) - subGroup.POST("/request-email-change", api.requestEmailChange, RequireSameContextRecordAuth()) - subGroup.POST("/confirm-email-change", api.confirmEmailChange) - subGroup.GET("/records/:id/external-auths", api.listExternalAuths, RequireAdminOrOwnerAuth("id")) - subGroup.DELETE("/records/:id/external-auths/:provider", api.unlinkExternalAuth, RequireAdminOrOwnerAuth("id")) + + sub.POST("/auth-refresh", recordAuthRefresh).Bind( + collectionPathRateLimit("", "authRefresh"), + RequireSameCollectionContextAuth(""), + ) + + sub.POST("/auth-with-password", recordAuthWithPassword).Bind( + collectionPathRateLimit("", "authWithPassword", "auth"), + ) + + sub.POST("/auth-with-oauth2", recordAuthWithOAuth2).Bind( + collectionPathRateLimit("", "authWithOAuth2", "auth"), + ) + + sub.POST("/request-otp", recordRequestOTP).Bind( + collectionPathRateLimit("", "requestOTP"), + ) + sub.POST("/auth-with-otp", recordAuthWithOTP).Bind( + collectionPathRateLimit("", "authWithOTP", "auth"), + ) + + sub.POST("/request-password-reset", recordRequestPasswordReset).Bind( + collectionPathRateLimit("", "requestPasswordReset"), + ) + sub.POST("/confirm-password-reset", recordConfirmPasswordReset).Bind( + collectionPathRateLimit("", "confirmPasswordReset"), + ) + + sub.POST("/request-verification", recordRequestVerification).Bind( + collectionPathRateLimit("", "requestVerification"), + ) + sub.POST("/confirm-verification", recordConfirmVerification).Bind( + collectionPathRateLimit("", "confirmVerification"), + ) + + sub.POST("/request-email-change", recordRequestEmailChange).Bind( + collectionPathRateLimit("", "requestEmailChange"), + RequireSameCollectionContextAuth(""), + ) + sub.POST("/confirm-email-change", recordConfirmEmailChange).Bind( + collectionPathRateLimit("", "confirmEmailChange"), + ) + + sub.POST("/impersonate/{id}", recordAuthImpersonate).Bind(RequireSuperuserAuth()) } -type recordAuthApi struct { - app core.App -} - -func (api *recordAuthApi) authRefresh(c echo.Context) error { - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record == nil { - return NewNotFoundError("Missing auth record context.", nil) - } - - event := new(core.RecordAuthRefreshEvent) - event.HttpContext = c - event.Collection = record.Collection() - event.Record = record - - return api.app.OnRecordBeforeAuthRefreshRequest().Trigger(event, func(e *core.RecordAuthRefreshEvent) error { - return api.app.OnRecordAfterAuthRefreshRequest().Trigger(event, func(e *core.RecordAuthRefreshEvent) error { - return RecordAuthResponse(api.app, e.HttpContext, e.Record, nil) - }) - }) -} - -type providerInfo struct { - Name string `json:"name"` - DisplayName string `json:"displayName"` - State string `json:"state"` - AuthUrl string `json:"authUrl"` - // technically could be omitted if the provider doesn't support PKCE, - // but to avoid breaking existing typed clients we'll return them as empty string - CodeVerifier string `json:"codeVerifier"` - CodeChallenge string `json:"codeChallenge"` - CodeChallengeMethod string `json:"codeChallengeMethod"` -} - -func (api *recordAuthApi) authMethods(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - authOptions := collection.AuthOptions() - - result := struct { - AuthProviders []providerInfo `json:"authProviders"` - UsernamePassword bool `json:"usernamePassword"` - EmailPassword bool `json:"emailPassword"` - OnlyVerified bool `json:"onlyVerified"` - }{ - UsernamePassword: authOptions.AllowUsernameAuth, - EmailPassword: authOptions.AllowEmailAuth, - OnlyVerified: authOptions.OnlyVerified, - AuthProviders: []providerInfo{}, - } - - if !authOptions.AllowOAuth2Auth { - return c.JSON(http.StatusOK, result) - } - - nameConfigMap := api.app.Settings().NamedAuthProviderConfigs() - for name, config := range nameConfigMap { - if !config.Enabled { - continue - } - - provider, err := auth.NewProviderByName(name) - if err != nil { - api.app.Logger().Debug("Missing or invalid provider name", slog.String("name", name)) - continue // skip provider - } - - if err := config.SetupProvider(provider); err != nil { - api.app.Logger().Debug( - "Failed to setup provider", - slog.String("name", name), - slog.String("error", err.Error()), - ) - continue // skip provider - } - - info := providerInfo{ - Name: name, - DisplayName: provider.DisplayName(), - State: security.RandomString(30), - } - - if info.DisplayName == "" { - info.DisplayName = name - } - - urlOpts := []oauth2.AuthCodeOption{} - - // custom providers url options - switch name { - case auth.NameApple: - // see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113 - urlOpts = append(urlOpts, oauth2.SetAuthURLParam("response_mode", "form_post")) - } - - if provider.PKCE() { - info.CodeVerifier = security.RandomString(43) - info.CodeChallenge = security.S256Challenge(info.CodeVerifier) - info.CodeChallengeMethod = "S256" - urlOpts = append(urlOpts, - oauth2.SetAuthURLParam("code_challenge", info.CodeChallenge), - oauth2.SetAuthURLParam("code_challenge_method", info.CodeChallengeMethod), - ) - } - - info.AuthUrl = provider.BuildAuthUrl( - info.State, - urlOpts..., - ) + "&redirect_uri=" // empty redirect_uri so that users can append their redirect url - - result.AuthProviders = append(result.AuthProviders, info) - } - - // sort providers - sort.SliceStable(result.AuthProviders, func(i, j int) bool { - return result.AuthProviders[i].Name < result.AuthProviders[j].Name - }) - - return c.JSON(http.StatusOK, result) -} - -func (api *recordAuthApi) authWithOAuth2(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - if !collection.AuthOptions().AllowOAuth2Auth { - return NewBadRequestError("The collection is not configured to allow OAuth2 authentication.", nil) - } - - var fallbackAuthRecord *models.Record - - loggedAuthRecord, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if loggedAuthRecord != nil && loggedAuthRecord.Collection().Id == collection.Id { - fallbackAuthRecord = loggedAuthRecord - } - - form := forms.NewRecordOAuth2Login(api.app, collection, fallbackAuthRecord) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.RecordAuthWithOAuth2Event) - event.HttpContext = c - event.Collection = collection - event.ProviderName = form.Provider - - form.SetBeforeNewRecordCreateFunc(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error { - return createForm.DrySubmit(func(txDao *daos.Dao) error { - event.IsNewRecord = true - - // clone the current request data and assign the form create data as its body data - requestInfo := *RequestInfo(c) - requestInfo.Context = models.RequestInfoContextOAuth2 - requestInfo.Data = form.CreateData - - createRuleFunc := func(q *dbx.SelectQuery) error { - admin, _ := c.Get(ContextAdminKey).(*models.Admin) - if admin != nil { - return nil // either admin or the rule is empty - } - - if collection.CreateRule == nil { - return errors.New("Only admins can create new accounts with OAuth2") - } - - if *collection.CreateRule != "" { - resolver := resolvers.NewRecordFieldResolver(txDao, collection, &requestInfo, true) - expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver) - if err != nil { - return err - } - resolver.UpdateQuery(q) - q.AndWhere(expr) - } - - return nil - } - - if _, err := txDao.FindRecordById(collection.Id, createForm.Id, createRuleFunc); err != nil { - return fmt.Errorf("Failed create rule constraint: %w", err) - } - - return nil - }) - }) - - _, _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*forms.RecordOAuth2LoginData]) forms.InterceptorNextFunc[*forms.RecordOAuth2LoginData] { - return func(data *forms.RecordOAuth2LoginData) error { - event.Record = data.Record - event.OAuth2User = data.OAuth2User - event.ProviderClient = data.ProviderClient - event.IsNewRecord = data.Record == nil - - return api.app.OnRecordBeforeAuthWithOAuth2Request().Trigger(event, func(e *core.RecordAuthWithOAuth2Event) error { - data.Record = e.Record - data.OAuth2User = e.OAuth2User - - if err := next(data); err != nil { - return NewBadRequestError("Failed to authenticate.", err) - } - - e.Record = data.Record - e.OAuth2User = data.OAuth2User - - meta := struct { - *auth.AuthUser - IsNew bool `json:"isNew"` - }{ - AuthUser: e.OAuth2User, - IsNew: event.IsNewRecord, - } - - return api.app.OnRecordAfterAuthWithOAuth2Request().Trigger(event, func(e *core.RecordAuthWithOAuth2Event) error { - // clear the lastLoginAlertSentAt field so that we can enforce password auth notifications - if !e.Record.LastLoginAlertSentAt().IsZero() { - e.Record.Set(schema.FieldNameLastLoginAlertSentAt, "") - if err := api.app.Dao().SaveRecord(e.Record); err != nil { - api.app.Logger().Warn("Failed to reset lastLoginAlertSentAt", "error", err, "recordId", e.Record.Id) - } - } - - return RecordAuthResponse(api.app, e.HttpContext, e.Record, meta) - }) - }) - } - }) - - return submitErr -} - -func (api *recordAuthApi) authWithPassword(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - form := forms.NewRecordPasswordLogin(api.app, collection) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.RecordAuthWithPasswordEvent) - event.HttpContext = c - event.Collection = collection - event.Password = form.Password - event.Identity = form.Identity - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeAuthWithPasswordRequest().Trigger(event, func(e *core.RecordAuthWithPasswordEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to authenticate.", err) - } - - // @todo remove after the refactoring - if collection.AuthOptions().AllowOAuth2Auth && e.Record.Email() != "" { - externalAuths, err := api.app.Dao().FindAllExternalAuthsByRecord(e.Record) - if err != nil { - return NewBadRequestError("Failed to authenticate.", err) - } - if len(externalAuths) > 0 { - lastLoginAlert := e.Record.LastLoginAlertSentAt().Time() - - // send an email alert if the password auth is after OAuth2 auth (lastLoginAlert will be empty) - // or if it has been ~7 days since the last alert - if lastLoginAlert.IsZero() || time.Now().UTC().Sub(lastLoginAlert).Hours() > 168 { - providerNames := make([]string, len(externalAuths)) - for i, ea := range externalAuths { - var name string - if provider, err := auth.NewProviderByName(ea.Provider); err == nil { - name = provider.DisplayName() - } - if name == "" { - name = ea.Provider - } - providerNames[i] = name - } - - if err := mails.SendRecordPasswordLoginAlert(api.app, e.Record, providerNames...); err != nil { - return NewBadRequestError("Failed to authenticate.", err) - } - - e.Record.SetLastLoginAlertSentAt(types.NowDateTime()) - if err := api.app.Dao().SaveRecord(e.Record); err != nil { - api.app.Logger().Warn("Failed to update lastLoginAlertSentAt", "error", err, "recordId", e.Record.Id) - } - } - } - } - - return api.app.OnRecordAfterAuthWithPasswordRequest().Trigger(event, func(e *core.RecordAuthWithPasswordEvent) error { - return RecordAuthResponse(api.app, e.HttpContext, e.Record, nil) - }) - }) - } - }) - - return submitErr -} - -func (api *recordAuthApi) requestPasswordReset(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - authOptions := collection.AuthOptions() - if !authOptions.AllowUsernameAuth && !authOptions.AllowEmailAuth { - return NewBadRequestError("The collection is not configured to allow password authentication.", nil) - } - - form := forms.NewRecordPasswordResetRequest(api.app, collection) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - if err := form.Validate(); err != nil { - return NewBadRequestError("An error occurred while validating the form.", err) - } - - event := new(core.RecordRequestPasswordResetEvent) - event.HttpContext = c - event.Collection = collection - - submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeRequestPasswordResetRequest().Trigger(event, func(e *core.RecordRequestPasswordResetEvent) error { - // run in background because we don't need to show the result to the client - routine.FireAndForget(func() { - if err := next(e.Record); err != nil { - api.app.Logger().Debug( - "Failed to send password reset email", - slog.String("error", err.Error()), - ) - } - }) - - return api.app.OnRecordAfterRequestPasswordResetRequest().Trigger(event, func(e *core.RecordRequestPasswordResetEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - // eagerly write 204 response and skip submit errors - // as a measure against emails enumeration - if !c.Response().Committed { - c.NoContent(http.StatusNoContent) - } - - return submitErr -} - -func (api *recordAuthApi) confirmPasswordReset(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - form := forms.NewRecordPasswordResetConfirm(api.app, collection) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.RecordConfirmPasswordResetEvent) - event.HttpContext = c - event.Collection = collection - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeConfirmPasswordResetRequest().Trigger(event, func(e *core.RecordConfirmPasswordResetEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to set new password.", err) - } - - return api.app.OnRecordAfterConfirmPasswordResetRequest().Trigger(event, func(e *core.RecordConfirmPasswordResetEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - return submitErr -} - -func (api *recordAuthApi) requestVerification(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - form := forms.NewRecordVerificationRequest(api.app, collection) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - if err := form.Validate(); err != nil { - return NewBadRequestError("An error occurred while validating the form.", err) - } - - event := new(core.RecordRequestVerificationEvent) - event.HttpContext = c - event.Collection = collection - - submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeRequestVerificationRequest().Trigger(event, func(e *core.RecordRequestVerificationEvent) error { - // run in background because we don't need to show the result to the client - routine.FireAndForget(func() { - if err := next(e.Record); err != nil { - api.app.Logger().Debug( - "Failed to send verification email", - slog.String("error", err.Error()), - ) - } - }) - - return api.app.OnRecordAfterRequestVerificationRequest().Trigger(event, func(e *core.RecordRequestVerificationEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - // eagerly write 204 response and skip submit errors - // as a measure against users enumeration - if !c.Response().Committed { - c.NoContent(http.StatusNoContent) - } - - return submitErr -} - -func (api *recordAuthApi) confirmVerification(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - form := forms.NewRecordVerificationConfirm(api.app, collection) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.RecordConfirmVerificationEvent) - event.HttpContext = c - event.Collection = collection - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeConfirmVerificationRequest().Trigger(event, func(e *core.RecordConfirmVerificationEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("An error occurred while submitting the form.", err) - } - - return api.app.OnRecordAfterConfirmVerificationRequest().Trigger(event, func(e *core.RecordConfirmVerificationEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - return submitErr -} - -func (api *recordAuthApi) requestEmailChange(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - record, _ := c.Get(ContextAuthRecordKey).(*models.Record) - if record == nil { - return NewUnauthorizedError("The request requires valid auth record.", nil) - } - - form := forms.NewRecordEmailChangeRequest(api.app, record) - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) - } - - event := new(core.RecordRequestEmailChangeEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - - return form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - return api.app.OnRecordBeforeRequestEmailChangeRequest().Trigger(event, func(e *core.RecordRequestEmailChangeEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to request email change.", err) - } - - return api.app.OnRecordAfterRequestEmailChangeRequest().Trigger(event, func(e *core.RecordRequestEmailChangeEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) -} - -func (api *recordAuthApi) confirmEmailChange(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - form := forms.NewRecordEmailChangeConfirm(api.app, collection) - if readErr := c.Bind(form); readErr != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", readErr) - } - - event := new(core.RecordConfirmEmailChangeEvent) - event.HttpContext = c - event.Collection = collection - - _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - event.Record = record - - return api.app.OnRecordBeforeConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to confirm email change.", err) - } - - return api.app.OnRecordAfterConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) - } - }) - - return submitErr -} - -func (api *recordAuthApi) listExternalAuths(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - id := c.PathParam("id") - if id == "" { - return NewNotFoundError("", nil) - } - - record, err := api.app.Dao().FindRecordById(collection.Id, id) - if err != nil || record == nil { - return NewNotFoundError("", err) - } - - externalAuths, err := api.app.Dao().FindAllExternalAuthsByRecord(record) - if err != nil { - return NewBadRequestError("Failed to fetch the external auths for the specified auth record.", err) - } - - event := new(core.RecordListExternalAuthsEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - event.ExternalAuths = externalAuths - - return api.app.OnRecordListExternalAuthsRequest().Trigger(event, func(e *core.RecordListExternalAuthsEvent) error { - return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths) - }) -} - -func (api *recordAuthApi) unlinkExternalAuth(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("Missing collection context.", nil) - } - - id := c.PathParam("id") - provider := c.PathParam("provider") - if id == "" || provider == "" { - return NewNotFoundError("", nil) - } - - record, err := api.app.Dao().FindRecordById(collection.Id, id) - if err != nil || record == nil { - return NewNotFoundError("", err) - } - - externalAuth, err := api.app.Dao().FindExternalAuthByRecordAndProvider(record, provider) - if err != nil { - return NewNotFoundError("Missing external auth provider relation.", err) - } - - event := new(core.RecordUnlinkExternalAuthEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - event.ExternalAuth = externalAuth - - return api.app.OnRecordBeforeUnlinkExternalAuthRequest().Trigger(event, func(e *core.RecordUnlinkExternalAuthEvent) error { - if err := api.app.Dao().DeleteExternalAuth(externalAuth); err != nil { - return NewBadRequestError("Cannot unlink the external auth provider.", err) - } - - return api.app.OnRecordAfterUnlinkExternalAuthRequest().Trigger(event, func(e *core.RecordUnlinkExternalAuthEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.NoContent(http.StatusNoContent) - }) - }) -} - -// ------------------------------------------------------------------- - -const ( - oauth2SubscriptionTopic string = "@oauth2" - oauth2RedirectFailurePath string = "../_/#/auth/oauth2-redirect-failure" - oauth2RedirectSuccessPath string = "../_/#/auth/oauth2-redirect-success" -) - -type oauth2RedirectData struct { - State string `form:"state" query:"state" json:"state"` - Code string `form:"code" query:"code" json:"code"` - Error string `form:"error" query:"error" json:"error,omitempty"` -} - -func (api *recordAuthApi) oauth2SubscriptionRedirect(c echo.Context) error { - redirectStatusCode := http.StatusTemporaryRedirect - if c.Request().Method != http.MethodGet { - redirectStatusCode = http.StatusSeeOther - } - - data := oauth2RedirectData{} - if err := c.Bind(&data); err != nil { - api.app.Logger().Debug("Failed to read OAuth2 redirect data", "error", err) - return c.Redirect(redirectStatusCode, oauth2RedirectFailurePath) - } - - if data.State == "" { - api.app.Logger().Debug("Missing OAuth2 state parameter") - return c.Redirect(redirectStatusCode, oauth2RedirectFailurePath) - } - - client, err := api.app.SubscriptionsBroker().ClientById(data.State) - if err != nil || client.IsDiscarded() || !client.HasSubscription(oauth2SubscriptionTopic) { - api.app.Logger().Debug("Missing or invalid OAuth2 subscription client", "error", err, "clientId", data.State) - return c.Redirect(redirectStatusCode, oauth2RedirectFailurePath) - } - defer client.Unsubscribe(oauth2SubscriptionTopic) - - encodedData, err := json.Marshal(data) - if err != nil { - api.app.Logger().Debug("Failed to marshalize OAuth2 redirect data", "error", err) - return c.Redirect(redirectStatusCode, oauth2RedirectFailurePath) - } - - msg := subscriptions.Message{ - Name: oauth2SubscriptionTopic, - Data: encodedData, - } - - client.Send(msg) - - if data.Error != "" || data.Code == "" { - api.app.Logger().Debug("Failed OAuth2 redirect due to an error or missing code parameter", "error", data.Error, "clientId", data.State) - return c.Redirect(redirectStatusCode, oauth2RedirectFailurePath) - } - - return c.Redirect(redirectStatusCode, oauth2RedirectSuccessPath) +func findAuthCollection(e *core.RequestEvent) (*core.Collection, error) { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) + + if err != nil || !collection.IsAuth() { + return nil, e.NotFoundError("Missing or invalid auth collection context.", err) + } + + return collection, nil } diff --git a/apis/record_auth_email_change_confirm.go b/apis/record_auth_email_change_confirm.go new file mode 100644 index 00000000..70579bf6 --- /dev/null +++ b/apis/record_auth_email_change_confirm.go @@ -0,0 +1,121 @@ +package apis + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/security" +) + +func recordConfirmEmailChange(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if collection.Name == core.CollectionNameSuperusers { + return e.BadRequestError("All superusers can change their emails directly.", nil) + } + + form := newEmailChangeConfirmForm(e.App, collection) + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + authRecord, newEmail, err := form.parseToken() + if err != nil { + return firstApiError(err, e.BadRequestError("Invalid or expired token.", err)) + } + + event := new(core.RecordConfirmEmailChangeRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = authRecord + event.NewEmail = newEmail + + return e.App.OnRecordConfirmEmailChangeRequest().Trigger(event, func(e *core.RecordConfirmEmailChangeRequestEvent) error { + authRecord.Set(core.FieldNameEmail, e.NewEmail) + authRecord.Set(core.FieldNameVerified, true) + authRecord.RefreshTokenKey() // invalidate old tokens + + if err := e.App.Save(e.Record); err != nil { + return firstApiError(err, e.BadRequestError("Failed to confirm email change.", err)) + } + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +func newEmailChangeConfirmForm(app core.App, collection *core.Collection) *EmailChangeConfirmForm { + return &EmailChangeConfirmForm{ + app: app, + collection: collection, + } +} + +type EmailChangeConfirmForm struct { + app core.App + collection *core.Collection + + Token string `form:"token" json:"token"` + Password string `form:"password" json:"password"` +} + +func (form *EmailChangeConfirmForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + validation.Field(&form.Password, validation.Required, validation.Length(1, 100), validation.By(form.checkPassword)), + ) +} + +func (form *EmailChangeConfirmForm) checkToken(value any) error { + _, _, err := form.parseToken() + return err +} + +func (form *EmailChangeConfirmForm) checkPassword(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + authRecord, _, _ := form.parseToken() + if authRecord == nil || !authRecord.ValidatePassword(v) { + return validation.NewError("validation_invalid_password", "Missing or invalid auth record password.") + } + + return nil +} + +func (form *EmailChangeConfirmForm) parseToken() (*core.Record, string, error) { + // check token payload + claims, _ := security.ParseUnverifiedJWT(form.Token) + newEmail, _ := claims[core.TokenClaimNewEmail].(string) + if newEmail == "" { + return nil, "", validation.NewError("validation_invalid_token_payload", "Invalid token payload - newEmail must be set.") + } + + // ensure that there aren't other users with the new email + _, err := form.app.FindAuthRecordByEmail(form.collection, newEmail) + if err == nil { + return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail) + } + + // verify that the token is not expired and its signature is valid + authRecord, err := form.app.FindAuthRecordByToken(form.Token, core.TokenTypeEmailChange) + if err != nil { + return nil, "", validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + if authRecord.Collection().Id != form.collection.Id { + return nil, "", validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") + } + + return authRecord, newEmail, nil +} diff --git a/apis/record_auth_email_change_confirm_test.go b/apis/record_auth_email_change_confirm_test.go new file mode 100644 index 00000000..8eb95838 --- /dev/null +++ b/apis/record_auth_email_change_confirm_test.go @@ -0,0 +1,205 @@ +package apis_test + +import ( + "errors" + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordConfirmEmailChange(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/confirm-email-change", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"token":{"code":"validation_required"`, + `"password":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{"token`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "expired token and correct password", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjYxfQ.dff842MO0mgRTHY8dktp0dqG9-7LGQOgRuiAbQpYBls", + "password":"1234567890" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{`, + `"code":"validation_invalid_token"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-email change token", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + "password":"1234567890" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{`, + `"code":"validation_invalid_token_payload"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid token and incorrect password", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567891" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"password":{`, + `"code":"validation_invalid_password"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid token and correct password", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567890" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmEmailChangeRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindAuthRecordByEmail("users", "change@example.com") + if err != nil { + t.Fatalf("Expected to find user with email %q, got error: %v", "change@example.com", err) + } + }, + }, + { + Name: "valid token in different auth collection", + Method: http.MethodPost, + URL: "/api/collections/clients/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567890" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_token_collection_mismatch"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "OnRecordAfterConfirmEmailChangeRequest error response", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordConfirmEmailChangeRequest().BindFunc(func(e *core.RecordConfirmEmailChangeRequestEvent) error { + return errors.New("error") + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmEmailChangeRequest": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:confirmEmailChange", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:confirmEmailChange"}, + {MaxRequests: 0, Label: "users:confirmEmailChange"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:confirmEmailChange", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-email-change", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJlbWFpbENoYW5nZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoyNTI0NjA0NDYxfQ.Y7mVlaEPhJiNPoIvIqbIosZU4c4lEhwysOrRR8c95iU", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:confirmEmailChange"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_email_change_request.go b/apis/record_auth_email_change_request.go new file mode 100644 index 00000000..686db572 --- /dev/null +++ b/apis/record_auth_email_change_request.go @@ -0,0 +1,90 @@ +package apis + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" +) + +func recordRequestEmailChange(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if collection.Name == core.CollectionNameSuperusers { + return e.BadRequestError("All superusers can change their emails directly.", nil) + } + + record := e.Auth + if record == nil { + return e.UnauthorizedError("The request requires valid auth record.", nil) + } + + form := newEmailChangeRequestForm(e.App, record) + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + event := new(core.RecordRequestEmailChangeRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + event.NewEmail = form.NewEmail + + return e.App.OnRecordRequestEmailChangeRequest().Trigger(event, func(e *core.RecordRequestEmailChangeRequestEvent) error { + if err := mails.SendRecordChangeEmail(e.App, e.Record, e.NewEmail); err != nil { + return firstApiError(err, e.BadRequestError("Failed to request email change.", err)) + } + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +func newEmailChangeRequestForm(app core.App, record *core.Record) *emailChangeRequestForm { + return &emailChangeRequestForm{ + app: app, + record: record, + } +} + +type emailChangeRequestForm struct { + app core.App + record *core.Record + + NewEmail string `form:"newEmail" json:"newEmail"` +} + +func (form *emailChangeRequestForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.NewEmail, + validation.Required, + validation.Length(1, 255), + is.EmailFormat, + validation.NotIn(form.record.Email()), + validation.By(form.checkUniqueEmail), + ), + ) +} + +func (form *emailChangeRequestForm) checkUniqueEmail(value any) error { + v, _ := value.(string) + if v == "" { + return nil + } + + found, _ := form.app.FindAuthRecordByEmail(form.record.Collection(), v) + if found != nil && found.Id != form.record.Id { + return validation.NewError("validation_invalid_new_email", "Invalid new email address.") + } + + return nil +} diff --git a/apis/record_auth_email_change_request_test.go b/apis/record_auth_email_change_request_test.go new file mode 100644 index 00000000..2ec9ec6f --- /dev/null +++ b/apis/record_auth_email_change_request_test.go @@ -0,0 +1,168 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordRequestEmailChange(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "record authentication but from different auth collection", + Method: http.MethodPost, + URL: "/api/collections/clients/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superuser authentication", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"newEmail":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid data (existing email)", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"test2@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"newEmail":{"code":"validation_invalid_new_email"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid data (new email)", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestEmailChangeRequest": 1, + "OnMailerSend": 1, + "OnMailerRecordEmailChangeSend": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if !strings.Contains(app.TestMailer.LastMessage().HTML, "/auth/confirm-email-change") { + t.Fatalf("Expected email change email, got\n%v", app.TestMailer.LastMessage().HTML) + } + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:requestEmailChange", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:requestEmailChange"}, + {MaxRequests: 0, Label: "users:requestEmailChange"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:requestEmailChange", + Method: http.MethodPost, + URL: "/api/collections/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:requestEmailChange"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_impersonate.go b/apis/record_auth_impersonate.go new file mode 100644 index 00000000..75ac2a96 --- /dev/null +++ b/apis/record_auth_impersonate.go @@ -0,0 +1,54 @@ +package apis + +import ( + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" +) + +// note: for now allow superusers but it may change in the future to allow access +// also to users with "Manage API" rule access depending on the use cases that will arise +func recordAuthImpersonate(e *core.RequestEvent) error { + if !e.HasSuperuserAuth() { + return e.ForbiddenError("", nil) + } + + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + record, err := e.App.FindRecordById(collection, e.Request.PathValue("id")) + if err != nil { + return e.NotFoundError("", err) + } + + form := &impersonateForm{} + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + token, err := record.NewStaticAuthToken(time.Duration(form.Duration) * time.Second) + if err != nil { + e.InternalServerError("Failed to generate static auth token", err) + } + + return recordAuthResponse(e, record, token, "", nil) +} + +// ------------------------------------------------------------------- + +type impersonateForm struct { + // Duration is the optional custom token duration in seconds. + Duration int64 `form:"duration" json:"duration"` +} + +func (form *impersonateForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Duration, validation.Min(0)), + ) +} diff --git a/apis/record_auth_impersonate_test.go b/apis/record_auth_impersonate_test.go new file mode 100644 index 00000000..7d9fd404 --- /dev/null +++ b/apis/record_auth_impersonate_test.go @@ -0,0 +1,109 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordAuthImpersonate(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as different user", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as the same user", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as superuser", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":"`, + `"id":"4q1xlclmfloku33"`, + `"record":{`, + }, + NotExpectedContent: []string{ + // hidden fields should remain hidden even though we are authenticated as superuser + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "authorized as superuser with custom invalid duration", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{"duration":-1}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"duration":{`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "authorized as superuser with custom valid duration", + Method: http.MethodPost, + URL: "/api/collections/users/impersonate/4q1xlclmfloku33", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: strings.NewReader(`{"duration":100}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":"`, + `"id":"4q1xlclmfloku33"`, + `"record":{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_methods.go b/apis/record_auth_methods.go new file mode 100644 index 00000000..df39d662 --- /dev/null +++ b/apis/record_auth_methods.go @@ -0,0 +1,170 @@ +package apis + +import ( + "log/slog" + "net/http" + "slices" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/security" + "golang.org/x/oauth2" +) + +type otpResponse struct { + Enabled bool `json:"enabled"` + Duration int64 `json:"duration"` // in seconds +} + +type mfaResponse struct { + Enabled bool `json:"enabled"` + Duration int64 `json:"duration"` // in seconds +} + +type passwordResponse struct { + IdentityFields []string `json:"identityFields"` + Enabled bool `json:"enabled"` +} + +type oauth2Response struct { + Providers []providerInfo `json:"providers"` + Enabled bool `json:"enabled"` +} + +type providerInfo struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + State string `json:"state"` + AuthURL string `json:"authURL"` + + // @todo + // deprecated: use AuthURL instead + // AuthUrl will be removed after dropping v0.22 support + AuthUrl string `json:"authUrl"` + + // technically could be omitted if the provider doesn't support PKCE, + // but to avoid breaking existing typed clients we'll return them as empty string + CodeVerifier string `json:"codeVerifier"` + CodeChallenge string `json:"codeChallenge"` + CodeChallengeMethod string `json:"codeChallengeMethod"` +} + +type authMethodsResponse struct { + Password passwordResponse `json:"password"` + OAuth2 oauth2Response `json:"oauth2"` + MFA mfaResponse `json:"mfa"` + OTP otpResponse `json:"otp"` + + // legacy fields + // @todo remove after dropping v0.22 support + AuthProviders []providerInfo `json:"authProviders"` + UsernamePassword bool `json:"usernamePassword"` + EmailPassword bool `json:"emailPassword"` +} + +func (amr *authMethodsResponse) fillLegacyFields() { + amr.EmailPassword = amr.Password.Enabled && slices.Contains(amr.Password.IdentityFields, "email") + + amr.UsernamePassword = amr.Password.Enabled && slices.Contains(amr.Password.IdentityFields, "username") + + if amr.OAuth2.Enabled { + amr.AuthProviders = amr.OAuth2.Providers + } +} + +func recordAuthMethods(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + result := authMethodsResponse{ + Password: passwordResponse{ + IdentityFields: make([]string, 0, len(collection.PasswordAuth.IdentityFields)), + }, + OAuth2: oauth2Response{ + Providers: make([]providerInfo, 0, len(collection.OAuth2.Providers)), + }, + OTP: otpResponse{ + Enabled: collection.OTP.Enabled, + }, + MFA: mfaResponse{ + Enabled: collection.MFA.Enabled, + }, + } + + if collection.PasswordAuth.Enabled { + result.Password.Enabled = true + result.Password.IdentityFields = collection.PasswordAuth.IdentityFields + } + + if collection.OTP.Enabled { + result.OTP.Duration = collection.OTP.Duration + } + + if collection.MFA.Enabled { + result.MFA.Duration = collection.MFA.Duration + } + + if !collection.OAuth2.Enabled { + result.fillLegacyFields() + + return e.JSON(http.StatusOK, result) + } + + result.OAuth2.Enabled = true + + for _, config := range collection.OAuth2.Providers { + provider, err := config.InitProvider() + if err != nil { + e.App.Logger().Debug( + "Failed to setup OAuth2 provider", + slog.String("name", config.Name), + slog.String("error", err.Error()), + ) + continue // skip provider + } + + info := providerInfo{ + Name: config.Name, + DisplayName: provider.DisplayName(), + State: security.RandomString(30), + } + + if info.DisplayName == "" { + info.DisplayName = config.Name + } + + urlOpts := []oauth2.AuthCodeOption{} + + // custom providers url options + switch config.Name { + case auth.NameApple: + // see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113 + urlOpts = append(urlOpts, oauth2.SetAuthURLParam("response_mode", "form_post")) + } + + if provider.PKCE() { + info.CodeVerifier = security.RandomString(43) + info.CodeChallenge = security.S256Challenge(info.CodeVerifier) + info.CodeChallengeMethod = "S256" + urlOpts = append(urlOpts, + oauth2.SetAuthURLParam("code_challenge", info.CodeChallenge), + oauth2.SetAuthURLParam("code_challenge_method", info.CodeChallengeMethod), + ) + } + + info.AuthURL = provider.BuildAuthURL( + info.State, + urlOpts..., + ) + "&redirect_uri=" // empty redirect_uri so that users can append their redirect url + + info.AuthUrl = info.AuthURL + + result.OAuth2.Providers = append(result.OAuth2.Providers, info) + } + + result.fillLegacyFields() + + return e.JSON(http.StatusOK, result) +} diff --git a/apis/record_auth_methods_test.go b/apis/record_auth_methods_test.go new file mode 100644 index 00000000..ec624681 --- /dev/null +++ b/apis/record_auth_methods_test.go @@ -0,0 +1,106 @@ +package apis_test + +import ( + "net/http" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordAuthMethodsList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodGet, + URL: "/api/collections/missing/auth-methods", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non auth collection", + Method: http.MethodGet, + URL: "/api/collections/demo1/auth-methods", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth collection with none auth methods allowed", + Method: http.MethodGet, + URL: "/api/collections/nologin/auth-methods", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"password":{"identityFields":[],"enabled":false}`, + `"oauth2":{"providers":[],"enabled":false}`, + `"mfa":{"enabled":false,"duration":0}`, + `"otp":{"enabled":false,"duration":0}`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth collection with all auth methods allowed", + Method: http.MethodGet, + URL: "/api/collections/users/auth-methods", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"password":{"identityFields":["email","username"],"enabled":true}`, + `"mfa":{"enabled":true,"duration":1800}`, + `"otp":{"enabled":true,"duration":300}`, + `"oauth2":{`, + `"providers":[{`, + `"name":"google"`, + `"name":"gitlab"`, + `"state":`, + `"displayName":`, + `"codeVerifier":`, + `"codeChallenge":`, + `"codeChallengeMethod":`, + `"authURL":`, + `redirect_uri="`, // ensures that the redirect_uri is the last url param + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - nologin:listAuthMethods", + Method: http.MethodGet, + URL: "/api/collections/nologin/auth-methods", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:listAuthMethods"}, + {MaxRequests: 0, Label: "nologin:listAuthMethods"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:listAuthMethods", + Method: http.MethodGet, + URL: "/api/collections/nologin/auth-methods", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:listAuthMethods"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_otp_request.go b/apis/record_auth_otp_request.go new file mode 100644 index 00000000..39ebf77e --- /dev/null +++ b/apis/record_auth_otp_request.go @@ -0,0 +1,118 @@ +package apis + +import ( + "errors" + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/routine" + "github.com/pocketbase/pocketbase/tools/security" +) + +func recordRequestOTP(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if !collection.OTP.Enabled { + return e.ForbiddenError("The collection is not configured to allow OTP authentication.", nil) + } + + form := &createOTPForm{} + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + record, err := e.App.FindAuthRecordByEmail(collection, form.Email) + if err != nil { + // eagerly write a dummy 200 response as a very rudimentary user emails enumeration protection + e.JSON(http.StatusOK, map[string]string{ + "otpId": core.GenerateDefaultRandomId(), + }) + return fmt.Errorf("failed to fetch %s record with email %s: %w", collection.Name, form.Email, err) + } + + event := new(core.RecordCreateOTPRequestEvent) + event.RequestEvent = e + event.Password = security.RandomStringWithAlphabet(collection.OTP.Length, "1234567890") + event.Collection = collection + event.Record = record + + return e.App.OnRecordRequestOTPRequest().Trigger(event, func(e *core.RecordCreateOTPRequestEvent) error { + var otp *core.OTP + + // limit the new OTP creations for a single user + if !e.App.IsDev() { + otps, err := e.App.FindAllOTPsByRecord(e.Record) + if err != nil { + return firstApiError(err, e.InternalServerError("Failed to fetch previous record OTPs.", err)) + } + + totalRecent := 0 + for _, existingOTP := range otps { + if !existingOTP.HasExpired(collection.OTP.DurationTime()) { + totalRecent++ + } + // use the last issued one + if totalRecent > 9 { + otp = otps[0] // otps are DESC sorted + e.App.Logger().Warn( + "Too many OTP requests - reusing the last issued", + "email", form.Email, + "recordId", e.Record.Id, + "otpId", existingOTP.Id, + ) + break + } + } + } + + if otp == nil { + // create new OTP + // --- + otp = core.NewOTP(e.App) + otp.SetCollectionRef(e.Record.Collection().Id) + otp.SetRecordRef(e.Record.Id) + otp.SetPassword(e.Password) + err = e.App.Save(otp) + if err != nil { + return err + } + + // send OTP email + // (in the background as a very basic timing attacks and emails enumeration protection) + // --- + app := e.App + routine.FireAndForget(func() { + err = mails.SendRecordOTP(app, e.Record, otp.Id, e.Password) + if err != nil { + app.Logger().Error("Failed to send OTP email", "error", errors.Join(err, e.App.Delete(otp))) + } + }) + } + + return e.JSON(http.StatusOK, map[string]string{ + "otpId": otp.Id, + }) + }) +} + +// ------------------------------------------------------------------- + +type createOTPForm struct { + Email string `form:"email" json:"email"` +} + +func (form createOTPForm) validate() error { + return validation.ValidateStruct(&form, + validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.EmailFormat), + ) +} diff --git a/apis/record_auth_otp_request_test.go b/apis/record_auth_otp_request_test.go new file mode 100644 index 00000000..84ba4c96 --- /dev/null +++ b/apis/record_auth_otp_request_test.go @@ -0,0 +1,231 @@ +package apis_test + +import ( + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestRecordRequestOTP(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth collection with disabled otp", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + usersCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + usersCol.OTP.Enabled = false + + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty body", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid body", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid request data", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"invalid"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{"code":"validation_is_email`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "missing auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"otpId":"`, // some fake random generated string + }, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected zero emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + { + Name: "existing auth record (with < 9 non-expired)", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + // insert 8 non-expired and 2 expired + for i := 0; i < 10; i++ { + otp := core.NewOTP(app) + otp.Id = "otp_" + strconv.Itoa(i) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if i >= 8 { + expiredDate := types.NowDateTime().AddDate(-3, 0, 0) + otp.SetRaw("created", expiredDate) + otp.SetRaw("updated", expiredDate) + } + if err := app.SaveNoValidate(otp); err != nil { + t.Fatal(err) + } + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"otpId":"`, + }, + NotExpectedContent: []string{ + `"otpId":"otp_`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestOTPRequest": 1, + "OnMailerSend": 1, + "OnMailerRecordOTPSend": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected 1 email, got %d", app.TestMailer.TotalSend()) + } + }, + }, + { + Name: "existing auth record (with > 9 non-expired)", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + // insert 10 non-expired + for i := 0; i < 10; i++ { + otp := core.NewOTP(app) + otp.Id = "otp_" + strconv.Itoa(i) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if err := app.SaveNoValidate(otp); err != nil { + t.Fatal(err) + } + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"otpId":"otp_9"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestOTPRequest": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected 0 sent emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:requestOTP", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:requestOTP"}, + {MaxRequests: 0, Label: "users:requestOTP"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:requestOTP", + Method: http.MethodPost, + URL: "/api/collections/users/request-otp", + Body: strings.NewReader(`{"email":"test@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:requestOTP"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_password_reset_confirm.go b/apis/record_auth_password_reset_confirm.go new file mode 100644 index 00000000..9a45820f --- /dev/null +++ b/apis/record_auth_password_reset_confirm.go @@ -0,0 +1,102 @@ +package apis + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +func recordConfirmPasswordReset(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + form := new(recordConfirmPasswordResetForm) + form.app = e.App + form.collection = collection + if err = e.BindBody(form); err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + authRecord, err := e.App.FindAuthRecordByToken(form.Token, core.TokenTypePasswordReset) + if err != nil { + return firstApiError(err, e.BadRequestError("Invalid or expired password reset token.", err)) + } + + event := new(core.RecordConfirmPasswordResetRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = authRecord + + return e.App.OnRecordConfirmPasswordResetRequest().Trigger(event, func(e *core.RecordConfirmPasswordResetRequestEvent) error { + authRecord.SetPassword(form.Password) + + if !authRecord.Verified() { + payload, err := security.ParseUnverifiedJWT(form.Token) + if err == nil && authRecord.Email() == cast.ToString(payload[core.TokenClaimEmail]) { + // mark as verified if the email hasn't changed + authRecord.SetVerified(true) + } + } + + err = form.app.Save(authRecord) + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to set new password.", err)) + } + + form.app.Store().Remove(getPasswordResetResendKey(authRecord)) + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +type recordConfirmPasswordResetForm struct { + app core.App + collection *core.Collection + + Token string `form:"token" json:"token"` + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` +} + +func (form *recordConfirmPasswordResetForm) validate() error { + min := 1 + passField, ok := form.collection.Fields.GetByName(core.FieldNamePassword).(*core.PasswordField) + if ok && passField != nil && passField.Min > 0 { + min = passField.Min + } + + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + validation.Field(&form.Password, validation.Required, validation.Length(min, 255)), // the FieldPassword validator will check further the specicic length constraints + validation.Field(&form.PasswordConfirm, validation.Required, validation.By(validators.Equal(form.Password))), + ) +} + +func (form *recordConfirmPasswordResetForm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil + } + + record, err := form.app.FindAuthRecordByToken(v, core.TokenTypePasswordReset) + if err != nil || record == nil { + return validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + if record.Collection().Id != form.collection.Id { + return validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") + } + + return nil +} diff --git a/apis/record_auth_password_reset_confirm_test.go b/apis/record_auth_password_reset_confirm_test.go new file mode 100644 index 00000000..c6b34614 --- /dev/null +++ b/apis/record_auth_password_reset_confirm_test.go @@ -0,0 +1,345 @@ +package apis_test + +import ( + "errors" + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordConfirmPasswordReset(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"password":{"code":"validation_required"`, + `"passwordConfirm":{"code":"validation_required"`, + `"token":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data format", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{"password`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "expired token and invalid password", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.5Tm6_6amQqOlX3urAnXlEdmxwG5qQJfiTg6U0hHR1hk", + "password":"1234567", + "passwordConfirm":"7654321" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_invalid_token"`, + `"password":{"code":"validation_length_out_of_range"`, + `"passwordConfirm":{"code":"validation_values_mismatch"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-password reset token", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SetHpu2H-x-q4TIUz-xiQjwi7MNwLCLvSs4O0hUSp0E", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_invalid_token"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/confirm-password-reset?expand=rel,missing", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "different auth collection", + Method: http.MethodPost, + URL: "/api/collections/clients/confirm-password-reset?expand=rel,missing", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{"token":{"code":"validation_token_collection_mismatch"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid token and data (unverified user)", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmPasswordResetRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + if user.Verified() { + t.Fatal("Expected the user to be unverified") + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindAuthRecordByToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + core.TokenTypePasswordReset, + ) + if err == nil { + t.Fatal("Expected the password reset token to be invalidated") + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + if !user.Verified() { + t.Fatal("Expected the user to be marked as verified") + } + + if !user.ValidatePassword("1234567!") { + t.Fatal("Password wasn't changed") + } + }, + }, + { + Name: "valid token and data (unverified user with different email from the one in the token)", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmPasswordResetRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + if user.Verified() { + t.Fatal("Expected the user to be unverified") + } + + // manually change the email to check whether the verified state will be updated + user.SetEmail("test_update@example.com") + if err := app.Save(user); err != nil { + t.Fatalf("Failed to update user test email: %v", err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindAuthRecordByToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + core.TokenTypePasswordReset, + ) + if err == nil { + t.Fatalf("Expected the password reset token to be invalidated") + } + + user, err := app.FindAuthRecordByEmail("users", "test_update@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + if user.Verified() { + t.Fatal("Expected the user to remain unverified") + } + + if !user.ValidatePassword("1234567!") { + t.Fatal("Password wasn't changed") + } + }, + }, + { + Name: "valid token and data (verified user)", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmPasswordResetRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + // ensure that the user is already verified + user.SetVerified(true) + if err := app.Save(user); err != nil { + t.Fatalf("Failed to update user verified state") + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + _, err := app.FindAuthRecordByToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + core.TokenTypePasswordReset, + ) + if err == nil { + t.Fatal("Expected the password reset token to be invalidated") + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatalf("Failed to fetch confirm password user: %v", err) + } + + if !user.Verified() { + t.Fatal("Expected the user to remain verified") + } + + if !user.ValidatePassword("1234567!") { + t.Fatal("Password wasn't changed") + } + }, + }, + { + Name: "OnRecordAfterConfirmPasswordResetRequest error response", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordConfirmPasswordResetRequest().BindFunc(func(e *core.RecordConfirmPasswordResetRequestEvent) error { + return errors.New("error") + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmPasswordResetRequest": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:confirmPasswordReset", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:confirmPasswordReset"}, + {MaxRequests: 0, Label: "users:confirmPasswordReset"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:confirmPasswordReset", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-password-reset", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY", + "password":"1234567!", + "passwordConfirm":"1234567!" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:confirmPasswordReset"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_password_reset_request.go b/apis/record_auth_password_reset_request.go new file mode 100644 index 00000000..3c0592d4 --- /dev/null +++ b/apis/record_auth_password_reset_request.go @@ -0,0 +1,86 @@ +package apis + +import ( + "errors" + "fmt" + "net/http" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/routine" +) + +func recordRequestPasswordReset(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if !collection.PasswordAuth.Enabled { + return e.BadRequestError("The collection is not configured to allow password authentication.", nil) + } + + form := new(recordRequestPasswordResetForm) + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + record, err := e.App.FindAuthRecordByEmail(collection, form.Email) + if err != nil { + // eagerly write 204 response as a very basic measure against emails enumeration + e.NoContent(http.StatusNoContent) + return fmt.Errorf("failed to fetch %s record with email %s: %w", collection.Name, form.Email, err) + } + + resendKey := getPasswordResetResendKey(record) + if e.App.Store().Has(resendKey) { + // eagerly write 204 response as a very basic measure against emails enumeration + e.NoContent(http.StatusNoContent) + return errors.New("try again later - you've already requested a password reset email") + } + + event := new(core.RecordRequestPasswordResetRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + return e.App.OnRecordRequestPasswordResetRequest().Trigger(event, func(e *core.RecordRequestPasswordResetRequestEvent) error { + // run in background because we don't need to show the result to the client + app := e.App + routine.FireAndForget(func() { + if err := mails.SendRecordPasswordReset(app, e.Record); err != nil { + app.Logger().Error("Failed to send password reset email", "error", err) + return + } + + app.Store().Set(resendKey, struct{}{}) + time.AfterFunc(2*time.Minute, func() { + app.Store().Remove(resendKey) + }) + }) + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +type recordRequestPasswordResetForm struct { + Email string `form:"email" json:"email"` +} + +func (form *recordRequestPasswordResetForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.EmailFormat), + ) +} + +func getPasswordResetResendKey(record *core.Record) string { + return "@limitPasswordResetEmail_" + record.Collection().Id + record.Id +} diff --git a/apis/record_auth_password_reset_request_test.go b/apis/record_auth_password_reset_request_test.go new file mode 100644 index 00000000..cb5ec956 --- /dev/null +++ b/apis/record_auth_password_reset_request_test.go @@ -0,0 +1,145 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordRequestPasswordReset(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/request-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "existing auth record in a collection with disabled password login", + Method: http.MethodPost, + URL: "/api/collections/nologin/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "missing auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected zero emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + { + Name: "existing auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestPasswordResetRequest": 1, + "OnMailerSend": 1, + "OnMailerRecordPasswordResetSend": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if !strings.Contains(app.TestMailer.LastMessage().HTML, "/auth/confirm-password-reset") { + t.Fatalf("Expected password reset email, got\n%v", app.TestMailer.LastMessage().HTML) + } + }, + }, + { + Name: "existing auth record (after already sent)", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // simulate recent verification sent + authRecord, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + resendKey := "@limitPasswordResetEmail_" + authRecord.Collection().Id + authRecord.Id + app.Store().Set(resendKey, struct{}{}) + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:requestPasswordReset", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:requestPasswordReset"}, + {MaxRequests: 0, Label: "users:requestPasswordReset"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:requestPasswordReset", + Method: http.MethodPost, + URL: "/api/collections/users/request-password-reset", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:requestPasswordReset"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_refresh.go b/apis/record_auth_refresh.go new file mode 100644 index 00000000..63230a87 --- /dev/null +++ b/apis/record_auth_refresh.go @@ -0,0 +1,29 @@ +package apis + +import ( + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +func recordAuthRefresh(e *core.RequestEvent) error { + record := e.Auth + if record == nil { + return e.NotFoundError("Missing auth record context.", nil) + } + + currentToken := getAuthTokenFromRequest(e) + claims, _ := security.ParseUnverifiedJWT(currentToken) + if v, ok := claims[core.TokenClaimRefreshable]; !ok || !cast.ToBool(v) { + return e.ForbiddenError("The current auth token is not refreshable.", nil) + } + + event := new(core.RecordAuthRefreshRequestEvent) + event.RequestEvent = e + event.Collection = record.Collection() + event.Record = record + + return e.App.OnRecordAuthRefreshRequest().Trigger(event, func(e *core.RecordAuthRefreshRequestEvent) error { + return RecordAuthResponse(e.RequestEvent, e.Record, "", nil) + }) +} diff --git a/apis/record_auth_refresh_test.go b/apis/record_auth_refresh_test.go new file mode 100644 index 00000000..6d41fbf1 --- /dev/null +++ b/apis/record_auth_refresh_test.go @@ -0,0 +1,196 @@ +package apis_test + +import ( + "errors" + "net/http" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordAuthRefresh(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superuser trying to refresh the auth of another auth collection", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth record + not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth record + different auth collection", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-refresh?expand=rel,missing", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth record + same auth collection as the token", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh?expand=rel,missing", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"record":`, + `"id":"4q1xlclmfloku33"`, + `"emailVisibility":false`, + `"email":"test@example.com"`, // the owner can always view their email address + `"expand":`, + `"rel":`, + `"id":"llvuca81nly1qls"`, + }, + NotExpectedContent: []string{ + `"missing":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRefreshRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 2, + }, + }, + { + Name: "auth record + same auth collection as the token but static/unrefreshable", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6ZmFsc2V9.4IsO6YMsR19crhwl_YWzvRH8pfq2Ri4Gv2dzGyneLak", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "unverified auth record in onlyVerified collection", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im8xeTBkZDBzcGQ3ODZtZCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.Zi0yXE-CNmnbTdVaQEzYZVuECqRdn3LgEM6pmB3XWBE", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRefreshRequest": 1, + }, + }, + { + Name: "verified auth record in onlyVerified collection", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"record":`, + `"id":"gk390qegs4y47wn"`, + `"verified":true`, + `"email":"test@example.com"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRefreshRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "OnRecordAfterAuthRefreshRequest error response", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh?expand=rel,missing", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordAuthRefreshRequest().BindFunc(func(e *core.RecordAuthRefreshRequestEvent) error { + return errors.New("error") + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthRefreshRequest": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:authRefresh", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authRefresh"}, + {MaxRequests: 0, Label: "users:authRefresh"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:authRefresh", + Method: http.MethodPost, + URL: "/api/collections/users/auth-refresh", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:authRefresh"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_test.go b/apis/record_auth_test.go deleted file mode 100644 index 112f0f86..00000000 --- a/apis/record_auth_test.go +++ /dev/null @@ -1,1742 +0,0 @@ -package apis_test - -import ( - "context" - "errors" - "net/http" - "strings" - "testing" - "time" - - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/subscriptions" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestRecordAuthMethodsList(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "missing collection", - Method: http.MethodGet, - Url: "/api/collections/missing/auth-methods", - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "non auth collection", - Method: http.MethodGet, - Url: "/api/collections/demo1/auth-methods", - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth collection with all auth methods allowed", - Method: http.MethodGet, - Url: "/api/collections/users/auth-methods", - ExpectedStatus: 200, - ExpectedContent: []string{ - `"usernamePassword":true`, - `"emailPassword":true`, - `"onlyVerified":false`, - `"authProviders":[{`, - `"name":"gitlab"`, - `"state":`, - `"codeVerifier":`, - `"codeChallenge":`, - `"codeChallengeMethod":`, - `"authUrl":`, - `redirect_uri="`, // ensures that the redirect_uri is the last url param - }, - }, - { - Name: "auth collection with only email/password auth allowed", - Method: http.MethodGet, - Url: "/api/collections/clients/auth-methods", - ExpectedStatus: 200, - ExpectedContent: []string{ - `"usernamePassword":false`, - `"emailPassword":true`, - `"onlyVerified":true`, - `"authProviders":[]`, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthWithPassword(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "invalid body format", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{"identity`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "empty body params", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{"identity":"","password":""}`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"identity":{`, - `"password":{`, - }, - }, - - // username - { - Name: "invalid username and valid password", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"invalid", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid username and invalid password", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test2_username", - "password":"invalid" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid username and valid password in restricted collection", - Method: http.MethodPost, - Url: "/api/collections/nologin/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test_username", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid username and valid password in allowed collection", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test2_username", - "password":"1234567890" - }`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"record":{`, - `"token":"`, - `"id":"oap640cot4yru2s"`, - `"email":"test2@example.com"`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - "OnRecordAuthRequest": 1, - }, - }, - - // email - { - Name: "invalid email and valid password", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"missing@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid email and invalid password", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"invalid" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid email and valid password in restricted collection", - Method: http.MethodPost, - Url: "/api/collections/nologin/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - }, - }, - { - Name: "valid email (unverified) and valid password in allowed collection", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"record":{`, - `"token":"`, - `"id":"4q1xlclmfloku33"`, - `"email":"test@example.com"`, - `"verified":false`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - "OnRecordAuthRequest": 1, - // lastLoginAlertSentAt update - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - }, - }, - - // onlyVerified collection check - { - Name: "unverified user in onlyVerified collection", - Method: http.MethodPost, - Url: "/api/collections/clients/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test2@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 403, - ExpectedContent: []string{ - `"data":{}`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - }, - }, - { - Name: "verified user in onlyVerified collection", - Method: http.MethodPost, - Url: "/api/collections/clients/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"record":{`, - `"token":"`, - `"id":"gk390qegs4y47wn"`, - `"email":"test@example.com"`, - `"verified":true`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - "OnRecordAuthRequest": 1, - }, - }, - - // with already authenticated record or admin - { - Name: "authenticated record", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"record":{`, - `"token":"`, - `"id":"4q1xlclmfloku33"`, - `"email":"test@example.com"`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - "OnRecordAuthRequest": 1, - // lastLoginAlertSentAt update - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - }, - }, - { - Name: "authenticated admin", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - Body: strings.NewReader(`{ - "identity":"test@example.com", - "password":"1234567890" - }`), - ExpectedStatus: 200, - ExpectedContent: []string{ - `"record":{`, - `"token":"`, - `"id":"4q1xlclmfloku33"`, - `"email":"test@example.com"`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - "OnRecordAuthRequest": 1, - // lastLoginAlertSentAt update - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - }, - }, - - // after hooks error checks - { - Name: "OnRecordAfterAuthWithPasswordRequest error response", - Method: http.MethodPost, - Url: "/api/collections/users/auth-with-password", - Body: strings.NewReader(`{ - "identity":"test2_username", - "password":"1234567890" - }`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterAuthWithPasswordRequest().Add(func(e *core.RecordAuthWithPasswordEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthWithPasswordRequest": 1, - "OnRecordAfterAuthWithPasswordRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthRefresh(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodPost, - Url: "/api/collections/users/auth-refresh", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin", - Method: http.MethodPost, - Url: "/api/collections/users/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record + not an auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record + different auth collection", - Method: http.MethodPost, - Url: "/api/collections/clients/auth-refresh?expand=rel,missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record + same auth collection as the token", - Method: http.MethodPost, - Url: "/api/collections/users/auth-refresh?expand=rel,missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"token":`, - `"record":`, - `"id":"4q1xlclmfloku33"`, - `"emailVisibility":false`, - `"email":"test@example.com"`, // the owner can always view their email address - `"expand":`, - `"rel":`, - `"id":"llvuca81nly1qls"`, - }, - NotExpectedContent: []string{ - `"missing":`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthRefreshRequest": 1, - "OnRecordAuthRequest": 1, - "OnRecordAfterAuthRefreshRequest": 1, - }, - }, - { - Name: "unverified auth record in onlyVerified collection", - Method: http.MethodPost, - Url: "/api/collections/clients/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im8xeTBkZDBzcGQ3ODZtZCIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.-JYlrz5DcGzvb0nYx-xqnSFMu9dupyKY7Vg_FUm0OaM", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthRefreshRequest": 1, - "OnRecordAfterAuthRefreshRequest": 1, - }, - }, - { - Name: "verified auth record in onlyVerified collection", - Method: http.MethodPost, - Url: "/api/collections/clients/auth-refresh", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"token":`, - `"record":`, - `"id":"gk390qegs4y47wn"`, - `"verified":true`, - `"email":"test@example.com"`, - }, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthRefreshRequest": 1, - "OnRecordAuthRequest": 1, - "OnRecordAfterAuthRefreshRequest": 1, - }, - }, - { - Name: "OnRecordAfterAuthRefreshRequest error response", - Method: http.MethodPost, - Url: "/api/collections/users/auth-refresh?expand=rel,missing", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterAuthRefreshRequest().Add(func(e *core.RecordAuthRefreshEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnRecordBeforeAuthRefreshRequest": 1, - "OnRecordAfterAuthRefreshRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthRequestPasswordReset(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "not an auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/request-password-reset", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/request-password-reset", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/collections/users/request-password-reset", - Body: strings.NewReader(`{"email`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "missing auth record", - Method: http.MethodPost, - Url: "/api/collections/users/request-password-reset", - Body: strings.NewReader(`{"email":"missing@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - }, - { - Name: "existing auth record", - Method: http.MethodPost, - Url: "/api/collections/users/request-password-reset", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnRecordBeforeRequestPasswordResetRequest": 1, - "OnRecordAfterRequestPasswordResetRequest": 1, - "OnMailerBeforeRecordResetPasswordSend": 1, - "OnMailerAfterRecordResetPasswordSend": 1, - }, - }, - { - Name: "existing auth record (after already sent)", - Method: http.MethodPost, - Url: "/api/collections/clients/request-password-reset", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // simulate recent password request sent - authRecord, err := app.Dao().FindFirstRecordByData("clients", "email", "test@example.com") - if err != nil { - t.Fatal(err) - } - authRecord.SetLastResetSentAt(types.NowDateTime()) - dao := daos.New(app.Dao().DB()) // new dao to ignore hooks - if err := dao.Save(authRecord); err != nil { - t.Fatal(err) - } - }, - }, - { - Name: "existing auth record in a collection with disabled password login", - Method: http.MethodPost, - Url: "/api/collections/nologin/request-password-reset", - Body: strings.NewReader(`{"email":"test@example.com"}`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthConfirmPasswordReset(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"password":{"code":"validation_required"`, - `"passwordConfirm":{"code":"validation_required"`, - `"token":{"code":"validation_required"`, - }, - }, - { - Name: "invalid data format", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{"password`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "expired token and invalid password", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoxNjQwOTkxNjYxfQ.TayHoXkOTM0w8InkBEb86npMJEaf6YVUrxrRmMgFjeY", - "password":"1234567", - "passwordConfirm":"7654321" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"token":{"code":"validation_invalid_token"`, - `"password":{"code":"validation_length_out_of_range"`, - `"passwordConfirm":{"code":"validation_values_mismatch"`, - }, - }, - { - Name: "non auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/confirm-password-reset?expand=rel,missing", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "different auth collection", - Method: http.MethodPost, - Url: "/api/collections/clients/confirm-password-reset?expand=rel,missing", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{"token":{"code":"validation_token_collection_mismatch"`, - }, - }, - { - Name: "valid token and data (unverified user)", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmPasswordResetRequest": 1, - "OnRecordAfterConfirmPasswordResetRequest": 1, - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - if user.Verified() { - t.Fatalf("Expected the user to be unverified") - } - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - user, err := app.Dao().FindAuthRecordByToken( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - app.Settings().RecordPasswordResetToken.Secret, - ) - if err == nil { - t.Fatalf("Expected the password reset token to be invalidated") - } - - user, err = app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - if !user.Verified() { - t.Fatalf("Expected the user to be marked as verified") - } - }, - }, - { - Name: "valid token and data (unverified user with different email from the one in the token)", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmPasswordResetRequest": 1, - "OnRecordAfterConfirmPasswordResetRequest": 1, - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - if user.Verified() { - t.Fatalf("Expected the user to be unverified") - } - - // manually change the email to check whether the verified state will be updated - user.SetEmail("test_update@example.com") - if err := app.Dao().WithoutHooks().SaveRecord(user); err != nil { - t.Fatalf("Failed to update user test email") - } - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - user, err := app.Dao().FindAuthRecordByToken( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - app.Settings().RecordPasswordResetToken.Secret, - ) - if err == nil { - t.Fatalf("Expected the password reset token to be invalidated") - } - - user, err = app.Dao().FindAuthRecordByEmail("users", "test_update@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - if user.Verified() { - t.Fatalf("Expected the user to remain unverified") - } - }, - }, - { - Name: "valid token and data (verified user)", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmPasswordResetRequest": 1, - "OnRecordAfterConfirmPasswordResetRequest": 1, - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - // ensure that the user is already verified - user.SetVerified(true) - if err := app.Dao().WithoutHooks().SaveRecord(user); err != nil { - t.Fatalf("Failed to update user verified state") - } - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - user, err := app.Dao().FindAuthRecordByToken( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - app.Settings().RecordPasswordResetToken.Secret, - ) - if err == nil { - t.Fatalf("Expected the password reset token to be invalidated") - } - - user, err = app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatalf("Failed to fetch confirm password user: %v", err) - } - - if !user.Verified() { - t.Fatalf("Expected the user to remain verified") - } - }, - }, - { - Name: "OnRecordAfterConfirmPasswordResetRequest error response", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-password-reset", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterConfirmPasswordResetRequest().Add(func(e *core.RecordConfirmPasswordResetEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmPasswordResetRequest": 1, - "OnRecordAfterConfirmPasswordResetRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthRequestVerification(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "not an auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/request-verification", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(`{"email`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "missing auth record", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(`{"email":"missing@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - }, - { - Name: "already verified auth record", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(`{"email":"test2@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnRecordBeforeRequestVerificationRequest": 1, - "OnRecordAfterRequestVerificationRequest": 1, - }, - }, - { - Name: "existing auth record", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnRecordBeforeRequestVerificationRequest": 1, - "OnRecordAfterRequestVerificationRequest": 1, - "OnMailerBeforeRecordVerificationSend": 1, - "OnMailerAfterRecordVerificationSend": 1, - }, - }, - { - Name: "existing auth record (after already sent)", - Method: http.MethodPost, - Url: "/api/collections/users/request-verification", - Body: strings.NewReader(`{"email":"test@example.com"}`), - Delay: 100 * time.Millisecond, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - // "OnRecordBeforeRequestVerificationRequest": 1, - // "OnRecordAfterRequestVerificationRequest": 1, - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - // simulate recent verification sent - authRecord, err := app.Dao().FindFirstRecordByData("users", "email", "test@example.com") - if err != nil { - t.Fatal(err) - } - authRecord.SetLastVerificationSentAt(types.NowDateTime()) - dao := daos.New(app.Dao().DB()) // new dao to ignore hooks - if err := dao.Save(authRecord); err != nil { - t.Fatal(err) - } - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthConfirmVerification(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"token":{"code":"validation_required"`, - }, - }, - { - Name: "invalid data format", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(`{"password`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "expired token", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoxNjQwOTkxNjYxfQ.Avbt9IP8sBisVz_2AGrlxLDvangVq4PhL2zqQVYLKlE" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"token":{"code":"validation_invalid_token"`, - }, - }, - { - Name: "non auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/confirm-verification?expand=rel,missing", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "different auth collection", - Method: http.MethodPost, - Url: "/api/collections/clients/confirm-verification?expand=rel,missing", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.hL16TVmStHFdHLc4a860bRqJ3sFfzjv0_NRNzwsvsrc" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{"token":{"code":"validation_token_collection_mismatch"`, - }, - }, - { - Name: "valid token", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.hL16TVmStHFdHLc4a860bRqJ3sFfzjv0_NRNzwsvsrc" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmVerificationRequest": 1, - "OnRecordAfterConfirmVerificationRequest": 1, - }, - }, - { - Name: "valid token (already verified)", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJ0eXBlIjoiYXV0aFJlY29yZCIsImV4cCI6MjIwODk4NTI2MX0.PsOABmYUzGbd088g8iIBL4-pf7DUZm0W5Ju6lL5JVRg" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnRecordBeforeConfirmVerificationRequest": 1, - "OnRecordAfterConfirmVerificationRequest": 1, - }, - }, - { - Name: "valid verification token from a collection without allowed login", - Method: http.MethodPost, - Url: "/api/collections/nologin/confirm-verification", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.coREjeTDS3_Go7DP1nxHtevIX5rujwHU-_mRB6oOm3w" - }`), - ExpectedStatus: 204, - ExpectedContent: []string{}, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmVerificationRequest": 1, - "OnRecordAfterConfirmVerificationRequest": 1, - }, - }, - { - Name: "OnRecordAfterConfirmVerificationRequest error response", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-verification", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.hL16TVmStHFdHLc4a860bRqJ3sFfzjv0_NRNzwsvsrc" - }`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterConfirmVerificationRequest().Add(func(e *core.RecordConfirmVerificationEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmVerificationRequest": 1, - "OnRecordAfterConfirmVerificationRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthRequestEmailChange(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{"newEmail":"change@example.com"}`), - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "not an auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/request-email-change", - Body: strings.NewReader(`{"newEmail":"change@example.com"}`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin authentication", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{"newEmail":"change@example.com"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "record authentication but from different auth collection", - Method: http.MethodPost, - Url: "/api/collections/clients/request-email-change", - Body: strings.NewReader(`{"newEmail":"change@example.com"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{"newEmail`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":`, - `"newEmail":{"code":"validation_required"`, - }, - }, - { - Name: "valid data (existing email)", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{"newEmail":"test2@example.com"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":`, - `"newEmail":{"code":"validation_record_email_invalid"`, - }, - }, - { - Name: "valid data (new email)", - Method: http.MethodPost, - Url: "/api/collections/users/request-email-change", - Body: strings.NewReader(`{"newEmail":"change@example.com"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnMailerBeforeRecordChangeEmailSend": 1, - "OnMailerAfterRecordChangeEmailSend": 1, - "OnRecordBeforeRequestEmailChangeRequest": 1, - "OnRecordAfterRequestEmailChangeRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthConfirmEmailChange(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "not an auth collection", - Method: http.MethodPost, - Url: "/api/collections/demo1/confirm-email-change", - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{}`, - }, - }, - { - Name: "empty data", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(``), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":`, - `"token":{"code":"validation_required"`, - `"password":{"code":"validation_required"`, - }, - }, - { - Name: "invalid data", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(`{"token`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "expired token and correct password", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJjaGFuZ2VAZXhhbXBsZS5jb20iLCJleHAiOjE2NDA5OTE2NjF9.D20jh5Ss7SZyXRUXjjEyLCYo9Ky0N5cE5dKB_MGJ8G8", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"token":{`, - `"code":"validation_invalid_token"`, - }, - }, - { - Name: "valid token and incorrect password", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJjaGFuZ2VAZXhhbXBsZS5jb20iLCJleHAiOjIyMDg5ODUyNjF9.1sG6cL708pRXXjiHRZhG-in0X5fnttSf5nNcadKoYRs", - "password":"1234567891" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"password":{`, - `"code":"validation_invalid_password"`, - }, - }, - { - Name: "valid token and correct password", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJjaGFuZ2VAZXhhbXBsZS5jb20iLCJleHAiOjIyMDg5ODUyNjF9.1sG6cL708pRXXjiHRZhG-in0X5fnttSf5nNcadKoYRs", - "password":"1234567890" - }`), - ExpectedStatus: 204, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmEmailChangeRequest": 1, - "OnRecordAfterConfirmEmailChangeRequest": 1, - }, - }, - { - Name: "valid token and correct password in different auth collection", - Method: http.MethodPost, - Url: "/api/collections/clients/confirm-email-change", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJjaGFuZ2VAZXhhbXBsZS5jb20iLCJleHAiOjIyMDg5ODUyNjF9.1sG6cL708pRXXjiHRZhG-in0X5fnttSf5nNcadKoYRs", - "password":"1234567890" - }`), - ExpectedStatus: 400, - ExpectedContent: []string{ - `"data":{`, - `"token":{"code":"validation_token_collection_mismatch"`, - }, - }, - { - Name: "OnRecordAfterConfirmEmailChangeRequest error response", - Method: http.MethodPost, - Url: "/api/collections/users/confirm-email-change", - Body: strings.NewReader(`{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJjaGFuZ2VAZXhhbXBsZS5jb20iLCJleHAiOjIyMDg5ODUyNjF9.1sG6cL708pRXXjiHRZhG-in0X5fnttSf5nNcadKoYRs", - "password":"1234567890" - }`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterConfirmEmailChangeRequest().Add(func(e *core.RecordConfirmEmailChangeEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordBeforeConfirmEmailChangeRequest": 1, - "OnRecordAfterConfirmEmailChangeRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthListExternalsAuths(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodGet, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin + nonexisting record id", - Method: http.MethodGet, - Url: "/api/collections/users/records/missing/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin + existing record id and no external auths", - Method: http.MethodGet, - Url: "/api/collections/users/records/oap640cot4yru2s/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{`[]`}, - ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1}, - }, - { - Name: "admin + existing user id and 2 external auths", - Method: http.MethodGet, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":"clmflokuq1xl341"`, - `"id":"dlmflokuq1xl342"`, - `"recordId":"4q1xlclmfloku33"`, - `"collectionId":"_pb_users_auth_"`, - }, - ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1}, - }, - { - Name: "auth record + trying to list another user external auths", - Method: http.MethodGet, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.uatnTBFqMnF0p4FkmwEpA9R-uGFu0Putwyk6NJCKBno", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record + trying to list another user external auths from different collection", - Method: http.MethodGet, - Url: "/api/collections/clients/records/o1y0dd0spd786md/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.uatnTBFqMnF0p4FkmwEpA9R-uGFu0Putwyk6NJCKBno", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record + owner without external auths", - Method: http.MethodGet, - Url: "/api/collections/users/records/oap640cot4yru2s/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.uatnTBFqMnF0p4FkmwEpA9R-uGFu0Putwyk6NJCKBno", - }, - ExpectedStatus: 200, - ExpectedContent: []string{`[]`}, - ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1}, - }, - { - Name: "authorized as user - owner with 2 external auths", - Method: http.MethodGet, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 200, - ExpectedContent: []string{ - `"id":"clmflokuq1xl341"`, - `"id":"dlmflokuq1xl342"`, - `"recordId":"4q1xlclmfloku33"`, - `"collectionId":"_pb_users_auth_"`, - }, - ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1}, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthUnlinkExternalsAuth(t *testing.T) { - t.Parallel() - - scenarios := []tests.ApiScenario{ - { - Name: "unauthorized", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/google", - ExpectedStatus: 401, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin - nonexisting recod id", - Method: http.MethodDelete, - Url: "/api/collections/users/records/missing/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin - nonlinked provider", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/facebook", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 404, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "admin - linked provider", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 204, - ExpectedContent: []string{}, - ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterUnlinkExternalAuthRequest": 1, - "OnRecordBeforeUnlinkExternalAuthRequest": 1, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - record, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") - if err != nil { - t.Fatal(err) - } - auth, _ := app.Dao().FindExternalAuthByRecordAndProvider(record, "google") - if auth != nil { - t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth) - } - }, - }, - { - Name: "auth record - trying to unlink another user external auth", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.uatnTBFqMnF0p4FkmwEpA9R-uGFu0Putwyk6NJCKBno", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record - trying to unlink another user external auth from different collection", - Method: http.MethodDelete, - Url: "/api/collections/clients/records/o1y0dd0spd786md/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 403, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "auth record - owner with existing external auth", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - }, - ExpectedStatus: 204, - ExpectedContent: []string{}, - ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterUnlinkExternalAuthRequest": 1, - "OnRecordBeforeUnlinkExternalAuthRequest": 1, - }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - record, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") - if err != nil { - t.Fatal(err) - } - auth, _ := app.Dao().FindExternalAuthByRecordAndProvider(record, "google") - if auth != nil { - t.Fatalf("Expected the google ExternalAuth to be deleted, got got \n%v", auth) - } - }, - }, - { - Name: "OnRecordBeforeUnlinkExternalAuthRequest error response", - Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33/external-auths/google", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterUnlinkExternalAuthRequest().Add(func(e *core.RecordUnlinkExternalAuthEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterUnlinkExternalAuthRequest": 1, - "OnRecordBeforeUnlinkExternalAuthRequest": 1, - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} - -func TestRecordAuthOAuth2Redirect(t *testing.T) { - t.Parallel() - - clientStubs := make([]map[string]subscriptions.Client, 0, 10) - - for i := 0; i < 10; i++ { - c1 := subscriptions.NewDefaultClient() - - c2 := subscriptions.NewDefaultClient() - c2.Subscribe("@oauth2") - - c3 := subscriptions.NewDefaultClient() - c3.Subscribe("test1", "@oauth2") - - c4 := subscriptions.NewDefaultClient() - c4.Subscribe("test1", "test2") - - c5 := subscriptions.NewDefaultClient() - c5.Subscribe("@oauth2") - c5.Discard() - - clientStubs = append(clientStubs, map[string]subscriptions.Client{ - "c1": c1, - "c2": c2, - "c3": c3, - "c4": c4, - "c5": c5, - }) - } - - checkFailureRedirect := func(t *testing.T, app *tests.TestApp, res *http.Response) { - loc := res.Header.Get("Location") - if !strings.Contains(loc, "/oauth2-redirect-failure") { - t.Fatalf("Expected failure redirect, got %q", loc) - } - } - - checkSuccessRedirect := func(t *testing.T, app *tests.TestApp, res *http.Response) { - loc := res.Header.Get("Location") - if !strings.Contains(loc, "/oauth2-redirect-success") { - t.Fatalf("Expected success redirect, got %q", loc) - } - } - - checkClientMessages := func(t *testing.T, clientId string, msg subscriptions.Message, expectedMessages map[string][]string) { - if len(expectedMessages[clientId]) == 0 { - t.Fatalf("Unexpected client %q message, got %s:\n%s", clientId, msg.Name, msg.Data) - } - - if msg.Name != "@oauth2" { - t.Fatalf("Expected @oauth2 msg.Name, got %q", msg.Name) - } - - for _, txt := range expectedMessages[clientId] { - if !strings.Contains(string(msg.Data), txt) { - t.Fatalf("Failed to find %q in \n%s", txt, msg.Data) - } - } - } - - beforeTestFunc := func( - clients map[string]subscriptions.Client, - expectedMessages map[string][]string, - ) func(*testing.T, *tests.TestApp, *echo.Echo) { - return func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - for _, client := range clients { - app.SubscriptionsBroker().Register(client) - } - - ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) - - // add to the app store so that it can be cancelled manually after test completion - app.Store().Set("cancelFunc", cancelFunc) - - go func() { - defer cancelFunc() - - for { - select { - case msg := <-clients["c1"].Channel(): - checkClientMessages(t, "c1", msg, expectedMessages) - case msg := <-clients["c2"].Channel(): - checkClientMessages(t, "c2", msg, expectedMessages) - case msg := <-clients["c3"].Channel(): - checkClientMessages(t, "c3", msg, expectedMessages) - case msg := <-clients["c4"].Channel(): - checkClientMessages(t, "c4", msg, expectedMessages) - case msg := <-clients["c5"].Channel(): - checkClientMessages(t, "c5", msg, expectedMessages) - case <-ctx.Done(): - for _, c := range clients { - close(c.Channel()) - } - return - } - } - }() - } - } - - scenarios := []tests.ApiScenario{ - { - Name: "no state query param", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?code=123", - BeforeTestFunc: beforeTestFunc(clientStubs[0], nil), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - }, - }, - { - Name: "invalid or missing client", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?code=123&state=missing", - BeforeTestFunc: beforeTestFunc(clientStubs[1], nil), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - }, - }, - { - Name: "no code query param", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?state=" + clientStubs[2]["c3"].Id(), - BeforeTestFunc: beforeTestFunc(clientStubs[2], map[string][]string{ - "c3": {`"state":"` + clientStubs[2]["c3"].Id(), `"code":""`}, - }), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - - if clientStubs[2]["c3"].HasSubscription("@oauth2") { - t.Fatalf("Expected oauth2 subscription to be removed") - } - }, - }, - { - Name: "error query param", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?error=example&code=123&state=" + clientStubs[3]["c3"].Id(), - BeforeTestFunc: beforeTestFunc(clientStubs[3], map[string][]string{ - "c3": {`"state":"` + clientStubs[3]["c3"].Id(), `"code":"123"`, `"error":"example"`}, - }), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - - if clientStubs[3]["c3"].HasSubscription("@oauth2") { - t.Fatalf("Expected oauth2 subscription to be removed") - } - }, - }, - { - Name: "discarded client with @oauth2 subscription", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?code=123&state=" + clientStubs[4]["c5"].Id(), - BeforeTestFunc: beforeTestFunc(clientStubs[4], nil), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - }, - }, - { - Name: "client without @oauth2 subscription", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?code=123&state=" + clientStubs[4]["c4"].Id(), - BeforeTestFunc: beforeTestFunc(clientStubs[5], nil), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkFailureRedirect(t, app, res) - }, - }, - { - Name: "client with @oauth2 subscription", - Method: http.MethodGet, - Url: "/api/oauth2-redirect?code=123&state=" + clientStubs[6]["c3"].Id(), - BeforeTestFunc: beforeTestFunc(clientStubs[6], map[string][]string{ - "c3": {`"state":"` + clientStubs[6]["c3"].Id(), `"code":"123"`}, - }), - ExpectedStatus: http.StatusTemporaryRedirect, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkSuccessRedirect(t, app, res) - - if clientStubs[6]["c3"].HasSubscription("@oauth2") { - t.Fatalf("Expected oauth2 subscription to be removed") - } - }, - }, - { - Name: "(POST) client with @oauth2 subscription", - Method: http.MethodPost, - Url: "/api/oauth2-redirect", - Body: strings.NewReader("code=123&state=" + clientStubs[7]["c3"].Id()), - RequestHeaders: map[string]string{ - "content-type": "application/x-www-form-urlencoded", - }, - BeforeTestFunc: beforeTestFunc(clientStubs[7], map[string][]string{ - "c3": {`"state":"` + clientStubs[7]["c3"].Id(), `"code":"123"`}, - }), - ExpectedStatus: http.StatusSeeOther, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - app.Store().Get("cancelFunc").(context.CancelFunc)() - - checkSuccessRedirect(t, app, res) - - if clientStubs[7]["c3"].HasSubscription("@oauth2") { - t.Fatalf("Expected oauth2 subscription to be removed") - } - }, - }, - } - - for _, scenario := range scenarios { - scenario.Test(t) - } -} diff --git a/apis/record_auth_verification_confirm.go b/apis/record_auth_verification_confirm.go new file mode 100644 index 00000000..509ebc42 --- /dev/null +++ b/apis/record_auth_verification_confirm.go @@ -0,0 +1,102 @@ +package apis + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +func recordConfirmVerification(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if collection.Name == core.CollectionNameSuperusers { + return e.BadRequestError("All superusers are verified by default.", nil) + } + + form := new(recordConfirmVerificationForm) + form.app = e.App + form.collection = collection + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + record, err := form.app.FindAuthRecordByToken(form.Token, core.TokenTypeVerification) + if err != nil { + return e.BadRequestError("Invalid or expired verification token.", err) + } + + wasVerified := record.Verified() + + event := new(core.RecordConfirmVerificationRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + return e.App.OnRecordConfirmVerificationRequest().Trigger(event, func(e *core.RecordConfirmVerificationRequestEvent) error { + if wasVerified { + return e.NoContent(http.StatusNoContent) + } + + e.Record.SetVerified(true) + + if err := e.App.Save(e.Record); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while saving the verified state.", err)) + } + + e.App.Store().Remove(getVerificationResendKey(e.Record)) + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +type recordConfirmVerificationForm struct { + app core.App + collection *core.Collection + + Token string `form:"token" json:"token"` +} + +func (form *recordConfirmVerificationForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + ) +} + +func (form *recordConfirmVerificationForm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + claims, _ := security.ParseUnverifiedJWT(v) + email := cast.ToString(claims["email"]) + if email == "" { + return validation.NewError("validation_invalid_token_claims", "Missing email token claim.") + } + + record, err := form.app.FindAuthRecordByToken(v, core.TokenTypeVerification) + if err != nil || record == nil { + return validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + if record.Collection().Id != form.collection.Id { + return validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") + } + + if record.Email() != email { + return validation.NewError("validation_token_email_mismatch", "The record email doesn't match with the requested token claims.") + } + + return nil +} diff --git a/apis/record_auth_verification_confirm_test.go b/apis/record_auth_verification_confirm_test.go new file mode 100644 index 00000000..ea70fd29 --- /dev/null +++ b/apis/record_auth_verification_confirm_test.go @@ -0,0 +1,210 @@ +package apis_test + +import ( + "errors" + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordConfirmVerification(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data format", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{"password`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "expired token", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.qqelNNL2Udl6K_TJ282sNHYCpASgA6SIuSVKGfBHMZU" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_invalid_token"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-verification token", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InBhc3N3b3JkUmVzZXQiLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.xR-xq1oHDy0D8Q4NDOAEyYKGHWd_swzoiSoL8FLFBHY" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{"code":"validation_invalid_token"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/confirm-verification?expand=rel,missing", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SetHpu2H-x-q4TIUz-xiQjwi7MNwLCLvSs4O0hUSp0E" + }`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "different auth collection", + Method: http.MethodPost, + URL: "/api/collections/clients/confirm-verification?expand=rel,missing", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SetHpu2H-x-q4TIUz-xiQjwi7MNwLCLvSs4O0hUSp0E" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{"token":{"code":"validation_token_collection_mismatch"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid token", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SetHpu2H-x-q4TIUz-xiQjwi7MNwLCLvSs4O0hUSp0E" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmVerificationRequest": 1, + "OnModelUpdate": 1, + "OnModelValidate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordValidate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + }, + }, + { + Name: "valid token (already verified)", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20ifQ.QQmM3odNFVk6u4J4-5H8IBM3dfk9YCD7mPW-8PhBAI8" + }`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmVerificationRequest": 1, + }, + }, + { + Name: "valid verification token from a collection without allowed login", + Method: http.MethodPost, + URL: "/api/collections/nologin/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.5GmuZr4vmwk3Cb_3ZZWNxwbE75KZC-j71xxIPR9AsVw" + }`), + ExpectedStatus: 204, + ExpectedContent: []string{}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmVerificationRequest": 1, + "OnModelUpdate": 1, + "OnModelValidate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordValidate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + }, + }, + { + Name: "OnRecordAfterConfirmVerificationRequest error response", + Method: http.MethodPost, + URL: "/api/collections/users/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.SetHpu2H-x-q4TIUz-xiQjwi7MNwLCLvSs4O0hUSp0E" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordConfirmVerificationRequest().BindFunc(func(e *core.RecordConfirmVerificationRequestEvent) error { + return errors.New("error") + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordConfirmVerificationRequest": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - nologin:confirmVerification", + Method: http.MethodPost, + URL: "/api/collections/nologin/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.5GmuZr4vmwk3Cb_3ZZWNxwbE75KZC-j71xxIPR9AsVw" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:confirmVerification"}, + {MaxRequests: 0, Label: "nologin:confirmVerification"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:confirmVerification", + Method: http.MethodPost, + URL: "/api/collections/nologin/confirm-verification", + Body: strings.NewReader(`{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.5GmuZr4vmwk3Cb_3ZZWNxwbE75KZC-j71xxIPR9AsVw" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:confirmVerification"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_verification_request.go b/apis/record_auth_verification_request.go new file mode 100644 index 00000000..fc980e41 --- /dev/null +++ b/apis/record_auth_verification_request.go @@ -0,0 +1,89 @@ +package apis + +import ( + "errors" + "fmt" + "net/http" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/routine" +) + +func recordRequestVerification(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if collection.Name == core.CollectionNameSuperusers { + return e.BadRequestError("All superusers are verified by default.", nil) + } + + form := new(recordRequestVerificationForm) + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + record, err := e.App.FindAuthRecordByEmail(collection, form.Email) + if err != nil { + // eagerly write 204 response as a very basic measure against emails enumeration + e.NoContent(http.StatusNoContent) + return fmt.Errorf("failed to fetch %s record with email %s: %w", collection.Name, form.Email, err) + } + + resendKey := getVerificationResendKey(record) + if !record.Verified() && e.App.Store().Has(resendKey) { + // eagerly write 204 response as a very basic measure against emails enumeration + e.NoContent(http.StatusNoContent) + return errors.New("try again later - you've already requested a verification email") + } + + event := new(core.RecordRequestVerificationRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + return e.App.OnRecordRequestVerificationRequest().Trigger(event, func(e *core.RecordRequestVerificationRequestEvent) error { + if e.Record.Verified() { + return e.NoContent(http.StatusNoContent) + } + + // run in background because we don't need to show the result to the client + app := e.App + routine.FireAndForget(func() { + if err := mails.SendRecordVerification(app, e.Record); err != nil { + app.Logger().Error("Failed to send verification email", "error", err) + } + + app.Store().Set(resendKey, struct{}{}) + time.AfterFunc(2*time.Minute, func() { + app.Store().Remove(resendKey) + }) + }) + + return e.NoContent(http.StatusNoContent) + }) +} + +// ------------------------------------------------------------------- + +type recordRequestVerificationForm struct { + Email string `form:"email" json:"email"` +} + +func (form *recordRequestVerificationForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.EmailFormat), + ) +} + +func getVerificationResendKey(record *core.Record) string { + return "@limitVerificationEmail_" + record.Collection().Id + record.Id +} diff --git a/apis/record_auth_verification_request_test.go b/apis/record_auth_verification_request_test.go new file mode 100644 index 00000000..a15ab1bd --- /dev/null +++ b/apis/record_auth_verification_request_test.go @@ -0,0 +1,162 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordRequestVerification(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/request-verification", + Body: strings.NewReader(``), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty data", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "missing auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected zero emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + { + Name: "already verified auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"test2@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestVerificationRequest": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected zero emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + { + Name: "existing auth record", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordRequestVerificationRequest": 1, + "OnMailerSend": 1, + "OnMailerRecordVerificationSend": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if !strings.Contains(app.TestMailer.LastMessage().HTML, "/auth/confirm-verification") { + t.Fatalf("Expected verification email, got\n%v", app.TestMailer.LastMessage().HTML) + } + }, + }, + { + Name: "existing auth record (after already sent)", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"test@example.com"}`), + Delay: 100 * time.Millisecond, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + // terminated before firing the event + // "OnRecordRequestVerificationRequest": 1, + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // simulate recent verification sent + authRecord, err := app.FindFirstRecordByData("users", "email", "test@example.com") + if err != nil { + t.Fatal(err) + } + resendKey := "@limitVerificationEmail_" + authRecord.Collection().Id + authRecord.Id + app.Store().Set(resendKey, struct{}{}) + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected zero emails, got %d", app.TestMailer.TotalSend()) + } + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:requestVerification", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"test@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:requestVerification"}, + {MaxRequests: 0, Label: "users:requestVerification"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:requestVerification", + Method: http.MethodPost, + URL: "/api/collections/users/request-verification", + Body: strings.NewReader(`{"email":"test@example.com"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:requestVerification"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_with_oauth2.go b/apis/record_auth_with_oauth2.go new file mode 100644 index 00000000..3f7f376e --- /dev/null +++ b/apis/record_auth_with_oauth2.go @@ -0,0 +1,355 @@ +package apis + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "net/http" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/dbutils" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/security" + "golang.org/x/oauth2" +) + +func recordAuthWithOAuth2(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if !collection.OAuth2.Enabled { + return e.ForbiddenError("The collection is not configured to allow OAuth2 authentication.", nil) + } + + var fallbackAuthRecord *core.Record + if e.Auth != nil && e.Auth.Collection().Id == collection.Id { + fallbackAuthRecord = e.Auth + } + + form := new(recordOAuth2LoginForm) + form.collection = collection + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + + if form.RedirectUrl != "" && form.RedirectURL == "" { + e.App.Logger().Warn("[recordAuthWithOAuth2] redirectUrl body param is deprecated and will be removed in the future. Please replace it with redirectURL.") + form.RedirectURL = form.RedirectUrl + } + + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + + // exchange token for OAuth2 user info and locate existing ExternalAuth rel + // --------------------------------------------------------------- + + // load provider configuration + providerConfig, ok := collection.OAuth2.GetProviderConfig(form.Provider) + if !ok { + return e.InternalServerError("Missing or invalid provider config.", nil) + } + + provider, err := providerConfig.InitProvider() + if err != nil { + return firstApiError(err, e.InternalServerError("Failed to init provider "+form.Provider, err)) + } + + ctx, cancel := context.WithTimeout(e.Request.Context(), 30*time.Second) + defer cancel() + + provider.SetContext(ctx) + provider.SetRedirectURL(form.RedirectURL) + + var opts []oauth2.AuthCodeOption + + if provider.PKCE() { + opts = append(opts, oauth2.SetAuthURLParam("code_verifier", form.CodeVerifier)) + } + + // fetch token + token, err := provider.FetchToken(form.Code, opts...) + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to fetch OAuth2 token.", err)) + } + + // fetch external auth user + authUser, err := provider.FetchAuthUser(token) + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to fetch OAuth2 user.", err)) + } + + var authRecord *core.Record + + // check for existing relation with the auth record + externalAuthRel, err := e.App.FindFirstExternalAuthByExpr(dbx.HashExp{ + "collectionRef": form.collection.Id, + "provider": form.Provider, + "providerId": authUser.Id, + }) + switch { + case err == nil && externalAuthRel != nil: + authRecord, err = e.App.FindRecordById(form.collection, externalAuthRel.RecordRef()) + if err != nil { + return err + } + case fallbackAuthRecord != nil && fallbackAuthRecord.Collection().Id == form.collection.Id: + // fallback to the logged auth record (if any) + authRecord = fallbackAuthRecord + case authUser.Email != "": + // look for an existing auth record by the external auth record's email + authRecord, _ = e.App.FindAuthRecordByEmail(form.collection.Id, authUser.Email) + } + + // --------------------------------------------------------------- + + event := new(core.RecordAuthWithOAuth2RequestEvent) + event.RequestEvent = e + event.Collection = collection + event.ProviderName = form.Provider + event.ProviderClient = provider + event.OAuth2User = authUser + event.CreateData = form.CreateData + event.Record = authRecord + event.IsNewRecord = authRecord == nil + + return e.App.OnRecordAuthWithOAuth2Request().Trigger(event, func(e *core.RecordAuthWithOAuth2RequestEvent) error { + if err := oauth2Submit(e, externalAuthRel); err != nil { + return firstApiError(err, e.BadRequestError("Failed to authenticate.", err)) + } + + meta := struct { + *auth.AuthUser + IsNew bool `json:"isNew"` + }{ + AuthUser: e.OAuth2User, + IsNew: e.IsNewRecord, + } + + return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOAuth2, meta) + }) +} + +// ------------------------------------------------------------------- + +type recordOAuth2LoginForm struct { + collection *core.Collection + + // Additional data that will be used for creating a new auth record + // if an existing OAuth2 account doesn't exist. + CreateData map[string]any `form:"createData" json:"createData"` + + // The name of the OAuth2 client provider (eg. "google") + Provider string `form:"provider" json:"provider"` + + // The authorization code returned from the initial request. + Code string `form:"code" json:"code"` + + // The optional PKCE code verifier as part of the code_challenge sent with the initial request. + CodeVerifier string `form:"codeVerifier" json:"codeVerifier"` + + // The redirect url sent with the initial request. + RedirectURL string `form:"redirectURL" json:"redirectURL"` + + // @todo + // deprecated: use RedirectURL instead + // RedirectUrl will be removed after dropping v0.22 support + RedirectUrl string `form:"redirectUrl" json:"redirectUrl"` +} + +func (form *recordOAuth2LoginForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Provider, validation.Required, validation.By(form.checkProviderName)), + validation.Field(&form.Code, validation.Required), + validation.Field(&form.RedirectURL, validation.Required), + ) +} + +func (form *recordOAuth2LoginForm) checkProviderName(value any) error { + name, _ := value.(string) + + _, ok := form.collection.OAuth2.GetProviderConfig(name) + if !ok { + return validation.NewError("validation_invalid_provider", fmt.Sprintf("Provider with name %q is missing or is not enabled.", name)). + SetParams(map[string]any{"name": name}) + } + + return nil +} + +func oldCanAssignUsername(txApp core.App, collection *core.Collection, username string) bool { + // ensure that username is unique + checkUnique := dbutils.HasSingleColumnUniqueIndex(collection.OAuth2.MappedFields.Username, collection.Indexes) + if checkUnique { + if _, err := txApp.FindFirstRecordByData(collection, collection.OAuth2.MappedFields.Username, username); err == nil { + return false // already exist + } + } + + // ensure that the value matches the pattern of the username field (if text) + txtField, _ := collection.Fields.GetByName(collection.OAuth2.MappedFields.Username).(*core.TextField) + + return txtField != nil && txtField.ValidatePlainValue(username) == nil +} + +func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *core.ExternalAuth) error { + return e.App.RunInTransaction(func(txApp core.App) error { + if e.Record == nil { + // extra check to prevent creating a superuser record via + // OAuth2 in case the method is used by another action + if e.Collection.Name == core.CollectionNameSuperusers { + return errors.New("superusers are not allowed to sign-up with OAuth2") + } + + payload := maps.Clone(e.CreateData) + if payload == nil { + payload = map[string]any{} + } + + payload[core.FieldNameEmail] = e.OAuth2User.Email + + // set a random password if none is set + if v, _ := payload[core.FieldNamePassword].(string); v == "" { + payload[core.FieldNamePassword] = security.RandomString(30) + payload[core.FieldNamePassword+"Confirm"] = payload[core.FieldNamePassword] + } + + // map known fields (unless the field was explicitly submitted as part of CreateData) + if _, ok := payload[e.Collection.OAuth2.MappedFields.Id]; !ok && e.Collection.OAuth2.MappedFields.Id != "" { + payload[e.Collection.OAuth2.MappedFields.Id] = e.OAuth2User.Id + } + if _, ok := payload[e.Collection.OAuth2.MappedFields.Name]; !ok && e.Collection.OAuth2.MappedFields.Name != "" { + payload[e.Collection.OAuth2.MappedFields.Name] = e.OAuth2User.Name + } + if _, ok := payload[e.Collection.OAuth2.MappedFields.Username]; !ok && + // no explicit username payload value and existing OAuth2 mapping + e.Collection.OAuth2.MappedFields.Username != "" && + // extra checks for backward compatibility with earlier versions + 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 != "" { + 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 + avatarFile, err := func() (*filesystem.File, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + return filesystem.NewFileFromURL(ctx, e.OAuth2User.AvatarURL) + }() + if err != nil { + return err + } + payload[e.Collection.OAuth2.MappedFields.AvatarURL] = avatarFile + } else { + // otherwise - assign the url string + payload[e.Collection.OAuth2.MappedFields.AvatarURL] = e.OAuth2User.AvatarURL + } + } + + createdRecord, err := sendOAuth2RecordCreateRequest(txApp, e, payload) + if err != nil { + return err + } + + e.Record = createdRecord + + if e.Record.Email() == e.OAuth2User.Email && !e.Record.Verified() { + // mark as verified as long as it matches the OAuth2 data (even if the email is empty) + e.Record.SetVerified(true) + if err := txApp.Save(e.Record); err != nil { + return err + } + } + } else { + var needUpdate bool + + isLoggedAuthRecord := e.Auth != nil && + e.Auth.Id == e.Record.Id && + e.Auth.Collection().Id == e.Record.Collection().Id + + // set random password for users with unverified email + // (this is in case a malicious actor has registered previously with the user email) + if !isLoggedAuthRecord && e.Record.Email() != "" && !e.Record.Verified() { + e.Record.SetPassword(security.RandomString(30)) + needUpdate = true + } + + // update the existing auth record empty email if the data.OAuth2User has one + // (this is in case previously the auth record was created + // with an OAuth2 provider that didn't return an email address) + if e.Record.Email() == "" && e.OAuth2User.Email != "" { + e.Record.SetEmail(e.OAuth2User.Email) + needUpdate = true + } + + // update the existing auth record verified state + // (only if the auth record doesn't have an email or the auth record email match with the one in data.OAuth2User) + if !e.Record.Verified() && (e.Record.Email() == "" || e.Record.Email() == e.OAuth2User.Email) { + e.Record.SetVerified(true) + needUpdate = true + } + + if needUpdate { + if err := txApp.Save(e.Record); err != nil { + return err + } + } + } + + // create ExternalAuth relation if missing + if optExternalAuth == nil { + optExternalAuth = core.NewExternalAuth(txApp) + optExternalAuth.SetCollectionRef(e.Record.Collection().Id) + optExternalAuth.SetRecordRef(e.Record.Id) + optExternalAuth.SetProvider(e.ProviderName) + optExternalAuth.SetProviderId(e.OAuth2User.Id) + + if err := txApp.Save(optExternalAuth); err != nil { + return fmt.Errorf("failed to save linked rel: %w", err) + } + } + + return nil + }) +} + +func sendOAuth2RecordCreateRequest(txApp core.App, e *core.RecordAuthWithOAuth2RequestEvent, payload map[string]any) (*core.Record, error) { + ir := &core.InternalRequest{ + Method: http.MethodPost, + URL: "/api/collections/" + e.Collection.Name + "/records", + Body: payload, + } + + response, err := processInternalRequest(txApp, e.RequestEvent, ir, core.RequestInfoContextOAuth2, nil) + if err != nil { + return nil, err + } + + if response.Status != http.StatusOK { + 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) +} diff --git a/apis/record_auth_with_oauth2_redirect.go b/apis/record_auth_with_oauth2_redirect.go new file mode 100644 index 00000000..f5bde653 --- /dev/null +++ b/apis/record_auth_with_oauth2_redirect.go @@ -0,0 +1,74 @@ +package apis + +import ( + "encoding/json" + "net/http" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +const ( + oauth2SubscriptionTopic string = "@oauth2" + oauth2RedirectFailurePath string = "../_/#/auth/oauth2-redirect-failure" + oauth2RedirectSuccessPath string = "../_/#/auth/oauth2-redirect-success" +) + +type oauth2RedirectData struct { + State string `form:"state" json:"state"` + Code string `form:"code" json:"code"` + Error string `form:"error" json:"error,omitempty"` +} + +func oauth2SubscriptionRedirect(e *core.RequestEvent) error { + redirectStatusCode := http.StatusTemporaryRedirect + if e.Request.Method != http.MethodGet { + redirectStatusCode = http.StatusSeeOther + } + + data := oauth2RedirectData{} + + if e.Request.Method == http.MethodPost { + if err := e.BindBody(&data); err != nil { + e.App.Logger().Debug("Failed to read OAuth2 redirect data", "error", err) + return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath) + } + } else { + query := e.Request.URL.Query() + data.State = query.Get("state") + data.Code = query.Get("code") + data.Error = query.Get("error") + } + + if data.State == "" { + e.App.Logger().Debug("Missing OAuth2 state parameter") + return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath) + } + + client, err := e.App.SubscriptionsBroker().ClientById(data.State) + if err != nil || client.IsDiscarded() || !client.HasSubscription(oauth2SubscriptionTopic) { + e.App.Logger().Debug("Missing or invalid OAuth2 subscription client", "error", err, "clientId", data.State) + return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath) + } + defer client.Unsubscribe(oauth2SubscriptionTopic) + + encodedData, err := json.Marshal(data) + if err != nil { + e.App.Logger().Debug("Failed to marshalize OAuth2 redirect data", "error", err) + return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath) + } + + msg := subscriptions.Message{ + Name: oauth2SubscriptionTopic, + Data: encodedData, + } + + client.Send(msg) + + if data.Error != "" || data.Code == "" { + e.App.Logger().Debug("Failed OAuth2 redirect due to an error or missing code parameter", "error", data.Error, "clientId", data.State) + return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath) + } + + return e.Redirect(redirectStatusCode, oauth2RedirectSuccessPath) +} diff --git a/apis/record_auth_with_oauth2_redirect_test.go b/apis/record_auth_with_oauth2_redirect_test.go new file mode 100644 index 00000000..d9cf77b1 --- /dev/null +++ b/apis/record_auth_with_oauth2_redirect_test.go @@ -0,0 +1,252 @@ +package apis_test + +import ( + "context" + "net/http" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +func TestRecordAuthWithOAuth2Redirect(t *testing.T) { + t.Parallel() + + clientStubs := make([]map[string]subscriptions.Client, 0, 10) + + for i := 0; i < 10; i++ { + c1 := subscriptions.NewDefaultClient() + + c2 := subscriptions.NewDefaultClient() + c2.Subscribe("@oauth2") + + c3 := subscriptions.NewDefaultClient() + c3.Subscribe("test1", "@oauth2") + + c4 := subscriptions.NewDefaultClient() + c4.Subscribe("test1", "test2") + + c5 := subscriptions.NewDefaultClient() + c5.Subscribe("@oauth2") + c5.Discard() + + clientStubs = append(clientStubs, map[string]subscriptions.Client{ + "c1": c1, + "c2": c2, + "c3": c3, + "c4": c4, + "c5": c5, + }) + } + + checkFailureRedirect := func(t testing.TB, app *tests.TestApp, res *http.Response) { + loc := res.Header.Get("Location") + if !strings.Contains(loc, "/oauth2-redirect-failure") { + t.Fatalf("Expected failure redirect, got %q", loc) + } + } + + checkSuccessRedirect := func(t testing.TB, app *tests.TestApp, res *http.Response) { + loc := res.Header.Get("Location") + if !strings.Contains(loc, "/oauth2-redirect-success") { + t.Fatalf("Expected success redirect, got %q", loc) + } + } + + checkClientMessages := func(t testing.TB, clientId string, msg subscriptions.Message, expectedMessages map[string][]string) { + if len(expectedMessages[clientId]) == 0 { + t.Fatalf("Unexpected client %q message, got %s:\n%s", clientId, msg.Name, msg.Data) + } + + if msg.Name != "@oauth2" { + t.Fatalf("Expected @oauth2 msg.Name, got %q", msg.Name) + } + + for _, txt := range expectedMessages[clientId] { + if !strings.Contains(string(msg.Data), txt) { + t.Fatalf("Failed to find %q in \n%s", txt, msg.Data) + } + } + } + + beforeTestFunc := func( + clients map[string]subscriptions.Client, + expectedMessages map[string][]string, + ) func(testing.TB, *tests.TestApp, *core.ServeEvent) { + return func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + for _, client := range clients { + app.SubscriptionsBroker().Register(client) + } + + ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) + + // add to the app store so that it can be cancelled manually after test completion + app.Store().Set("cancelFunc", cancelFunc) + + go func() { + defer cancelFunc() + + for { + select { + case msg := <-clients["c1"].Channel(): + checkClientMessages(t, "c1", msg, expectedMessages) + case msg := <-clients["c2"].Channel(): + checkClientMessages(t, "c2", msg, expectedMessages) + case msg := <-clients["c3"].Channel(): + checkClientMessages(t, "c3", msg, expectedMessages) + case msg := <-clients["c4"].Channel(): + checkClientMessages(t, "c4", msg, expectedMessages) + case msg := <-clients["c5"].Channel(): + checkClientMessages(t, "c5", msg, expectedMessages) + case <-ctx.Done(): + for _, c := range clients { + close(c.Channel()) + } + return + } + } + }() + } + } + + scenarios := []tests.ApiScenario{ + { + Name: "no state query param", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?code=123", + BeforeTestFunc: beforeTestFunc(clientStubs[0], nil), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + }, + }, + { + Name: "invalid or missing client", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?code=123&state=missing", + BeforeTestFunc: beforeTestFunc(clientStubs[1], nil), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + }, + }, + { + Name: "no code query param", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?state=" + clientStubs[2]["c3"].Id(), + BeforeTestFunc: beforeTestFunc(clientStubs[2], map[string][]string{ + "c3": {`"state":"` + clientStubs[2]["c3"].Id(), `"code":""`}, + }), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + + if clientStubs[2]["c3"].HasSubscription("@oauth2") { + t.Fatalf("Expected oauth2 subscription to be removed") + } + }, + }, + { + Name: "error query param", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?error=example&code=123&state=" + clientStubs[3]["c3"].Id(), + BeforeTestFunc: beforeTestFunc(clientStubs[3], map[string][]string{ + "c3": {`"state":"` + clientStubs[3]["c3"].Id(), `"code":"123"`, `"error":"example"`}, + }), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + + if clientStubs[3]["c3"].HasSubscription("@oauth2") { + t.Fatalf("Expected oauth2 subscription to be removed") + } + }, + }, + { + Name: "discarded client with @oauth2 subscription", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?code=123&state=" + clientStubs[4]["c5"].Id(), + BeforeTestFunc: beforeTestFunc(clientStubs[4], nil), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + }, + }, + { + Name: "client without @oauth2 subscription", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?code=123&state=" + clientStubs[4]["c4"].Id(), + BeforeTestFunc: beforeTestFunc(clientStubs[5], nil), + ExpectedStatus: http.StatusTemporaryRedirect, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkFailureRedirect(t, app, res) + }, + }, + { + Name: "client with @oauth2 subscription", + Method: http.MethodGet, + URL: "/api/oauth2-redirect?code=123&state=" + clientStubs[6]["c3"].Id(), + BeforeTestFunc: beforeTestFunc(clientStubs[6], map[string][]string{ + "c3": {`"state":"` + clientStubs[6]["c3"].Id(), `"code":"123"`}, + }), + ExpectedStatus: http.StatusTemporaryRedirect, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkSuccessRedirect(t, app, res) + + if clientStubs[6]["c3"].HasSubscription("@oauth2") { + t.Fatalf("Expected oauth2 subscription to be removed") + } + }, + }, + { + Name: "(POST) client with @oauth2 subscription", + Method: http.MethodPost, + URL: "/api/oauth2-redirect", + Body: strings.NewReader("code=123&state=" + clientStubs[7]["c3"].Id()), + Headers: map[string]string{ + "content-type": "application/x-www-form-urlencoded", + }, + BeforeTestFunc: beforeTestFunc(clientStubs[7], map[string][]string{ + "c3": {`"state":"` + clientStubs[7]["c3"].Id(), `"code":"123"`}, + }), + ExpectedStatus: http.StatusSeeOther, + ExpectedEvents: map[string]int{"*": 0}, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + app.Store().Get("cancelFunc").(context.CancelFunc)() + + checkSuccessRedirect(t, app, res) + + if clientStubs[7]["c3"].HasSubscription("@oauth2") { + t.Fatalf("Expected oauth2 subscription to be removed") + } + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_with_oauth2_test.go b/apis/record_auth_with_oauth2_test.go new file mode 100644 index 00000000..7d226ce5 --- /dev/null +++ b/apis/record_auth_with_oauth2_test.go @@ -0,0 +1,1430 @@ +package apis_test + +import ( + "bytes" + "errors" + "image" + "image/png" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/auth" + "golang.org/x/oauth2" +) + +var _ auth.Provider = (*oauth2MockProvider)(nil) + +type oauth2MockProvider struct { + auth.BaseProvider + + AuthUser *auth.AuthUser + Token *oauth2.Token +} + +func (p *oauth2MockProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + if p.Token == nil { + return nil, errors.New("failed to fetch OAuth2 token") + } + return p.Token, nil +} + +func (p *oauth2MockProvider) FetchAuthUser(token *oauth2.Token) (*auth.AuthUser, error) { + if p.AuthUser == nil { + return nil, errors.New("failed to fetch OAuth2 user") + } + return p.AuthUser, nil +} + +func TestRecordAuthWithOAuth2(t *testing.T) { + t.Parallel() + + // start a test server + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + buf := new(bytes.Buffer) + png.Encode(buf, image.Rect(0, 0, 1, 1)) // tiny 1x1 png + http.ServeContent(res, req, "test_avatar.png", time.Now(), bytes.NewReader(buf.Bytes())) + })) + defer server.Close() + + scenarios := []tests.ApiScenario{ + { + Name: "disabled OAuth2 auth", + Method: http.MethodPost, + URL: "/api/collections/nologin/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code": "123", + "codeVerifier": "456", + "redirectURL": "https://example.com" + }`), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid body", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{"provider"`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "trigger form validations (missing provider)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "missing" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"provider":`, + `"code":`, + `"redirectURL":`, + }, + NotExpectedContent: []string{ + `"codeVerifier":`, // should be optional + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "trigger form validations (existing but disabled provider)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "apple" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"provider":`, + `"code":`, + `"redirectURL":`, + }, + NotExpectedContent: []string{ + `"codeVerifier":`, // should be optional + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "existing linked OAuth2 (unverified user)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "name": "test_new" + } + }`), + Headers: map[string]string{ + // users, test2@example.com + // (auth with some other user from the same collection to ensure that it is ignored) + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.Verified() { + t.Fatalf("Expected user %q to be unverified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{Id: "test_id"}, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + + // stub linked provider + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(user.Collection().Id) + ea.SetRecordRef(user.Id) + ea.SetProvider("test") + ea.SetProviderId("test_id") + if err := app.Save(ea); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test@example.com"`, + `"id":"4q1xlclmfloku33"`, + `"id":"test_id"`, + `"verified":false`, // shouldn't change + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + // --- + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + // --- + "OnModelValidate": 2, // create + update + "OnRecordValidate": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if name := user.GetString("name"); name != "test1" { + t.Fatalf("Expected name to not change, got %q", name) + } + + if user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be changed", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "existing linked OAuth2 (verified user)", + 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) { + user, err := app.FindAuthRecordByEmail("users", "test2@example.com") + if err != nil { + t.Fatal(err) + } + + if !user.Verified() { + t.Fatalf("Expected user %q to be verified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{Id: "test_id"}, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + + // stub linked provider + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(user.Collection().Id) + ea.SetRecordRef(user.Id) + ea.SetProvider("test") + ea.SetProviderId("test_id") + if err := app.Save(ea); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test2@example.com"`, + `"id":"oap640cot4yru2s"`, + `"id":"test_id"`, + `"verified":true`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + // --- + "OnModelValidate": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test2@example.com") + if err != nil { + t.Fatal(err) + } + + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected old password %q to be valid", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "link by email", + 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) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.Verified() { + t.Fatalf("Expected user %q to be unverified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{Id: "test_id", Email: "test@example.com"}, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test@example.com"`, + `"id":"4q1xlclmfloku33"`, + `"id":"test_id"`, + `"verified":true`, // should be updated + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 2, // authOrigins + externalAuths + "OnModelCreateExecute": 2, + "OnModelAfterCreateSuccess": 2, + "OnRecordCreate": 2, + "OnRecordCreateExecute": 2, + "OnRecordAfterCreateSuccess": 2, + // --- + "OnModelUpdate": 1, // record password and verified states + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + // --- + "OnModelValidate": 3, // record + authOrigins + externalAuths + "OnRecordValidate": 3, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be changed", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "link by fallback user (OAuth2 user with different email)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com" + }`), + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.Verified() { + t.Fatalf("Expected user %q to be unverified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{ + Id: "test_id", + Email: "test2@example.com", // different email -> should be ignored + }, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test@example.com"`, + `"id":"4q1xlclmfloku33"`, + `"id":"test_id"`, + `"verified":false`, // shouldn't change because the OAuth2 user email is different + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 2, // authOrigins + externalAuths + "OnModelCreateExecute": 2, + "OnModelAfterCreateSuccess": 2, + "OnRecordCreate": 2, + "OnRecordCreateExecute": 2, + "OnRecordAfterCreateSuccess": 2, + // --- + "OnModelValidate": 2, + "OnRecordValidate": 2, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q not to be changed", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "link by fallback user (user without email)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com" + }`), + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.Verified() { + t.Fatalf("Expected user %q to be unverified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // manually unset the user email + user.SetEmail("") + if err := app.Save(user); err != nil { + t.Fatal(err) + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{ + Id: "test_id", + Email: "test_oauth2@example.com", + }, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test_oauth2@example.com"`, + `"id":"4q1xlclmfloku33"`, + `"id":"test_id"`, + `"verified":true`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 2, // authOrigins + externalAuths + "OnModelCreateExecute": 2, + "OnModelAfterCreateSuccess": 2, + "OnRecordCreate": 2, + "OnRecordCreateExecute": 2, + "OnRecordAfterCreateSuccess": 2, + // --- + "OnModelUpdate": 1, // record email set + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + // --- + "OnModelValidate": 3, // record + authOrigins + externalAuths + "OnRecordValidate": 3, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test_oauth2@example.com") + if err != nil { + t.Fatal(err) + } + + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q not to be changed", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "link by fallback user (unverified user with matching email)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com" + }`), + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if user.Verified() { + t.Fatalf("Expected user %q to be unverified", user.Email()) + } + + // ensure that the old password works + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{ + Id: "test_id", + Email: "test@example.com", // matching email -> should be marked as verified + }, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + user.Collection().MFA.Enabled = false + user.Collection().OAuth2.Enabled = true + user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":"test@example.com"`, + `"id":"4q1xlclmfloku33"`, + `"id":"test_id"`, + `"verified":true`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelCreate": 2, // authOrigins + externalAuths + "OnModelCreateExecute": 2, + "OnModelAfterCreateSuccess": 2, + "OnRecordCreate": 2, + "OnRecordCreateExecute": 2, + "OnRecordAfterCreateSuccess": 2, + // --- + "OnModelUpdate": 1, // record verified update + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + // --- + "OnModelValidate": 3, // record + authOrigins + externalAuths + "OnRecordValidate": 3, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q not to be changed", "1234567890") + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if len(devices) != 1 { + t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err) + } + }, + }, + { + Name: "creating user (no extra create data or custom fields mapping)", + 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: "test_id"}, + 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", + }} + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"record":{`, + `"token":"`, + `"meta":{`, + `"email":""`, + `"id":"test_id"`, + `"verified":true`, + }, + 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 (submit failure - form auth fields validator)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "verified": true, + "email": "invalid", + "rel": "invalid", + "file": "invalid" + } + }`), + 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: "test_id"}, + 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", + }} + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"verified":{"code":"validation_values_mismatch"`, + }, + NotExpectedContent: []string{ + `"email":`, // the value is always overwritten with the OAuth2 user email + `"rel":`, // ignored because the record validator never ran + `"file":`, // ignored because the record validator never ran + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordCreateRequest": 1, + }, + }, + { + Name: "creating user (submit failure - record fields validator)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "email": "invalid", + "rel": "invalid", + "file": "invalid" + } + }`), + 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: "test_id"}, + 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", + }} + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"rel":{"code":"validation_missing_rel_records"`, + `"file":{"code":"validation_invalid_file"`, + }, + NotExpectedContent: []string{ + `"email":`, // the value is always overwritten with the OAuth2 user email + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordCreateRequest": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnModelCreate": 1, + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordAfterCreateError": 1, + }, + }, + { + Name: "creating user (valid create data)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "email": "invalid", + "emailVisibility": true, + "name": "test_name", + "username": "test_username", + "rel": "0yxhwia2amd8gec" + } + }`), + 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: "test_id"}, + 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", + }} + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":""`, + `"emailVisibility":true`, + `"name":"test_name"`, + `"username":"test_username"`, + `"verified":true`, + `"rel":"0yxhwia2amd8gec"`, + }, + 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->file field)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "name": "test_name", + "emailVisibility": true, + "rel": "0yxhwia2amd8gec" + } + }`), + 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", + Username: "oauth2_username", + AvatarURL: server.URL + "/oauth2_avatar.png", + }, + 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{ + Username: "name", // should be ignored because of the explicit submitted value + Id: "username", + AvatarURL: "avatar", + } + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"oauth2@example.com"`, + `"emailVisibility":true`, + `"name":"test_name"`, + `"username":"oauth2_username"`, + `"verified":true`, + `"rel":"0yxhwia2amd8gec"`, + `"avatar":"oauth2_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, + 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", + Username: "oauth2_username", + Name: "oauth2_name", + AvatarURL: server.URL + "/oauth2_avatar.png", + }, + 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{ + Username: "username", + AvatarURL: "name", + } + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"oauth2@example.com"`, + `"emailVisibility":false`, + `"username":"oauth2_username"`, + `"name":"http://127.`, + `"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 duplicated username)", + 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", + Username: "test2_username", + Name: "oauth2_name", + }, + 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{ + Username: "username", + } + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"oauth2@example.com"`, + `"emailVisibility":false`, + `"verified":true`, + `"avatar":""`, + `"username":"users`, + }, + 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 username that doesn't match the field validations)", + 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", + Username: "!@invalid", + Name: "oauth2_name", + }, + 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{ + Username: "username", + } + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"oauth2@example.com"`, + `"emailVisibility":false`, + `"verified":true`, + `"avatar":""`, + `"username":"users`, + }, + 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, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:authWithOAuth2", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithOAuth2"}, + {MaxRequests: 100, Label: "users:auth"}, + {MaxRequests: 0, Label: "users:authWithOAuth2"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:authWithOAuth2", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:auth"}, + {MaxRequests: 0, Label: "*:authWithOAuth2"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit tag - users:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithOAuth2"}, + {MaxRequests: 0, Label: "users:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit tag - *:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_auth_with_otp.go b/apis/record_auth_with_otp.go new file mode 100644 index 00000000..f7f1bc5b --- /dev/null +++ b/apis/record_auth_with_otp.go @@ -0,0 +1,99 @@ +package apis + +import ( + "errors" + "fmt" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" +) + +func recordAuthWithOTP(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if !collection.OTP.Enabled { + return e.ForbiddenError("The collection is not configured to allow OTP authentication.", nil) + } + + form := &authWithOTPForm{} + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + event := new(core.RecordAuthWithOTPRequestEvent) + event.RequestEvent = e + event.Collection = collection + + // extra validations + // (note: returns a generic 400 as a very basic OTPs enumeration protection) + // --- + event.OTP, err = e.App.FindOTPById(form.OTPId) + if err != nil { + return e.BadRequestError("Invalid or expired OTP", err) + } + + if event.OTP.CollectionRef() != collection.Id { + return e.BadRequestError("Invalid or expired OTP", errors.New("the OTP is for a different collection")) + } + + if event.OTP.HasExpired(collection.OTP.DurationTime()) { + return e.BadRequestError("Invalid or expired OTP", errors.New("the OTP is expired")) + } + + event.Record, err = e.App.FindRecordById(event.OTP.CollectionRef(), event.OTP.RecordRef()) + if err != nil { + return e.BadRequestError("Invalid or expired OTP", fmt.Errorf("missing auth record: %w", err)) + } + + // since otps are usually simple digit numbers we enforce an extra rate limit rule to prevent enumerations + err = checkRateLimit(e, "@pb_otp_"+event.OTP.Id+event.Record.Id, core.RateLimitRule{MaxRequests: 4, Duration: 180}) + if err != nil { + return e.TooManyRequestsError("Too many attempts, please try again later with a new OTP.", nil) + } + + if !event.OTP.ValidatePassword(form.Password) { + return e.BadRequestError("Invalid or expired OTP", errors.New("incorrect password")) + } + // --- + + return e.App.OnRecordAuthWithOTPRequest().Trigger(event, func(e *core.RecordAuthWithOTPRequestEvent) error { + err = RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil) + if err != nil { + return err + } + + // try to delete the used otp + if e.OTP != nil { + err = e.App.Delete(e.OTP) + if err != nil { + e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id) + } + } + + // note: we don't update the user verified state the same way as in the password reset confirmation + // at the moment because it is not clear whether the otp confirmation came from the user email + // (e.g. it could be from an sms or some other channel) + + return nil + }) +} + +// ------------------------------------------------------------------- + +type authWithOTPForm struct { + OTPId string `form:"otpId" json:"otpId"` + Password string `form:"password" json:"password"` +} + +func (form *authWithOTPForm) validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.OTPId, validation.Required, validation.Length(1, 255)), + validation.Field(&form.Password, validation.Required, validation.Length(1, 71)), + ) +} diff --git a/apis/record_auth_with_otp_test.go b/apis/record_auth_with_otp_test.go new file mode 100644 index 00000000..9befdd7a --- /dev/null +++ b/apis/record_auth_with_otp_test.go @@ -0,0 +1,438 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestRecordAuthWithOTP(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "not an auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/auth-with-otp", + Body: strings.NewReader(`{"otpId":"test","password":"123456"}`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "auth collection with disabled otp", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{"otpId":"test","password":"123456"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + usersCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + usersCol.OTP.Enabled = false + + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid body", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty body", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"otpId":{"code":"validation_required"`, + `"password":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid request data", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 256) + `", + "password":"` + strings.Repeat("a", 72) + `" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"otpId":{"code":"validation_length_out_of_range"`, + `"password":{"code":"validation_length_out_of_range"`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "missing otp", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"missing", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "otp for different collection", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 15) + `", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + client, err := app.FindAuthRecordByEmail("clients", "test@example.com") + if err != nil { + t.Fatal(err) + } + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(client.Collection().Id) + otp.SetRecordRef(client.Id) + otp.SetPassword("123456") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "otp with wrong password", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 15) + `", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("1234567890") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "expired otp with valid password", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 15) + `", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + expiredDate := types.NowDateTime().AddDate(-3, 0, 0) + otp.SetRaw("created", expiredDate) + otp.SetRaw("updated", expiredDate) + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "valid otp with valid password", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 15) + `", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"mfaId":"`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOTPRequest": 1, + "OnRecordAuthRequest": 1, + // --- + "OnModelValidate": 1, + "OnModelCreate": 1, // mfa record + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelDelete": 1, // otp delete + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + // --- + "OnRecordValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + { + Name: "valid otp with valid password (disabled MFA)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + strings.Repeat("a", 15) + `", + "password":"123456" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + user.Collection().MFA.Enabled = false + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + + otp := core.NewOTP(app) + otp.Id = strings.Repeat("a", 15) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":"`, + `"record":{`, + `"email":"test@example.com"`, + }, + NotExpectedContent: []string{ + `"meta":`, + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOTPRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // --- + "OnModelValidate": 1, + "OnModelCreate": 1, // authOrigin + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelDelete": 1, // otp delete + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + // --- + "OnRecordValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:authWithOTP", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithOTP"}, + {MaxRequests: 100, Label: "users:auth"}, + {MaxRequests: 0, Label: "users:authWithOTP"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:authWithOTP", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:auth"}, + {MaxRequests: 0, Label: "*:authWithOTP"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - users:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithOTP"}, + {MaxRequests: 0, Label: "users:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordAuthWithOTPManualRateLimiterCheck(t *testing.T) { + t.Parallel() + + var storeCache map[string]any + + otpAId := strings.Repeat("a", 15) + otpBId := strings.Repeat("b", 15) + + scenarios := []struct { + otpId string + password string + expectedStatus int + }{ + {otpAId, "12345", 400}, + {otpAId, "12345", 400}, + {otpAId, "12345", 400}, + {otpAId, "12345", 400}, + {otpAId, "123456", 429}, + {otpBId, "12345", 400}, + {otpBId, "123456", 200}, + } + + for _, s := range scenarios { + (&tests.ApiScenario{ + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-otp", + Body: strings.NewReader(`{ + "otpId":"` + s.otpId + `", + "password":"` + s.password + `" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + for k, v := range storeCache { + app.Store().Set(k, v) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + user.Collection().MFA.Enabled = false + if err := app.Save(user.Collection()); err != nil { + t.Fatal(err) + } + + for _, id := range []string{otpAId, otpBId} { + otp := core.NewOTP(app) + otp.Id = id + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("123456") + if err := app.Save(otp); err != nil { + t.Fatal(err) + } + } + }, + ExpectedStatus: s.expectedStatus, + ExpectedContent: []string{`"`}, // it doesn't matter anything non-empty + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + storeCache = app.Store().GetAll() + }, + }).Test(t) + } +} diff --git a/apis/record_auth_with_password.go b/apis/record_auth_with_password.go new file mode 100644 index 00000000..a65bb3b7 --- /dev/null +++ b/apis/record_auth_with_password.go @@ -0,0 +1,97 @@ +package apis + +import ( + "database/sql" + "errors" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/list" +) + +func recordAuthWithPassword(e *core.RequestEvent) error { + collection, err := findAuthCollection(e) + if err != nil { + return err + } + + if !collection.PasswordAuth.Enabled { + return e.ForbiddenError("The collection is not configured to allow password authentication.", nil) + } + + form := &authWithPasswordForm{} + if err = e.BindBody(form); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err)) + } + if err = form.validate(collection); err != nil { + return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err)) + } + + var foundRecord *core.Record + var foundErr error + + if form.IdentityField != "" { + foundRecord, foundErr = e.App.FindFirstRecordByData(collection.Id, form.IdentityField, form.Identity) + } else { + // prioritize email lookup + isEmail := is.EmailFormat.Validate(form.Identity) == nil + if isEmail && list.ExistInSlice(core.FieldNameEmail, collection.PasswordAuth.IdentityFields) { + foundRecord, foundErr = e.App.FindAuthRecordByEmail(collection.Id, form.Identity) + } + + // search by the other identity fields + if !isEmail || foundErr != nil { + for _, name := range collection.PasswordAuth.IdentityFields { + if !isEmail && name == core.FieldNameEmail { + continue // no need to search by the email field if it is not an email + } + + foundRecord, foundErr = e.App.FindFirstRecordByData(collection.Id, name, form.Identity) + if foundErr == nil { + break + } + } + } + } + + // ignore not found errors to allow custom record find implementations + if foundErr != nil && !errors.Is(foundErr, sql.ErrNoRows) { + return e.InternalServerError("", foundErr) + } + + event := new(core.RecordAuthWithPasswordRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = foundRecord + event.Identity = form.Identity + event.Password = form.Password + event.IdentityField = form.IdentityField + + return e.App.OnRecordAuthWithPasswordRequest().Trigger(event, func(e *core.RecordAuthWithPasswordRequestEvent) error { + if e.Record == nil || !e.Record.ValidatePassword(e.Password) { + return e.BadRequestError("Failed to authenticate.", errors.New("invalid login credentials")) + } + + return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodPassword, nil) + }) +} + +// ------------------------------------------------------------------- + +type authWithPasswordForm struct { + Identity string `form:"identity" json:"identity"` + Password string `form:"password" json:"password"` + + // IdentityField specifies the field to use to search for the identity + // (leave it empty for "auto" detection). + IdentityField string `form:"identityField" json:"identityField"` +} + +func (form *authWithPasswordForm) validate(collection *core.Collection) error { + return validation.ValidateStruct(form, + validation.Field(&form.Identity, validation.Required, validation.Length(1, 255)), + validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), + validation.Field(&form.IdentityField, validation.In(list.ToInterfaceSlice(collection.PasswordAuth.IdentityFields)...)), + ) +} diff --git a/apis/record_auth_with_password_test.go b/apis/record_auth_with_password_test.go new file mode 100644 index 00000000..75d47bd2 --- /dev/null +++ b/apis/record_auth_with_password_test.go @@ -0,0 +1,514 @@ +package apis_test + +import ( + "errors" + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordAuthWithPassword(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "disabled password auth", + Method: http.MethodPost, + URL: "/api/collections/nologin/auth-with-password", + Body: strings.NewReader(`{"identity":"test@example.com","password":"1234567890"}`), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-auth collection", + Method: http.MethodPost, + URL: "/api/collections/demo1/auth-with-password", + Body: strings.NewReader(`{"identity":"test@example.com","password":"1234567890"}`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "invalid body format", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{"identity`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "empty body params", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{"identity":"","password":""}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"identity":{`, + `"password":{`, + }, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "OnRecordAuthWithPasswordRequest error response", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordAuthWithPasswordRequest().BindFunc(func(e *core.RecordAuthWithPasswordRequestEvent) error { + return errors.New("error") + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + }, + }, + { + Name: "valid identity field and invalid password", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"invalid" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{}`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + }, + }, + { + Name: "valid identity field (email) and valid password", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"1234567890" + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test@example.com"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 1, + "OnMailerRecordAuthAlertSend": 1, + }, + }, + { + Name: "valid identity field (username) and valid password", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"clients57772", + "password":"1234567890" + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test@example.com"`, + `"username":"clients57772"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 1, + "OnMailerRecordAuthAlertSend": 1, + }, + }, + { + Name: "valid identity field and valid password with mismatched explicit identityField", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identityField": "username", + "identity":"test@example.com", + "password":"1234567890" + }`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + }, + }, + { + Name: "valid identity field and valid password with matched explicit identityField", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identityField": "username", + "identity":"clients57772", + "password":"1234567890" + }`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test@example.com"`, + `"username":"clients57772"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 1, + "OnMailerRecordAuthAlertSend": 1, + }, + }, + { + Name: "valid identity (unverified) and valid password in onlyVerified collection", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test2@example.com", + "password":"1234567890" + }`), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + }, + }, + { + Name: "already authenticated record", + Method: http.MethodPost, + URL: "/api/collections/clients/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"1234567890" + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"gk390qegs4y47wn"`, + `"email":"test@example.com"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 1, + "OnMailerRecordAuthAlertSend": 1, + }, + }, + { + Name: "with mfa first auth check", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"1234567890" + }`), + ExpectedStatus: 401, + ExpectedContent: []string{ + `"mfaId":"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + // mfa create + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + mfas, err := app.FindAllMFAsByRecord(user) + if err != nil { + t.Fatal(err) + } + + if v := len(mfas); v != 1 { + t.Fatalf("Expected 1 mfa record to be created, got %d", v) + } + }, + }, + { + Name: "with mfa second auth check", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + Body: strings.NewReader(`{ + "mfaId": "` + strings.Repeat("a", 15) + `", + "identity":"test@example.com", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + // insert a dummy mfa record + mfa := core.NewMFA(app) + mfa.Id = strings.Repeat("a", 15) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("test") + if err := app.Save(mfa); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test@example.com"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 0, // disabled auth email alerts + "OnMailerRecordAuthAlertSend": 0, + // mfa delete + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + { + Name: "with enabled mfa but unsatisfied mfa rule (aka. skip the mfa check)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + Body: strings.NewReader(`{ + "identity":"test@example.com", + "password":"1234567890" + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + users, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + users.MFA.Enabled = true + users.MFA.Rule = "1=2" + + if err := app.Save(users); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test@example.com"`, + `"token":`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithPasswordRequest": 1, + "OnRecordAuthRequest": 1, + "OnRecordEnrich": 1, + // authOrigin track + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + "OnMailerSend": 0, // disabled auth email alerts + "OnMailerRecordAuthAlertSend": 0, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + mfas, err := app.FindAllMFAsByRecord(user) + if err != nil { + t.Fatal(err) + } + + if v := len(mfas); v != 0 { + t.Fatalf("Expected no mfa records to be created, got %d", v) + } + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - users:authWithPassword", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithPassword"}, + {MaxRequests: 100, Label: "users:auth"}, + {MaxRequests: 0, Label: "users:authWithPassword"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:authWithPassword", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:auth"}, + {MaxRequests: 0, Label: "*:authWithPassword"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - users:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:authWithPassword"}, + {MaxRequests: 0, Label: "users:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:auth", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-password", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:auth"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud.go b/apis/record_crud.go index 9fce86fc..ece65d37 100644 --- a/apis/record_crud.go +++ b/apis/record_crud.go @@ -1,121 +1,123 @@ package apis import ( + "errors" "fmt" - "log/slog" "net/http" + "strings" - "github.com/labstack/echo/v5" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/search" ) // bindRecordCrudApi registers the record crud api endpoints and // the corresponding handlers. -func bindRecordCrudApi(app core.App, rg *echo.Group) { - api := recordApi{app: app} - - subGroup := rg.Group( - "/collections/:collection", - ActivityLogger(app), - ) - - subGroup.GET("/records", api.list, LoadCollectionContext(app)) - subGroup.GET("/records/:id", api.view, LoadCollectionContext(app)) - subGroup.POST("/records", api.create, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth)) - subGroup.PATCH("/records/:id", api.update, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth)) - subGroup.DELETE("/records/:id", api.delete, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth)) +// +// note: the rate limiter is "inlined" because some of the crud actions are also used in the batch APIs +func bindRecordCrudApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + subGroup := rg.Group("/collections/{collection}/records").Unbind(DefaultRateLimitMiddlewareId) + subGroup.GET("", recordsList) + subGroup.GET("/{id}", recordView) + subGroup.POST("", recordCreate(nil)).Bind(dynamicCollectionBodyLimit("")) + subGroup.PATCH("/{id}", recordUpdate(nil)).Bind(dynamicCollectionBodyLimit("")) + subGroup.DELETE("/{id}", recordDelete(nil)) } -type recordApi struct { - app core.App -} - -func (api *recordApi) list(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", "Missing collection context.") +func recordsList(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) + if err != nil || collection == nil { + return e.NotFoundError("Missing collection context.", err) } - requestInfo := RequestInfo(c) - - // forbid users and guests to query special filter/sort fields - if err := checkForAdminOnlyRuleFields(requestInfo); err != nil { + err = checkCollectionRateLimit(e, collection, "list") + if err != nil { return err } - if requestInfo.Admin == nil && collection.ListRule == nil { - // only admins can access if the rule is nil - return NewForbiddenError("Only admins can perform this action.", nil) + requestInfo, err := e.RequestInfo() + if err != nil { + return firstApiError(err, e.BadRequestError("", err)) } - fieldsResolver := resolvers.NewRecordFieldResolver( - api.app.Dao(), + if collection.ListRule == nil && !requestInfo.HasSuperuserAuth() { + return e.ForbiddenError("Only superusers can perform this action.", nil) + } + + // forbid users and guests to query special filter/sort fields + err = checkForSuperuserOnlyRuleFields(requestInfo) + if err != nil { + return err + } + + fieldsResolver := core.NewRecordFieldResolver( + e.App, collection, requestInfo, - // hidden fields are searchable only by admins - requestInfo.Admin != nil, + // hidden fields are searchable only by superusers + requestInfo.HasSuperuserAuth(), ) searchProvider := search.NewProvider(fieldsResolver). - Query(api.app.Dao().RecordQuery(collection)) + Query(e.App.RecordQuery(collection)) - if requestInfo.Admin == nil && collection.ListRule != nil { + if !requestInfo.HasSuperuserAuth() && collection.ListRule != nil { searchProvider.AddFilter(search.FilterData(*collection.ListRule)) } - records := []*models.Record{} + records := []*core.Record{} - result, err := searchProvider.ParseAndExec(c.QueryParams().Encode(), &records) + result, err := searchProvider.ParseAndExec(e.Request.URL.Query().Encode(), &records) if err != nil { - return NewBadRequestError("", err) + return firstApiError(err, e.BadRequestError("", err)) } - event := new(core.RecordsListEvent) - event.HttpContext = c + event := new(core.RecordsListRequestEvent) + event.RequestEvent = e event.Collection = collection event.Records = records event.Result = result - return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error { - if e.HttpContext.Response().Committed { - return nil + return e.App.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListRequestEvent) error { + if err := EnrichRecords(e.RequestEvent, e.Records); err != nil { + return firstApiError(err, e.InternalServerError("Failed to enrich records", err)) } - if err := EnrichRecords(e.HttpContext, api.app.Dao(), e.Records); err != nil { - api.app.Logger().Debug("Failed to enrich list records", slog.String("error", err.Error())) - } - - return e.HttpContext.JSON(http.StatusOK, e.Result) + return e.JSON(http.StatusOK, e.Result) }) } -func (api *recordApi) view(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", "Missing collection context.") +func recordView(e *core.RequestEvent) error { + collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection")) + if err != nil || collection == nil { + return e.NotFoundError("Missing collection context.", err) } - recordId := c.PathParam("id") + err = checkCollectionRateLimit(e, collection, "view") + if err != nil { + return err + } + + recordId := e.Request.PathValue("id") if recordId == "" { - return NewNotFoundError("", nil) + return e.NotFoundError("", nil) } - requestInfo := RequestInfo(c) + requestInfo, err := e.RequestInfo() + if err != nil { + return firstApiError(err, e.BadRequestError("", err)) + } - if requestInfo.Admin == nil && collection.ViewRule == nil { - // only admins can access if the rule is nil - return NewForbiddenError("Only admins can perform this action.", nil) + if collection.ViewRule == nil && !requestInfo.HasSuperuserAuth() { + return e.ForbiddenError("Only superusers can perform this action.", nil) } ruleFunc := func(q *dbx.SelectQuery) error { - if requestInfo.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" { - resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true) + if !requestInfo.HasSuperuserAuth() && collection.ViewRule != nil && *collection.ViewRule != "" { + resolver := core.NewRecordFieldResolver(e.App, collection, requestInfo, true) expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver) if err != nil { return err @@ -126,290 +128,472 @@ func (api *recordApi) view(c echo.Context) error { return nil } - record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc) + record, fetchErr := e.App.FindRecordById(collection, recordId, ruleFunc) if fetchErr != nil || record == nil { - return NewNotFoundError("", fetchErr) + return firstApiError(err, e.NotFoundError("", fetchErr)) } - event := new(core.RecordViewEvent) - event.HttpContext = c + event := new(core.RecordRequestEvent) + event.RequestEvent = e event.Collection = collection event.Record = record - return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error { - if e.HttpContext.Response().Committed { - return nil + return e.App.OnRecordViewRequest().Trigger(event, func(e *core.RecordRequestEvent) error { + if err := EnrichRecord(e.RequestEvent, e.Record); err != nil { + return firstApiError(err, e.InternalServerError("Failed to enrich record", err)) } - if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil { - api.app.Logger().Debug( - "Failed to enrich view record", - slog.String("id", e.Record.Id), - slog.String("collectionName", e.Record.Collection().Name), - slog.String("error", err.Error()), - ) - } - - return e.HttpContext.JSON(http.StatusOK, e.Record) + return e.JSON(http.StatusOK, e.Record) }) } -func (api *recordApi) create(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", "Missing collection context.") - } +func recordCreate(optFinalizer func() 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 { + return e.NotFoundError("Missing collection context.", err) + } - requestInfo := RequestInfo(c) + if collection.IsView() { + return e.BadRequestError("Unsupported collection type.", nil) + } - if requestInfo.Admin == nil && collection.CreateRule == nil { - // only admins can access if the rule is nil - return NewForbiddenError("Only admins can perform this action.", nil) - } + err = checkCollectionRateLimit(e, collection, "create") + if err != nil { + return err + } - hasFullManageAccess := requestInfo.Admin != nil + requestInfo, err := e.RequestInfo() + if err != nil { + return firstApiError(err, e.BadRequestError("", err)) + } - // temporary save the record and check it against the create rule - if requestInfo.Admin == nil && collection.CreateRule != nil { - testRecord := models.NewRecord(collection) + hasSuperuserAuth := requestInfo.HasSuperuserAuth() + canSkipRuleCheck := hasSuperuserAuth + + // special case for the first superuser creation + // --- + if !canSkipRuleCheck && collection.Name == core.CollectionNameSuperusers { + total, totalErr := e.App.CountRecords(core.CollectionNameSuperusers) + canSkipRuleCheck = totalErr == nil && total == 0 + } + // --- + + if !canSkipRuleCheck && collection.CreateRule == nil { + return e.ForbiddenError("Only superusers can perform this action.", nil) + } + + record := core.NewRecord(collection) + + data, err := recordDataFromRequest(e, record) + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to read the submitted data.", err)) + } // replace modifiers fields so that the resolved value is always - // available when accessing requestInfo.Data using just the field name - if requestInfo.HasModifierDataKeys() { - requestInfo.Data = testRecord.ReplaceModifers(requestInfo.Data) - } + // available when accessing requestInfo.Body + requestInfo.Body = data - testForm := forms.NewRecordUpsert(api.app, testRecord) - testForm.SetFullManageAccess(true) - if err := testForm.LoadRequest(c.Request(), ""); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) + form := forms.NewRecordUpsert(e.App, record) + if hasSuperuserAuth { + form.GrantSuperuserAccess() } + form.Load(data) - // force unset the verified state to prevent ManageRule misuse - if !hasFullManageAccess { - testForm.Verified = false - } + var isOptFinalizerCalled bool - createRuleFunc := func(q *dbx.SelectQuery) error { - if *collection.CreateRule == "" { - return nil // no create rule to resolve + event := new(core.RecordRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + hookErr := e.App.OnRecordCreateRequest().Trigger(event, func(e *core.RecordRequestEvent) error { + form.SetApp(e.App) + form.SetRecord(e.Record) + + // temporary save the record and check it against the create and manage rules + if !canSkipRuleCheck && e.Collection.CreateRule != nil { + // temporary grant manager access level + form.GrantManagerAccess() + + // manually unset the verified field to prevent manage API rule misuse in case the rule relies on it + initialVerified := e.Record.Verified() + if initialVerified { + e.Record.SetVerified(false) + } + + createRuleFunc := func(q *dbx.SelectQuery) error { + if *e.Collection.CreateRule == "" { + return nil // no create rule to resolve + } + + resolver := core.NewRecordFieldResolver(e.App, e.Collection, requestInfo, true) + expr, err := search.FilterData(*e.Collection.CreateRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + + return nil + } + + testErr := form.DrySubmit(func(txApp core.App, drySavedRecord *core.Record) error { + foundRecord, err := txApp.FindRecordById(drySavedRecord.Collection(), drySavedRecord.Id, createRuleFunc) + if err != nil { + return fmt.Errorf("DrySubmit create rule failure: %w", err) + } + + // reset the form access level in case it satisfies the Manage API rule + if !hasAuthManageAccess(txApp, requestInfo, foundRecord) { + form.ResetAccess() + } + + return nil + }) + if testErr != nil { + return e.BadRequestError("Failed to create record.", testErr) + } + + // restore initial verified state (it will be further validated on submit) + if initialVerified != e.Record.Verified() { + e.Record.SetVerified(initialVerified) + } } - resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true) - expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver) + err := form.Submit() + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to create record.", err)) + } + + err = EnrichRecord(e.RequestEvent, e.Record) + if err != nil { + return firstApiError(err, e.InternalServerError("Failed to enrich record", err)) + } + + err = e.JSON(http.StatusOK, e.Record) if err != nil { return err } - resolver.UpdateQuery(q) - q.AndWhere(expr) - return nil - } - testErr := testForm.DrySubmit(func(txDao *daos.Dao) error { - foundRecord, err := txDao.FindRecordById(collection.Id, testRecord.Id, createRuleFunc) - if err != nil { - return fmt.Errorf("DrySubmit create rule failure: %w", err) + if optFinalizer != nil { + isOptFinalizerCalled = true + err = optFinalizer() + if err != nil { + return firstApiError(err, e.InternalServerError("", err)) + } } - hasFullManageAccess = hasAuthManageAccess(txDao, foundRecord, requestInfo) + return nil }) - - if testErr != nil { - return NewBadRequestError("Failed to create record.", testErr) + if hookErr != nil { + return hookErr } - } - record := models.NewRecord(collection) - form := forms.NewRecordUpsert(api.app, record) - form.SetFullManageAccess(hasFullManageAccess) - - // load request - if err := form.LoadRequest(c.Request(), ""); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) - } - - event := new(core.RecordCreateEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - event.UploadedFiles = form.FilesToUpload() - - // create the record - return form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(m *models.Record) error { - event.Record = m - - return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to create record.", err) - } - - if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil { - api.app.Logger().Debug( - "Failed to enrich create record", - slog.String("id", e.Record.Id), - slog.String("collectionName", e.Record.Collection().Name), - slog.String("error", err.Error()), - ) - } - - return api.app.OnRecordAfterCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Record) - }) - }) + // 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 { + return firstApiError(err, e.InternalServerError("", err)) + } } - }) + + return nil + } } -func (api *recordApi) update(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", "Missing collection context.") +func recordUpdate(optFinalizer func() 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 { + return e.NotFoundError("Missing collection context.", err) + } + + if collection.IsView() { + return e.BadRequestError("Unsupported collection type.", nil) + } + + err = checkCollectionRateLimit(e, collection, "update") + if err != nil { + return err + } + + recordId := e.Request.PathValue("id") + if recordId == "" { + return e.NotFoundError("", nil) + } + + requestInfo, err := e.RequestInfo() + if err != nil { + return firstApiError(err, e.BadRequestError("", err)) + } + + hasSuperuserAuth := requestInfo.HasSuperuserAuth() + + if !hasSuperuserAuth && collection.UpdateRule == nil { + return firstApiError(err, e.ForbiddenError("Only superusers can perform this action.", nil)) + } + + // eager fetch the record so that the modifiers field values can be resolved + record, err := e.App.FindRecordById(collection, recordId) + if err != nil { + return firstApiError(err, e.NotFoundError("", err)) + } + + data, err := recordDataFromRequest(e, record) + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to read the submitted data.", err)) + } + + // replace modifiers fields so that the resolved value is always + // available when accessing requestInfo.Body + requestInfo.Body = data + + ruleFunc := func(q *dbx.SelectQuery) error { + if !hasSuperuserAuth && collection.UpdateRule != nil && *collection.UpdateRule != "" { + resolver := core.NewRecordFieldResolver(e.App, collection, requestInfo, true) + expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + return nil + } + + // refetch with access checks + record, err = e.App.FindRecordById(collection, recordId, ruleFunc) + if err != nil { + return firstApiError(err, e.NotFoundError("", err)) + } + + form := forms.NewRecordUpsert(e.App, record) + if hasSuperuserAuth { + form.GrantSuperuserAccess() + } + form.Load(data) + + var isOptFinalizerCalled bool + + event := new(core.RecordRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + hookErr := e.App.OnRecordUpdateRequest().Trigger(event, func(e *core.RecordRequestEvent) error { + form.SetApp(e.App) + form.SetRecord(e.Record) + if !form.HasManageAccess() && hasAuthManageAccess(e.App, requestInfo, e.Record) { + form.GrantManagerAccess() + } + + err := form.Submit() + if err != nil { + return firstApiError(err, e.BadRequestError("Failed to update record.", err)) + } + + err = EnrichRecord(e.RequestEvent, e.Record) + if err != nil { + return firstApiError(err, e.InternalServerError("Failed to enrich record", err)) + } + + err = e.JSON(http.StatusOK, e.Record) + if err != nil { + return err + } + + if optFinalizer != nil { + isOptFinalizerCalled = true + err = optFinalizer() + if err != nil { + return firstApiError(err, e.InternalServerError("", fmt.Errorf("update optFinalizer error: %w", err))) + } + } + + return nil + }) + if hookErr != nil { + return hookErr + } + + // 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 { + return firstApiError(err, e.InternalServerError("", fmt.Errorf("update optFinalizer error: %w", err))) + } + } + + return nil } +} - recordId := c.PathParam("id") - if recordId == "" { - return NewNotFoundError("", nil) - } +func recordDelete(optFinalizer func() 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 { + return e.NotFoundError("Missing collection context.", err) + } - requestInfo := RequestInfo(c) + if collection.IsView() { + return e.BadRequestError("Unsupported collection type.", nil) + } - if requestInfo.Admin == nil && collection.UpdateRule == nil { - // only admins can access if the rule is nil - return NewForbiddenError("Only admins can perform this action.", nil) - } + err = checkCollectionRateLimit(e, collection, "delete") + if err != nil { + return err + } - // eager fetch the record so that the modifier field values are replaced - // and available when accessing requestInfo.Data using just the field name - if requestInfo.HasModifierDataKeys() { - record, err := api.app.Dao().FindRecordById(collection.Id, recordId) + recordId := e.Request.PathValue("id") + if recordId == "" { + return e.NotFoundError("", nil) + } + + requestInfo, err := e.RequestInfo() + if err != nil { + return firstApiError(err, e.BadRequestError("", err)) + } + + if !requestInfo.HasSuperuserAuth() && collection.DeleteRule == nil { + return e.ForbiddenError("Only superusers can perform this action.", nil) + } + + ruleFunc := func(q *dbx.SelectQuery) error { + if !requestInfo.HasSuperuserAuth() && collection.DeleteRule != nil && *collection.DeleteRule != "" { + resolver := core.NewRecordFieldResolver(e.App, collection, requestInfo, true) + expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + return nil + } + + record, err := e.App.FindRecordById(collection, recordId, ruleFunc) if err != nil || record == nil { - return NewNotFoundError("", err) + return e.NotFoundError("", err) } - requestInfo.Data = record.ReplaceModifers(requestInfo.Data) - } - ruleFunc := func(q *dbx.SelectQuery) error { - if requestInfo.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" { - resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true) - expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver) + var isOptFinalizerCalled bool + + event := new(core.RecordRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + + hookErr := e.App.OnRecordDeleteRequest().Trigger(event, func(e *core.RecordRequestEvent) error { + if err := e.App.Delete(e.Record); err != nil { + return firstApiError(err, e.BadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err)) + } + + err = e.NoContent(http.StatusNoContent) if err != nil { return err } - resolver.UpdateQuery(q) - q.AndWhere(expr) - } - return nil - } - // fetch record - record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc) - if fetchErr != nil || record == nil { - return NewNotFoundError("", fetchErr) - } - - form := forms.NewRecordUpsert(api.app, record) - form.SetFullManageAccess(requestInfo.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestInfo)) - - // load request - if err := form.LoadRequest(c.Request(), ""); err != nil { - return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) - } - - event := new(core.RecordUpdateEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - event.UploadedFiles = form.FilesToUpload() - - // update the record - return form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(m *models.Record) error { - event.Record = m - - return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error { - if err := next(e.Record); err != nil { - return NewBadRequestError("Failed to update record.", err) + if optFinalizer != nil { + isOptFinalizerCalled = true + err = optFinalizer() + if err != nil { + return firstApiError(err, e.InternalServerError("", fmt.Errorf("delete optFinalizer error: %w", err))) } - - if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil { - api.app.Logger().Debug( - "Failed to enrich update record", - slog.String("id", e.Record.Id), - slog.String("collectionName", e.Record.Collection().Name), - slog.String("error", err.Error()), - ) - } - - return api.app.OnRecordAfterUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.Record) - }) - }) - } - }) -} - -func (api *recordApi) delete(c echo.Context) error { - collection, _ := c.Get(ContextCollectionKey).(*models.Collection) - if collection == nil { - return NewNotFoundError("", "Missing collection context.") - } - - recordId := c.PathParam("id") - if recordId == "" { - return NewNotFoundError("", nil) - } - - requestInfo := RequestInfo(c) - - if requestInfo.Admin == nil && collection.DeleteRule == nil { - // only admins can access if the rule is nil - return NewForbiddenError("Only admins can perform this action.", nil) - } - - ruleFunc := func(q *dbx.SelectQuery) error { - if requestInfo.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" { - resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true) - expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver) - if err != nil { - return err - } - resolver.UpdateQuery(q) - q.AndWhere(expr) - } - return nil - } - - record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc) - if fetchErr != nil || record == nil { - return NewNotFoundError("", fetchErr) - } - - event := new(core.RecordDeleteEvent) - event.HttpContext = c - event.Collection = collection - event.Record = record - - return api.app.OnRecordBeforeDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error { - // delete the record - if err := api.app.Dao().DeleteRecord(e.Record); err != nil { - return NewBadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err) - } - - return api.app.OnRecordAfterDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error { - if e.HttpContext.Response().Committed { - return nil } - return e.HttpContext.NoContent(http.StatusNoContent) + return nil }) - }) + if hookErr != nil { + return hookErr + } + + // 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 { + return firstApiError(err, e.InternalServerError("", fmt.Errorf("delete optFinalizer error: %w", err))) + } + } + + return nil + } +} + +// ------------------------------------------------------------------- + +func recordDataFromRequest(e *core.RequestEvent, record *core.Record) (map[string]any, error) { + info, err := e.RequestInfo() + if err != nil { + return nil, err + } + + // resolve regular fields + result := record.ReplaceModifiers(info.Body) + + // resolve uploaded files + uploadedFiles, err := extractUploadedFiles(e.Request, record.Collection(), "") + if err != nil { + return nil, err + } + if len(uploadedFiles) > 0 { + for k, v := range uploadedFiles { + result[k] = v + } + result = record.ReplaceModifiers(result) + } + + isAuth := record.Collection().IsAuth() + + // unset hidden fields for non-superusers + if !info.HasSuperuserAuth() { + for _, f := range record.Collection().Fields { + if f.GetHidden() { + // exception for the auth collection "password" field + if isAuth && f.GetName() == core.FieldNamePassword { + continue + } + + delete(result, f.GetName()) + } + } + } + + return result, nil +} + +func extractUploadedFiles(request *http.Request, collection *core.Collection, prefix string) (map[string][]*filesystem.File, error) { + contentType := request.Header.Get("content-type") + if !strings.HasPrefix(contentType, "multipart/form-data") { + return nil, nil // not multipart/form-data request + } + + result := map[string][]*filesystem.File{} + + for _, field := range collection.Fields { + if field.Type() != core.FieldTypeFile { + continue + } + + baseKey := field.GetName() + + keys := []string{ + baseKey, + // prepend and append modifiers + "+" + baseKey, + baseKey + "+", + } + + for _, k := range keys { + if prefix != "" { + k = prefix + "." + k + } + files, err := FindUploadedFiles(request, k) + if err != nil && !errors.Is(err, http.ErrMissingFile) { + return nil, err + } + if len(files) > 0 { + result[k] = files + } + } + } + + return result, nil } diff --git a/apis/record_crud_auth_origin_test.go b/apis/record_crud_auth_origin_test.go new file mode 100644 index 00000000..3866ddd3 --- /dev/null +++ b/apis/record_crud_auth_origin_test.go @@ -0,0 +1,314 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordCrudAuthOriginList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + { + Name: "regular auth with authOrigins", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"totalPages":1`, + `"id":"9r2j0m74260ur8i"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "regular auth without authOrigins", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudAuthOriginView(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 200, + ExpectedContent: []string{`"id":"9r2j0m74260ur8i"`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudAuthOriginDelete(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudAuthOriginCreate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "recordRef": "4q1xlclmfloku33", + "collectionRef": "_pb_users_auth_", + "fingerprint": "abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"fingerprint":"abc"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudAuthOriginUpdate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "fingerprint":"abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameAuthOrigins + "/records/9r2j0m74260ur8i", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"id":"9r2j0m74260ur8i"`, + `"fingerprint":"abc"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnRecordEnrich": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud_external_auth_test.go b/apis/record_crud_external_auth_test.go new file mode 100644 index 00000000..21564c4b --- /dev/null +++ b/apis/record_crud_external_auth_test.go @@ -0,0 +1,316 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordCrudExternalAuthList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + { + Name: "regular auth with externalAuths", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"totalPages":1`, + `"id":"f1z5b3843pzc964"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "regular auth without externalAuths", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + Headers: map[string]string{ + // users, test2@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudExternalAuthView(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{`"id":"dlmflokuq1xl342"`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudExternalAuthDelete(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudExternalAuthCreate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "recordRef": "4q1xlclmfloku33", + "collectionRef": "_pb_users_auth_", + "provider": "github", + "providerId": "abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"recordRef":"4q1xlclmfloku33"`, + `"providerId":"abc"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudExternalAuthUpdate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "providerId": "abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameExternalAuths + "/records/dlmflokuq1xl342", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"id":"dlmflokuq1xl342"`, + `"providerId":"abc"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnRecordEnrich": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud_mfa_test.go b/apis/record_crud_mfa_test.go new file mode 100644 index 00000000..7b77ca4b --- /dev/null +++ b/apis/record_crud_mfa_test.go @@ -0,0 +1,388 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordCrudMFAList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + { + Name: "regular auth with mfas", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"totalPages":1`, + `"id":"user1_0"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "regular auth without mfas", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudMFAView(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{`"id":"user1_0"`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudMFADelete(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + 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, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudMFACreate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "recordRef": "4q1xlclmfloku33", + "collectionRef": "_pb_users_auth_", + "method": "abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedContent: []string{ + `"recordRef":"4q1xlclmfloku33"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudMFAUpdate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "method":"abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedContent: []string{ + `"id":"user1_0"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnRecordEnrich": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud_otp_test.go b/apis/record_crud_otp_test.go new file mode 100644 index 00000000..f1e4f829 --- /dev/null +++ b/apis/record_crud_otp_test.go @@ -0,0 +1,388 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordCrudOTPList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + { + Name: "regular auth with otps", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"totalPages":1`, + `"id":"user1_0"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "regular auth without otps", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"totalPages":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudOTPView(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{`"id":"user1_0"`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudOTPDelete(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + 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, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudOTPCreate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "recordRef": "4q1xlclmfloku33", + "collectionRef": "_pb_users_auth_", + "password": "abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedContent: []string{ + `"recordRef":"4q1xlclmfloku33"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudOTPUpdate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "password":"abc" + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "owner regular auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0", + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + }, + ExpectedContent: []string{ + `"id":"user1_0"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnRecordEnrich": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud_superuser_test.go b/apis/record_crud_superuser_test.go new file mode 100644 index 00000000..17445cdd --- /dev/null +++ b/apis/record_crud_superuser_test.go @@ -0,0 +1,371 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordCrudSuperuserList(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-superusers auth", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalPages":1`, + `"totalItems":4`, + `"items":[{`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 4, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudSuperuserView(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-superusers auth", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodGet, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"sywbhecnh46rhm0"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudSuperuserDelete(t *testing.T) { + t.Parallel() + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sbmbsdb40jyxf7h", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-superusers auth", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sbmbsdb40jyxf7h", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sbmbsdb40jyxf7h", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 4, // + 3 AuthOrigins + "OnModelDeleteExecute": 4, + "OnModelAfterDeleteSuccess": 4, + "OnRecordDelete": 4, + "OnRecordDeleteExecute": 4, + "OnRecordAfterDeleteSuccess": 4, + }, + }, + { + Name: "delete the last superuser", + Method: http.MethodDelete, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // delete all other superusers + superusers, err := app.FindAllRecords(core.CollectionNameSuperusers, dbx.Not(dbx.HashExp{"id": "sywbhecnh46rhm0"})) + if err != nil { + t.Fatal(err) + } + for _, superuser := range superusers { + if err = app.Delete(superuser); err != nil { + t.Fatal(err) + } + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelAfterDeleteError": 1, + "OnRecordDelete": 1, + "OnRecordAfterDeleteError": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudSuperuserCreate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "email": "test_new@example.com", + "password": "1234567890", + "passwordConfirm": "1234567890", + "verified": false + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "guest creating first superuser", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Body: body(), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + // delete all superusers + _, err := app.DB().NewQuery("DELETE FROM {{" + core.CollectionNameSuperusers + "}}").Execute() + if err != nil { + t.Fatal(err) + } + }, + ExpectedContent: []string{ + `"collectionName":"_superusers"`, + `"verified":true`, + }, + NotExpectedContent: []string{ + // because the action has no auth the email field shouldn't be returned if emailVisibility is not set + `"email"`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + { + Name: "superusers auth", + Method: http.MethodPost, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"collectionName":"_superusers"`, + `"email":"test_new@example.com"`, + `"verified":true`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCrudSuperuserUpdate(t *testing.T) { + t.Parallel() + + body := func() *strings.Reader { + return strings.NewReader(`{ + "email": "test_new@example.com", + "verified": true + }`) + } + + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "non-superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Headers: map[string]string{ + // users, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + }, + Body: body(), + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "superusers auth", + Method: http.MethodPatch, + URL: "/api/collections/" + core.CollectionNameSuperusers + "/records/sywbhecnh46rhm0", + Headers: map[string]string{ + // _superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + Body: body(), + ExpectedContent: []string{ + `"collectionName":"_superusers"`, + `"id":"sywbhecnh46rhm0"`, + `"email":"test_new@example.com"`, + `"verified":true`, + }, + ExpectedStatus: 200, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnRecordEnrich": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordValidate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/record_crud_test.go b/apis/record_crud_test.go index e6088775..6392211b 100644 --- a/apis/record_crud_test.go +++ b/apis/record_crud_test.go @@ -1,20 +1,21 @@ package apis_test import ( + "bytes" "errors" "net/http" "net/url" "os" "path/filepath" + "strconv" "strings" "testing" "time" - "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/types" ) @@ -25,52 +26,58 @@ func TestRecordCrudList(t *testing.T) { { Name: "missing collection", Method: http.MethodGet, - Url: "/api/collections/missing/records", + URL: "/api/collections/missing/records", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "unauthenticated trying to access nil rule collection (aka. need admin auth)", + Name: "unauthenticated trying to access nil rule collection (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records", + URL: "/api/collections/demo1/records", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authenticated record trying to access nil rule collection (aka. need admin auth)", + Name: "authenticated record trying to access nil rule collection (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/collections/demo1/records", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "public collection but with admin only filter param (aka. @collection, @request, etc.)", + Name: "public collection but with superuser only filter param (aka. @collection, @request, etc.)", Method: http.MethodGet, - Url: "/api/collections/demo2/records?filter=%40collection.demo2.title='test1'", + URL: "/api/collections/demo2/records?filter=%40collection.demo2.title='test1'", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "public collection but with admin only sort param (aka. @collection, @request, etc.)", + Name: "public collection but with superuser only sort param (aka. @collection, @request, etc.)", Method: http.MethodGet, - Url: "/api/collections/demo2/records?sort=@request.auth.title", + URL: "/api/collections/demo2/records?sort=@request.auth.title", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "public collection but with ENCODED admin only filter/sort (aka. @collection)", + Name: "public collection but with ENCODED superuser only filter/sort (aka. @collection)", Method: http.MethodGet, - Url: "/api/collections/demo2/records?filter=%40collection.demo2.title%3D%27test1%27", + URL: "/api/collections/demo2/records?filter=%40collection.demo2.title%3D%27test1%27", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "public collection", Method: http.MethodGet, - Url: "/api/collections/demo2/records", + URL: "/api/collections/demo2/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -82,12 +89,16 @@ func TestRecordCrudList(t *testing.T) { `"id":"achvryl401bhse3"`, `"id":"llvuca81nly1qls"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, { Name: "public collection (using the collection id)", Method: http.MethodGet, - Url: "/api/collections/sz5l5z67tg7gku0/records", + URL: "/api/collections/sz5l5z67tg7gku0/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -99,14 +110,18 @@ func TestRecordCrudList(t *testing.T) { `"id":"achvryl401bhse3"`, `"id":"llvuca81nly1qls"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, { - Name: "authorized as admin trying to access nil rule collection (aka. need admin auth)", + Name: "authorized as superuser trying to access nil rule collection (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -119,14 +134,18 @@ func TestRecordCrudList(t *testing.T) { `"id":"84nmscqy84lsi1t"`, `"id":"imy661ixudk5izi"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, { Name: "valid query params", Method: http.MethodGet, - Url: "/api/collections/demo1/records?filter=text~'test'&sort=-bool", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records?filter=text~'test'&sort=-bool", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -137,24 +156,29 @@ func TestRecordCrudList(t *testing.T) { `"id":"al1h9ijdeojtsjy"`, `"id":"84nmscqy84lsi1t"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 2, + }, }, { Name: "invalid filter", Method: http.MethodGet, - Url: "/api/collections/demo1/records?filter=invalid~'test'", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records?filter=invalid~'test'", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "expand relations", Method: http.MethodGet, - Url: "/api/collections/demo1/records?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -178,20 +202,24 @@ func TestRecordCrudList(t *testing.T) { // subrel items `"id":"0yxhwia2amd8gec"`, `"id":"llvuca81nly1qls"`, - // email visibility should be ignored for admins even in expanded rels + // email visibility should be ignored for superusers even in expanded rels `"email":"test@example.com"`, `"email":"test2@example.com"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 8, + }, }, { Name: "authenticated record model that DOESN'T match the collection list rule", Method: http.MethodGet, - Url: "/api/collections/demo3/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo3/records", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -200,15 +228,18 @@ func TestRecordCrudList(t *testing.T) { `"totalItems":0`, `"items":[]`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, }, { Name: "authenticated record that matches the collection list rule", Method: http.MethodGet, - Url: "/api/collections/demo3/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo3/records", + Headers: map[string]string{ // clients, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -222,12 +253,16 @@ func TestRecordCrudList(t *testing.T) { `"id":"7nwo8tuiatetxdm"`, `"id":"mk5fmymtx4wsprk"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 4, + }, }, { Name: ":rule modifer", Method: http.MethodGet, - Url: "/api/collections/demo5/records", + URL: "/api/collections/demo5/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -237,12 +272,16 @@ func TestRecordCrudList(t *testing.T) { `"items":[{`, `"id":"qjeql998mtp1azp"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "multi-match - at least one of", Method: http.MethodGet, - Url: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"), + URL: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"), ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -252,12 +291,16 @@ func TestRecordCrudList(t *testing.T) { `"items":[{`, `"id":"qzaqccwrmva4o1n"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "multi-match - all", Method: http.MethodGet, - Url: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"), + URL: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"), ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -266,7 +309,10 @@ func TestRecordCrudList(t *testing.T) { `"totalItems":0`, `"items":[]`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, }, // auth collection @@ -274,7 +320,7 @@ func TestRecordCrudList(t *testing.T) { { Name: "check email visibility as guest", Method: http.MethodGet, - Url: "/api/collections/nologin/records", + URL: "/api/collections/nologin/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -291,19 +337,23 @@ func TestRecordCrudList(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, `"email":"test@example.com"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, { Name: "check email visibility as any authenticated record", Method: http.MethodGet, - Url: "/api/collections/nologin/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records", + Headers: map[string]string{ // clients, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -320,20 +370,24 @@ func TestRecordCrudList(t *testing.T) { `"emailVisibility":false`, }, NotExpectedContent: []string{ - `"tokenKey"`, - `"passwordHash"`, + `"tokenKey":"`, + `"password":""`, `"email":"test@example.com"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, { Name: "check email visibility as manage auth record", Method: http.MethodGet, - Url: "/api/collections/nologin/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -353,16 +407,20 @@ func TestRecordCrudList(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, }, { - Name: "check email visibility as admin", + Name: "check email visibility as superuser", Method: http.MethodGet, - Url: "/api/collections/nologin/records", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/nologin/records", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -382,17 +440,21 @@ func TestRecordCrudList(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, }, { Name: "check self email visibility resolver", Method: http.MethodGet, - Url: "/api/collections/nologin/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records", + Headers: map[string]string{ // nologin, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyMjA4OTg1MjYxfQ.DOYSon3x1-C0hJbwjEU6dp2-6oLeEa8bOlkyP1CinyM", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.fdUPFLDx5b6RM_XFqnqsyiyNieyKA2HIIkRmUh9kIoY", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -411,10 +473,14 @@ func TestRecordCrudList(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 3, + }, }, // view collection @@ -422,7 +488,7 @@ func TestRecordCrudList(t *testing.T) { { Name: "public view records", Method: http.MethodGet, - Url: "/api/collections/view2/records?filter=state=false", + URL: "/api/collections/view2/records?filter=state=false", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -437,12 +503,16 @@ func TestRecordCrudList(t *testing.T) { `"created"`, `"updated"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 2, + }, }, { Name: "guest that doesn't match the view collection list rule", Method: http.MethodGet, - Url: "/api/collections/view1/records", + URL: "/api/collections/view1/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -451,15 +521,18 @@ func TestRecordCrudList(t *testing.T) { `"totalItems":0`, `"items":[]`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + }, }, { Name: "authenticated record that matches the view collection list rule", Method: http.MethodGet, - Url: "/api/collections/view1/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/view1/records", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -471,12 +544,16 @@ func TestRecordCrudList(t *testing.T) { `"id":"84nmscqy84lsi1t"`, `"bool":true`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "view collection with numeric ids", Method: http.MethodGet, - Url: "/api/collections/numeric_id_view/records", + URL: "/api/collections/numeric_id_view/records", ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, @@ -487,7 +564,45 @@ func TestRecordCrudList(t *testing.T) { `"id":"1"`, `"id":"2"`, }, - ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordsListRequest": 1, + "OnRecordEnrich": 2, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - view2:list", + Method: http.MethodGet, + URL: "/api/collections/view2/records", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:list"}, + {MaxRequests: 0, Label: "view2:list"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:list", + Method: http.MethodGet, + URL: "/api/collections/view2/records", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:list"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -503,89 +618,106 @@ func TestRecordCrudView(t *testing.T) { { Name: "missing collection", Method: http.MethodGet, - Url: "/api/collections/missing/records/0yxhwia2amd8gec", + URL: "/api/collections/missing/records/0yxhwia2amd8gec", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "missing record", Method: http.MethodGet, - Url: "/api/collections/demo2/records/missing", + URL: "/api/collections/demo2/records/missing", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "unauthenticated trying to access nil rule collection (aka. need admin auth)", + Name: "unauthenticated trying to access nil rule collection (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records/imy661ixudk5izi", + URL: "/api/collections/demo1/records/imy661ixudk5izi", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authenticated record trying to access nil rule collection (aka. need admin auth)", + Name: "authenticated record trying to access nil rule collection (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records/imy661ixudk5izi", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo1/records/imy661ixudk5izi", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "authenticated record that doesn't match the collection view rule", Method: http.MethodGet, - Url: "/api/collections/users/records/bgs820n361vj1qd", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records/bgs820n361vj1qd", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "public collection view", Method: http.MethodGet, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", ExpectedStatus: 200, ExpectedContent: []string{ `"id":"0yxhwia2amd8gec"`, `"collectionName":"demo2"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "public collection view (using the collection id)", Method: http.MethodGet, - Url: "/api/collections/sz5l5z67tg7gku0/records/0yxhwia2amd8gec", + URL: "/api/collections/sz5l5z67tg7gku0/records/0yxhwia2amd8gec", ExpectedStatus: 200, ExpectedContent: []string{ `"id":"0yxhwia2amd8gec"`, `"collectionName":"demo2"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { - Name: "authorized as admin trying to access nil rule collection view (aka. need admin auth)", + Name: "authorized as superuser trying to access nil rule collection view (aka. need superuser auth)", Method: http.MethodGet, - Url: "/api/collections/demo1/records/imy661ixudk5izi", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records/imy661ixudk5izi", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"imy661ixudk5izi"`, `"collectionName":"demo1"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "authenticated record that does match the collection view rule", Method: http.MethodGet, - Url: "/api/collections/users/records/4q1xlclmfloku33", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records/4q1xlclmfloku33", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -595,14 +727,18 @@ func TestRecordCrudView(t *testing.T) { `"emailVisibility":false`, `"email":"test@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "expand relations", Method: http.MethodGet, - Url: "/api/collections/demo1/records/al1h9ijdeojtsjy?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo1/records/al1h9ijdeojtsjy?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -616,7 +752,11 @@ func TestRecordCrudView(t *testing.T) { `"id":"0yxhwia2amd8gec"`, `"collectionName":"demo2"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 7, + }, }, // auth collection @@ -624,7 +764,7 @@ func TestRecordCrudView(t *testing.T) { { Name: "check email visibility as guest", Method: http.MethodGet, - Url: "/api/collections/nologin/records/oos036e9xvqeexy", + URL: "/api/collections/nologin/records/oos036e9xvqeexy", ExpectedStatus: 200, ExpectedContent: []string{ `"id":"oos036e9xvqeexy"`, @@ -633,18 +773,22 @@ func TestRecordCrudView(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "check email visibility as any authenticated record", Method: http.MethodGet, - Url: "/api/collections/nologin/records/oos036e9xvqeexy", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records/oos036e9xvqeexy", + Headers: map[string]string{ // clients, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -654,18 +798,22 @@ func TestRecordCrudView(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, `"email":"test3@example.com"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "check email visibility as manage auth record", Method: http.MethodGet, - Url: "/api/collections/nologin/records/oos036e9xvqeexy", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records/oos036e9xvqeexy", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -674,14 +822,18 @@ func TestRecordCrudView(t *testing.T) { `"email":"test3@example.com"`, `"verified":true`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { - Name: "check email visibility as admin", + Name: "check email visibility as superuser", Method: http.MethodGet, - Url: "/api/collections/nologin/records/oos036e9xvqeexy", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/nologin/records/oos036e9xvqeexy", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -692,17 +844,21 @@ func TestRecordCrudView(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, }, { Name: "check self email visibility resolver", Method: http.MethodGet, - Url: "/api/collections/nologin/records/dc49k6jgejn40h3", - RequestHeaders: map[string]string{ + URL: "/api/collections/nologin/records/dc49k6jgejn40h3", + Headers: map[string]string{ // nologin, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyMjA4OTg1MjYxfQ.DOYSon3x1-C0hJbwjEU6dp2-6oLeEa8bOlkyP1CinyM", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.fdUPFLDx5b6RM_XFqnqsyiyNieyKA2HIIkRmUh9kIoY", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -713,9 +869,13 @@ func TestRecordCrudView(t *testing.T) { }, NotExpectedContent: []string{ `"tokenKey"`, - `"passwordHash"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, }, // view collection @@ -723,7 +883,7 @@ func TestRecordCrudView(t *testing.T) { { Name: "public view record", Method: http.MethodGet, - Url: "/api/collections/view2/records/84nmscqy84lsi1t", + URL: "/api/collections/view2/records/84nmscqy84lsi1t", ExpectedStatus: 200, ExpectedContent: []string{ `"id":"84nmscqy84lsi1t"`, @@ -735,22 +895,27 @@ func TestRecordCrudView(t *testing.T) { `"created"`, `"updated"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "guest that doesn't match the view collection view rule", Method: http.MethodGet, - Url: "/api/collections/view1/records/84nmscqy84lsi1t", + URL: "/api/collections/view1/records/84nmscqy84lsi1t", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "authenticated record that matches the view collection view rule", Method: http.MethodGet, - Url: "/api/collections/view1/records/84nmscqy84lsi1t", - RequestHeaders: map[string]string{ + URL: "/api/collections/view1/records/84nmscqy84lsi1t", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -758,17 +923,59 @@ func TestRecordCrudView(t *testing.T) { `"bool":true`, `"text":"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, }, { Name: "view record with numeric id", Method: http.MethodGet, - Url: "/api/collections/numeric_id_view/records/1", + URL: "/api/collections/numeric_id_view/records/1", ExpectedStatus: 200, ExpectedContent: []string{ `"id":"1"`, }, - ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordViewRequest": 1, + "OnRecordEnrich": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - numeric_id_view:view", + Method: http.MethodGet, + URL: "/api/collections/numeric_id_view/records/1", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:view"}, + {MaxRequests: 0, Label: "numeric_id_view:view"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:view", + Method: http.MethodGet, + URL: "/api/collections/numeric_id_view/records/1", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:view"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -785,7 +992,7 @@ func TestRecordCrudDelete(t *testing.T) { entries, _ := os.ReadDir(storageDir) if len(entries) != 0 { - t.Errorf("Expected empty/deleted dir, found %d", len(entries)) + t.Errorf("Expected empty/deleted dir, found: %d\n%v", len(entries), entries) } } @@ -793,139 +1000,163 @@ func TestRecordCrudDelete(t *testing.T) { { Name: "missing collection", Method: http.MethodDelete, - Url: "/api/collections/missing/records/0yxhwia2amd8gec", + URL: "/api/collections/missing/records/0yxhwia2amd8gec", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "missing record", Method: http.MethodDelete, - Url: "/api/collections/demo2/records/missing", + URL: "/api/collections/demo2/records/missing", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "unauthenticated trying to delete nil rule collection (aka. need admin auth)", + Name: "unauthenticated trying to delete nil rule collection (aka. need superuser auth)", Method: http.MethodDelete, - Url: "/api/collections/demo1/records/imy661ixudk5izi", + URL: "/api/collections/demo1/records/imy661ixudk5izi", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authenticated record trying to delete nil rule collection (aka. need admin auth)", + Name: "authenticated record trying to delete nil rule collection (aka. need superuser auth)", Method: http.MethodDelete, - Url: "/api/collections/demo1/records/imy661ixudk5izi", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo1/records/imy661ixudk5izi", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "authenticated record that doesn't match the collection delete rule", Method: http.MethodDelete, - Url: "/api/collections/users/records/bgs820n361vj1qd", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records/bgs820n361vj1qd", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "trying to delete a view collection record", Method: http.MethodDelete, - Url: "/api/collections/view1/records/imy661ixudk5izi", + URL: "/api/collections/view1/records/imy661ixudk5izi", ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "public collection record delete", Method: http.MethodDelete, - Url: "/api/collections/nologin/records/dc49k6jgejn40h3", + URL: "/api/collections/nologin/records/dc49k6jgejn40h3", ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, }, }, { Name: "public collection record delete (using the collection id as identifier)", Method: http.MethodDelete, - Url: "/api/collections/kpv709sk2lqbqk8/records/dc49k6jgejn40h3", + URL: "/api/collections/kpv709sk2lqbqk8/records/dc49k6jgejn40h3", ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, }, }, { - Name: "authorized as admin trying to delete nil rule collection view (aka. need admin auth)", + Name: "authorized as superuser trying to delete nil rule collection view (aka. need superuser auth)", Method: http.MethodDelete, - Url: "/api/collections/clients/records/o1y0dd0spd786md", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/clients/records/o1y0dd0spd786md", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, }, }, { - Name: "OnRecordAfterDeleteRequest error response", + Name: "OnRecordAfterDeleteSuccessRequest error response", Method: http.MethodDelete, - Url: "/api/collections/clients/records/o1y0dd0spd786md", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/clients/records/o1y0dd0spd786md", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterDeleteRequest().Add(func(e *core.RecordDeleteEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordDeleteRequest().BindFunc(func(e *core.RecordRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, }, }, { Name: "authenticated record that match the collection delete rule", Method: http.MethodDelete, - Url: "/api/collections/users/records/4q1xlclmfloku33", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records/4q1xlclmfloku33", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, Delay: 100 * time.Millisecond, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 3, // +2 because of the external auths - "OnModelBeforeDelete": 3, // +2 because of the external auths - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 3, // +2 for the externalAuths + "OnModelDeleteExecute": 3, + "OnModelAfterDeleteSuccess": 3, + "OnRecordDelete": 3, + "OnRecordDeleteExecute": 3, + "OnRecordAfterDeleteSuccess": 3, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnRecordUpdateExecute": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { ensureDeletedFiles(app, "_pb_users_auth_", "4q1xlclmfloku33") // check if all the external auths records were deleted - collection, _ := app.Dao().FindCollectionByNameOrId("users") - record := models.NewRecord(collection) - record.Id = "4q1xlclmfloku33" - externalAuths, err := app.Dao().FindAllExternalAuthsByRecord(record) + collection, _ := app.FindCollectionByNameOrId("users") + record := core.NewRecord(collection) + record.Set("id", "4q1xlclmfloku33") + externalAuths, err := app.FindAllExternalAuthsByRecord(record) if err != nil { t.Errorf("Failed to fetch external auths: %v", err) } @@ -937,20 +1168,25 @@ func TestRecordCrudDelete(t *testing.T) { { Name: "@request :isset (rule failure check)", Method: http.MethodDelete, - Url: "/api/collections/demo5/records/la4y2w4o98acwuj", + URL: "/api/collections/demo5/records/la4y2w4o98acwuj", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "@request :isset (rule pass check)", Method: http.MethodDelete, - Url: "/api/collections/demo5/records/la4y2w4o98acwuj?test=1", + URL: "/api/collections/demo5/records/la4y2w4o98acwuj?test=1", ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelAfterDelete": 1, - "OnModelBeforeDelete": 1, - "OnRecordAfterDeleteRequest": 1, - "OnRecordBeforeDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, }, }, @@ -959,55 +1195,82 @@ func TestRecordCrudDelete(t *testing.T) { { Name: "trying to delete a record while being part of a non-cascade required relation", Method: http.MethodDelete, - Url: "/api/collections/demo3/records/7nwo8tuiatetxdm", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo3/records/7nwo8tuiatetxdm", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnRecordBeforeDeleteRequest": 1, - "OnModelBeforeUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5 - "OnModelBeforeDelete": 2, // the record itself + rel_one_cascade of test1 record + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 2, // the record itself + rel_one_cascade of test1 record + "OnModelDeleteExecute": 2, + "OnModelAfterDeleteError": 2, + "OnRecordDelete": 2, + "OnRecordDeleteExecute": 2, + "OnRecordAfterDeleteError": 2, + "OnModelUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5 + "OnModelUpdateExecute": 2, + "OnModelAfterUpdateError": 2, + "OnRecordUpdate": 2, + "OnRecordUpdateExecute": 2, + "OnRecordAfterUpdateError": 2, }, }, { Name: "delete a record with non-cascade references", Method: http.MethodDelete, - Url: "/api/collections/demo3/records/1tmknxy2868d869", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/demo3/records/1tmknxy2868d869", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 1, - "OnModelAfterDelete": 1, - "OnModelBeforeUpdate": 2, - "OnModelAfterUpdate": 2, - "OnRecordBeforeDeleteRequest": 1, - "OnRecordAfterDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 1, + "OnModelDeleteExecute": 1, + "OnModelAfterDeleteSuccess": 1, + "OnRecordDelete": 1, + "OnRecordDeleteExecute": 1, + "OnRecordAfterDeleteSuccess": 1, + "OnModelUpdate": 2, + "OnModelUpdateExecute": 2, + "OnModelAfterUpdateSuccess": 2, + "OnRecordUpdate": 2, + "OnRecordUpdateExecute": 2, + "OnRecordAfterUpdateSuccess": 2, }, }, { Name: "delete a record with cascade references", Method: http.MethodDelete, - Url: "/api/collections/users/records/oap640cot4yru2s", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/collections/users/records/oap640cot4yru2s", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, Delay: 100 * time.Millisecond, ExpectedStatus: 204, ExpectedEvents: map[string]int{ - "OnModelBeforeDelete": 2, - "OnModelAfterDelete": 2, - "OnModelBeforeUpdate": 2, - "OnModelAfterUpdate": 2, - "OnRecordBeforeDeleteRequest": 1, - "OnRecordAfterDeleteRequest": 1, + "*": 0, + "OnRecordDeleteRequest": 1, + "OnModelDelete": 2, + "OnModelDeleteExecute": 2, + "OnModelAfterDeleteSuccess": 2, + "OnRecordDelete": 2, + "OnRecordDeleteExecute": 2, + "OnRecordAfterDeleteSuccess": 2, + "OnModelUpdate": 2, + "OnModelUpdateExecute": 2, + "OnModelAfterUpdateSuccess": 2, + "OnRecordUpdate": 2, + "OnRecordUpdateExecute": 2, + "OnRecordAfterUpdateSuccess": 2, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { recId := "84nmscqy84lsi1t" - rec, _ := app.Dao().FindRecordById("demo1", recId, nil) + rec, _ := app.FindRecordById("demo1", recId, nil) if rec != nil { t.Errorf("Expected record %s to be cascade deleted", recId) } @@ -1015,6 +1278,40 @@ func TestRecordCrudDelete(t *testing.T) { ensureDeletedFiles(app, "_pb_users_auth_", "oap640cot4yru2s") }, }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - demo5:delete", + Method: http.MethodDelete, + URL: "/api/collections/demo5/records/la4y2w4o98acwuj?test=1", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:delete"}, + {MaxRequests: 0, Label: "demo5:delete"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:delete", + Method: http.MethodDelete, + URL: "/api/collections/demo5/records/la4y2w4o98acwuj?test=1", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:delete"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, } for _, scenario := range scenarios { @@ -1033,14 +1330,14 @@ func TestRecordCrudCreate(t *testing.T) { } formData2, mp2, err2 := tests.MockMultipartData(map[string]string{ - rest.MultipartJsonKey: `{"title": "title_test2", "testPayload": 123}`, + router.JSONPayloadKey: `{"title": "title_test2", "testPayload": 123}`, }, "files") if err2 != nil { t.Fatal(err2) } formData3, mp3, err3 := tests.MockMultipartData(map[string]string{ - rest.MultipartJsonKey: `{"title": "title_test3", "testPayload": 123}`, + router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123}`, }, "files") if err3 != nil { t.Fatal(err3) @@ -1050,69 +1347,90 @@ func TestRecordCrudCreate(t *testing.T) { { Name: "missing collection", Method: http.MethodPost, - Url: "/api/collections/missing/records", + URL: "/api/collections/missing/records", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "guest trying to access nil-rule collection", Method: http.MethodPost, - Url: "/api/collections/demo1/records", + URL: "/api/collections/demo1/records", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "auth record trying to access nil-rule collection", Method: http.MethodPost, - Url: "/api/collections/demo1/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo1/records", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "trying to create a new view collection record", Method: http.MethodPost, - Url: "/api/collections/view1/records", + URL: "/api/collections/view1/records", Body: strings.NewReader(`{"text":"new"}`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "submit nil body", + Name: "submit invalid body", Method: http.MethodPost, - Url: "/api/collections/demo2/records", - Body: nil, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "submit invalid format", - Method: http.MethodPost, - Url: "/api/collections/demo2/records", + URL: "/api/collections/demo2/records", Body: strings.NewReader(`{"`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "submit nil body", + Method: http.MethodPost, + URL: "/api/collections/demo2/records", + Body: nil, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"title":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelValidate": 1, + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordValidate": 1, + "OnRecordAfterCreateError": 1, + }, }, { Name: "submit empty json body", Method: http.MethodPost, - Url: "/api/collections/nologin/records", + URL: "/api/collections/nologin/records", Body: strings.NewReader(`{}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"email":{"code":"validation_required"`, `"password":{"code":"validation_required"`, `"passwordConfirm":{"code":"validation_required"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + }, }, { Name: "guest submit in public collection", Method: http.MethodPost, - Url: "/api/collections/demo2/records", + URL: "/api/collections/demo2/records", Body: strings.NewReader(`{"title":"new"}`), ExpectedStatus: 200, ExpectedContent: []string{ @@ -1121,36 +1439,51 @@ func TestRecordCrudCreate(t *testing.T) { `"active":false`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "guest trying to submit in restricted collection", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{"title":"test123"}`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + }, }, { Name: "auth record submit in restricted collection (rule failure check)", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{"title":"test123"}`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + }, }, { Name: "auth record submit in restricted collection (rule pass check) + expand relations", Method: http.MethodPost, - Url: "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", + URL: "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", Body: strings.NewReader(`{ "title":"test123", "rel_one_no_cascade":"mk5fmymtx4wsprk", @@ -1160,14 +1493,15 @@ func TestRecordCrudCreate(t *testing.T) { "rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"], "rel_many_cascade":"lcl9d87w22ml6jy" }`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":`, `"title":"test123"`, + `"expand":{}`, // empty expand even because of the query param `"rel_one_no_cascade":"mk5fmymtx4wsprk"`, `"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`, `"rel_one_cascade":"mk5fmymtx4wsprk"`, @@ -1177,23 +1511,29 @@ func TestRecordCrudCreate(t *testing.T) { }, NotExpectedContent: []string{ // the users auth records don't have access to view the demo3 expands - `"expand":{`, `"missing"`, `"id":"mk5fmymtx4wsprk"`, `"id":"7nwo8tuiatetxdm"`, `"id":"lcl9d87w22ml6jy"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { - Name: "admin submit in restricted collection (rule skip check) + expand relations", + Name: "superuser submit in restricted collection (rule skip check) + expand relations", Method: http.MethodPost, - Url: "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", + URL: "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", Body: strings.NewReader(`{ "title":"test123", "rel_one_no_cascade":"mk5fmymtx4wsprk", @@ -1203,8 +1543,8 @@ func TestRecordCrudCreate(t *testing.T) { "rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"], "rel_many_cascade":"lcl9d87w22ml6jy" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1225,20 +1565,27 @@ func TestRecordCrudCreate(t *testing.T) { `"missing"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 4, }, }, { - Name: "submit via multipart form data", + Name: "superuser submit via multipart form data", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: formData, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp.FormDataContentType(), - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1247,52 +1594,61 @@ func TestRecordCrudCreate(t *testing.T) { `"files":["`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { - Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.data rule", + Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.body rule", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: formData2, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp2.FormDataContentType(), }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - collection, err := app.Dao().FindCollectionByNameOrId("demo3") + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatalf("failed to find demo3 collection: %v", err) } - collection.CreateRule = types.Pointer("@request.data.testPayload != 123") - if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { + collection.CreateRule = types.Pointer("@request.body.testPayload != 123") + if err := app.Save(collection); err != nil { t.Fatalf("failed to update demo3 collection create rule: %v", err) } - core.ReloadCachedCollections(app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + }, }, { - Name: "submit via multipart form data with @jsonPayload key and satisfied @request.data rule", + Name: "submit via multipart form data with @jsonPayload key and satisfied @request.body rule", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: formData3, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp3.FormDataContentType(), }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - collection, err := app.Dao().FindCollectionByNameOrId("demo3") + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatalf("failed to find demo3 collection: %v", err) } - collection.CreateRule = types.Pointer("@request.data.testPayload = 123") - if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { + collection.CreateRule = types.Pointer("@request.body.testPayload = 123") + if err := app.Save(collection); err != nil { t.Fatalf("failed to update demo3 collection create rule: %v", err) } - core.ReloadCachedCollections(app) }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1301,16 +1657,23 @@ func TestRecordCrudCreate(t *testing.T) { `"files":["`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "unique field error check", Method: http.MethodPost, - Url: "/api/collections/demo2/records", + URL: "/api/collections/demo2/records", Body: strings.NewReader(`{ "title":"test2" }`), @@ -1320,24 +1683,29 @@ func TestRecordCrudCreate(t *testing.T) { `"title":{`, `"code":"validation_not_unique"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + // validate events are not fired because the unique check will fail during dry submit + // "OnModelValidate": 1, + // "OnRecordValidate": 1, + }, }, { - Name: "OnRecordAfterCreateRequest error response", + Name: "OnRecordAfterCreateSuccessRequest error response", Method: http.MethodPost, - Url: "/api/collections/demo2/records", + URL: "/api/collections/demo2/records", Body: strings.NewReader(`{"title":"new"}`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordCreateRequest().BindFunc(func(e *core.RecordRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, }, }, @@ -1346,45 +1714,65 @@ func TestRecordCrudCreate(t *testing.T) { { Name: "invalid custom insertion id (less than 15 chars)", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{ "id": "12345678901234", "title": "test" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ - `"id":{"code":"validation_length_invalid"`, + `"id":{"code":"validation_min_text_constraint"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelValidate": 1, + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordValidate": 1, + "OnRecordAfterCreateError": 1, }, }, { Name: "invalid custom insertion id (more than 15 chars)", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{ "id": "1234567890123456", "title": "test" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ - `"id":{"code":"validation_length_invalid"`, + `"id":{"code":"validation_max_text_constraint"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelValidate": 1, + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordValidate": 1, + "OnRecordAfterCreateError": 1, }, }, { Name: "valid custom insertion id (exactly 15 chars)", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{ "id": "123456789012345", "title": "test" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1392,22 +1780,29 @@ func TestRecordCrudCreate(t *testing.T) { `"title":"test"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "valid custom insertion id existing in another non-auth collection", Method: http.MethodPost, - Url: "/api/collections/demo3/records", + URL: "/api/collections/demo3/records", Body: strings.NewReader(`{ "id": "0yxhwia2amd8gec", "title": "test" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1415,73 +1810,76 @@ func TestRecordCrudCreate(t *testing.T) { `"title":"test"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, - "OnRecordAfterCreateRequest": 1, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "valid custom insertion auth id duplicating in another auth collection", Method: http.MethodPost, - Url: "/api/collections/users/records", + URL: "/api/collections/users/records", Body: strings.NewReader(`{ "id":"o1y0dd0spd786md", "title":"test", "password":"1234567890", "passwordConfirm":"1234567890" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"id":{"code":"validation_invalid_auth_id"`, }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnRecordBeforeCreateRequest": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, // unique constraints are handled on db level + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateError": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, }, }, - // fields modifier checks + // check whether if @request.body modifer fields are properly resolved // ----------------------------------------------------------- { - Name: "trying to delete a record while being part of a non-cascade required relation", - Method: http.MethodDelete, - Url: "/api/collections/demo3/records/7nwo8tuiatetxdm", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnRecordBeforeDeleteRequest": 1, - "OnModelBeforeUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5 - "OnModelBeforeDelete": 2, // the record itself + rel_one_cascade of test1 record - }, - }, - - // check whether if @request.data modifer fields are properly resolved - // ----------------------------------------------------------- - { - Name: "@request.data.field with compute modifers (rule failure check)", + Name: "@request.body.field with compute modifers (rule failure check)", Method: http.MethodPost, - Url: "/api/collections/demo5/records", + URL: "/api/collections/demo5/records", Body: strings.NewReader(`{ - "total":1, "total+":4, - "total-":1 + "total-":2 }`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{}`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + }, }, { - Name: "@request.data.field with compute modifers (rule pass check)", + Name: "@request.body.field with compute modifers (rule pass check)", Method: http.MethodPost, - Url: "/api/collections/demo5/records", + URL: "/api/collections/demo5/records", Body: strings.NewReader(`{ - "total":1, - "total+":3, + "total+":4, "total-":1 }`), ExpectedStatus: 200, @@ -1491,65 +1889,83 @@ func TestRecordCrudCreate(t *testing.T) { `"total":3`, }, ExpectedEvents: map[string]int{ - "OnModelAfterCreate": 1, - "OnModelBeforeCreate": 1, - "OnRecordAfterCreateRequest": 1, - "OnRecordBeforeCreateRequest": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, // auth records // ----------------------------------------------------------- { - Name: "auth record with invalid data", + Name: "auth record with invalid form data", Method: http.MethodPost, - Url: "/api/collections/users/records", + URL: "/api/collections/users/records", Body: strings.NewReader(`{ - "id":"o1y0pd786mq", - "username":"Users75657", - "email":"invalid", "password":"1234567", - "passwordConfirm":"1234560" + "passwordConfirm":"1234560", + "email":"invalid", + "username":"Users75657" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"id":{"code":"validation_length_invalid"`, - `"username":{"code":"validation_invalid_username"`, // for duplicated case-insensitive username - `"email":{"code":"validation_is_email"`, - `"password":{"code":"validation_length_out_of_range"`, `"passwordConfirm":{"code":"validation_values_mismatch"`, }, NotExpectedContent: []string{ - // schema fields are not checked if the base fields has errors - `"rel":{"code":`, + // record fields are not checked if the base auth form fields have errors + `"rel":`, + `"email":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, }, }, { - Name: "auth record with valid base fields but invalid schema data", + Name: "auth record with valid form data but invalid record fields", Method: http.MethodPost, - Url: "/api/collections/users/records", + URL: "/api/collections/users/records", Body: strings.NewReader(`{ - "password":"12345678", - "passwordConfirm":"12345678", + "password":"1234567", + "passwordConfirm":"1234567", "rel":"invalid" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"rel":{"code":`, + `"password":{"code":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelValidate": 1, + "OnModelAfterCreateError": 1, + "OnRecordCreate": 1, + "OnRecordValidate": 1, + "OnRecordAfterCreateError": 1, }, }, { Name: "auth record with valid data and explicitly verified state by guest", Method: http.MethodPost, - Url: "/api/collections/users/records", + URL: "/api/collections/users/records", Body: strings.NewReader(`{ "password":"12345678", "passwordConfirm":"12345678", @@ -1560,14 +1976,19 @@ func TestRecordCrudCreate(t *testing.T) { `"data":{`, `"verified":{"code":`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + // no validation hooks because it should fail before save by the form auth fields validator + }, }, { Name: "auth record with valid data and explicitly verified state by random user", Method: http.MethodPost, - Url: "/api/collections/users/records", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, Body: strings.NewReader(`{ "password":"12345678", @@ -1583,11 +2004,16 @@ func TestRecordCrudCreate(t *testing.T) { NotExpectedContent: []string{ `"emailVisibility":{"code":`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + // no validation hooks because it should fail before save by the form auth fields validator + }, }, { - Name: "auth record with valid data by admin", + Name: "auth record with valid data by superuser", Method: http.MethodPost, - Url: "/api/collections/users/records", + URL: "/api/collections/users/records", Body: strings.NewReader(`{ "id":"o1o1y0pd78686mq", "username":"test.valid", @@ -1598,8 +2024,8 @@ func TestRecordCrudCreate(t *testing.T) { "emailVisibility":true, "verified":true }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1614,19 +2040,25 @@ func TestRecordCrudCreate(t *testing.T) { `"tokenKey"`, `"password"`, `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterCreate": 1, - "OnModelBeforeCreate": 1, - "OnRecordAfterCreateRequest": 1, - "OnRecordBeforeCreateRequest": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "auth record with valid data by auth record with manage access", Method: http.MethodPost, - Url: "/api/collections/nologin/records", + URL: "/api/collections/nologin/records", Body: strings.NewReader(`{ "email":"new@example.com", "password":"12345678", @@ -1635,9 +2067,9 @@ func TestRecordCrudCreate(t *testing.T) { "emailVisibility":true, "verified":true }`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1652,15 +2084,242 @@ func TestRecordCrudCreate(t *testing.T) { `"tokenKey"`, `"password"`, `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterCreate": 1, - "OnModelBeforeCreate": 1, - "OnRecordAfterCreateRequest": 1, - "OnRecordBeforeCreateRequest": 1, + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, + + // ensure that hidden fields cannot be set by non-superusers + // ----------------------------------------------------------- + { + Name: "create with hidden field as regular user", + Method: http.MethodPost, + URL: "/api/collections/demo3/records", + Body: strings.NewReader(`{ + "id": "abcde1234567890", + "title": "test_create" + }`), + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + col, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + // mock hidden field + col.Fields.GetByName("title").SetHidden(true) + + if err = app.Save(col); err != nil { + t.Fatal(err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, err := app.FindRecordById("demo3", "abcde1234567890") + if err != nil { + t.Fatal(err) + } + + // ensure that the title wasn't saved + if v := record.GetString("title"); v != "" { + t.Fatalf("Expected empty title, got %q", v) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"abcde1234567890"`, + }, + NotExpectedContent: []string{ + `"title"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "create with hidden field as superuser", + Method: http.MethodPost, + URL: "/api/collections/demo3/records", + Body: strings.NewReader(`{ + "id": "abcde1234567890", + "title": "test_create" + }`), + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + col, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + // mock hidden field + col.Fields.GetByName("title").SetHidden(true) + + if err = app.Save(col); err != nil { + t.Fatal(err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, err := app.FindRecordById("demo3", "abcde1234567890") + if err != nil { + t.Fatal(err) + } + + // ensure that the title was saved + if v := record.GetString("title"); v != "test_create" { + t.Fatalf("Expected title %q, got %q", "test_create", v) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"abcde1234567890"`, + `"title":"test_create"`, + }, + NotExpectedContent: []string{}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordCreateRequest": 1, + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnRecordCreate": 1, + "OnRecordCreateExecute": 1, + "OnRecordAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - demo2:create", + Method: http.MethodPost, + URL: "/api/collections/demo2/records", + Body: strings.NewReader(`{"title":"new"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:create"}, + {MaxRequests: 0, Label: "demo2:create"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:create", + Method: http.MethodPost, + URL: "/api/collections/demo2/records", + Body: strings.NewReader(`{"title":"new"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:create"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + + // dynamic body limit checks + // ----------------------------------------------------------- + { + Name: "body > collection BodyLimit", + Method: http.MethodPost, + URL: "/api/collections/demo1/records", + // the exact body doesn't matter as long as it returns 413 + Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2+1)), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // adjust field sizes for the test + // --- + fileOneField := collection.Fields.GetByName("file_one").(*core.FileField) + fileOneField.MaxSize = 5 + + fileManyField := collection.Fields.GetByName("file_many").(*core.FileField) + fileManyField.MaxSize = 10 + fileManyField.MaxSelect = 2 + + jsonField := collection.Fields.GetByName("json").(*core.JSONField) + jsonField.MaxSize = 2 + + err = app.Save(collection) + if err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 413, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "body <= collection BodyLimit", + Method: http.MethodPost, + URL: "/api/collections/demo1/records", + // the exact body doesn't matter as long as it doesn't return 413 + Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2)), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // adjust field sizes for the test + // --- + fileOneField := collection.Fields.GetByName("file_one").(*core.FileField) + fileOneField.MaxSize = 5 + + fileManyField := collection.Fields.GetByName("file_many").(*core.FileField) + fileManyField.MaxSize = 10 + fileManyField.MaxSelect = 2 + + jsonField := collection.Fields.GetByName("json").(*core.JSONField) + jsonField.MaxSize = 2 + + err = app.Save(collection) + if err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, } for _, scenario := range scenarios { @@ -1679,14 +2338,14 @@ func TestRecordCrudUpdate(t *testing.T) { } formData2, mp2, err2 := tests.MockMultipartData(map[string]string{ - rest.MultipartJsonKey: `{"title": "title_test2", "testPayload": 123}`, + router.JSONPayloadKey: `{"title": "title_test2", "testPayload": 123}`, }, "files") if err2 != nil { t.Fatal(err2) } formData3, mp3, err3 := tests.MockMultipartData(map[string]string{ - rest.MultipartJsonKey: `{"title": "title_test3", "testPayload": 123}`, + router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123}`, }, "files") if err3 != nil { t.Fatal(err3) @@ -1696,56 +2355,77 @@ func TestRecordCrudUpdate(t *testing.T) { { Name: "missing collection", Method: http.MethodPatch, - Url: "/api/collections/missing/records/0yxhwia2amd8gec", + URL: "/api/collections/missing/records/0yxhwia2amd8gec", ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "guest trying to access nil-rule collection record", Method: http.MethodPatch, - Url: "/api/collections/demo1/records/imy661ixudk5izi", + URL: "/api/collections/demo1/records/imy661ixudk5izi", ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "auth record trying to access nil-rule collection", Method: http.MethodPatch, - Url: "/api/collections/demo1/records/imy661ixudk5izi", - RequestHeaders: map[string]string{ + URL: "/api/collections/demo1/records/imy661ixudk5izi", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, - }, - { - Name: "submit invalid body", - Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", - Body: strings.NewReader(`{"`), - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "trying to update a view collection record", Method: http.MethodPatch, - Url: "/api/collections/view1/records/imy661ixudk5izi", + URL: "/api/collections/view1/records/imy661ixudk5izi", Body: strings.NewReader(`{"text":"new"}`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "submit nil body", + Name: "submit invalid body", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", - Body: nil, + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", + Body: strings.NewReader(`{"`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "submit nil body (aka. no fields change)", + Method: http.MethodPatch, + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", + Body: nil, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"collectionName":"demo2"`, + `"id":"0yxhwia2amd8gec"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, }, { Name: "submit empty body (aka. no fields change)", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", Body: strings.NewReader(`{}`), ExpectedStatus: 200, ExpectedContent: []string{ @@ -1753,27 +2433,44 @@ func TestRecordCrudUpdate(t *testing.T) { `"id":"0yxhwia2amd8gec"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "trigger field validation", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", Body: strings.NewReader(`{"title":"a"}`), ExpectedStatus: 400, ExpectedContent: []string{ `data":{`, `"title":{"code":"validation_min_text_constraint"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelValidate": 1, + "OnModelAfterUpdateError": 1, + "OnRecordUpdate": 1, + "OnRecordValidate": 1, + "OnRecordAfterUpdateError": 1, + }, }, { Name: "guest submit in public collection", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", Body: strings.NewReader(`{"title":"new"}`), ExpectedStatus: 200, ExpectedContent: []string{ @@ -1782,36 +2479,45 @@ func TestRecordCrudUpdate(t *testing.T) { `"active":true`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { Name: "guest trying to submit in restricted collection", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: strings.NewReader(`{"title":"new"}`), ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "auth record submit in restricted collection (rule failure check)", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: strings.NewReader(`{"title":"new"}`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { Name: "auth record submit in restricted collection (rule pass check) + expand relations", Method: http.MethodPatch, - Url: "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", + URL: "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", Body: strings.NewReader(`{ "title":"test123", "rel_one_no_cascade":"mk5fmymtx4wsprk", @@ -1821,14 +2527,15 @@ func TestRecordCrudUpdate(t *testing.T) { "rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"], "rel_many_cascade":"lcl9d87w22ml6jy" }`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"i9naidtvr6qsgb4"`, `"title":"test123"`, + `"expand":{}`, // empty expand even because of the query param `"rel_one_no_cascade":"mk5fmymtx4wsprk"`, `"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`, `"rel_one_cascade":"mk5fmymtx4wsprk"`, @@ -1838,23 +2545,29 @@ func TestRecordCrudUpdate(t *testing.T) { }, NotExpectedContent: []string{ // the users auth records don't have access to view the demo3 expands - `"expand":{`, `"missing"`, `"id":"mk5fmymtx4wsprk"`, `"id":"7nwo8tuiatetxdm"`, `"id":"lcl9d87w22ml6jy"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { - Name: "admin submit in restricted collection (rule skip check) + expand relations", + Name: "superuser submit in restricted collection (rule skip check) + expand relations", Method: http.MethodPatch, - Url: "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", + URL: "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required", Body: strings.NewReader(`{ "title":"test123", "rel_one_no_cascade":"mk5fmymtx4wsprk", @@ -1864,8 +2577,8 @@ func TestRecordCrudUpdate(t *testing.T) { "rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"], "rel_many_cascade":"lcl9d87w22ml6jy" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1886,20 +2599,27 @@ func TestRecordCrudUpdate(t *testing.T) { `"missing"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 4, }, }, { - Name: "submit via multipart form data", + Name: "superuser submit via multipart form data", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: formData, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp.FormDataContentType(), - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1908,52 +2628,58 @@ func TestRecordCrudUpdate(t *testing.T) { `"files":["`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { - Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.data rule", + Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.body rule", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: formData2, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp2.FormDataContentType(), }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - collection, err := app.Dao().FindCollectionByNameOrId("demo3") + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatalf("failed to find demo3 collection: %v", err) } - collection.UpdateRule = types.Pointer("@request.data.testPayload != 123") - if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { + collection.UpdateRule = types.Pointer("@request.body.testPayload != 123") + if err := app.Save(collection); err != nil { t.Fatalf("failed to update demo3 collection update rule: %v", err) } - core.ReloadCachedCollections(app) }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "submit via multipart form data with @jsonPayload key and satisfied @request.data rule", + Name: "submit via multipart form data with @jsonPayload key and satisfied @request.body rule", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: formData3, - RequestHeaders: map[string]string{ + Headers: map[string]string{ "Content-Type": mp3.FormDataContentType(), }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - collection, err := app.Dao().FindCollectionByNameOrId("demo3") + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo3") if err != nil { t.Fatalf("failed to find demo3 collection: %v", err) } - collection.UpdateRule = types.Pointer("@request.data.testPayload = 123") - if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { + collection.UpdateRule = types.Pointer("@request.body.testPayload = 123") + if err := app.Save(collection); err != nil { t.Fatalf("failed to update demo3 collection update rule: %v", err) } - core.ReloadCachedCollections(app) }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1962,51 +2688,66 @@ func TestRecordCrudUpdate(t *testing.T) { `"files":["`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, { - Name: "OnRecordAfterUpdateRequest error response", + Name: "OnRecordAfterUpdateSuccessRequest error response", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/0yxhwia2amd8gec", + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", Body: strings.NewReader(`{"title":"new"}`), - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnRecordAfterUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnRecordUpdateRequest().BindFunc(func(e *core.RecordRequestEvent) error { return errors.New("error") }) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnRecordAfterUpdateRequest": 1, - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, }, }, { Name: "try to change the id of an existing record", Method: http.MethodPatch, - Url: "/api/collections/demo3/records/mk5fmymtx4wsprk", + URL: "/api/collections/demo3/records/mk5fmymtx4wsprk", Body: strings.NewReader(`{ "id": "mk5fmymtx4wspra" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"id":{"code":"validation_in_invalid"`, + `"id":{"code":"validation_pk_change"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelValidate": 1, + "OnModelAfterUpdateError": 1, + "OnRecordUpdate": 1, + "OnRecordValidate": 1, + "OnRecordAfterUpdateError": 1, }, }, { Name: "unique field error check", Method: http.MethodPatch, - Url: "/api/collections/demo2/records/llvuca81nly1qls", + URL: "/api/collections/demo2/records/llvuca81nly1qls", Body: strings.NewReader(`{ "title":"test2" }`), @@ -2017,17 +2758,25 @@ func TestRecordCrudUpdate(t *testing.T) { `"code":"validation_not_unique"`, }, ExpectedEvents: map[string]int{ - "OnRecordBeforeUpdateRequest": 1, - "OnModelBeforeUpdate": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateError": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateError": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, }, }, - // check whether if @request.data modifer fields are properly resolved + // check whether if @request.body modifer fields are properly resolved // ----------------------------------------------------------- { - Name: "@request.data.field with compute modifers (rule failure check)", + Name: "@request.body.field with compute modifers (rule failure check)", Method: http.MethodPatch, - Url: "/api/collections/demo5/records/la4y2w4o98acwuj", + URL: "/api/collections/demo5/records/la4y2w4o98acwuj", Body: strings.NewReader(`{ "total+":3, "total-":1 @@ -2036,11 +2785,12 @@ func TestRecordCrudUpdate(t *testing.T) { ExpectedContent: []string{ `"data":{}`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "@request.data.field with compute modifers (rule pass check)", + Name: "@request.body.field with compute modifers (rule pass check)", Method: http.MethodPatch, - Url: "/api/collections/demo5/records/la4y2w4o98acwuj", + URL: "/api/collections/demo5/records/la4y2w4o98acwuj", Body: strings.NewReader(`{ "total+":2, "total-":1 @@ -2052,66 +2802,84 @@ func TestRecordCrudUpdate(t *testing.T) { `"total":3`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, }, // auth records // ----------------------------------------------------------- { - Name: "auth record with invalid data", + Name: "auth record with invalid form data", Method: http.MethodPatch, - Url: "/api/collections/users/records/bgs820n361vj1qd", + URL: "/api/collections/users/records/bgs820n361vj1qd", Body: strings.NewReader(`{ - "username":"Users75657", - "email":"invalid", - "password":"1234567", + "password":"", "passwordConfirm":"1234560", + "email":"invalid", "verified":false }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, - `"username":{"code":"validation_invalid_username"`, // for duplicated case-insensitive username - `"email":{"code":"validation_is_email"`, - `"password":{"code":"validation_length_out_of_range"`, - `"passwordConfirm":{"code":"validation_values_mismatch"`, + `"passwordConfirm":{`, + `"password":{`, }, NotExpectedContent: []string{ - // admins are allowed to change the verified state - `"verified"`, - // schema fields are not checked if the base fields has errors - `"rel":{"code":`, + // record fields are not checked if the base auth form fields have errors + `"email":`, + "verified", // superusers are allowed to change the verified state + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, }, }, { - Name: "auth record with valid base fields but invalid schema data", + Name: "auth record with valid form data but invalid record fields", Method: http.MethodPatch, - Url: "/api/collections/users/records/bgs820n361vj1qd", + URL: "/api/collections/users/records/bgs820n361vj1qd", Body: strings.NewReader(`{ - "password":"12345678", - "passwordConfirm":"12345678", + "password":"1234567", + "passwordConfirm":"1234567", "rel":"invalid" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"rel":{"code":`, + `"password":{"code":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelValidate": 1, + "OnModelAfterUpdateError": 1, + "OnRecordUpdate": 1, + "OnRecordValidate": 1, + "OnRecordAfterUpdateError": 1, }, }, { Name: "try to change account managing fields by guest", Method: http.MethodPatch, - Url: "/api/collections/nologin/records/phhq3wr65cap535", + URL: "/api/collections/nologin/records/phhq3wr65cap535", Body: strings.NewReader(`{ "password":"12345678", "passwordConfirm":"12345678", @@ -2127,14 +2895,18 @@ func TestRecordCrudUpdate(t *testing.T) { NotExpectedContent: []string{ `"emailVisibility":{"code":`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + }, }, { Name: "try to change account managing fields by auth record (owner)", Method: http.MethodPatch, - Url: "/api/collections/users/records/4q1xlclmfloku33", - RequestHeaders: map[string]string{ + URL: "/api/collections/users/records/4q1xlclmfloku33", + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, Body: strings.NewReader(`{ "password":"12345678", @@ -2151,11 +2923,38 @@ func TestRecordCrudUpdate(t *testing.T) { NotExpectedContent: []string{ `"emailVisibility":{"code":`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + }, + }, + { + Name: "try to unset/downgrade email and verified fields (owner)", + Method: http.MethodPatch, + URL: "/api/collections/users/records/oap640cot4yru2s", + Headers: map[string]string{ + // users, test2@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY", + }, + Body: strings.NewReader(`{ + "email":"", + "verified":false + }`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{"code":`, + `"verified":{"code":`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + }, }, { Name: "try to change account managing fields by auth record with managing rights", Method: http.MethodPatch, - Url: "/api/collections/nologin/records/phhq3wr65cap535", + URL: "/api/collections/nologin/records/phhq3wr65cap535", Body: strings.NewReader(`{ "email":"new@example.com", "password":"12345678", @@ -2164,9 +2963,9 @@ func TestRecordCrudUpdate(t *testing.T) { "emailVisibility":true, "verified":true }`), - RequestHeaders: map[string]string{ + Headers: map[string]string{ // users, test@example.com - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -2179,25 +2978,31 @@ func TestRecordCrudUpdate(t *testing.T) { `"tokenKey"`, `"password"`, `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - record, _ := app.Dao().FindRecordById("nologin", "phhq3wr65cap535") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, _ := app.FindRecordById("nologin", "phhq3wr65cap535") if !record.ValidatePassword("12345678") { t.Fatal("Password update failed.") } }, }, { - Name: "update auth record with valid data by admin", + Name: "update auth record with valid data by superuser", Method: http.MethodPatch, - Url: "/api/collections/users/records/oap640cot4yru2s", + URL: "/api/collections/users/records/oap640cot4yru2s", Body: strings.NewReader(`{ "username":"test.valid", "email":"new@example.com", @@ -2207,8 +3012,8 @@ func TestRecordCrudUpdate(t *testing.T) { "emailVisibility":true, "verified":false }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -2221,31 +3026,53 @@ func TestRecordCrudUpdate(t *testing.T) { NotExpectedContent: []string{ `"tokenKey"`, `"password"`, - `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - record, _ := app.Dao().FindRecordById("users", "oap640cot4yru2s") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, _ := app.FindRecordById("users", "oap640cot4yru2s") if !record.ValidatePassword("12345678") { t.Fatal("Password update failed.") } }, }, { - Name: "update auth record with valid data by guest (empty update filter)", + Name: "update auth record with valid data by guest (empty update filter + auth origins check)", Method: http.MethodPatch, - Url: "/api/collections/nologin/records/dc49k6jgejn40h3", + URL: "/api/collections/nologin/records/dc49k6jgejn40h3", Body: strings.NewReader(`{ "username":"test_new", "emailVisibility":true, "name":"test" }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + nologin, err := app.FindCollectionByNameOrId("nologin") + if err != nil { + t.Fatal(err) + } + + // add dummy auth origins for the record + for i := 0; i < 3; i++ { + d := core.NewAuthOrigin(app) + d.SetCollectionRef(nologin.Id) + d.SetRecordRef("dc49k6jgejn40h3") + d.SetFingerprint("abc_" + strconv.Itoa(i)) + if err = app.Save(d); err != nil { + t.Fatalf("Failed to save dummy auth origin %d: %v", i, err) + } + } + }, ExpectedStatus: 200, ExpectedContent: []string{ `"username":"test_new"`, @@ -2258,24 +3085,59 @@ func TestRecordCrudUpdate(t *testing.T) { `"tokenKey"`, `"password"`, `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, _ := app.FindRecordById("nologin", "dc49k6jgejn40h3") + + // the dummy auth origins should NOT have been removed since we didn't change the password + devices, err := app.FindAllAuthOriginsByRecord(record) + if err != nil { + t.Fatalf("Failed to retrieve dummy auth origins: %v", err) + } + if len(devices) != 3 { + t.Fatalf("Expected %d auth origins, got %d", 3, len(devices)) + } }, }, { - Name: "success password change with oldPassword", + Name: "success password change with oldPassword (+authOrigins reset check)", Method: http.MethodPatch, - Url: "/api/collections/nologin/records/dc49k6jgejn40h3", + URL: "/api/collections/nologin/records/dc49k6jgejn40h3", Body: strings.NewReader(`{ "password":"123456789", "passwordConfirm":"123456789", "oldPassword":"1234567890" }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + nologin, err := app.FindCollectionByNameOrId("nologin") + if err != nil { + t.Fatal(err) + } + + // add dummy auth origins for the record + for i := 0; i < 3; i++ { + d := core.NewAuthOrigin(app) + d.SetCollectionRef(nologin.Id) + d.SetRecordRef("dc49k6jgejn40h3") + d.SetFingerprint("abc_" + strconv.Itoa(i)) + if err = app.Save(d); err != nil { + t.Fatalf("Failed to save dummy auth origin %d: %v", i, err) + } + } + }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"dc49k6jgejn40h3"`, @@ -2284,21 +3146,261 @@ func TestRecordCrudUpdate(t *testing.T) { `"tokenKey"`, `"password"`, `"passwordConfirm"`, - `"passwordHash"`, }, ExpectedEvents: map[string]int{ - "OnModelAfterUpdate": 1, - "OnModelBeforeUpdate": 1, - "OnRecordAfterUpdateRequest": 1, - "OnRecordBeforeUpdateRequest": 1, + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + // auth origins + "OnModelDelete": 3, + "OnModelDeleteExecute": 3, + "OnModelAfterDeleteSuccess": 3, + "OnRecordDelete": 3, + "OnRecordDeleteExecute": 3, + "OnRecordAfterDeleteSuccess": 3, }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - record, _ := app.Dao().FindRecordById("nologin", "dc49k6jgejn40h3") + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, _ := app.FindRecordById("nologin", "dc49k6jgejn40h3") if !record.ValidatePassword("123456789") { t.Fatal("Password update failed.") } + + // the dummy auth origins should have been removed + devices, err := app.FindAllAuthOriginsByRecord(record) + if err != nil { + t.Fatalf("Failed to retrieve dummy auth origins: %v", err) + } + if len(devices) > 0 { + t.Fatalf("Expected auth origins to be removed, got %d", len(devices)) + } }, }, + + // ensure that hidden fields cannot be set by non-superusers + // ----------------------------------------------------------- + { + Name: "update with hidden field as regular user", + Method: http.MethodPatch, + URL: "/api/collections/demo3/records/1tmknxy2868d869", + Body: strings.NewReader(`{ + "title": "test_update" + }`), + Headers: map[string]string{ + // clients, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + col, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + // mock hidden field + col.Fields.GetByName("title").SetHidden(true) + + if err = app.Save(col); err != nil { + t.Fatal(err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, err := app.FindRecordById("demo3", "1tmknxy2868d869") + if err != nil { + t.Fatal(err) + } + + // ensure that the title wasn't saved + if v := record.GetString("title"); v != "test1" { + t.Fatalf("Expected no title change, got %q", v) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"1tmknxy2868d869"`, + }, + NotExpectedContent: []string{ + `"title"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, + }, + { + Name: "update with hidden field as superuser", + Method: http.MethodPatch, + URL: "/api/collections/demo3/records/1tmknxy2868d869", + Body: strings.NewReader(`{ + "title": "test_update" + }`), + Headers: map[string]string{ + // superusers, test@example.com + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + col, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + // mock hidden field + col.Fields.GetByName("title").SetHidden(true) + + if err = app.Save(col); err != nil { + t.Fatal(err) + } + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + record, err := app.FindRecordById("demo3", "1tmknxy2868d869") + if err != nil { + t.Fatal(err) + } + + // ensure that the title has been updated + if v := record.GetString("title"); v != "test_update" { + t.Fatalf("Expected title %q, got %q", "test_update", v) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"1tmknxy2868d869"`, + `"title":"test_update"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnRecordUpdate": 1, + "OnRecordUpdateExecute": 1, + "OnRecordAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnRecordValidate": 1, + "OnRecordEnrich": 1, + }, + }, + + // rate limit checks + // ----------------------------------------------------------- + { + Name: "RateLimit rule - demo2:update", + Method: http.MethodPatch, + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", + Body: strings.NewReader(`{"title":"new"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 100, Label: "*:update"}, + {MaxRequests: 0, Label: "demo2:update"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "RateLimit rule - *:update", + Method: http.MethodPatch, + URL: "/api/collections/demo2/records/0yxhwia2amd8gec", + Body: strings.NewReader(`{"title":"new"}`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.Settings().RateLimits.Enabled = true + app.Settings().RateLimits.Rules = []core.RateLimitRule{ + {MaxRequests: 100, Label: "abc"}, + {MaxRequests: 0, Label: "*:update"}, + } + }, + ExpectedStatus: 429, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + + // dynamic body limit checks + // ----------------------------------------------------------- + { + Name: "body > collection BodyLimit", + Method: http.MethodPatch, + URL: "/api/collections/demo1/records/imy661ixudk5izi", + // the exact body doesn't matter as long as it returns 413 + Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2+1)), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // adjust field sizes for the test + // --- + fileOneField := collection.Fields.GetByName("file_one").(*core.FileField) + fileOneField.MaxSize = 5 + + fileManyField := collection.Fields.GetByName("file_many").(*core.FileField) + fileManyField.MaxSize = 10 + fileManyField.MaxSelect = 2 + + jsonField := collection.Fields.GetByName("json").(*core.JSONField) + jsonField.MaxSize = 2 + + err = app.Save(collection) + if err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 413, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, + { + Name: "body <= collection BodyLimit", + Method: http.MethodPatch, + URL: "/api/collections/demo1/records/imy661ixudk5izi", + // the exact body doesn't matter as long as it doesn't return 413 + Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2)), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // adjust field sizes for the test + // --- + fileOneField := collection.Fields.GetByName("file_one").(*core.FileField) + fileOneField.MaxSize = 5 + + fileManyField := collection.Fields.GetByName("file_many").(*core.FileField) + fileManyField.MaxSize = 10 + fileManyField.MaxSelect = 2 + + jsonField := collection.Fields.GetByName("json").(*core.JSONField) + jsonField.MaxSize = 2 + + err = app.Save(collection) + if err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, + }, } for _, scenario := range scenarios { diff --git a/apis/record_helpers.go b/apis/record_helpers.go index 719104f5..eb74a1c9 100644 --- a/apis/record_helpers.go +++ b/apis/record_helpers.go @@ -1,121 +1,111 @@ package apis import ( + "database/sql" + "errors" "fmt" - "log" - "log/slog" "net/http" "strings" - "github.com/labstack/echo/v5" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/resolvers" - "github.com/pocketbase/pocketbase/tokens" - "github.com/pocketbase/pocketbase/tools/inflector" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/security" ) -const ContextRequestInfoKey = "requestInfo" +const ( + expandQueryParam = "expand" + fieldsQueryParam = "fields" +) -const expandQueryParam = "expand" -const fieldsQueryParam = "fields" - -// Deprecated: Use RequestInfo instead. -func RequestData(c echo.Context) *models.RequestInfo { - log.Println("RequestData(c) is deprecated and will be removed in the future! You can replace it with RequestInfo(c).") - return RequestInfo(c) -} - -// RequestInfo exports cached common request data fields -// (query, body, logged auth state, etc.) from the provided context. -func RequestInfo(c echo.Context) *models.RequestInfo { - // return cached to avoid copying the body multiple times - if v := c.Get(ContextRequestInfoKey); v != nil { - if data, ok := v.(*models.RequestInfo); ok { - // refresh auth state - data.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record) - data.Admin, _ = c.Get(ContextAdminKey).(*models.Admin) - return data - } - } - - result := &models.RequestInfo{ - Context: models.RequestInfoContextDefault, - Method: c.Request().Method, - Query: map[string]any{}, - Data: map[string]any{}, - Headers: map[string]any{}, - } - - // extract the first value of all headers and normalizes the keys - // ("X-Token" is converted to "x_token") - for k, v := range c.Request().Header { - if len(v) > 0 { - result.Headers[inflector.Snakecase(k)] = v[0] - } - } - - result.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record) - result.Admin, _ = c.Get(ContextAdminKey).(*models.Admin) - echo.BindQueryParams(c, &result.Query) - rest.BindBody(c, &result.Data) - - c.Set(ContextRequestInfoKey, result) - - return result -} - -// RecordAuthResponse writes standardised json record auth response +// RecordAuthResponse writes standardized json record auth response // into the specified request context. -func RecordAuthResponse( - app core.App, - c echo.Context, - authRecord *models.Record, - meta any, - finalizers ...func(token string) error, -) error { - if !authRecord.Verified() && authRecord.Collection().AuthOptions().OnlyVerified { - return NewForbiddenError("Please verify your account first.", nil) - } - - token, tokenErr := tokens.NewRecordAuthToken(app, authRecord) +// +// The authMethod argument specify the name of the current authentication method (eg. password, oauth2, etc.) +// that it is used primarily as an auth identifier during MFA and for login alerts. +// +// Set authMethod to empty string if you want to ignore the MFA checks and the login alerts +// (can be also adjusted additionally via the OnRecordAuthRequest hook). +func RecordAuthResponse(e *core.RequestEvent, authRecord *core.Record, authMethod string, meta any) error { + token, tokenErr := authRecord.NewAuthToken() if tokenErr != nil { - return NewBadRequestError("Failed to create auth token.", tokenErr) + return e.InternalServerError("Failed to create auth token.", tokenErr) } - event := new(core.RecordAuthEvent) - event.HttpContext = c + return recordAuthResponse(e, authRecord, token, authMethod, meta) +} + +func recordAuthResponse(e *core.RequestEvent, authRecord *core.Record, token string, authMethod string, meta any) error { + originalRequestInfo, err := e.RequestInfo() + if err != nil { + return err + } + + ok, err := e.App.CanAccessRecord(authRecord, originalRequestInfo, authRecord.Collection().AuthRule) + if !ok { + return firstApiError(err, e.ForbiddenError("The request doesn't satisfy the collection requirements to authenticate.", err)) + } + + event := new(core.RecordAuthRequestEvent) + event.RequestEvent = e event.Collection = authRecord.Collection() event.Record = authRecord event.Token = token event.Meta = meta + event.AuthMethod = authMethod - return app.OnRecordAuthRequest().Trigger(event, func(e *core.RecordAuthEvent) error { - if e.HttpContext.Response().Committed { + return e.App.OnRecordAuthRequest().Trigger(event, func(e *core.RecordAuthRequestEvent) error { + if e.Written() { return nil } - // allow always returning the email address of the authenticated account - e.Record.IgnoreEmailVisibility(true) + // MFA + // --- + mfaId, err := checkMFA(e.RequestEvent, e.Record, e.AuthMethod) + if err != nil { + return err + } - // expand record relations - expands := strings.Split(c.QueryParam(expandQueryParam), ",") - if len(expands) > 0 { - // create a copy of the cached request data and adjust it to the current auth record - requestInfo := *RequestInfo(e.HttpContext) - requestInfo.Admin = nil - requestInfo.AuthRecord = e.Record - failed := app.Dao().ExpandRecord( - e.Record, - expands, - expandFetch(app.Dao(), &requestInfo), - ) - if len(failed) > 0 { - app.Logger().Debug("[RecordAuthResponse] Failed to expand relations", slog.Any("errors", failed)) + // require additional authentication + if mfaId != "" { + return e.JSON(http.StatusUnauthorized, map[string]string{ + "mfaId": mfaId, + }) + } + // --- + + // create a shallow copy of the cached request data and adjust it to the current auth record + requestInfo := *originalRequestInfo + requestInfo.Auth = e.Record + + err = triggerRecordEnrichHooks(e.App, &requestInfo, []*core.Record{e.Record}, func() error { + if e.Record.IsSuperuser() { + e.Record.Unhide(e.Record.Collection().Fields.FieldNames()...) + } + + // allow always returning the email address of the authenticated model + e.Record.IgnoreEmailVisibility(true) + + // expand record relations + expands := strings.Split(e.Request.URL.Query().Get(expandQueryParam), ",") + if len(expands) > 0 { + failed := e.App.ExpandRecord(e.Record, expands, expandFetch(e.App, &requestInfo)) + if len(failed) > 0 { + e.App.Logger().Warn("[recordAuthResponse] Failed to expand relations", "error", failed) + } + } + + return nil + }) + if err != nil { + return err + } + + if e.AuthMethod != "" && authRecord.Collection().AuthAlert.Enabled { + if err = authAlert(e.RequestEvent, e.Record); err != nil { + e.App.Logger().Warn("[recordAuthResponse] Failed to send login alert", "error", err) } } @@ -128,68 +118,254 @@ func RecordAuthResponse( result["meta"] = e.Meta } - for _, f := range finalizers { - if err := f(e.Token); err != nil { - return err - } + return e.JSON(http.StatusOK, result) + }) +} + +// wantsMFA checks whether to enable MFA for the specified auth record based on its MFA rule. +func wantsMFA(e *core.RequestEvent, record *core.Record) (bool, error) { + rule := record.Collection().MFA.Rule + if rule == "" { + return true, nil + } + + requestInfo, err := e.RequestInfo() + if err != nil { + return false, err + } + + var exists bool + + query := e.App.RecordQuery(record.Collection()). + Select("(1)"). + AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id}) + + // parse and apply the access rule filter + resolver := core.NewRecordFieldResolver(e.App, record.Collection(), requestInfo, true) + expr, err := search.FilterData(rule).BuildExpr(resolver) + if err != nil { + return false, err + } + resolver.UpdateQuery(query) + + err = query.AndWhere(expr).Limit(1).Row(&exists) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return false, err + } + + return exists, nil +} + +// checkMFA handles any MFA auth checks that needs to be performed for the specified request event. +// Returns the mfaId that needs to be written as response to the user. +// +// (note: all auth methods are treated as equal and there is no requirement for "pairing"). +func checkMFA(e *core.RequestEvent, authRecord *core.Record, currentAuthMethod string) (string, error) { + if !authRecord.Collection().MFA.Enabled || currentAuthMethod == "" { + return "", nil + } + + ok, err := wantsMFA(e, authRecord) + if !ok { + if err != nil { + return "", e.BadRequestError("Failed to authenticate.", fmt.Errorf("MFA rule failure: %w", err)) } - return e.HttpContext.JSON(http.StatusOK, result) - }) + return "", nil // no mfa needed for this auth record + } + + // read the mfaId either from the qyery params or request body + mfaId := e.Request.URL.Query().Get("mfaId") + if mfaId == "" { + // check the body + data := struct { + MfaId string `form:"mfaId" json:"mfaId" xml:"mfaId"` + }{} + if err := e.BindBody(&data); err != nil { + return "", firstApiError(err, e.BadRequestError("Failed to read MFA Id", err)) + } + mfaId = data.MfaId + } + + // first-time auth + // --- + if mfaId == "" { + mfa := core.NewMFA(e.App) + mfa.SetCollectionRef(authRecord.Collection().Id) + mfa.SetRecordRef(authRecord.Id) + mfa.SetMethod(currentAuthMethod) + if err := e.App.Save(mfa); err != nil { + return "", firstApiError(err, e.InternalServerError("Failed to create MFA record", err)) + } + + return mfa.Id, nil + } + + // second-time auth + // --- + mfa, err := e.App.FindMFAById(mfaId) + deleteMFA := func() { + // try to delete the expired mfa + if mfa != nil { + if deleteErr := e.App.Delete(mfa); deleteErr != nil { + e.App.Logger().Warn("Failed to delete expired MFA record", "error", deleteErr, "mfaId", mfa.Id) + } + } + } + if err != nil || mfa.HasExpired(authRecord.Collection().MFA.DurationTime()) { + deleteMFA() + return "", firstApiError(err, e.BadRequestError("Invalid or expired MFA session.", err)) + } + + if mfa.RecordRef() != authRecord.Id || mfa.CollectionRef() != authRecord.Collection().Id { + return "", e.BadRequestError("Invalid MFA session.", nil) + } + + if mfa.Method() == currentAuthMethod { + return "", e.BadRequestError("A different authentication method is required.", nil) + } + + deleteMFA() + + return "", nil } // EnrichRecord parses the request context and enrich the provided record: // - expands relations (if defaultExpands and/or ?expand query param is set) // - ensures that the emails of the auth record and its expanded auth relations -// are visible only for the current logged admin, record owner or record with manage access -func EnrichRecord(c echo.Context, dao *daos.Dao, record *models.Record, defaultExpands ...string) error { - return EnrichRecords(c, dao, []*models.Record{record}, defaultExpands...) +// are visible only for the current logged superuser, record owner or record with manage access +func EnrichRecord(e *core.RequestEvent, record *core.Record, defaultExpands ...string) error { + return EnrichRecords(e, []*core.Record{record}, defaultExpands...) } // EnrichRecords parses the request context and enriches the provided records: // - expands relations (if defaultExpands and/or ?expand query param is set) // - ensures that the emails of the auth records and their expanded auth relations -// are visible only for the current logged admin, record owner or record with manage access -func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defaultExpands ...string) error { - requestInfo := RequestInfo(c) - - if err := autoIgnoreAuthRecordsEmailVisibility(dao, records, requestInfo); err != nil { - return fmt.Errorf("failed to resolve email visibility: %w", err) +// are visible only for the current logged superuser, record owner or record with manage access +// +// Note: Expects all records to be from the same collection! +func EnrichRecords(e *core.RequestEvent, records []*core.Record, defaultExpands ...string) error { + if len(records) == 0 { + return nil } - expands := defaultExpands - if param := c.QueryParam(expandQueryParam); param != "" { - expands = append(expands, strings.Split(param, ",")...) - } - if len(expands) == 0 { - return nil // nothing to expand + info, err := e.RequestInfo() + if err != nil { + return err } - errs := dao.ExpandRecords(records, expands, expandFetch(dao, requestInfo)) - if len(errs) > 0 { - return fmt.Errorf("failed to expand: %v", errs) + return triggerRecordEnrichHooks(e.App, info, records, func() error { + expands := defaultExpands + if param := e.Request.URL.Query().Get(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 + e.App.Logger().Warn("failed to apply default enriching", "error", err) + } + + return nil + }) +} + +var iterate func(record *core.Record) error + +type iterator[T any] struct { + items []T + index int +} + +func (ri *iterator[T]) next() T { + var item T + + if ri.index < len(ri.items) { + item = ri.items[ri.index] + ri.index++ + } + + return item +} + +func triggerRecordEnrichHooks(app core.App, requestInfo *core.RequestInfo, records []*core.Record, finalizer func() error) error { + it := iterator[*core.Record]{items: records} + + enrichHook := app.OnRecordEnrich() + + event := new(core.RecordEnrichEvent) + event.App = app + event.RequestInfo = requestInfo + + iterate = func(record *core.Record) error { + if record == nil { + return nil + } + + event.Record = record + + return enrichHook.Trigger(event, func(ee *core.RecordEnrichEvent) error { + next := it.next() + if next == nil { + if finalizer != nil { + return finalizer() + } + return nil + } + + event.App = ee.App // in case it was replaced with a transaction + event.Record = next + + err := iterate(next) + + event.App = app + event.Record = record + + return err + }) + } + + return iterate(it.next()) +} + +func defaultEnrichRecords(app core.App, requestInfo *core.RequestInfo, records []*core.Record, expands ...string) error { + err := autoResolveRecordsFlags(app, records, requestInfo) + if err != nil { + return fmt.Errorf("failed to resolve records flags: %w", err) + } + + if len(expands) > 0 { + expandErrs := app.ExpandRecords(records, expands, expandFetch(app, requestInfo)) + if len(expandErrs) > 0 { + errsSlice := make([]error, 0, len(expandErrs)) + for key, err := range expandErrs { + errsSlice = append(errsSlice, fmt.Errorf("failed to expand %q: %w", key, err)) + } + return fmt.Errorf("failed to expand records: %w", errors.Join(errsSlice...)) + } } return nil } // expandFetch is the records fetch function that is used to expand related records. -func expandFetch( - dao *daos.Dao, - requestInfo *models.RequestInfo, -) daos.ExpandFetchFunc { - return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) { - records, err := dao.FindRecordsByIds(relCollection.Id, relIds, func(q *dbx.SelectQuery) error { - if requestInfo.Admin != nil { - return nil // admins can access everything +func expandFetch(app core.App, originalRequestInfo *core.RequestInfo) core.ExpandFetchFunc { + requestInfoClone := *originalRequestInfo + requestInfoPtr := &requestInfoClone + requestInfoPtr.Context = core.RequestInfoContextExpand + + return func(relCollection *core.Collection, relIds []string) ([]*core.Record, error) { + records, findErr := app.FindRecordsByIds(relCollection.Id, relIds, func(q *dbx.SelectQuery) error { + if requestInfoPtr.Auth != nil && requestInfoPtr.Auth.IsSuperuser() { + return nil // superusers can access everything } if relCollection.ViewRule == nil { - return fmt.Errorf("only admins can view collection %q records", relCollection.Name) + return fmt.Errorf("only superusers can view collection %q records", relCollection.Name) } if *relCollection.ViewRule != "" { - resolver := resolvers.NewRecordFieldResolver(dao, relCollection, requestInfo, true) + resolver := core.NewRecordFieldResolver(app, relCollection, requestInfoPtr, true) expr, err := search.FilterData(*(relCollection.ViewRule)).BuildExpr(resolver) if err != nil { return err @@ -200,50 +376,66 @@ func expandFetch( return nil }) - - if err == nil && len(records) > 0 { - autoIgnoreAuthRecordsEmailVisibility(dao, records, requestInfo) + if findErr != nil { + return nil, findErr } - return records, err + enrichErr := triggerRecordEnrichHooks(app, requestInfoPtr, records, func() error { + if err := autoResolveRecordsFlags(app, records, requestInfoPtr); err != nil { + // non-critical error + app.Logger().Warn("Failed to apply autoResolveRecordsFlags for the expanded records", "error", err) + } + + return nil + }) + if enrichErr != nil { + return nil, enrichErr + } + + return records, nil } } -// autoIgnoreAuthRecordsEmailVisibility ignores the email visibility check for -// the provided record if the current auth model is admin, owner or a "manager". +// autoResolveRecordsFlags resolves various visibility flags of the provided records. // -// Note: Expects all records to be from the same auth collection! -func autoIgnoreAuthRecordsEmailVisibility( - dao *daos.Dao, - records []*models.Record, - requestInfo *models.RequestInfo, -) error { - if len(records) == 0 || !records[0].Collection().IsAuth() { - return nil // nothing to check +// Currently it enables: +// - export of hidden fields if the current auth model is a superuser +// - email export ignoring the emailVisibity checks if the current auth model is superuser, owner or a "manager". +// +// Note: Expects all records to be from the same collection! +func autoResolveRecordsFlags(app core.App, records []*core.Record, requestInfo *core.RequestInfo) error { + if len(records) == 0 { + return nil // nothing to resolve } - if requestInfo.Admin != nil { + if requestInfo.HasSuperuserAuth() { + hiddenFields := records[0].Collection().Fields.FieldNames() for _, rec := range records { + rec.Unhide(hiddenFields...) rec.IgnoreEmailVisibility(true) } - return nil + } + + // additional emailVisibility checks + // --------------------------------------------------------------- + if !records[0].Collection().IsAuth() { + return nil // not auth collection records } collection := records[0].Collection() - mappedRecords := make(map[string]*models.Record, len(records)) + mappedRecords := make(map[string]*core.Record, len(records)) recordIds := make([]any, len(records)) for i, rec := range records { mappedRecords[rec.Id] = rec recordIds[i] = rec.Id } - if requestInfo != nil && requestInfo.AuthRecord != nil && mappedRecords[requestInfo.AuthRecord.Id] != nil { - mappedRecords[requestInfo.AuthRecord.Id].IgnoreEmailVisibility(true) + if requestInfo.Auth != nil && mappedRecords[requestInfo.Auth.Id] != nil { + mappedRecords[requestInfo.Auth.Id].IgnoreEmailVisibility(true) } - authOptions := collection.AuthOptions() - if authOptions.ManageRule == nil || *authOptions.ManageRule == "" { + if collection.ManageRule == nil || *collection.ManageRule == "" { return nil // no manage rule to check } @@ -251,12 +443,12 @@ func autoIgnoreAuthRecordsEmailVisibility( // --- managedIds := []string{} - query := dao.RecordQuery(collection). - Select(dao.DB().QuoteSimpleColumnName(collection.Name) + ".id"). - AndWhere(dbx.In(dao.DB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...)) + query := app.RecordQuery(collection). + Select(app.DB().QuoteSimpleColumnName(collection.Name) + ".id"). + AndWhere(dbx.In(app.DB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...)) - resolver := resolvers.NewRecordFieldResolver(dao, collection, requestInfo, true) - expr, err := search.FilterData(*authOptions.ManageRule).BuildExpr(resolver) + resolver := core.NewRecordFieldResolver(app, collection, requestInfo, true) + expr, err := search.FilterData(*collection.ManageRule).BuildExpr(resolver) if err != nil { return err } @@ -278,30 +470,26 @@ func autoIgnoreAuthRecordsEmailVisibility( return nil } -// hasAuthManageAccess checks whether the client is allowed to have full +// hasAuthManageAccess checks whether the client is allowed to have // [forms.RecordUpsert] auth management permissions -// (aka. allowing to change system auth fields without oldPassword). -func hasAuthManageAccess( - dao *daos.Dao, - record *models.Record, - requestInfo *models.RequestInfo, -) bool { +// (e.g. allowing to change system auth fields without oldPassword). +func hasAuthManageAccess(app core.App, requestInfo *core.RequestInfo, record *core.Record) bool { if !record.Collection().IsAuth() { return false } - manageRule := record.Collection().AuthOptions().ManageRule + manageRule := record.Collection().ManageRule if manageRule == nil || *manageRule == "" { - return false // only for admins (manageRule can't be empty) + return false // only for superusers (manageRule can't be empty) } - if requestInfo == nil || requestInfo.AuthRecord == nil { + if requestInfo == nil || requestInfo.Auth == nil { return false // no auth record } ruleFunc := func(q *dbx.SelectQuery) error { - resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestInfo, true) + resolver := core.NewRecordFieldResolver(app, record.Collection(), requestInfo, true) expr, err := search.FilterData(*manageRule).BuildExpr(resolver) if err != nil { return err @@ -311,35 +499,118 @@ func hasAuthManageAccess( return nil } - _, findErr := dao.FindRecordById(record.Collection().Id, record.Id, ruleFunc) + _, findErr := app.FindRecordById(record.Collection().Id, record.Id, ruleFunc) return findErr == nil } var ruleQueryParams = []string{search.FilterQueryParam, search.SortQueryParam} -var adminOnlyRuleFields = []string{"@collection.", "@request."} +var superuserOnlyRuleFields = []string{"@collection.", "@request."} -// @todo consider moving the rules check to the RecordFieldResolver. -// -// checkForAdminOnlyRuleFields loosely checks and returns an error if -// the provided RequestInfo contains rule fields that only the admin can use. -func checkForAdminOnlyRuleFields(requestInfo *models.RequestInfo) error { - if requestInfo.Admin != nil || len(requestInfo.Query) == 0 { - return nil // admin or nothing to check +// checkForSuperuserOnlyRuleFields loosely checks and returns an error if +// the provided RequestInfo contains rule fields that only the superuser can use. +func checkForSuperuserOnlyRuleFields(requestInfo *core.RequestInfo) error { + if len(requestInfo.Query) == 0 || requestInfo.HasSuperuserAuth() { + return nil // superuser or nothing to check } for _, param := range ruleQueryParams { - v, _ := requestInfo.Query[param].(string) + v := requestInfo.Query[param] if v == "" { continue } - for _, field := range adminOnlyRuleFields { + for _, field := range superuserOnlyRuleFields { if strings.Contains(v, field) { - return NewForbiddenError("Only admins can filter by "+field, nil) + return router.NewForbiddenError("Only superusers can filter by "+field, nil) } } } return nil } + +// firstApiError returns the first ApiError from the errors list +// (this is used usually to prevent unnecessary wraping and to allow bubling ApiError from nested hooks) +// +// If no ApiError is found, returns a default "Internal server" error. +func firstApiError(errs ...error) *router.ApiError { + var apiErr *router.ApiError + var ok bool + + for _, err := range errs { + if err == nil { + continue + } + + // quick assert to avoid the reflection checks + apiErr, ok = err.(*router.ApiError) + if ok { + return apiErr + } + + // nested/wrapped errors + if errors.As(err, &apiErr) { + return apiErr + } + } + + return router.NewInternalServerError("", errors.Join(errs...)) +} + +// ------------------------------------------------------------------- + +const maxAuthOrigins = 5 + +func authAlert(e *core.RequestEvent, authRecord *core.Record) error { + // generating fingerprint + // --- + userAgent := e.Request.UserAgent() + if len(userAgent) > 300 { + userAgent = userAgent[:300] + } + fingerprint := security.MD5(e.RealIP() + userAgent) + // --- + + origins, err := e.App.FindAllAuthOriginsByRecord(authRecord) + if err != nil { + return err + } + + isFirstLogin := len(origins) == 0 + + var currentOrigin *core.AuthOrigin + for _, origin := range origins { + if origin.Fingerprint() == fingerprint { + currentOrigin = origin + break + } + } + if currentOrigin == nil { + currentOrigin = core.NewAuthOrigin(e.App) + currentOrigin.SetCollectionRef(authRecord.Collection().Id) + currentOrigin.SetRecordRef(authRecord.Id) + currentOrigin.SetFingerprint(fingerprint) + } + + // send email alert for the new origin auth (skip first login) + if !isFirstLogin && currentOrigin.IsNew() && authRecord.Email() != "" { + if err := mails.SendRecordAuthAlert(e.App, authRecord); err != nil { + return err + } + } + + // try to keep only up to maxAuthOrigins + // (pop the last used ones; it is not executed in a transaction to avoid unnecessary locks) + if currentOrigin.IsNew() && len(origins) >= maxAuthOrigins { + for i := len(origins) - 1; i >= maxAuthOrigins-1; i-- { + if err := e.App.Delete(origins[i]); err != nil { + // treat as non-critical error, just log for now + e.App.Logger().Warn("Failed to delete old AuthOrigin record", "error", err, "authOriginId", origins[i].Id) + } + } + } + + // create/update the origin fingerprint + return e.App.Save(currentOrigin) +} diff --git a/apis/record_helpers_test.go b/apis/record_helpers_test.go index 8eb679f3..60a3cbd3 100644 --- a/apis/record_helpers_test.go +++ b/apis/record_helpers_test.go @@ -6,231 +6,742 @@ import ( "net/http/httptest" "strings" "testing" + "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" - "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/router" + "github.com/pocketbase/pocketbase/tools/types" ) -func TestRequestInfo(t *testing.T) { - t.Parallel() - - e := echo.New() - req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - req.Header.Set("X-Token-Test", "123") - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - dummyRecord := &models.Record{} - dummyRecord.Id = "id1" - c.Set(apis.ContextAuthRecordKey, dummyRecord) - - dummyAdmin := &models.Admin{} - dummyAdmin.Id = "id2" - c.Set(apis.ContextAdminKey, dummyAdmin) - - result := apis.RequestInfo(c) - - if result == nil { - t.Fatal("Expected *models.RequestInfo instance, got nil") - } - - if result.Method != http.MethodPost { - t.Fatalf("Expected Method %v, got %v", http.MethodPost, result.Method) - } - - rawHeaders, _ := json.Marshal(result.Headers) - expectedHeaders := `{"content_type":"application/json","x_token_test":"123"}` - if v := string(rawHeaders); v != expectedHeaders { - t.Fatalf("Expected Query %v, got %v", expectedHeaders, v) - } - - rawQuery, _ := json.Marshal(result.Query) - expectedQuery := `{"test":"123"}` - if v := string(rawQuery); v != expectedQuery { - t.Fatalf("Expected Query %v, got %v", expectedQuery, v) - } - - rawData, _ := json.Marshal(result.Data) - expectedData := `{"test":456}` - if v := string(rawData); v != expectedData { - t.Fatalf("Expected Data %v, got %v", expectedData, v) - } - - if result.AuthRecord == nil || result.AuthRecord.Id != dummyRecord.Id { - t.Fatalf("Expected AuthRecord %v, got %v", dummyRecord, result.AuthRecord) - } - - if result.Admin == nil || result.Admin.Id != dummyAdmin.Id { - t.Fatalf("Expected Admin %v, got %v", dummyAdmin, result.Admin) - } -} - -func TestRecordAuthResponse(t *testing.T) { +func TestEnrichRecords(t *testing.T) { t.Parallel() + // mock test data + // --- app, _ := tests.NewTestApp() defer app.Cleanup() - dummyAdmin := &models.Admin{} - dummyAdmin.Id = "id1" - - nonAuthRecord, err := app.Dao().FindRecordById("demo1", "al1h9ijdeojtsjy") + user, err := app.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } - authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") if err != nil { t.Fatal(err) } - unverifiedAuthRecord, err := app.Dao().FindRecordById("clients", "o1y0dd0spd786md") + usersRecords, err := app.FindRecordsByIds("users", []string{"4q1xlclmfloku33", "bgs820n361vj1qd"}) if err != nil { t.Fatal(err) } + nologinRecords, err := app.FindRecordsByIds("nologin", []string{"dc49k6jgejn40h3", "oos036e9xvqeexy"}) + if err != nil { + t.Fatal(err) + } + + demo1Records, err := app.FindRecordsByIds("demo1", []string{"al1h9ijdeojtsjy", "84nmscqy84lsi1t"}) + if err != nil { + t.Fatal(err) + } + + demo5Records, err := app.FindRecordsByIds("demo5", []string{"la4y2w4o98acwuj", "qjeql998mtp1azp"}) + if err != nil { + t.Fatal(err) + } + + // temp update the view rule to ensure that request context is set to "expand" + demo4, err := app.FindCollectionByNameOrId("demo4") + if err != nil { + t.Fatal(err) + } + demo4.ViewRule = types.Pointer("@request.context = 'expand'") + if err := app.Save(demo4); err != nil { + t.Fatal(err) + } + // --- + scenarios := []struct { - name string - record *models.Record - meta any - expectError bool - expectedContent []string - notExpectedContent []string - expectedEvents map[string]int + name string + auth *core.Record + records []*core.Record + queryExpand string + defaultExpands []string + expected []string + notExpected []string }{ + // email visibility checks { - name: "non auth record", - record: nonAuthRecord, - expectError: true, - }, - { - name: "valid auth record but with unverified email in onlyVerified collection", - record: unverifiedAuthRecord, - expectError: true, - }, - { - name: "valid auth record - without meta", - record: authRecord, - expectError: false, - expectedContent: []string{ - `"token":"`, - `"record":{`, - `"id":"`, - `"expand":{"rel":{`, + name: "[emailVisibility] guest", + auth: nil, + records: usersRecords, + queryExpand: "", + defaultExpands: nil, + expected: []string{ + `"customField":"123"`, + `"test3@example.com"`, // emailVisibility=true }, - notExpectedContent: []string{ - `"meta":`, - }, - expectedEvents: map[string]int{ - "OnRecordAuthRequest": 1, + notExpected: []string{ + `"test@example.com"`, }, }, { - name: "valid auth record - with meta", - record: authRecord, - meta: map[string]any{"meta_test": 123}, - expectError: false, - expectedContent: []string{ - `"token":"`, - `"record":{`, - `"id":"`, - `"expand":{"rel":{`, - `"meta":{"meta_test":123`, + name: "[emailVisibility] owner", + auth: user, + records: usersRecords, + queryExpand: "", + defaultExpands: nil, + expected: []string{ + `"customField":"123"`, + `"test3@example.com"`, // emailVisibility=true + `"test@example.com"`, // owner }, - expectedEvents: map[string]int{ - "OnRecordAuthRequest": 1, + }, + { + name: "[emailVisibility] manager", + auth: user, + records: nologinRecords, + queryExpand: "", + defaultExpands: nil, + expected: []string{ + `"customField":"123"`, + `"test3@example.com"`, + `"test@example.com"`, + }, + }, + { + name: "[emailVisibility] superuser", + auth: superuser, + records: nologinRecords, + queryExpand: "", + defaultExpands: nil, + expected: []string{ + `"customField":"123"`, + `"test3@example.com"`, + `"test@example.com"`, + }, + }, + { + name: "[emailVisibility + expand] recursive auth rule checks (regular user)", + auth: user, + records: demo1Records, + queryExpand: "", + defaultExpands: []string{"rel_many"}, + expected: []string{ + `"customField":"123"`, + `"expand":{"rel_many"`, + `"expand":{}`, + `"test@example.com"`, + }, + notExpected: []string{ + `"id":"bgs820n361vj1qd"`, + `"id":"oap640cot4yru2s"`, + }, + }, + { + name: "[emailVisibility + expand] recursive auth rule checks (superuser)", + auth: superuser, + records: demo1Records, + queryExpand: "", + defaultExpands: []string{"rel_many"}, + expected: []string{ + `"customField":"123"`, + `"test@example.com"`, + `"expand":{"rel_many"`, + `"id":"bgs820n361vj1qd"`, + `"id":"oap640cot4yru2s"`, + }, + notExpected: []string{ + `"expand":{}`, + }, + }, + + // expand checks + { + name: "[expand] guest (query)", + auth: nil, + records: usersRecords, + queryExpand: "rel", + defaultExpands: nil, + expected: []string{ + `"customField":"123"`, + `"expand":{"rel"`, + `"id":"llvuca81nly1qls"`, + `"id":"0yxhwia2amd8gec"`, + }, + notExpected: []string{ + `"expand":{}`, + }, + }, + { + name: "[expand] guest (default expands)", + auth: nil, + records: usersRecords, + queryExpand: "", + defaultExpands: []string{"rel"}, + expected: []string{ + `"customField":"123"`, + `"expand":{"rel"`, + `"id":"llvuca81nly1qls"`, + `"id":"0yxhwia2amd8gec"`, + }, + }, + { + name: "[expand] @request.context=expand check", + auth: nil, + records: demo5Records, + queryExpand: "rel_one", + defaultExpands: []string{"rel_many"}, + expected: []string{ + `"customField":"123"`, + `"expand":{}`, + `"expand":{"`, + `"rel_many":[{`, + `"rel_one":{`, + `"id":"i9naidtvr6qsgb4"`, + `"id":"qzaqccwrmva4o1n"`, }, }, } for _, s := range scenarios { - app.ResetEventCalls() + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/?expand=rel", nil) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.Set(apis.ContextAdminKey, dummyAdmin) + app.OnRecordEnrich().BindFunc(func(e *core.RecordEnrichEvent) error { + e.Record.WithCustomData(true) + e.Record.Set("customField", "123") + return e.Next() + }) - responseErr := apis.RecordAuthResponse(app, c, s.record, s.meta) + req := httptest.NewRequest(http.MethodGet, "/?expand="+s.queryExpand, nil) + rec := httptest.NewRecorder() - hasErr := responseErr != nil - if hasErr != s.expectError { - t.Fatalf("[%s] Expected hasErr to be %v, got %v (%v)", s.name, s.expectError, hasErr, responseErr) - } + requestEvent := new(core.RequestEvent) + requestEvent.App = app + requestEvent.Request = req + requestEvent.Response = rec + requestEvent.Auth = s.auth - if len(app.EventCalls) != len(s.expectedEvents) { - t.Fatalf("[%s] Expected events \n%v, \ngot \n%v", s.name, s.expectedEvents, app.EventCalls) - } - for k, v := range s.expectedEvents { - if app.EventCalls[k] != v { - t.Fatalf("[%s] Expected event %s to be called %d times, got %d", s.name, k, v, app.EventCalls[k]) + err := apis.EnrichRecords(requestEvent, s.records, s.defaultExpands...) + if err != nil { + t.Fatal(err) } - } - if hasErr { - continue - } - - response := rec.Body.String() - - for _, v := range s.expectedContent { - if !strings.Contains(response, v) { - t.Fatalf("[%s] Missing %v in response \n%v", s.name, v, response) + raw, err := json.Marshal(s.records) + if err != nil { + t.Fatal(err) } - } + rawStr := string(raw) - for _, v := range s.notExpectedContent { - if strings.Contains(response, v) { - t.Fatalf("[%s] Unexpected %v in response \n%v", s.name, v, response) + for _, str := range s.expected { + if !strings.Contains(rawStr, str) { + t.Fatalf("Expected\n%q\nin\n%v", str, rawStr) + } } - } + + for _, str := range s.notExpected { + if strings.Contains(rawStr, str) { + t.Fatalf("Didn't expected\n%q\nin\n%v", str, rawStr) + } + } + }) } } -func TestEnrichRecords(t *testing.T) { - t.Parallel() - - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/?expand=rel_many", nil) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - dummyAdmin := &models.Admin{} - dummyAdmin.Id = "test_id" - c.Set(apis.ContextAdminKey, dummyAdmin) - +func TestRecordAuthResponseAuthRuleCheck(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - records, err := app.Dao().FindRecordsByIds("demo1", []string{"al1h9ijdeojtsjy", "84nmscqy84lsi1t"}) + event := new(core.RequestEvent) + event.App = app + event.Request = httptest.NewRequest(http.MethodGet, "/", nil) + event.Response = httptest.NewRecorder() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } - apis.EnrichRecords(c, app.Dao(), records, "rel_one") + scenarios := []struct { + name string + rule *string + expectError bool + }{ + { + "admin only rule", + nil, + true, + }, + { + "empty rule", + types.Pointer(""), + false, + }, + { + "false rule", + types.Pointer("1=2"), + true, + }, + { + "true rule", + types.Pointer("1=1"), + false, + }, + } - for _, record := range records { - expand := record.Expand() - if len(expand) == 0 { - t.Fatalf("Expected non-empty expand, got nil for record %v", record) - } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + user.Collection().AuthRule = s.rule - if len(record.GetStringSlice("rel_one")) != 0 { - if _, ok := expand["rel_one"]; !ok { - t.Fatalf("Expected rel_one to be expanded for record %v, got \n%v", record, expand) + err := apis.RecordAuthResponse(event, user, "", nil) + + hasErr := err != nil + if s.expectError != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } - } - if len(record.GetStringSlice("rel_many")) != 0 { - if _, ok := expand["rel_many"]; !ok { - t.Fatalf("Expected rel_many to be expanded for record %v, got \n%v", record, expand) + // in all cases login alert shouldn't be send because of the empty auth method + if app.TestMailer.TotalSend() != 0 { + t.Fatalf("Expected no emails send, got %d:\n%v", app.TestMailer.TotalSend(), app.TestMailer.LastMessage().HTML) } - } + + if !hasErr { + return + } + + apiErr, ok := err.(*router.ApiError) + + if !ok || apiErr == nil { + t.Fatalf("Expected ApiError, got %v", apiErr) + } + + if apiErr.Status != http.StatusForbidden { + t.Fatalf("Expected ApiError.Status %d, got %d", http.StatusForbidden, apiErr.Status) + } + }) } } + +func TestRecordAuthResponseAuthAlertCheck(t *testing.T) { + const testFingerprint = "d0f88d6c87767262ba8e93d6acccd784" + + scenarios := []struct { + name string + devices []string // mock existing device fingerprints + expectDevices []string + enabled bool + expectEmail bool + }{ + { + name: "first login", + devices: nil, + expectDevices: []string{testFingerprint}, + enabled: true, + expectEmail: false, + }, + { + name: "existing device", + devices: []string{"1", testFingerprint}, + expectDevices: []string{"1", testFingerprint}, + enabled: true, + expectEmail: false, + }, + { + name: "new device (< 5)", + devices: []string{"1", "2"}, + expectDevices: []string{"1", "2", testFingerprint}, + enabled: true, + expectEmail: true, + }, + { + name: "new device (>= 5)", + devices: []string{"1", "2", "3", "4", "5"}, + expectDevices: []string{"2", "3", "4", "5", testFingerprint}, + enabled: true, + expectEmail: true, + }, + { + name: "with disabled auth alert collection flag", + devices: []string{"1", "2"}, + expectDevices: []string{"1", "2"}, + enabled: false, + expectEmail: false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + event := new(core.RequestEvent) + event.App = app + event.Request = httptest.NewRequest(http.MethodGet, "/", nil) + event.Response = httptest.NewRecorder() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + user.Collection().MFA.Enabled = false + user.Collection().AuthRule = types.Pointer("") + user.Collection().AuthAlert.Enabled = s.enabled + + // ensure that there are no other auth origins + err = app.DeleteAllAuthOriginsByRecord(user) + if err != nil { + t.Fatal(err) + } + + // insert the mock devices + for _, fingerprint := range s.devices { + d := core.NewAuthOrigin(app) + d.SetCollectionRef(user.Collection().Id) + d.SetRecordRef(user.Id) + d.SetFingerprint(fingerprint) + if err = app.Save(d); err != nil { + t.Fatal(err) + } + } + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err != nil { + t.Fatalf("Failed to resolve auth response: %v", err) + } + + var expectTotalSend int + if s.expectEmail { + expectTotalSend = 1 + } + if total := app.TestMailer.TotalSend(); total != expectTotalSend { + t.Fatalf("Expected %d sent emails, got %d", expectTotalSend, total) + } + + devices, err := app.FindAllAuthOriginsByRecord(user) + if err != nil { + t.Fatalf("Failed to retrieve auth origins: %v", err) + } + + if len(devices) != len(s.expectDevices) { + t.Fatalf("Expected %d devices, got %d", len(s.expectDevices), len(devices)) + } + + for _, fingerprint := range s.expectDevices { + var exists bool + fingerprints := make([]string, 0, len(devices)) + for _, d := range devices { + if d.Fingerprint() == fingerprint { + exists = true + break + } + fingerprints = append(fingerprints, d.Fingerprint()) + } + if !exists { + t.Fatalf("Missing device with fingerprint %q:\n%v", fingerprint, fingerprints) + } + } + }) + } +} + +func TestRecordAuthResponseMFACheck(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + user2, err := app.FindAuthRecordByEmail("users", "test2@example.com") + if err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + + event := new(core.RequestEvent) + event.App = app + event.Request = httptest.NewRequest(http.MethodGet, "/", nil) + event.Response = rec + + resetMFAs := func(authRecord *core.Record) { + // ensure that mfa is enabled + user.Collection().MFA.Enabled = true + user.Collection().MFA.Duration = 5 + user.Collection().MFA.Rule = "" + + mfas, err := app.FindAllMFAsByRecord(authRecord) + if err != nil { + t.Fatalf("Failed to retrieve mfas: %v", err) + } + for _, mfa := range mfas { + if err := app.Delete(mfa); err != nil { + t.Fatalf("Failed to delete mfa %q: %v", mfa.Id, err) + } + } + + // reset response + rec = httptest.NewRecorder() + event.Response = rec + } + + totalMFAs := func(authRecord *core.Record) int { + mfas, err := app.FindAllMFAsByRecord(authRecord) + if err != nil { + t.Fatalf("Failed to retrieve mfas: %v", err) + } + return len(mfas) + } + + t.Run("no collection MFA enabled", func(t *testing.T) { + resetMFAs(user) + + user.Collection().MFA.Enabled = false + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + body := rec.Body.String() + if strings.Contains(body, "mfaId") { + t.Fatalf("Expected no mfaId in the response body, got\n%v", body) + } + if !strings.Contains(body, "token") { + t.Fatalf("Expected auth token in the response body, got\n%v", body) + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected no mfa records to be created, got %d", total) + } + }) + + t.Run("no explicit auth method", func(t *testing.T) { + resetMFAs(user) + + err = apis.RecordAuthResponse(event, user, "", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + body := rec.Body.String() + if strings.Contains(body, "mfaId") { + t.Fatalf("Expected no mfaId in the response body, got\n%v", body) + } + if !strings.Contains(body, "token") { + t.Fatalf("Expected auth token in the response body, got\n%v", body) + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected no mfa records to be created, got %d", total) + } + }) + + t.Run("no mfa wanted (mfa rule check failure)", func(t *testing.T) { + resetMFAs(user) + user.Collection().MFA.Rule = "1=2" + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + body := rec.Body.String() + if strings.Contains(body, "mfaId") { + t.Fatalf("Expected no mfaId in the response body, got\n%v", body) + } + if !strings.Contains(body, "token") { + t.Fatalf("Expected auth token in the response body, got\n%v", body) + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected no mfa records to be created, got %d", total) + } + }) + + t.Run("mfa wanted (mfa rule check success)", func(t *testing.T) { + resetMFAs(user) + user.Collection().MFA.Rule = "1=1" + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + body := rec.Body.String() + if !strings.Contains(body, "mfaId") { + t.Fatalf("Expected the created mfaId to be returned in the response body, got\n%v", body) + } + + if total := totalMFAs(user); total != 1 { + t.Fatalf("Expected a single mfa record to be created, got %d", total) + } + }) + + t.Run("mfa first-time", func(t *testing.T) { + resetMFAs(user) + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + body := rec.Body.String() + if !strings.Contains(body, "mfaId") { + t.Fatalf("Expected the created mfaId to be returned in the response body, got\n%v", body) + } + + if total := totalMFAs(user); total != 1 { + t.Fatalf("Expected a single mfa record to be created, got %d", total) + } + }) + + t.Run("mfa second-time with the same auth method", func(t *testing.T) { + resetMFAs(user) + + // create a dummy mfa record + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("example") + if err = app.Save(mfa); err != nil { + t.Fatal(err) + } + + event.Request = httptest.NewRequest(http.MethodGet, "/?mfaId="+mfa.Id, nil) + + err = apis.RecordAuthResponse(event, user, "example", nil) + if err == nil { + t.Fatal("Expected error, got nil") + } + + if total := totalMFAs(user); total != 1 { + t.Fatalf("Expected only 1 mfa record (the existing one), got %d", total) + } + }) + + t.Run("mfa second-time with the different auth method (query param)", func(t *testing.T) { + resetMFAs(user) + + // create a dummy mfa record + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("example1") + if err = app.Save(mfa); err != nil { + t.Fatal(err) + } + + event.Request = httptest.NewRequest(http.MethodGet, "/?mfaId="+mfa.Id, nil) + + err = apis.RecordAuthResponse(event, user, "example2", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected the dummy mfa record to be deleted, found %d", total) + } + }) + + t.Run("mfa second-time with the different auth method (body param)", func(t *testing.T) { + resetMFAs(user) + + // create a dummy mfa record + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("example1") + if err = app.Save(mfa); err != nil { + t.Fatal(err) + } + + event.Request = httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{"mfaId":"`+mfa.Id+`"}`)) + event.Request.Header.Add("content-type", "application/json") + + err = apis.RecordAuthResponse(event, user, "example2", nil) + if err != nil { + t.Fatalf("Expected nil, got error: %v", err) + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected the dummy mfa record to be deleted, found %d", total) + } + }) + + t.Run("missing mfa", func(t *testing.T) { + resetMFAs(user) + + event.Request = httptest.NewRequest(http.MethodGet, "/?mfaId=missing", nil) + + err = apis.RecordAuthResponse(event, user, "example2", nil) + if err == nil { + t.Fatal("Expected error, got nil") + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected 0 mfa records, got %d", total) + } + }) + + t.Run("expired mfa", func(t *testing.T) { + resetMFAs(user) + + // create a dummy expired mfa record + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("example1") + mfa.SetRaw("created", types.NowDateTime().Add(-1*time.Hour)) + mfa.SetRaw("updated", types.NowDateTime().Add(-1*time.Hour)) + if err = app.Save(mfa); err != nil { + t.Fatal(err) + } + + event.Request = httptest.NewRequest(http.MethodGet, "/?mfaId="+mfa.Id, nil) + + err = apis.RecordAuthResponse(event, user, "example2", nil) + if err == nil { + t.Fatal("Expected error, got nil") + } + + if totalMFAs(user) != 0 { + t.Fatal("Expected the expired mfa record to be deleted") + } + }) + + t.Run("mfa for different auth record", func(t *testing.T) { + resetMFAs(user) + + // create a dummy expired mfa record + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user2.Collection().Id) + mfa.SetRecordRef(user2.Id) + mfa.SetMethod("example1") + if err = app.Save(mfa); err != nil { + t.Fatal(err) + } + + event.Request = httptest.NewRequest(http.MethodGet, "/?mfaId="+mfa.Id, nil) + + err = apis.RecordAuthResponse(event, user, "example2", nil) + if err == nil { + t.Fatal("Expected error, got nil") + } + + if total := totalMFAs(user); total != 0 { + t.Fatalf("Expected no user mfas, got %d", total) + } + + if total := totalMFAs(user2); total != 1 { + t.Fatalf("Expected only 1 user2 mfa, got %d", total) + } + }) +} diff --git a/apis/serve.go b/apis/serve.go index 1984c131..5706d619 100644 --- a/apis/serve.go +++ b/apis/serve.go @@ -3,6 +3,7 @@ package apis import ( "context" "crypto/tls" + "errors" "log" "net" "net/http" @@ -12,14 +13,10 @@ import ( "time" "github.com/fatih/color" - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/migrations/logs" + "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/migrate" + "github.com/pocketbase/pocketbase/ui" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" ) @@ -29,10 +26,16 @@ type ServeConfig struct { // ShowStartBanner indicates whether to show or hide the server start console message. ShowStartBanner bool - // HttpAddr is the TCP address to listen for the HTTP server (eg. `127.0.0.1:80`). + // DashboardPath specifies the route path to the superusers dashboard interface + // (default to "/_/{path...}"). + // + // Note: Must include the "{path...}" wildcard parameter. + DashboardPath string + + // HttpAddr is the TCP address to listen for the HTTP server (eg. "127.0.0.1:80"). HttpAddr string - // HttpsAddr is the TCP address to listen for the HTTPS server (eg. `127.0.0.1:443`). + // HttpsAddr is the TCP address to listen for the HTTPS server (eg. "127.0.0.1:443"). HttpsAddr string // Optional domains list to use when issuing the TLS certificate. @@ -58,36 +61,43 @@ type ServeConfig struct { // HttpAddr: "127.0.0.1:8080", // ShowStartBanner: false, // }) -func Serve(app core.App, config ServeConfig) (*http.Server, error) { +func Serve(app core.App, config ServeConfig) error { if len(config.AllowedOrigins) == 0 { config.AllowedOrigins = []string{"*"} } + if config.DashboardPath == "" { + config.DashboardPath = "/_/{path...}" + } else if !strings.HasSuffix(config.DashboardPath, "{path...}") { + return errors.New("invalid dashboard path - missing {path...} wildcard") + } + // ensure that the latest migrations are applied before starting the server - if err := runMigrations(app); err != nil { - return nil, err - } - - // reload app settings in case a new default value was set with a migration - // (or if this is the first time the init migration was executed) - if err := app.RefreshSettings(); err != nil { - color.Yellow("=====================================") - color.Yellow("WARNING: Settings load error! \n%v", err) - color.Yellow("Fallback to the application defaults.") - color.Yellow("=====================================") - } - - router, err := InitApi(app) + err := app.RunAllMigrations() if err != nil { - return nil, err + return err } - // configure cors - router.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - Skipper: middleware.DefaultSkipper, - AllowOrigins: config.AllowedOrigins, - AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, - })) + pbRouter, err := NewRouter(app) + if err != nil { + 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.BindFunc(installerRedirect(app, config.DashboardPath)) + + pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)). + BindFunc(dashboardRemoveInstallerParam()). + BindFunc(dashboardCacheControl()). + BindFunc(Gzip()) // start http server // --- @@ -118,25 +128,12 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { // implicit www->non-www redirect(s) if len(wwwRedirects) > 0 { - router.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - host := c.Request().Host - - if strings.HasPrefix(host, "www.") && list.ExistInSlice(host, wwwRedirects) { - return c.Redirect( - http.StatusTemporaryRedirect, - (c.Scheme() + "://" + host[4:] + c.Request().RequestURI), - ) - } - - return next(c) - } - }) + pbRouter.Bind(wwwRedirect(wwwRedirects)) } certManager := &autocert.Manager{ Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")), + Cache: autocert.DirCache(filepath.Join(app.DataDir(), core.LocalAutocertCacheDirName)), HostPolicy: autocert.HostWhitelist(hostNames...), } @@ -151,24 +148,96 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { GetCertificate: certManager.GetCertificate, NextProtos: []string{acme.ALPNProto}, }, - ReadTimeout: 10 * time.Minute, + // higher defaults to accommodate large file uploads/downloads + WriteTimeout: 3 * time.Minute, + ReadTimeout: 3 * time.Minute, ReadHeaderTimeout: 30 * time.Second, - // WriteTimeout: 60 * time.Second, // breaks sse! - Handler: router, - Addr: mainAddr, + Addr: mainAddr, BaseContext: func(l net.Listener) context.Context { return baseCtx }, + ErrorLog: log.New(&serverErrorLogWriter{app: app}, "", 0), } - serveEvent := &core.ServeEvent{ - App: app, - Router: router, - Server: server, - CertManager: certManager, - } - if err := app.OnBeforeServe().Trigger(serveEvent); err != nil { - return nil, err + serveEvent := new(core.ServeEvent) + serveEvent.App = app + serveEvent.Router = pbRouter + serveEvent.Server = server + serveEvent.CertManager = certManager + + var listener net.Listener + + // graceful shutdown + // --------------------------------------------------------------- + // WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately. + // Note that the WaitGroup would do nothing if the app.OnTerminate() hook isn't triggered. + var wg sync.WaitGroup + + // try to gracefully shutdown the server on app termination + app.OnTerminate().Bind(&hook.Handler[*core.TerminateEvent]{ + Id: "pbGracefulShutdown", + Func: func(te *core.TerminateEvent) error { + cancelBaseCtx() + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + wg.Add(1) + + _ = server.Shutdown(ctx) + + if te.IsRestart { + // wait for execve and other handlers up to 3 seconds before exit + time.AfterFunc(3*time.Second, func() { + wg.Done() + }) + } else { + wg.Done() + } + + return te.Next() + }, + Priority: -9999, + }) + + // wait for the graceful shutdown to complete before exit + defer func() { + wg.Wait() + + if listener != nil { + _ = listener.Close() + } + }() + // --------------------------------------------------------------- + + // trigger the OnServe hook and start the tcp listener + serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error { + handler, err := e.Router.BuildMux() + if err != nil { + return err + } + + e.Server.Handler = handler + + addr := e.Server.Addr + + // fallback similar to the std Server.ListenAndServe/ListenAndServeTLS + if addr == "" { + if config.HttpsAddr != "" { + addr = ":https" + } else { + addr = ":http" + } + } + + var lnErr error + + listener, lnErr = net.Listen("tcp", addr) + + return lnErr + }) + if serveHookErr != nil { + return serveHookErr } if config.ShowStartBanner { @@ -198,80 +267,32 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, addr)) } - // WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately. - // Note that the WaitGroup would not do anything if the app.OnTerminate() hook isn't triggered. - var wg sync.WaitGroup - - // try to gracefully shutdown the server on app termination - app.OnTerminate().Add(func(e *core.TerminateEvent) error { - cancelBaseCtx() - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - wg.Add(1) - server.Shutdown(ctx) - if e.IsRestart { - // wait for execve and other handlers up to 5 seconds before exit - time.AfterFunc(5*time.Second, func() { - wg.Done() - }) - } else { - wg.Done() - } - - return nil - }) - - // wait for the graceful shutdown to complete before exit - defer wg.Wait() - - // --- - // @todo consider removing the server return value because it is - // not really useful when combined with the blocking serve calls - // --- - - // start HTTPS server + var serveErr error if config.HttpsAddr != "" { - // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version if config.HttpAddr != "" { + // start an additional HTTP server for redirecting the traffic to the HTTPS version go http.ListenAndServe(config.HttpAddr, certManager.HTTPHandler(nil)) } - return server, server.ListenAndServeTLS("", "") + // start HTTPS server + serveErr = server.ServeTLS(listener, "", "") + } else { + // OR start HTTP server + serveErr = server.Serve(listener) } - - // OR start HTTP server - return server, server.ListenAndServe() -} - -type migrationsConnection struct { - DB *dbx.DB - MigrationsList migrate.MigrationsList -} - -func runMigrations(app core.App) error { - connections := []migrationsConnection{ - { - DB: app.DB(), - MigrationsList: migrations.AppMigrations, - }, - { - DB: app.LogsDB(), - MigrationsList: logs.LogsMigrations, - }, - } - - for _, c := range connections { - runner, err := migrate.NewRunner(c.DB, c.MigrationsList) - if err != nil { - return err - } - - if _, err := runner.Up(); err != nil { - return err - } + if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { + return serveErr } return nil } + +type serverErrorLogWriter struct { + app core.App +} + +func (s *serverErrorLogWriter) Write(p []byte) (int, error) { + s.app.Logger().Debug(strings.TrimSpace(string(p))) + + return len(p), nil +} diff --git a/apis/settings.go b/apis/settings.go index e5e96ddf..6ef31acd 100644 --- a/apis/settings.go +++ b/apis/settings.go @@ -4,136 +4,121 @@ import ( "net/http" validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models/settings" + "github.com/pocketbase/pocketbase/tools/router" ) // bindSettingsApi registers the settings api endpoints. -func bindSettingsApi(app core.App, rg *echo.Group) { - api := settingsApi{app: app} - - subGroup := rg.Group("/settings", ActivityLogger(app), RequireAdminAuth()) - subGroup.GET("", api.list) - subGroup.PATCH("", api.set) - subGroup.POST("/test/s3", api.testS3) - subGroup.POST("/test/email", api.testEmail) - subGroup.POST("/apple/generate-client-secret", api.generateAppleClientSecret) +func bindSettingsApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) { + subGroup := rg.Group("/settings").Bind(RequireSuperuserAuth()) + subGroup.GET("", settingsList) + subGroup.PATCH("", settingsSet) + subGroup.POST("/test/s3", settingsTestS3) + subGroup.POST("/test/email", settingsTestEmail) + subGroup.POST("/apple/generate-client-secret", settingsGenerateAppleClientSecret) } -type settingsApi struct { - app core.App -} - -func (api *settingsApi) list(c echo.Context) error { - settings, err := api.app.Settings().RedactClone() +func settingsList(e *core.RequestEvent) error { + clone, err := e.App.Settings().Clone() if err != nil { - return NewBadRequestError("", err) + return e.InternalServerError("", err) } - event := new(core.SettingsListEvent) - event.HttpContext = c - event.RedactedSettings = settings + event := new(core.SettingsListRequestEvent) + event.RequestEvent = e + event.Settings = clone - return api.app.OnSettingsListRequest().Trigger(event, func(e *core.SettingsListEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - return e.HttpContext.JSON(http.StatusOK, e.RedactedSettings) + return e.App.OnSettingsListRequest().Trigger(event, func(e *core.SettingsListRequestEvent) error { + return e.JSON(http.StatusOK, e.Settings) }) } -func (api *settingsApi) set(c echo.Context) error { - form := forms.NewSettingsUpsert(api.app) +func settingsSet(e *core.RequestEvent) error { + event := new(core.SettingsUpdateRequestEvent) + event.RequestEvent = e - // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) + if clone, err := e.App.Settings().Clone(); err == nil { + event.OldSettings = clone + } else { + return e.BadRequestError("", err) } - event := new(core.SettingsUpdateEvent) - event.HttpContext = c - event.OldSettings = api.app.Settings() + if clone, err := e.App.Settings().Clone(); err == nil { + event.NewSettings = clone + } else { + return e.BadRequestError("", err) + } - // update the settings - return form.Submit(func(next forms.InterceptorNextFunc[*settings.Settings]) forms.InterceptorNextFunc[*settings.Settings] { - return func(s *settings.Settings) error { - event.NewSettings = s + if err := e.BindBody(&event.NewSettings); err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) + } - return api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error { - if err := next(e.NewSettings); err != nil { - return NewBadRequestError("An error occurred while submitting the form.", err) - } - - return api.app.OnSettingsAfterUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error { - if e.HttpContext.Response().Committed { - return nil - } - - redactedSettings, err := api.app.Settings().RedactClone() - if err != nil { - return NewBadRequestError("", err) - } - - return e.HttpContext.JSON(http.StatusOK, redactedSettings) - }) - }) + return e.App.OnSettingsUpdateRequest().Trigger(event, func(e *core.SettingsUpdateRequestEvent) error { + err := e.App.Save(e.NewSettings) + if err != nil { + return e.BadRequestError("An error occurred while saving the new settings.", err) } + + appSettings, err := e.App.Settings().Clone() + if err != nil { + return e.InternalServerError("Failed to clone app settings.", err) + } + + return e.JSON(http.StatusOK, appSettings) }) } -func (api *settingsApi) testS3(c echo.Context) error { - form := forms.NewTestS3Filesystem(api.app) +func settingsTestS3(e *core.RequestEvent) error { + form := forms.NewTestS3Filesystem(e.App) // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) + if err := e.BindBody(form); err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) } // send if err := form.Submit(); err != nil { // form error if fErr, ok := err.(validation.Errors); ok { - return NewBadRequestError("Failed to test the S3 filesystem.", fErr) + return e.BadRequestError("Failed to test the S3 filesystem.", fErr) } // mailer error - return NewBadRequestError("Failed to test the S3 filesystem. Raw error: \n"+err.Error(), nil) + return e.BadRequestError("Failed to test the S3 filesystem. Raw error: \n"+err.Error(), nil) } - return c.NoContent(http.StatusNoContent) + return e.NoContent(http.StatusNoContent) } -func (api *settingsApi) testEmail(c echo.Context) error { - form := forms.NewTestEmailSend(api.app) +func settingsTestEmail(e *core.RequestEvent) error { + form := forms.NewTestEmailSend(e.App) // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) + if err := e.BindBody(form); err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) } // send if err := form.Submit(); err != nil { // form error if fErr, ok := err.(validation.Errors); ok { - return NewBadRequestError("Failed to send the test email.", fErr) + return e.BadRequestError("Failed to send the test email.", fErr) } // mailer error - return NewBadRequestError("Failed to send the test email. Raw error: \n"+err.Error(), nil) + return e.BadRequestError("Failed to send the test email. Raw error: \n"+err.Error(), nil) } - return c.NoContent(http.StatusNoContent) + return e.NoContent(http.StatusNoContent) } -func (api *settingsApi) generateAppleClientSecret(c echo.Context) error { - form := forms.NewAppleClientSecretCreate(api.app) +func settingsGenerateAppleClientSecret(e *core.RequestEvent) error { + form := forms.NewAppleClientSecretCreate(e.App) // load request - if err := c.Bind(form); err != nil { - return NewBadRequestError("An error occurred while loading the submitted data.", err) + if err := e.BindBody(form); err != nil { + return e.BadRequestError("An error occurred while loading the submitted data.", err) } // generate @@ -141,14 +126,14 @@ func (api *settingsApi) generateAppleClientSecret(c echo.Context) error { if err != nil { // form error if fErr, ok := err.(validation.Errors); ok { - return NewBadRequestError("Invalid client secret data.", fErr) + return e.BadRequestError("Invalid client secret data.", fErr) } // secret generation error - return NewBadRequestError("Failed to generate client secret. Raw error: \n"+err.Error(), nil) + return e.BadRequestError("Failed to generate client secret. Raw error: \n"+err.Error(), nil) } - return c.JSON(http.StatusOK, map[string]any{ + return e.JSON(http.StatusOK, map[string]string{ "secret": secret, }) } diff --git a/apis/settings_test.go b/apis/settings_test.go index 0ec4fbd2..7a2c0e1b 100644 --- a/apis/settings_test.go +++ b/apis/settings_test.go @@ -6,14 +6,11 @@ import ( "crypto/rand" "crypto/x509" "encoding/pem" - "errors" "fmt" "net/http" "strings" "testing" - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" ) @@ -24,26 +21,28 @@ func TestSettingsList(t *testing.T) { { Name: "unauthorized", Method: http.MethodGet, - Url: "/api/settings", + URL: "/api/settings", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodGet, - Url: "/api/settings", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/settings", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin", + Name: "authorized as superuser", Method: http.MethodGet, - Url: "/api/settings", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/settings", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -52,44 +51,10 @@ func TestSettingsList(t *testing.T) { `"smtp":{`, `"s3":{`, `"backups":{`, - `"adminAuthToken":{`, - `"adminPasswordResetToken":{`, - `"adminFileToken":{`, - `"recordAuthToken":{`, - `"recordPasswordResetToken":{`, - `"recordEmailChangeToken":{`, - `"recordVerificationToken":{`, - `"recordFileToken":{`, - `"emailAuth":{`, - `"googleAuth":{`, - `"facebookAuth":{`, - `"githubAuth":{`, - `"gitlabAuth":{`, - `"twitterAuth":{`, - `"discordAuth":{`, - `"microsoftAuth":{`, - `"spotifyAuth":{`, - `"kakaoAuth":{`, - `"twitchAuth":{`, - `"stravaAuth":{`, - `"giteeAuth":{`, - `"livechatAuth":{`, - `"giteaAuth":{`, - `"oidcAuth":{`, - `"oidc2Auth":{`, - `"oidc3Auth":{`, - `"appleAuth":{`, - `"instagramAuth":{`, - `"vkAuth":{`, - `"yandexAuth":{`, - `"patreonAuth":{`, - `"mailcowAuth":{`, - `"bitbucketAuth":{`, - `"planningcenterAuth":{`, - `"secret":"******"`, - `"clientSecret":"******"`, + `"batch":{`, }, ExpectedEvents: map[string]int{ + "*": 0, "OnSettingsListRequest": 1, }, }, @@ -103,35 +68,41 @@ func TestSettingsList(t *testing.T) { func TestSettingsSet(t *testing.T) { t.Parallel() - validData := `{"meta":{"appName":"update_test"}}` + validData := `{ + "meta":{"appName":"update_test"}, + "s3":{"secret": "s3_secret"}, + "backups":{"s3":{"secret":"backups_s3_secret"}} + }` scenarios := []tests.ApiScenario{ { Name: "unauthorized", Method: http.MethodPatch, - Url: "/api/settings", + URL: "/api/settings", Body: strings.NewReader(validData), ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPatch, - Url: "/api/settings", + URL: "/api/settings", Body: strings.NewReader(validData), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin submitting empty data", + Name: "authorized as superuser submitting empty data", Method: http.MethodPatch, - Url: "/api/settings", + URL: "/api/settings", Body: strings.NewReader(``), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -140,71 +111,46 @@ func TestSettingsSet(t *testing.T) { `"smtp":{`, `"s3":{`, `"backups":{`, - `"adminAuthToken":{`, - `"adminPasswordResetToken":{`, - `"adminFileToken":{`, - `"recordAuthToken":{`, - `"recordPasswordResetToken":{`, - `"recordEmailChangeToken":{`, - `"recordVerificationToken":{`, - `"recordFileToken":{`, - `"emailAuth":{`, - `"googleAuth":{`, - `"facebookAuth":{`, - `"githubAuth":{`, - `"gitlabAuth":{`, - `"discordAuth":{`, - `"microsoftAuth":{`, - `"spotifyAuth":{`, - `"kakaoAuth":{`, - `"twitchAuth":{`, - `"stravaAuth":{`, - `"giteeAuth":{`, - `"livechatAuth":{`, - `"giteaAuth":{`, - `"oidcAuth":{`, - `"oidc2Auth":{`, - `"oidc3Auth":{`, - `"appleAuth":{`, - `"instagramAuth":{`, - `"vkAuth":{`, - `"yandexAuth":{`, - `"patreonAuth":{`, - `"mailcowAuth":{`, - `"bitbucketAuth":{`, - `"planningcenterAuth":{`, - `"secret":"******"`, - `"clientSecret":"******"`, - `"appName":"acme_test"`, + `"batch":{`, }, ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnSettingsBeforeUpdateRequest": 1, - "OnSettingsAfterUpdateRequest": 1, + "*": 0, + "OnSettingsUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnSettingsReload": 1, }, }, { - Name: "authorized as admin submitting invalid data", + Name: "authorized as superuser submitting invalid data", Method: http.MethodPatch, - Url: "/api/settings", + URL: "/api/settings", Body: strings.NewReader(`{"meta":{"appName":""}}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"meta":{"appName":{"code":"validation_required"`, }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnModelUpdate": 1, + "OnModelAfterUpdateError": 1, + "OnModelValidate": 1, + "OnSettingsUpdateRequest": 1, + }, }, { - Name: "authorized as admin submitting valid data", + Name: "authorized as superuser submitting valid data", Method: http.MethodPatch, - Url: "/api/settings", + URL: "/api/settings", Body: strings.NewReader(validData), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -213,71 +159,21 @@ func TestSettingsSet(t *testing.T) { `"smtp":{`, `"s3":{`, `"backups":{`, - `"adminAuthToken":{`, - `"adminPasswordResetToken":{`, - `"adminFileToken":{`, - `"recordAuthToken":{`, - `"recordPasswordResetToken":{`, - `"recordEmailChangeToken":{`, - `"recordVerificationToken":{`, - `"recordFileToken":{`, - `"emailAuth":{`, - `"googleAuth":{`, - `"facebookAuth":{`, - `"githubAuth":{`, - `"gitlabAuth":{`, - `"twitterAuth":{`, - `"discordAuth":{`, - `"microsoftAuth":{`, - `"spotifyAuth":{`, - `"kakaoAuth":{`, - `"twitchAuth":{`, - `"stravaAuth":{`, - `"giteeAuth":{`, - `"livechatAuth":{`, - `"giteaAuth":{`, - `"oidcAuth":{`, - `"oidc2Auth":{`, - `"oidc3Auth":{`, - `"appleAuth":{`, - `"instagramAuth":{`, - `"vkAuth":{`, - `"yandexAuth":{`, - `"patreonAuth":{`, - `"mailcowAuth":{`, - `"bitbucketAuth":{`, - `"planningcenterAuth":{`, - `"secret":"******"`, - `"clientSecret":"******"`, + `"batch":{`, `"appName":"update_test"`, }, + NotExpectedContent: []string{ + "secret", + "password", + }, ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnSettingsBeforeUpdateRequest": 1, - "OnSettingsAfterUpdateRequest": 1, - }, - }, - { - Name: "OnSettingsAfterUpdateRequest error response", - Method: http.MethodPatch, - Url: "/api/settings", - Body: strings.NewReader(validData), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - }, - BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { - app.OnSettingsAfterUpdateRequest().Add(func(e *core.SettingsUpdateEvent) error { - return errors.New("error") - }) - }, - ExpectedStatus: 400, - ExpectedContent: []string{`"data":{}`}, - ExpectedEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnSettingsBeforeUpdateRequest": 1, - "OnSettingsAfterUpdateRequest": 1, + "*": 0, + "OnSettingsUpdateRequest": 1, + "OnModelUpdate": 1, + "OnModelUpdateExecute": 1, + "OnModelAfterUpdateSuccess": 1, + "OnModelValidate": 1, + "OnSettingsReload": 1, }, }, } @@ -294,59 +190,64 @@ func TestSettingsTestS3(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/settings/test/s3", + URL: "/api/settings/test/s3", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/settings/test/s3", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/settings/test/s3", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (missing body + no s3)", + Name: "authorized as superuser (missing body + no s3)", Method: http.MethodPost, - Url: "/api/settings/test/s3", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + URL: "/api/settings/test/s3", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"filesystem":{`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (invalid filesystem)", + Name: "authorized as superuser (invalid filesystem)", Method: http.MethodPost, - Url: "/api/settings/test/s3", + URL: "/api/settings/test/s3", Body: strings.NewReader(`{"filesystem":"invalid"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"filesystem":{`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (valid filesystem and no s3)", + Name: "authorized as superuser (valid filesystem and no s3)", Method: http.MethodPost, - Url: "/api/settings/test/s3", + URL: "/api/settings/test/s3", Body: strings.NewReader(`{"filesystem":"storage"}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{}`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } @@ -362,156 +263,199 @@ func TestSettingsTestEmail(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{ "template": "verification", "email": "test@example.com" }`), ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{ "template": "verification", "email": "test@example.com" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (invalid body)", + Name: "authorized as superuser (invalid body)", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (empty json)", + Name: "authorized as superuser (empty json)", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ `"email":{"code":"validation_required"`, `"template":{"code":"validation_required"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (verifiation template)", + Name: "authorized as superuser (verifiation template)", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{ "template": "verification", "email": "test@example.com" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - if app.TestMailer.TotalSend != 1 { - t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend) + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 1 { + t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend()) } - if len(app.TestMailer.LastMessage.To) != 1 { - t.Fatalf("[verification] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To) + if len(app.TestMailer.LastMessage().To) != 1 { + t.Fatalf("[verification] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To) } - if app.TestMailer.LastMessage.To[0].Address != "test@example.com" { - t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address) + if app.TestMailer.LastMessage().To[0].Address != "test@example.com" { + t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage().To[0].Address) } - if !strings.Contains(app.TestMailer.LastMessage.HTML, "Verify") { - t.Fatalf("[verification] Expected to sent a verification email, got \n%v\n%v", app.TestMailer.LastMessage.Subject, app.TestMailer.LastMessage.HTML) + if !strings.Contains(app.TestMailer.LastMessage().HTML, "Verify") { + t.Fatalf("[verification] Expected to sent a verification email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML) } }, ExpectedStatus: 204, ExpectedContent: []string{}, ExpectedEvents: map[string]int{ - "OnMailerBeforeRecordVerificationSend": 1, - "OnMailerAfterRecordVerificationSend": 1, + "*": 0, + "OnMailerSend": 1, + "OnMailerRecordVerificationSend": 1, }, }, { - Name: "authorized as admin (password reset template)", + Name: "authorized as superuser (password reset template)", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{ "template": "password-reset", "email": "test@example.com" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - if app.TestMailer.TotalSend != 1 { - t.Fatalf("[password-reset] Expected 1 sent email, got %d", app.TestMailer.TotalSend) + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 1 { + t.Fatalf("[password-reset] Expected 1 sent email, got %d", app.TestMailer.TotalSend()) } - if len(app.TestMailer.LastMessage.To) != 1 { - t.Fatalf("[password-reset] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To) + if len(app.TestMailer.LastMessage().To) != 1 { + t.Fatalf("[password-reset] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To) } - if app.TestMailer.LastMessage.To[0].Address != "test@example.com" { - t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address) + if app.TestMailer.LastMessage().To[0].Address != "test@example.com" { + t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage().To[0].Address) } - if !strings.Contains(app.TestMailer.LastMessage.HTML, "Reset password") { - t.Fatalf("[password-reset] Expected to sent a password-reset email, got \n%v\n%v", app.TestMailer.LastMessage.Subject, app.TestMailer.LastMessage.HTML) + if !strings.Contains(app.TestMailer.LastMessage().HTML, "Reset password") { + t.Fatalf("[password-reset] Expected to sent a password-reset email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML) } }, ExpectedStatus: 204, ExpectedContent: []string{}, ExpectedEvents: map[string]int{ - "OnMailerBeforeRecordResetPasswordSend": 1, - "OnMailerAfterRecordResetPasswordSend": 1, + "*": 0, + "OnMailerSend": 1, + "OnMailerRecordPasswordResetSend": 1, }, }, { - Name: "authorized as admin (email change)", + Name: "authorized as superuser (email change)", Method: http.MethodPost, - Url: "/api/settings/test/email", + URL: "/api/settings/test/email", Body: strings.NewReader(`{ "template": "email-change", "email": "test@example.com" }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, - AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { - if app.TestMailer.TotalSend != 1 { - t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend) + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 1 { + t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend()) } - if len(app.TestMailer.LastMessage.To) != 1 { - t.Fatalf("[email-change] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To) + if len(app.TestMailer.LastMessage().To) != 1 { + t.Fatalf("[email-change] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To) } - if app.TestMailer.LastMessage.To[0].Address != "test@example.com" { - t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address) + if app.TestMailer.LastMessage().To[0].Address != "test@example.com" { + t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage().To[0].Address) } - if !strings.Contains(app.TestMailer.LastMessage.HTML, "Confirm new email") { - t.Fatalf("[email-change] Expected to sent a confirm new email email, got \n%v\n%v", app.TestMailer.LastMessage.Subject, app.TestMailer.LastMessage.HTML) + if !strings.Contains(app.TestMailer.LastMessage().HTML, "Confirm new email") { + t.Fatalf("[email-change] Expected to sent a confirm new email email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML) } }, ExpectedStatus: 204, ExpectedContent: []string{}, ExpectedEvents: map[string]int{ - "OnMailerBeforeRecordChangeEmailSend": 1, - "OnMailerAfterRecordChangeEmailSend": 1, + "*": 0, + "OnMailerSend": 1, + "OnMailerRecordEmailChangeSend": 1, + }, + }, + { + Name: "authorized as superuser (otp)", + Method: http.MethodPost, + URL: "/api/settings/test/email", + Body: strings.NewReader(`{ + "template": "otp", + "email": "test@example.com" + }`), + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + if app.TestMailer.TotalSend() != 1 { + t.Fatalf("[otp] Expected 1 sent email, got %d", app.TestMailer.TotalSend()) + } + + if len(app.TestMailer.LastMessage().To) != 1 { + t.Fatalf("[otp] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To) + } + + if app.TestMailer.LastMessage().To[0].Address != "test@example.com" { + t.Fatalf("[otp] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage().To[0].Address) + } + + if !strings.Contains(app.TestMailer.LastMessage().HTML, "one-time password") { + t.Fatalf("[otp] Expected to sent OTP email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML) + } + }, + ExpectedStatus: 204, + ExpectedContent: []string{}, + ExpectedEvents: map[string]int{ + "*": 0, + "OnMailerSend": 1, + "OnMailerRecordOTPSend": 1, }, }, } @@ -545,38 +489,41 @@ func TestGenerateAppleClientSecret(t *testing.T) { { Name: "unauthorized", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", + URL: "/api/settings/apple/generate-client-secret", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as auth record", + Name: "authorized as regular user", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", + URL: "/api/settings/apple/generate-client-secret", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", }, - ExpectedStatus: 401, + ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (invalid body)", + Name: "authorized as superuser (invalid body)", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", + URL: "/api/settings/apple/generate-client-secret", Body: strings.NewReader(`{`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (empty json)", + Name: "authorized as superuser (empty json)", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", + URL: "/api/settings/apple/generate-client-secret", Body: strings.NewReader(`{}`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ @@ -586,11 +533,12 @@ func TestGenerateAppleClientSecret(t *testing.T) { `"privateKey":{"code":"validation_required"`, `"duration":{"code":"validation_required"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (invalid data)", + Name: "authorized as superuser (invalid data)", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", + URL: "/api/settings/apple/generate-client-secret", Body: strings.NewReader(`{ "clientId": "", "teamId": "123456789", @@ -598,8 +546,8 @@ func TestGenerateAppleClientSecret(t *testing.T) { "privateKey": "invalid", "duration": -1 }`), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 400, ExpectedContent: []string{ @@ -609,11 +557,12 @@ func TestGenerateAppleClientSecret(t *testing.T) { `"privateKey":{"code":"validation_match_invalid"`, `"duration":{"code":"validation_min_greater_equal_than_required"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, { - Name: "authorized as admin (valid data)", + Name: "authorized as superuser (valid data)", Method: http.MethodPost, - Url: "/api/settings/apple/generate-client-secret", + URL: "/api/settings/apple/generate-client-secret", Body: strings.NewReader(fmt.Sprintf(`{ "clientId": "123", "teamId": "1234567890", @@ -621,13 +570,14 @@ func TestGenerateAppleClientSecret(t *testing.T) { "privateKey": %q, "duration": 1 }`, privatePem)), - RequestHeaders: map[string]string{ - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", + Headers: map[string]string{ + "Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg", }, ExpectedStatus: 200, ExpectedContent: []string{ `"secret":"`, }, + ExpectedEvents: map[string]int{"*": 0}, }, } diff --git a/cmd/admin.go b/cmd/admin.go deleted file mode 100644 index 3cac1a04..00000000 --- a/cmd/admin.go +++ /dev/null @@ -1,141 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - - "github.com/fatih/color" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/spf13/cobra" -) - -// NewAdminCommand creates and returns new command for managing -// admin accounts (create, update, delete). -func NewAdminCommand(app core.App) *cobra.Command { - command := &cobra.Command{ - Use: "admin", - Short: "Manages admin accounts", - } - - command.AddCommand(adminCreateCommand(app)) - command.AddCommand(adminUpdateCommand(app)) - command.AddCommand(adminDeleteCommand(app)) - - return command -} - -func adminCreateCommand(app core.App) *cobra.Command { - command := &cobra.Command{ - Use: "create", - Example: "admin create test@example.com 1234567890", - Short: "Creates a new admin account", - SilenceUsage: true, - RunE: func(command *cobra.Command, args []string) error { - if len(args) != 2 { - return errors.New("Missing email and password arguments.") - } - - if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { - return errors.New("Missing or invalid email address.") - } - - if len(args[1]) < 8 { - return errors.New("The password must be at least 8 chars long.") - } - - admin := &models.Admin{} - admin.Email = args[0] - admin.SetPassword(args[1]) - - if !app.Dao().HasTable(admin.TableName()) { - return errors.New("Migration are not initialized yet. Please run 'migrate up' and try again.") - } - - if err := app.Dao().SaveAdmin(admin); err != nil { - return fmt.Errorf("Failed to create new admin account: %v", err) - } - - color.Green("Successfully created new admin %s!", admin.Email) - return nil - }, - } - - return command -} - -func adminUpdateCommand(app core.App) *cobra.Command { - command := &cobra.Command{ - Use: "update", - Example: "admin update test@example.com 1234567890", - Short: "Changes the password of a single admin account", - SilenceUsage: true, - RunE: func(command *cobra.Command, args []string) error { - if len(args) != 2 { - return errors.New("Missing email and password arguments.") - } - - if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { - return errors.New("Missing or invalid email address.") - } - - if len(args[1]) < 8 { - return errors.New("The new password must be at least 8 chars long.") - } - - if !app.Dao().HasTable((&models.Admin{}).TableName()) { - return errors.New("Migration are not initialized yet. Please run 'migrate up' and try again.") - } - - admin, err := app.Dao().FindAdminByEmail(args[0]) - if err != nil { - return fmt.Errorf("Admin with email %s doesn't exist.", args[0]) - } - - admin.SetPassword(args[1]) - - if err := app.Dao().SaveAdmin(admin); err != nil { - return fmt.Errorf("Failed to change admin %s password: %v", admin.Email, err) - } - - color.Green("Successfully changed admin %s password!", admin.Email) - return nil - }, - } - - return command -} - -func adminDeleteCommand(app core.App) *cobra.Command { - command := &cobra.Command{ - Use: "delete", - Example: "admin delete test@example.com", - Short: "Deletes an existing admin account", - SilenceUsage: true, - RunE: func(command *cobra.Command, args []string) error { - if len(args) == 0 || args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { - return errors.New("Invalid or missing email address.") - } - - if !app.Dao().HasTable((&models.Admin{}).TableName()) { - return errors.New("Migration are not initialized yet. Please run 'migrate up' and try again.") - } - - admin, err := app.Dao().FindAdminByEmail(args[0]) - if err != nil { - color.Yellow("Admin %s is already deleted.", args[0]) - return nil - } - - if err := app.Dao().DeleteAdmin(admin); err != nil { - return fmt.Errorf("Failed to delete admin %s: %v", admin.Email, err) - } - - color.Green("Successfully deleted admin %s!", admin.Email) - return nil - }, - } - - return command -} diff --git a/cmd/admin_test.go b/cmd/admin_test.go deleted file mode 100644 index 7e2dd6c9..00000000 --- a/cmd/admin_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package cmd_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/cmd" - "github.com/pocketbase/pocketbase/tests" -) - -func TestAdminCreateCommand(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - email string - password string - expectError bool - }{ - { - "empty email and password", - "", - "", - true, - }, - { - "empty email", - "", - "1234567890", - true, - }, - { - "invalid email", - "invalid", - "1234567890", - true, - }, - { - "duplicated email", - "test@example.com", - "1234567890", - true, - }, - { - "empty password", - "test@example.com", - "", - true, - }, - { - "short password", - "test_new@example.com", - "1234567", - true, - }, - { - "valid email and password", - "test_new@example.com", - "12345678", - false, - }, - } - - for _, s := range scenarios { - command := cmd.NewAdminCommand(app) - command.SetArgs([]string{"create", s.email, s.password}) - - err := command.Execute() - - hasErr := err != nil - if s.expectError != hasErr { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - } - - if hasErr { - continue - } - - // check whether the admin account was actually created - admin, err := app.Dao().FindAdminByEmail(s.email) - if err != nil { - t.Errorf("[%s] Failed to fetch created admin %s: %v", s.name, s.email, err) - } else if !admin.ValidatePassword(s.password) { - t.Errorf("[%s] Expected the admin password to match", s.name) - } - } -} - -func TestAdminUpdateCommand(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - email string - password string - expectError bool - }{ - { - "empty email and password", - "", - "", - true, - }, - { - "empty email", - "", - "1234567890", - true, - }, - { - "invalid email", - "invalid", - "1234567890", - true, - }, - { - "nonexisting admin", - "test_missing@example.com", - "1234567890", - true, - }, - { - "empty password", - "test@example.com", - "", - true, - }, - { - "short password", - "test_new@example.com", - "1234567", - true, - }, - { - "valid email and password", - "test@example.com", - "12345678", - false, - }, - } - - for _, s := range scenarios { - command := cmd.NewAdminCommand(app) - command.SetArgs([]string{"update", s.email, s.password}) - - err := command.Execute() - - hasErr := err != nil - if s.expectError != hasErr { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - } - - if hasErr { - continue - } - - // check whether the admin password was actually changed - admin, err := app.Dao().FindAdminByEmail(s.email) - if err != nil { - t.Errorf("[%s] Failed to fetch admin %s: %v", s.name, s.email, err) - } else if !admin.ValidatePassword(s.password) { - t.Errorf("[%s] Expected the admin password to match", s.name) - } - } -} - -func TestAdminDeleteCommand(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - email string - expectError bool - }{ - { - "empty email", - "", - true, - }, - { - "invalid email", - "invalid", - true, - }, - { - "nonexisting admin", - "test_missing@example.com", - false, - }, - { - "existing admin", - "test@example.com", - false, - }, - } - - for _, s := range scenarios { - command := cmd.NewAdminCommand(app) - command.SetArgs([]string{"delete", s.email}) - - err := command.Execute() - - hasErr := err != nil - if s.expectError != hasErr { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - } - - if hasErr { - continue - } - - // check whether the admin account was actually deleted - if _, err := app.Dao().FindAdminByEmail(s.email); err == nil { - t.Errorf("[%s] Expected the admin account to be deleted", s.name) - } - } -} diff --git a/cmd/serve.go b/cmd/serve.go index 99ff0859..8ebc45b3 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -15,6 +15,7 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { var allowedOrigins []string var httpAddr string var httpsAddr string + var dashboardPath string command := &cobra.Command{ Use: "serve [domain(s)]", @@ -36,9 +37,10 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { } } - _, err := apis.Serve(app, apis.ServeConfig{ + err := apis.Serve(app, apis.ServeConfig{ HttpAddr: httpAddr, HttpsAddr: httpsAddr, + DashboardPath: dashboardPath, ShowStartBanner: showStartBanner, AllowedOrigins: allowedOrigins, CertificateDomains: args, @@ -73,5 +75,12 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { "TCP address to listen for the HTTPS server\n(if domain args are specified - default to 0.0.0.0:443, otherwise - default to empty string, aka. no TLS)\nThe incoming HTTP traffic also will be auto redirected to the HTTPS version", ) + command.PersistentFlags().StringVar( + &dashboardPath, + "dashboard", + "/_/{path...}", + "The route path to the superusers dashboard; must include the '{path...}' wildcard parameter", + ) + return command } diff --git a/cmd/superuser.go b/cmd/superuser.go new file mode 100644 index 00000000..6c8d8e1b --- /dev/null +++ b/cmd/superuser.go @@ -0,0 +1,166 @@ +package cmd + +import ( + "errors" + "fmt" + + "github.com/fatih/color" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/spf13/cobra" +) + +// NewSuperuserCommand creates and returns new command for managing +// superuser accounts (create, update, delete). +func NewSuperuserCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "superuser", + Short: "Manages superuser accounts", + } + + command.AddCommand(superuserUpsertCommand(app)) + command.AddCommand(superuserCreateCommand(app)) + command.AddCommand(superuserUpdateCommand(app)) + command.AddCommand(superuserDeleteCommand(app)) + + return command +} + +func superuserUpsertCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "upsert", + Example: "superuser upsert test@example.com 1234567890", + Short: "Creates, or updates if email exists, a single superuser account", + SilenceUsage: true, + RunE: func(command *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("Missing email and password arguments.") + } + + if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { + return errors.New("Missing or invalid email address.") + } + + superusersCol, err := app.FindCachedCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + return fmt.Errorf("Failed to fetch %q collection: %w.", core.CollectionNameSuperusers, err) + } + + superuser, err := app.FindAuthRecordByEmail(superusersCol, args[0]) + if err != nil { + superuser = core.NewRecord(superusersCol) + } + + superuser.SetEmail(args[0]) + superuser.SetPassword(args[1]) + + if err := app.Save(superuser); err != nil { + return fmt.Errorf("Failed to upsert superuser account: %w.", err) + } + + color.Green("Successfully saved superuser %q!", superuser.Email()) + return nil + }, + } + + return command +} + +func superuserCreateCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "create", + Example: "superuser create test@example.com 1234567890", + Short: "Creates a new superuser account", + SilenceUsage: true, + RunE: func(command *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("Missing email and password arguments.") + } + + if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { + return errors.New("Missing or invalid email address.") + } + + superusersCol, err := app.FindCachedCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + return fmt.Errorf("Failed to fetch %q collection: %w.", core.CollectionNameSuperusers, err) + } + + superuser := core.NewRecord(superusersCol) + superuser.SetEmail(args[0]) + superuser.SetPassword(args[1]) + + if err := app.Save(superuser); err != nil { + return fmt.Errorf("Failed to create new superuser account: %w.", err) + } + + color.Green("Successfully created new superuser %q!", superuser.Email()) + return nil + }, + } + + return command +} + +func superuserUpdateCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "update", + Example: "superuser update test@example.com 1234567890", + Short: "Changes the password of a single superuser account", + SilenceUsage: true, + RunE: func(command *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("Missing email and password arguments.") + } + + if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { + return errors.New("Missing or invalid email address.") + } + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, args[0]) + if err != nil { + return fmt.Errorf("Superuser with email %q doesn't exist.", args[0]) + } + + superuser.SetPassword(args[1]) + + if err := app.Save(superuser); err != nil { + return fmt.Errorf("Failed to change superuser %q password: %w.", superuser.Email(), err) + } + + color.Green("Successfully changed superuser %q password!", superuser.Email()) + return nil + }, + } + + return command +} + +func superuserDeleteCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "delete", + Example: "superuser delete test@example.com", + Short: "Deletes an existing superuser account", + SilenceUsage: true, + RunE: func(command *cobra.Command, args []string) error { + if len(args) == 0 || args[0] == "" || is.EmailFormat.Validate(args[0]) != nil { + return errors.New("Invalid or missing email address.") + } + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, args[0]) + if err != nil { + color.Yellow("Superuser %q is missing or already deleted.", args[0]) + return nil + } + + if err := app.Delete(superuser); err != nil { + return fmt.Errorf("Failed to delete superuser %q: %w.", superuser.Email(), err) + } + + color.Green("Successfully deleted superuser %q!", superuser.Email()) + return nil + }, + } + + return command +} diff --git a/cmd/superuser_test.go b/cmd/superuser_test.go new file mode 100644 index 00000000..976279a5 --- /dev/null +++ b/cmd/superuser_test.go @@ -0,0 +1,310 @@ +package cmd_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/cmd" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestSuperuserUpsertCommand(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + email string + password string + expectError bool + }{ + { + "empty email and password", + "", + "", + true, + }, + { + "empty email", + "", + "1234567890", + true, + }, + { + "invalid email", + "invalid", + "1234567890", + true, + }, + { + "empty password", + "test@example.com", + "", + true, + }, + { + "short password", + "test_new@example.com", + "1234567", + true, + }, + { + "existing user", + "test@example.com", + "1234567890!", + false, + }, + { + "new user", + "test_new@example.com", + "1234567890!", + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + command := cmd.NewSuperuserCommand(app) + command.SetArgs([]string{"upsert", s.email, s.password}) + + err := command.Execute() + + hasErr := err != nil + if s.expectError != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + // check whether the superuser account was actually upserted + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, s.email) + if err != nil { + t.Fatalf("Failed to fetch superuser %s: %v", s.email, err) + } else if !superuser.ValidatePassword(s.password) { + t.Fatal("Expected the superuser password to match") + } + }) + } +} + +func TestSuperuserCreateCommand(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + email string + password string + expectError bool + }{ + { + "empty email and password", + "", + "", + true, + }, + { + "empty email", + "", + "1234567890", + true, + }, + { + "invalid email", + "invalid", + "1234567890", + true, + }, + { + "duplicated email", + "test@example.com", + "1234567890", + true, + }, + { + "empty password", + "test@example.com", + "", + true, + }, + { + "short password", + "test_new@example.com", + "1234567", + true, + }, + { + "valid email and password", + "test_new@example.com", + "12345678", + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + command := cmd.NewSuperuserCommand(app) + command.SetArgs([]string{"create", s.email, s.password}) + + err := command.Execute() + + hasErr := err != nil + if s.expectError != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + // check whether the superuser account was actually created + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, s.email) + if err != nil { + t.Fatalf("Failed to fetch created superuser %s: %v", s.email, err) + } else if !superuser.ValidatePassword(s.password) { + t.Fatal("Expected the superuser password to match") + } + }) + } +} + +func TestSuperuserUpdateCommand(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + email string + password string + expectError bool + }{ + { + "empty email and password", + "", + "", + true, + }, + { + "empty email", + "", + "1234567890", + true, + }, + { + "invalid email", + "invalid", + "1234567890", + true, + }, + { + "nonexisting superuser", + "test_missing@example.com", + "1234567890", + true, + }, + { + "empty password", + "test@example.com", + "", + true, + }, + { + "short password", + "test_new@example.com", + "1234567", + true, + }, + { + "valid email and password", + "test@example.com", + "12345678", + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + command := cmd.NewSuperuserCommand(app) + command.SetArgs([]string{"update", s.email, s.password}) + + err := command.Execute() + + hasErr := err != nil + if s.expectError != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + // check whether the superuser password was actually changed + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, s.email) + if err != nil { + t.Fatalf("Failed to fetch superuser %s: %v", s.email, err) + } else if !superuser.ValidatePassword(s.password) { + t.Fatal("Expected the superuser password to match") + } + }) + } +} + +func TestSuperuserDeleteCommand(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + email string + expectError bool + }{ + { + "empty email", + "", + true, + }, + { + "invalid email", + "invalid", + true, + }, + { + "nonexisting superuser", + "test_missing@example.com", + false, + }, + { + "existing superuser", + "test@example.com", + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + command := cmd.NewSuperuserCommand(app) + command.SetArgs([]string{"delete", s.email}) + + err := command.Execute() + + hasErr := err != nil + if s.expectError != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if _, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, s.email); err == nil { + t.Fatal("Expected the superuser account to be deleted") + } + }) + } +} diff --git a/core/app.go b/core/app.go index f1dad330..1e7f8abf 100644 --- a/core/app.go +++ b/core/app.go @@ -6,10 +6,10 @@ package core import ( "context" "log/slog" + "time" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models/settings" + "github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/mailer" @@ -18,96 +18,89 @@ import ( ) // App defines the main PocketBase app interface. +// +// Note that the interface is not intended to be implemented manually by users +// and instead they should use core.BaseApp (either directly or as embedded field in a custom struct). +// +// This interface exists to make testing easier and to allow users to +// create common and pluggable helpers and methods that doesn't rely +// on a specific wrapped app struct (hence the large interface size). type App interface { - // Deprecated: - // This method may get removed in the near future. - // It is recommended to access the app db instance from app.Dao().DB() or - // if you want more flexibility - app.Dao().ConcurrentDB() and app.Dao().NonconcurrentDB(). + // UnsafeWithoutHooks returns a shallow copy of the current app WITHOUT any registered hooks. // - // DB returns the default app database instance. - DB() *dbx.DB + // 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. + UnsafeWithoutHooks() App - // Dao returns the default app Dao instance. + // Logger returns the default app logger. // - // This Dao could operate only on the tables and models - // associated with the default app database. For example, - // trying to access the request logs table will result in error. - Dao() *daos.Dao - - // Deprecated: - // This method may get removed in the near future. - // It is recommended to access the logs db instance from app.LogsDao().DB() or - // if you want more flexibility - app.LogsDao().ConcurrentDB() and app.LogsDao().NonconcurrentDB(). - // - // LogsDB returns the app logs database instance. - LogsDB() *dbx.DB - - // LogsDao returns the app logs Dao instance. - // - // This Dao could operate only on the tables and models - // associated with the logs database. For example, trying to access - // the users table from LogsDao will result in error. - LogsDao() *daos.Dao - - // Logger returns the active app logger. + // If the application is not bootstrapped yet, fallbacks to slog.Default(). Logger() *slog.Logger - // DataDir returns the app data directory path. - DataDir() string - - // EncryptionEnv returns the name of the app secret env key - // (used for settings encryption). - EncryptionEnv() string - - // IsDev returns whether the app is in dev mode. - IsDev() bool - - // Settings returns the loaded app settings. - Settings() *settings.Settings - - // Deprecated: Use app.Store() instead. - Cache() *store.Store[any] - - // Store returns the app runtime store. - Store() *store.Store[any] - - // SubscriptionsBroker returns the app realtime subscriptions broker instance. - SubscriptionsBroker() *subscriptions.Broker - - // NewMailClient creates and returns a configured app mail client. - NewMailClient() mailer.Mailer - - // NewFilesystem creates and returns a configured filesystem.System instance - // for managing regular app files (eg. collection uploads). - // - // NB! Make sure to call Close() on the returned result - // after you are done working with it. - NewFilesystem() (*filesystem.System, error) - - // NewBackupsFilesystem creates and returns a configured filesystem.System instance - // for managing app backups. - // - // NB! Make sure to call Close() on the returned result - // after you are done working with it. - NewBackupsFilesystem() (*filesystem.System, error) - - // RefreshSettings reinitializes and reloads the stored application settings. - RefreshSettings() error - // IsBootstrapped checks if the application was initialized // (aka. whether Bootstrap() was called). IsBootstrapped() bool - // Bootstrap takes care for initializing the application - // (open db connections, load settings, etc.). + // IsTransactional checks if the current app instance is part of a transaction. + IsTransactional() bool + + // Bootstrap initializes the application + // (aka. create data dir, open db connections, load settings, etc.). // // It will call ResetBootstrapState() if the application was already bootstrapped. Bootstrap() error - // ResetBootstrapState takes care for releasing initialized app resources - // (eg. closing db connections). + // ResetBootstrapState releases the initialized core app resources + // (closing db connections, stopping cron ticker, etc.). ResetBootstrapState() error + // DataDir returns the app data directory path. + DataDir() string + + // EncryptionEnv returns the name of the app secret env key + // (currently used primarily for optional settings encryption but this may change in the future). + EncryptionEnv() string + + // IsDev returns whether the app is in dev mode. + // + // When enabled logs, executed sql statements, etc. are printed to the stderr. + IsDev() bool + + // Settings returns the loaded app settings. + Settings() *Settings + + // Store returns the app runtime store. + Store() *store.Store[any] + + // Cron returns the app cron instance. + Cron() *cron.Cron + + // SubscriptionsBroker returns the app realtime subscriptions broker instance. + SubscriptionsBroker() *subscriptions.Broker + + // NewMailClient creates and returns a new SMTP or Sendmail client + // based on the current app settings. + NewMailClient() mailer.Mailer + + // NewFilesystem creates a new local or S3 filesystem instance + // for managing regular app files (ex. record uploads) + // based on the current app settings. + // + // NB! Make sure to call Close() on the returned result + // after you are done working with it. + NewFilesystem() (*filesystem.System, error) + + // NewFilesystem creates a new local or S3 filesystem instance + // for managing app backups based on the current app settings. + // + // NB! Make sure to call Close() on the returned result + // after you are done working with it. + NewBackupsFilesystem() (*filesystem.System, error) + + // ReloadSettings reinitializes and reloads the stored application settings. + ReloadSettings() error + // CreateBackup creates a new backup of the current app pb_data directory. // // Backups can be stored on S3 if it is configured in app.Settings().Backups. @@ -128,210 +121,1098 @@ type App interface { // NB! This feature is experimental and currently is expected to work only on UNIX based systems. RestoreBackup(ctx context.Context, name string) error - // Restart restarts the current running application process. + // Restart restarts (aka. replaces) the current running application process. // - // Currently it is relying on execve so it is supported only on UNIX based systems. + // NB! It relies on execve which is supported only on UNIX based systems. Restart() error + // RunSystemMigrations applies all new migrations registered in the [core.SystemMigrations] list. + RunSystemMigrations() error + + // RunAppMigrations applies all new migrations registered in the [core.AppMigrations] list. + RunAppMigrations() error + + // RunAllMigrations applies all system and app migrations + // (aka. from both [core.SystemMigrations] and [core.AppMigrations]). + RunAllMigrations() error + + // --------------------------------------------------------------- + // DB methods + // --------------------------------------------------------------- + + // DB returns the default app data db instance (pb_data/data.db). + DB() dbx.Builder + + // NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db). + // + // The returned db instance is limited only to a single open connection, + // meaning that it can process only 1 db operation at a time (other operations will be queued up). + // + // This method is used mainly internally and in the tests to execute write + // (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. + // + // For the majority of cases you would want to use the regular DB() method + // since it allows concurrent db read operations. + // + // In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance. + NonconcurrentDB() dbx.Builder + + // AuxDB returns the default app auxiliary db instance (pb_data/aux.db). + AuxDB() dbx.Builder + + // AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/aux.db).. + // + // The returned db instance is limited only to a single open connection, + // meaning that it can process only 1 db operation at a time (other operations will be queued up). + // + // This method is used mainly internally and in the tests to execute write + // (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. + // + // For the majority of cases you would want to use the regular DB() method + // since it allows concurrent db read operations. + // + // In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance. + AuxNonconcurrentDB() dbx.Builder + + // HasTable checks if a table (or view) with the provided name exists (case insensitive). + HasTable(tableName string) bool + + // TableColumns returns all column names of a single table by its name. + TableColumns(tableName string) ([]string, error) + + // TableInfo returns the "table_info" pragma result for the specified table. + TableInfo(tableName string) ([]*TableInfoRow, error) + + // TableIndexes returns a name grouped map with all non empty index of the specified table. + // + // Note: This method doesn't return an error on nonexisting table. + TableIndexes(tableName string) (map[string]string, error) + + // DeleteTable drops the specified table. + // + // This method is a no-op if a table with the provided name doesn't exist. + // + // NB! Be aware that this method is vulnerable to SQL injection and the + // "tableName" argument must come only from trusted input! + DeleteTable(tableName string) error + + // DeleteView drops the specified view name. + // + // This method is a no-op if a view with the provided name doesn't exist. + // + // NB! Be aware that this method is vulnerable to SQL injection and the + // "name" argument must come only from trusted input! + DeleteView(name string) error + + // SaveView creates (or updates already existing) persistent SQL view. + // + // NB! Be aware that this method is vulnerable to SQL injection and the + // "selectQuery" argument must come only from trusted input! + SaveView(name string, selectQuery string) error + + // CreateViewFields creates a new FieldsList from the provided select query. + // + // There are some caveats: + // - The select query must have an "id" column. + // - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data. + CreateViewFields(selectQuery string) (FieldsList, error) + + // FindRecordByViewFile returns the original Record of the provided view collection file. + FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error) + + // Vacuum executes VACUUM on the current app.DB() instance + // in order to reclaim unused data db disk space. + Vacuum() error + + // AuxVacuum executes VACUUM on the current app.AuxDB() instance + // in order to reclaim unused auxiliary db disk space. + AuxVacuum() error + + // --------------------------------------------------------------- + + // ModelQuery creates a new preconfigured select app.DB() query with preset + // SELECT, FROM and other common fields based on the provided model. + ModelQuery(model Model) *dbx.SelectQuery + + // AuxModelQuery creates a new preconfigured select app.AuxDB() query with preset + // SELECT, FROM and other common fields based on the provided model. + AuxModelQuery(model Model) *dbx.SelectQuery + + // Delete deletes the specified model from the regular app database. + Delete(model Model) error + + // Delete deletes the specified model from the regular app database + // (the context could be used to limit the query execution). + DeleteWithContext(ctx context.Context, model Model) error + + // AuxDelete deletes the specified model from the auxiliary database. + AuxDelete(model Model) error + + // AuxDeleteWithContext deletes the specified model from the auxiliary database + // (the context could be used to limit the query execution). + AuxDeleteWithContext(ctx context.Context, model Model) error + + // Save validates and saves the specified model into the regular app database. + // + // If you don't want to run validations, use [App.SaveNoValidate()]. + Save(model Model) error + + // SaveWithContext is the same as [App.Save()] but allows specifying a context to limit the db execution. + // + // If you don't want to run validations, use [App.SaveNoValidateWithContext()]. + SaveWithContext(ctx context.Context, model Model) error + + // SaveNoValidate saves the specified model into the regular app database without performing validations. + // + // If you want to also run validations before persisting, use [App.Save()]. + SaveNoValidate(model Model) error + + // SaveNoValidateWithContext is the same as [App.SaveNoValidate()] + // but allows specifying a context to limit the db execution. + // + // If you want to also run validations before persisting, use [App.SaveWithContext()]. + SaveNoValidateWithContext(ctx context.Context, model Model) error + + // AuxSave validates and saves the specified model into the auxiliary app database. + // + // If you don't want to run validations, use [App.AuxSaveNoValidate()]. + AuxSave(model Model) error + + // AuxSaveWithContext is the same as [App.AuxSave()] but allows specifying a context to limit the db execution. + // + // If you don't want to run validations, use [App.AuxSaveNoValidateWithContext()]. + AuxSaveWithContext(ctx context.Context, model Model) error + + // AuxSaveNoValidate saves the specified model into the auxiliary app database without performing validations. + // + // If you want to also run validations before persisting, use [App.AuxSave()]. + AuxSaveNoValidate(model Model) error + + // AuxSaveNoValidateWithContext is the same as [App.AuxSaveNoValidate()] + // but allows specifying a context to limit the db execution. + // + // If you want to also run validations before persisting, use [App.AuxSaveWithContext()]. + AuxSaveNoValidateWithContext(ctx context.Context, model Model) error + + // Validate triggers the OnModelValidate hook for the specified model. + Validate(model Model) error + + // ValidateWithContext is the same as Validate but allows specifying the ModelEvent context. + ValidateWithContext(ctx context.Context, model Model) error + + // RunInTransaction wraps fn into a transaction for the regular app database. + // + // It is safe to nest RunInTransaction calls as long as you use the callback's txApp. + RunInTransaction(fn func(txApp App) error) error + + // AuxRunInTransaction wraps fn into a transaction for the auxiliary app database. + // + // It is safe to nest RunInTransaction calls as long as you use the callback's txApp. + AuxRunInTransaction(fn func(txApp App) error) error + + // --------------------------------------------------------------- + + // LogQuery returns a new Log select query. + LogQuery() *dbx.SelectQuery + + // FindLogById finds a single Log entry by its id. + FindLogById(id string) (*Log, error) + + // LogsStatsItem defines the total number of logs for a specific time period. + LogsStats(expr dbx.Expression) ([]*LogsStatsItem, error) + + // DeleteOldLogs delete all requests that are created before createdBefore. + DeleteOldLogs(createdBefore time.Time) error + + // --------------------------------------------------------------- + + // CollectionQuery returns a new Collection select query. + CollectionQuery() *dbx.SelectQuery + + // FindCollections finds all collections by the given type(s). + // + // If collectionTypes is not set, it returns all collections. + // + // Example: + // + // app.FindAllCollections() // all collections + // app.FindAllCollections("auth", "view") // only auth and view collections + FindAllCollections(collectionTypes ...string) ([]*Collection, error) + + // ReloadCachedCollections fetches all collections and caches them into the app store. + ReloadCachedCollections() error + + // FindCollectionByNameOrId finds a single collection by its name (case insensitive) or id.s + FindCollectionByNameOrId(nameOrId string) (*Collection, error) + + // FindCachedCollectionByNameOrId is similar to [App.FindCollectionByNameOrId] + // but retrieves the Collection from the app cache instead of making a db call. + // + // NB! This method is suitable for read-only Collection operations. + // + // Returns [sql.ErrNoRows] if no Collection is found for consistency + // with the [App.FindCollectionByNameOrId] method. + // + // If you plan making changes to the returned Collection model, + // use [App.FindCollectionByNameOrId] instead. + // + // Caveats: + // + // - The returned Collection should be used only for read-only operations. + // Avoid directly modifying the returned cached Collection as it will affect + // the global cached value even if you don't persist the changes in the database! + // - If you are updating a Collection in a transaction and then call this method before commit, + // it'll return the cached Collection state and not the one from the uncommitted transaction. + // - The cache is automatically updated on collections db change (create/update/delete). + // To manually reload the cache you can call [App.ReloadCachedCollections()] + FindCachedCollectionByNameOrId(nameOrId string) (*Collection, error) + + // IsCollectionNameUnique checks that there is no existing collection + // with the provided name (case insensitive!). + // + // Note: case insensitive check because the name is used also as + // table name for the records. + IsCollectionNameUnique(name string, excludeIds ...string) bool + + // FindCollectionReferences returns information for all relation + // fields referencing the provided collection. + // + // If the provided collection has reference to itself then it will be + // also included in the result. To exclude it, pass the collection id + // as the excludeIds argument. + FindCollectionReferences(collection *Collection, excludeIds ...string) (map[*Collection][]Field, error) + + // TruncateCollection deletes all records associated with the provided collection. + // + // The truncate operation is executed in a single transaction, + // aka. either everything is deleted or none. + // + // Note that this method will also trigger the records related + // cascade and file delete actions. + TruncateCollection(collection *Collection) error + + // ImportCollections imports the provided collections data in a single transaction. + // + // For existing matching collections, the imported data is unmarshaled on top of the existing model. + // + // NB! If deleteMissing is true, ALL NON-SYSTEM COLLECTIONS AND SCHEMA FIELDS, + // that are not present in the imported configuration, WILL BE DELETED + // (this includes their related records data). + ImportCollections(toImport []map[string]any, deleteMissing bool) error + + // ImportCollectionsByMarshaledJSON is the same as [ImportCollections] + // but accept marshaled json array as import data (usually used for the autogenerated snapshots). + ImportCollectionsByMarshaledJSON(rawSliceOfMaps []byte, deleteMissing bool) error + + // SyncRecordTableSchema compares the two provided collections + // and applies the necessary related record table changes. + // + // If oldCollection is null, then only newCollection is used to create the record table. + // + // This method is automatically invoked as part of a collection create/update/delete operation. + SyncRecordTableSchema(newCollection *Collection, oldCollection *Collection) error + + // --------------------------------------------------------------- + + // FindAllExternalAuthsByRecord returns all ExternalAuth models + // linked to the provided auth record. + FindAllExternalAuthsByRecord(authRecord *Record) ([]*ExternalAuth, error) + + // FindAllExternalAuthsByCollection returns all ExternalAuth models + // linked to the provided auth collection. + FindAllExternalAuthsByCollection(collection *Collection) ([]*ExternalAuth, error) + + // FindFirstExternalAuthByExpr returns the first available (the most recent created) + // ExternalAuth model that satisfies the non-nil expression. + FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalAuth, error) + + // --------------------------------------------------------------- + + // FindAllMFAsByRecord returns all MFA models linked to the provided auth record. + FindAllMFAsByRecord(authRecord *Record) ([]*MFA, error) + + // FindAllMFAsByCollection returns all MFA models linked to the provided collection. + FindAllMFAsByCollection(collection *Collection) ([]*MFA, error) + + // FindMFAById returns a single MFA model by its id. + FindMFAById(id string) (*MFA, error) + + // DeleteAllMFAsByRecord deletes all MFA models associated with the provided record. + // + // Returns a combined error with the failed deletes. + DeleteAllMFAsByRecord(authRecord *Record) error + + // DeleteExpiredMFAs deletes the expired MFAs for all auth collections. + DeleteExpiredMFAs() error + + // --------------------------------------------------------------- + + // FindAllOTPsByRecord returns all OTP models linked to the provided auth record. + FindAllOTPsByRecord(authRecord *Record) ([]*OTP, error) + + // FindAllOTPsByCollection returns all OTP models linked to the provided collection. + FindAllOTPsByCollection(collection *Collection) ([]*OTP, error) + + // FindOTPById returns a single OTP model by its id. + FindOTPById(id string) (*OTP, error) + + // DeleteAllOTPsByRecord deletes all OTP models associated with the provided record. + // + // Returns a combined error with the failed deletes. + DeleteAllOTPsByRecord(authRecord *Record) error + + // DeleteExpiredOTPs deletes the expired OTPs for all auth collections. + DeleteExpiredOTPs() error + + // --------------------------------------------------------------- + + // FindAllAuthOriginsByRecord returns all AuthOrigin models linked to the provided auth record (in DESC order). + FindAllAuthOriginsByRecord(authRecord *Record) ([]*AuthOrigin, error) + + // FindAllAuthOriginsByCollection returns all AuthOrigin models linked to the provided collection (in DESC order). + FindAllAuthOriginsByCollection(collection *Collection) ([]*AuthOrigin, error) + + // FindAuthOriginById returns a single AuthOrigin model by its id. + FindAuthOriginById(id string) (*AuthOrigin, error) + + // FindAuthOriginByRecordAndFingerprint returns a single AuthOrigin model + // by its authRecord relation and fingerprint. + FindAuthOriginByRecordAndFingerprint(authRecord *Record, fingerprint string) (*AuthOrigin, error) + + // DeleteAllAuthOriginsByRecord deletes all AuthOrigin models associated with the provided record. + // + // Returns a combined error with the failed deletes. + DeleteAllAuthOriginsByRecord(authRecord *Record) error + + // --------------------------------------------------------------- + + // RecordQuery returns a new Record select query from a collection model, id or name. + // + // In case a collection id or name is provided and that collection doesn't + // actually exists, the generated query will be created with a cancelled context + // and will fail once an executor (Row(), One(), All(), etc.) is called. + RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuery + + // FindRecordById finds the Record model by its id. + FindRecordById(collectionModelOrIdentifier any, recordId string, optFilters ...func(q *dbx.SelectQuery) error) (*Record, error) + + // FindRecordsByIds finds all records by the specified ids. + // If no records are found, returns an empty slice. + FindRecordsByIds(collectionModelOrIdentifier any, recordIds []string, optFilters ...func(q *dbx.SelectQuery) error) ([]*Record, error) + + // FindAllRecords finds all records matching specified db expressions. + // + // Returns all collection records if no expression is provided. + // + // Returns an empty slice if no records are found. + // + // Example: + // + // // no extra expressions + // app.FindAllRecords("example") + // + // // with extra expressions + // expr1 := dbx.HashExp{"email": "test@example.com"} + // expr2 := dbx.NewExp("LOWER(username) = {:username}", dbx.Params{"username": "test"}) + // app.FindAllRecords("example", expr1, expr2) + FindAllRecords(collectionModelOrIdentifier any, exprs ...dbx.Expression) ([]*Record, error) + + // FindFirstRecordByData returns the first found record matching + // the provided key-value pair. + FindFirstRecordByData(collectionModelOrIdentifier any, key string, value any) (*Record, error) + + // FindRecordsByFilter returns limit number of records matching the + // provided string filter. + // + // NB! Use the last "params" argument to bind untrusted user variables! + // + // The filter argument is optional and can be empty string to target + // all available records. + // + // The sort argument is optional and can be empty string OR the same format + // used in the web APIs, ex. "-created,title". + // + // If the limit argument is <= 0, no limit is applied to the query and + // all matching records are returned. + // + // Returns an empty slice if no records are found. + // + // Example: + // + // app.FindRecordsByFilter( + // "posts", + // "title ~ {:title} && visible = {:visible}", + // "-created", + // 10, + // 0, + // dbx.Params{"title": "lorem ipsum", "visible": true} + // ) + FindRecordsByFilter( + collectionModelOrIdentifier any, + filter string, + sort string, + limit int, + offset int, + params ...dbx.Params, + ) ([]*Record, error) + + // FindFirstRecordByFilter returns the first available record matching the provided filter (if any). + // + // NB! Use the last params argument to bind untrusted user variables! + // + // Returns sql.ErrNoRows if no record is found. + // + // Example: + // + // app.FindFirstRecordByFilter("posts", "") + // app.FindFirstRecordByFilter("posts", "slug={:slug} && status='public'", dbx.Params{"slug": "test"}) + FindFirstRecordByFilter( + collectionModelOrIdentifier any, + filter string, + params ...dbx.Params, + ) (*Record, error) + + // CountRecords returns the total number of records in a collection. + CountRecords(collectionModelOrIdentifier any, exprs ...dbx.Expression) (int64, error) + + // FindAuthRecordByToken finds the auth record associated with the provided JWT + // (auth, file, verifyEmail, changeEmail, passwordReset types). + // + // Optionally specify a list of validTypes to check tokens only from those types. + // + // Returns an error if the JWT is invalid, expired or not associated to an auth collection record. + FindAuthRecordByToken(token string, validTypes ...string) (*Record, error) + + // FindAuthRecordByEmail finds the auth record associated with the provided email. + // + // Returns an error if it is not an auth collection or the record is not found. + FindAuthRecordByEmail(collectionModelOrIdentifier any, email string) (*Record, error) + + // 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. + // + // The returned error indicate that something unexpected happened during + // the check (eg. invalid rule or db query error). + // + // The method always return false on invalid rule or db query error. + // + // Example: + // + // requestInfo, _ := e.RequestInfo() + // record, _ := app.FindRecordById("example", "RECORD_ID") + // rule := types.Pointer("@request.auth.id != '' || status = 'public'") + // // ... or use one of the record collection's rule, eg. record.Collection().ViewRule + // + // if ok, _ := app.CanAccessRecord(record, requestInfo, rule); ok { ... } + CanAccessRecord(record *Record, requestInfo *RequestInfo, accessRule *string) (bool, error) + + // ExpandRecord expands the relations of a single Record model. + // + // If optFetchFunc is not set, then a default function will be used + // that returns all relation records. + // + // Returns a map with the failed expand parameters and their errors. + ExpandRecord(record *Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error + + // ExpandRecords expands the relations of the provided Record models list. + // + // If optFetchFunc is not set, then a default function will be used + // that returns all relation records. + // + // Returns a map with the failed expand parameters and their errors. + ExpandRecords(records []*Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error + // --------------------------------------------------------------- // App event hooks // --------------------------------------------------------------- - // OnBeforeBootstrap hook is triggered before initializing the main - // application resources (eg. before db open and initial settings load). - OnBeforeBootstrap() *hook.Hook[*BootstrapEvent] + // OnBootstrap hook is triggered on initializing the main application + // resources (db, app settings, etc). + OnBootstrap() *hook.Hook[*BootstrapEvent] - // OnAfterBootstrap hook is triggered after initializing the main - // application resources (eg. after db open and initial settings load). - OnAfterBootstrap() *hook.Hook[*BootstrapEvent] - - // OnBeforeServe hook is triggered before serving the internal router (echo), + // OnServe hook is triggered on 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. - OnBeforeServe() *hook.Hook[*ServeEvent] - - // OnBeforeApiError hook is triggered right before sending an error API - // response to the client, allowing you to further modify the error data - // or to return a completely different API response. - OnBeforeApiError() *hook.Hook[*ApiErrorEvent] - - // OnAfterApiError hook is triggered right after sending an error API - // response to the client. - // It could be used to log the final API error in external services. - OnAfterApiError() *hook.Hook[*ApiErrorEvent] + OnServe() *hook.Hook[*ServeEvent] // OnTerminate hook is triggered when the app is in the process - // of being terminated (eg. on SIGTERM signal). + // of being terminated (ex. on SIGTERM signal). OnTerminate() *hook.Hook[*TerminateEvent] + // OnBackupCreate hook is triggered on each [App.CreateBackup] call. + OnBackupCreate() *hook.Hook[*BackupEvent] + + // OnBackupRestore hook is triggered before app backup restore (aka. [App.RestoreBackup] call). + // + // Note that by default on success the application is restarted and the after state of the hook is ignored. + OnBackupRestore() *hook.Hook[*BackupEvent] + // --------------------------------------------------------------- - // Dao event hooks + // DB models event hooks // --------------------------------------------------------------- - // OnModelBeforeCreate hook is triggered before inserting a new - // model in the DB, allowing you to modify or validate the stored data. + // OnModelValidate is triggered every time when a model is being validated + // (e.g. triggered by App.Validate() or App.Save()). // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelBeforeCreate(tags ...string) *hook.TaggedHook[*ModelEvent] + // 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. + OnModelValidate(tags ...string) *hook.TaggedHook[*ModelEvent] - // OnModelAfterCreate hook is triggered after successfully - // inserting a new model in the DB. - // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelAfterCreate(tags ...string) *hook.TaggedHook[*ModelEvent] + // --------------------------------------------------------------- - // OnModelBeforeUpdate hook is triggered before updating existing - // model in the DB, allowing you to modify or validate the stored data. + // OnModelCreate is triggered every time when a new model is being created + // (e.g. triggered by App.Save()). // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelBeforeUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] + // Operations BEFORE the e.Next() execute before the model validation + // and the INSERT DB statement. + // + // Operations AFTER the e.Next() execute after the model validation + // and the INSERT DB statement. + // + // 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 + // bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks. + // + // 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. + OnModelCreate(tags ...string) *hook.TaggedHook[*ModelEvent] - // OnModelAfterUpdate hook is triggered after successfully updating - // existing model in the DB. + // OnModelCreateExecute is triggered after successful Model validation + // and right before the model INSERT DB statement execution. // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelAfterUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] + // Usually it is triggered as part of the App.Save() in the following firing order: + // OnModelCreate { + // -> OnModelValidate (skipped with App.SaveNoValidate()) + // -> OnModelCreateExecute + // } + // + // 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, + // you can bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks. + // + // 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. + OnModelCreateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] - // OnModelBeforeDelete hook is triggered before deleting an - // existing model from the DB. + // OnModelAfterCreateSuccess is triggered after each successful + // Model DB create persistence. // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelBeforeDelete(tags ...string) *hook.TaggedHook[*ModelEvent] + // 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 NOT triggered in case the transaction rollbacks + // (aka. when the model wasn't persisted). + // + // 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. + OnModelAfterCreateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] - // OnModelAfterDelete hook is triggered after successfully deleting an - // existing model from the DB. + // 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 // - // If the optional "tags" list (table names and/or the Collection id for Record models) - // 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. - OnModelAfterDelete(tags ...string) *hook.TaggedHook[*ModelEvent] + // 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. + OnModelAfterCreateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] + + // --------------------------------------------------------------- + + // OnModelUpdate is triggered every time when a new model is being updated + // (e.g. triggered by App.Save()). + // + // Operations BEFORE the e.Next() execute before the model validation + // and the UPDATE DB statement. + // + // Operations AFTER the e.Next() execute after the model validation + // and the UPDATE DB statement. + // + // 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 + // bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks. + // + // 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. + OnModelUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] + + // OnModelUpdateExecute is triggered after successful Model validation + // and right before the model UPDATE DB statement execution. + // + // Usually it is triggered as part of the App.Save() in the following firing order: + // OnModelUpdate { + // -> OnModelValidate (skipped with App.SaveNoValidate()) + // -> OnModelUpdateExecute + // } + // + // 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, + // you can bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks. + // + // 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. + OnModelUpdateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] + + // OnModelAfterUpdateSuccess is triggered after each successful + // 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 NOT triggered in case the transaction rollbacks + // (aka. when the model changes weren't persisted). + // + // 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. + OnModelAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] + + // 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 + // + // 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. + OnModelAfterUpdateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] + + // --------------------------------------------------------------- + + // OnModelDelete is triggered every time when a new model is being deleted + // (e.g. triggered by App.Delete()). + // + // 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 + // bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks. + // + // 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. + OnModelDelete(tags ...string) *hook.TaggedHook[*ModelEvent] + + // OnModelUpdateExecute is triggered right before the model + // DELETE DB statement execution. + // + // Usually it is triggered as part of the App.Delete() in the following firing order: + // OnModelDelete { + // -> (internal delete checks) + // -> OnModelDeleteExecute + // } + // + // 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 + // bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks. + // + // 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. + OnModelDeleteExecute(tags ...string) *hook.TaggedHook[*ModelEvent] + + // OnModelAfterDeleteSuccess is triggered after each successful + // 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 NOT triggered in case the transaction rollbacks + // (aka. when the model delete wasn't persisted). + // + // 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. + OnModelAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] + + // 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 + // + // 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. + OnModelAfterDeleteError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] + + // --------------------------------------------------------------- + // Record models event hooks + // --------------------------------------------------------------- + + // 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). + // + // It could be used for example to redact/hide or add computed temp + // Record model props only for the specific request info. For example: + // + // app.OnRecordEnrich("posts").BindFunc(func(e core.*RecordEnrichEvent) { + // // hide one or more fields + // e.Record.Hide("role") + // + // // add new custom field for registered users + // if e.RequestInfo.Auth != nil && e.RequestInfo.Auth.Collection().Name == "users" { + // e.Record.WithCustomData(true) // for security requires explicitly allowing it + // e.Record.Set("computedScore", e.Record.GetInt("score") * e.RequestInfo.Auth.GetInt("baseScore")) + // } + // + // return e.Next() + // }) + // + // 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. + OnRecordEnrich(tags ...string) *hook.TaggedHook[*RecordEnrichEvent] + + // OnRecordValidate is a proxy Record model hook for [OnModelValidate]. + // + // 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. + OnRecordValidate(tags ...string) *hook.TaggedHook[*RecordEvent] + + // --------------------------------------------------------------- + + // OnRecordCreate is a proxy Record model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnRecordAfterCreateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] + + // --------------------------------------------------------------- + + // OnRecordUpdate is a proxy Record model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnRecordAfterUpdateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] + + // --------------------------------------------------------------- + + // OnRecordDelete is a proxy Record model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnRecordAfterDeleteError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] + + // --------------------------------------------------------------- + // Collection models event hooks + // --------------------------------------------------------------- + + // OnCollectionValidate is a proxy Collection model hook for [OnModelValidate]. + // + // 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. + OnCollectionValidate(tags ...string) *hook.TaggedHook[*CollectionEvent] + + // --------------------------------------------------------------- + + // OnCollectionCreate is a proxy Collection model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnCollectionAfterCreateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] + + // --------------------------------------------------------------- + + // OnCollectionUpdate is a proxy Collection model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnCollectionAfterUpdateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] + + // --------------------------------------------------------------- + + // OnCollectionDelete is a proxy Collection model hook for [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]. + // + // 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]. + // + // 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]. + // + // 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. + OnCollectionAfterDeleteError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] // --------------------------------------------------------------- // Mailer event hooks // --------------------------------------------------------------- - // OnMailerBeforeAdminResetPasswordSend hook is triggered right - // before sending a password reset email to an admin, allowing you - // to inspect and customize the email message that is being sent. - OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] + // OnMailerSend hook is triggered every time when a new email is + // being send using the App.NewMailClient() instance. + // + // It allows intercepting the email message or to use a custom mailer client. + OnMailerSend() *hook.Hook[*MailerEvent] - // OnMailerAfterAdminResetPasswordSend hook is triggered after - // admin password reset email was successfully sent. - OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] - - // OnMailerBeforeRecordResetPasswordSend hook is triggered right - // before sending a password reset email to an auth record, allowing - // you to inspect and customize the email message that is being sent. + // OnMailerRecordAuthAlertSend hook is triggered when + // sending a new device login auth alert email, allowing you to + // intercept and customize the email message that is being sent. // // 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. - OnMailerBeforeRecordResetPasswordSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] + OnMailerRecordAuthAlertSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] - // OnMailerAfterRecordResetPasswordSend hook is triggered after - // an auth record password reset email was successfully sent. + // OnMailerBeforeRecordResetPasswordSend hook is triggered when + // sending a password reset email to an auth record, allowing + // you to intercept and customize the email message that is being sent. // // 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. - OnMailerAfterRecordResetPasswordSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] + OnMailerRecordPasswordResetSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] - // OnMailerBeforeRecordVerificationSend hook is triggered right - // before sending a verification email to an auth record, allowing - // you to inspect and customize the email message that is being sent. + // OnMailerBeforeRecordVerificationSend hook is triggered when + // sending a verification email to an auth record, allowing + // you to intercept and customize the email message that is being sent. // // 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. - OnMailerBeforeRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] + OnMailerRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] - // OnMailerAfterRecordVerificationSend hook is triggered after a - // verification email was successfully sent to an auth record. + // OnMailerRecordEmailChangeSend hook is triggered when sending a + // confirmation new address email to an auth record, allowing + // you to intercept and customize the email message that is being sent. // // 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. - OnMailerAfterRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] + OnMailerRecordEmailChangeSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] - // OnMailerBeforeRecordChangeEmailSend hook is triggered right before - // sending a confirmation new address email to an auth record, allowing - // you to inspect and customize the email message that is being sent. + // OnMailerRecordOTPSend hook is triggered when sending an OTP email + // to an auth record, allowing you to intercept and customize the + // email message that is being sent. // // 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. - OnMailerBeforeRecordChangeEmailSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] - - // OnMailerAfterRecordChangeEmailSend hook is triggered after a - // verification email was successfully sent to an auth record. - // - // 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. - OnMailerAfterRecordChangeEmailSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] + OnMailerRecordOTPSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] // --------------------------------------------------------------- // Realtime API event hooks // --------------------------------------------------------------- - // OnRealtimeConnectRequest hook is triggered right before establishing - // the SSE client connection. - OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectEvent] - - // OnRealtimeDisconnectRequest hook is triggered on disconnected/interrupted - // SSE client connection. - OnRealtimeDisconnectRequest() *hook.Hook[*RealtimeDisconnectEvent] - - // OnRealtimeBeforeMessageSend hook is triggered right before sending - // an SSE message to a client. + // OnRealtimeConnectRequest hook is triggered when establishing the SSE client connection. // - // Returning [hook.StopPropagation] will prevent sending the message. - // Returning any other non-nil error will close the realtime connection. - OnRealtimeBeforeMessageSend() *hook.Hook[*RealtimeMessageEvent] + // Any execution after [e.Next()] of a hook handler happens after the client disconnects. + OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectRequestEvent] - // OnRealtimeAfterMessageSend hook is triggered right after sending - // an SSE message to a client. - OnRealtimeAfterMessageSend() *hook.Hook[*RealtimeMessageEvent] + // OnRealtimeMessageSend hook is triggered when sending an SSE message to a client. + OnRealtimeMessageSend() *hook.Hook[*RealtimeMessageEvent] - // OnRealtimeBeforeSubscribeRequest hook is triggered before changing - // the client subscriptions, allowing you to further validate and + // OnRealtimeSubscribeRequest hook is triggered when updating the + // client subscriptions, allowing you to further validate and // modify the submitted change. - OnRealtimeBeforeSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] - - // OnRealtimeAfterSubscribeRequest hook is triggered after the client - // subscriptions were successfully changed. - OnRealtimeAfterSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] + OnRealtimeSubscribeRequest() *hook.Hook[*RealtimeSubscribeRequestEvent] // --------------------------------------------------------------- // Settings API event hooks // --------------------------------------------------------------- - // OnSettingsListRequest hook is triggered on each successful - // API Settings list request. + // OnSettingsListRequest hook is triggered on each API Settings list request. // - // Could be used to validate or modify the response before - // returning it to the client. - OnSettingsListRequest() *hook.Hook[*SettingsListEvent] + // Could be used to validate or modify the response before returning it to the client. + OnSettingsListRequest() *hook.Hook[*SettingsListRequestEvent] - // OnSettingsBeforeUpdateRequest hook is triggered before each API - // Settings update request (after request data load and before settings persistence). + // OnSettingsUpdateRequest hook is triggered on each API Settings update request. // // Could be used to additionally validate the request data or // implement completely different persistence behavior. - OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] + OnSettingsUpdateRequest() *hook.Hook[*SettingsUpdateRequestEvent] - // OnSettingsAfterUpdateRequest hook is triggered after each - // successful API Settings update request. - OnSettingsAfterUpdateRequest() *hook.Hook[*SettingsUpdateEvent] + // 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. + OnSettingsReload() *hook.Hook[*SettingsReloadEvent] // --------------------------------------------------------------- // File API event hooks @@ -341,124 +1222,10 @@ type App interface { // // Could be used to validate or modify the file response before // returning it to the client. - OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadEvent] + OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadRequestEvent] - // OnFileBeforeTokenRequest hook is triggered before each file - // token API request. - // - // If no token or model was submitted, e.Model and e.Token will be empty, - // allowing you to implement your own custom model file auth implementation. - // - // 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. - OnFileBeforeTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenEvent] - - // OnFileAfterTokenRequest hook is triggered after each - // successful 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. - OnFileAfterTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenEvent] - - // --------------------------------------------------------------- - // Admin API event hooks - // --------------------------------------------------------------- - - // OnAdminsListRequest hook is triggered on each API Admins list request. - // - // Could be used to validate or modify the response before returning it to the client. - OnAdminsListRequest() *hook.Hook[*AdminsListEvent] - - // OnAdminViewRequest hook is triggered on each API Admin view request. - // - // Could be used to validate or modify the response before returning it to the client. - OnAdminViewRequest() *hook.Hook[*AdminViewEvent] - - // OnAdminBeforeCreateRequest hook is triggered before each API - // Admin create request (after request data load and before model persistence). - // - // Could be used to additionally validate the request data or implement - // completely different persistence behavior. - OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] - - // OnAdminAfterCreateRequest hook is triggered after each - // successful API Admin create request. - OnAdminAfterCreateRequest() *hook.Hook[*AdminCreateEvent] - - // OnAdminBeforeUpdateRequest hook is triggered before each API - // Admin update request (after request data load and before model persistence). - // - // Could be used to additionally validate the request data or implement - // completely different persistence behavior. - OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] - - // OnAdminAfterUpdateRequest hook is triggered after each - // successful API Admin update request. - OnAdminAfterUpdateRequest() *hook.Hook[*AdminUpdateEvent] - - // OnAdminBeforeDeleteRequest hook is triggered before each API - // Admin delete request (after model load and before actual deletion). - // - // Could be used to additionally validate the request data or implement - // completely different delete behavior. - OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] - - // OnAdminAfterDeleteRequest hook is triggered after each - // successful API Admin delete request. - OnAdminAfterDeleteRequest() *hook.Hook[*AdminDeleteEvent] - - // OnAdminAuthRequest hook is triggered on each successful API Admin - // authentication request (sign-in, token refresh, etc.). - // - // Could be used to additionally validate or modify the - // authenticated admin data and token. - OnAdminAuthRequest() *hook.Hook[*AdminAuthEvent] - - // OnAdminBeforeAuthWithPasswordRequest hook is triggered before each Admin - // auth with password API request (after request data load and before password validation). - // - // Could be used to implement for example a custom password validation - // or to locate a different Admin identity (by assigning [AdminAuthWithPasswordEvent.Admin]). - OnAdminBeforeAuthWithPasswordRequest() *hook.Hook[*AdminAuthWithPasswordEvent] - - // OnAdminAfterAuthWithPasswordRequest hook is triggered after each - // successful Admin auth with password API request. - OnAdminAfterAuthWithPasswordRequest() *hook.Hook[*AdminAuthWithPasswordEvent] - - // OnAdminBeforeAuthRefreshRequest hook is triggered before each Admin - // auth refresh API request (right before generating a new auth token). - // - // Could be used to additionally validate the request data or implement - // completely different auth refresh behavior. - OnAdminBeforeAuthRefreshRequest() *hook.Hook[*AdminAuthRefreshEvent] - - // OnAdminAfterAuthRefreshRequest hook is triggered after each - // successful auth refresh API request (right after generating a new auth token). - OnAdminAfterAuthRefreshRequest() *hook.Hook[*AdminAuthRefreshEvent] - - // OnAdminBeforeRequestPasswordResetRequest hook is triggered before each Admin - // request password reset API request (after request data load and before sending the reset email). - // - // Could be used to additionally validate the request data or implement - // completely different password reset behavior. - OnAdminBeforeRequestPasswordResetRequest() *hook.Hook[*AdminRequestPasswordResetEvent] - - // OnAdminAfterRequestPasswordResetRequest hook is triggered after each - // successful request password reset API request. - OnAdminAfterRequestPasswordResetRequest() *hook.Hook[*AdminRequestPasswordResetEvent] - - // OnAdminBeforeConfirmPasswordResetRequest hook is triggered before each Admin - // confirm password reset API request (after request data load and before persistence). - // - // Could be used to additionally validate the request data or implement - // completely different persistence behavior. - OnAdminBeforeConfirmPasswordResetRequest() *hook.Hook[*AdminConfirmPasswordResetEvent] - - // OnAdminAfterConfirmPasswordResetRequest hook is triggered after each - // successful confirm password reset API request. - OnAdminAfterConfirmPasswordResetRequest() *hook.Hook[*AdminConfirmPasswordResetEvent] + // OnFileBeforeTokenRequest hook is triggered on each file token API request. + OnFileTokenRequest() *hook.Hook[*FileTokenRequestEvent] // --------------------------------------------------------------- // Record Auth API event hooks @@ -473,50 +1240,35 @@ type App interface { // 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. - OnRecordAuthRequest(tags ...string) *hook.TaggedHook[*RecordAuthEvent] + OnRecordAuthRequest(tags ...string) *hook.TaggedHook[*RecordAuthRequestEvent] - // OnRecordBeforeAuthWithPasswordRequest hook is triggered before each Record - // auth with password API request (after request data load and before password validation). + // OnRecordAuthWithPasswordRequest hook is triggered on each + // Record auth with password API request. // - // Could be used to implement for example a custom password validation - // or to locate a different Record model (by reassigning [RecordAuthWithPasswordEvent.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 // triggered and called only if their event data origin matches the tags. - OnRecordBeforeAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordEvent] + OnRecordAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordRequestEvent] - // OnRecordAfterAuthWithPasswordRequest hook is triggered after each - // successful Record auth with password 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. - OnRecordAfterAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordEvent] - - // OnRecordBeforeAuthWithOAuth2Request hook is triggered before each Record + // OnRecordAuthWithOAuth2Request hook is triggered on each Record // OAuth2 sign-in/sign-up API request (after token exchange and before external provider linking). // - // If the [RecordAuthWithOAuth2Event.Record] is not set, then the OAuth2 + // If the [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 - // change the [RecordAuthWithOAuth2Event.Record] field. + // change the [RecordAuthWithOAuth2RequestEvent.Record] field. // // 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. - OnRecordBeforeAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2Event] + OnRecordAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2RequestEvent] - // OnRecordAfterAuthWithOAuth2Request hook is triggered after each - // successful Record OAuth2 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. - OnRecordAfterAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2Event] - - // OnRecordBeforeAuthRefreshRequest hook is triggered before each Record + // OnRecordAuthRefreshRequest hook is triggered on each Record // auth refresh API request (right before generating a new auth token). // // Could be used to additionally validate the request data or implement @@ -525,46 +1277,10 @@ type App interface { // 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. - OnRecordBeforeAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshEvent] + OnRecordAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshRequestEvent] - // OnRecordAfterAuthRefreshRequest hook is triggered after each - // successful auth refresh API request (right after generating a new auth token). - // - // 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. - OnRecordAfterAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshEvent] - - // OnRecordListExternalAuthsRequest hook is triggered on each API record external auths list request. - // - // Could be used to validate or modify the response before returning it to the client. - // - // 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. - OnRecordListExternalAuthsRequest(tags ...string) *hook.TaggedHook[*RecordListExternalAuthsEvent] - - // OnRecordBeforeUnlinkExternalAuthRequest hook is triggered before each API record - // external auth unlink request (after models load and before the actual relation deletion). - // - // Could be used to additionally validate the request data or implement - // completely different delete behavior. - // - // 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. - OnRecordBeforeUnlinkExternalAuthRequest(tags ...string) *hook.TaggedHook[*RecordUnlinkExternalAuthEvent] - - // OnRecordAfterUnlinkExternalAuthRequest hook is triggered after each - // successful API record external auth unlink 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. - OnRecordAfterUnlinkExternalAuthRequest(tags ...string) *hook.TaggedHook[*RecordUnlinkExternalAuthEvent] - - // OnRecordBeforeRequestPasswordResetRequest hook is triggered before each Record - // request password reset API request (after request data load and before sending the reset email). + // OnRecordRequestPasswordResetRequest hook is triggered on + // each Record request password reset API request. // // Could be used to additionally validate the request data or implement // completely different password reset behavior. @@ -572,18 +1288,10 @@ type App interface { // 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. - OnRecordBeforeRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetEvent] + OnRecordRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetRequestEvent] - // OnRecordAfterRequestPasswordResetRequest hook is triggered after each - // successful request password reset 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. - OnRecordAfterRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetEvent] - - // OnRecordBeforeConfirmPasswordResetRequest hook is triggered before each Record - // confirm password reset API request (after request data load and before persistence). + // OnRecordConfirmPasswordResetRequest hook is triggered on + // each Record confirm password reset API request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. @@ -591,18 +1299,10 @@ type App interface { // 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. - OnRecordBeforeConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetEvent] + OnRecordConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetRequestEvent] - // OnRecordAfterConfirmPasswordResetRequest hook is triggered after each - // successful confirm password reset 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. - OnRecordAfterConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetEvent] - - // OnRecordBeforeRequestVerificationRequest hook is triggered before each Record - // request verification API request (after request data load and before sending the verification email). + // OnRecordRequestVerificationRequest hook is triggered on + // each Record request verification API request. // // Could be used to additionally validate the loaded request data or implement // completely different verification behavior. @@ -610,18 +1310,10 @@ type App interface { // 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. - OnRecordBeforeRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationEvent] + OnRecordRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationRequestEvent] - // OnRecordAfterRequestVerificationRequest hook is triggered after each - // successful request verification 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. - OnRecordAfterRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationEvent] - - // OnRecordBeforeConfirmVerificationRequest hook is triggered before each Record - // confirm verification API request (after request data load and before persistence). + // OnRecordConfirmVerificationRequest hook is triggered on each + // Record confirm verification API request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. @@ -629,18 +1321,10 @@ type App interface { // 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. - OnRecordBeforeConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationEvent] + OnRecordConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationRequestEvent] - // OnRecordAfterConfirmVerificationRequest hook is triggered after each - // successful confirm verification 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. - OnRecordAfterConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationEvent] - - // OnRecordBeforeRequestEmailChangeRequest hook is triggered before each Record request email change API request - // (after request data load and before sending the email link to confirm the change). + // OnRecordRequestEmailChangeRequest hook is triggered on each + // Record request email change API request. // // Could be used to additionally validate the request data or implement // completely different request email change behavior. @@ -648,18 +1332,10 @@ type App interface { // 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. - OnRecordBeforeRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeEvent] + OnRecordRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeRequestEvent] - // OnRecordAfterRequestEmailChangeRequest hook is triggered after each - // successful request email change 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. - OnRecordAfterRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeEvent] - - // OnRecordBeforeConfirmEmailChangeRequest hook is triggered before each Record - // confirm email change API request (after request data load and before persistence). + // OnRecordConfirmEmailChangeRequest hook is triggered on each + // Record confirm email change API request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. @@ -667,15 +1343,23 @@ type App interface { // 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. - OnRecordBeforeConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeEvent] + OnRecordConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeRequestEvent] - // OnRecordAfterConfirmEmailChangeRequest hook is triggered after each - // successful confirm email change API request. + // OnRecordRequestOTPRequest hook is triggered on each Record + // request OTP 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. - OnRecordAfterConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeEvent] + OnRecordRequestOTPRequest(tags ...string) *hook.TaggedHook[*RecordCreateOTPRequestEvent] + + // OnRecordAuthWithOTPRequest hook is triggered on each Record + // auth with OTP 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. + OnRecordAuthWithOTPRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithOTPRequestEvent] // --------------------------------------------------------------- // Record CRUD API event hooks @@ -688,7 +1372,7 @@ type App interface { // 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. - OnRecordsListRequest(tags ...string) *hook.TaggedHook[*RecordsListEvent] + OnRecordsListRequest(tags ...string) *hook.TaggedHook[*RecordsListRequestEvent] // OnRecordViewRequest hook is triggered on each API Record view request. // @@ -697,10 +1381,9 @@ type App interface { // 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. - OnRecordViewRequest(tags ...string) *hook.TaggedHook[*RecordViewEvent] + OnRecordViewRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] - // OnRecordBeforeCreateRequest hook is triggered before each API Record - // create request (after request data load and before model persistence). + // OnRecordCreateRequest hook is triggered on each API Record create request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. @@ -708,18 +1391,9 @@ type App interface { // 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. - OnRecordBeforeCreateRequest(tags ...string) *hook.TaggedHook[*RecordCreateEvent] + OnRecordCreateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] - // OnRecordAfterCreateRequest hook is triggered after each - // successful API Record create 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. - OnRecordAfterCreateRequest(tags ...string) *hook.TaggedHook[*RecordCreateEvent] - - // OnRecordBeforeUpdateRequest hook is triggered before each API Record - // update request (after request data load and before model persistence). + // OnRecordUpdateRequest hook is triggered on each API Record update request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. @@ -727,18 +1401,9 @@ type App interface { // 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. - OnRecordBeforeUpdateRequest(tags ...string) *hook.TaggedHook[*RecordUpdateEvent] + OnRecordUpdateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] - // OnRecordAfterUpdateRequest hook is triggered after each - // successful API Record update 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. - OnRecordAfterUpdateRequest(tags ...string) *hook.TaggedHook[*RecordUpdateEvent] - - // OnRecordBeforeDeleteRequest hook is triggered before each API Record - // delete request (after model load and before actual deletion). + // OnRecordDeleteRequest hook is triggered on each API Record delete request. // // Could be used to additionally validate the request data or implement // completely different delete behavior. @@ -746,15 +1411,7 @@ type App interface { // 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. - OnRecordBeforeDeleteRequest(tags ...string) *hook.TaggedHook[*RecordDeleteEvent] - - // OnRecordAfterDeleteRequest hook is triggered after each - // successful API Record delete 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. - OnRecordAfterDeleteRequest(tags ...string) *hook.TaggedHook[*RecordDeleteEvent] + OnRecordDeleteRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] // --------------------------------------------------------------- // Collection API event hooks @@ -763,54 +1420,44 @@ type App interface { // OnCollectionsListRequest hook is triggered on each API Collections list request. // // Could be used to validate or modify the response before returning it to the client. - OnCollectionsListRequest() *hook.Hook[*CollectionsListEvent] + OnCollectionsListRequest() *hook.Hook[*CollectionsListRequestEvent] // OnCollectionViewRequest hook is triggered on each API Collection view request. // // Could be used to validate or modify the response before returning it to the client. - OnCollectionViewRequest() *hook.Hook[*CollectionViewEvent] + OnCollectionViewRequest() *hook.Hook[*CollectionRequestEvent] - // OnCollectionBeforeCreateRequest hook is triggered before each API Collection - // create request (after request data load and before model persistence). + // OnCollectionCreateRequest hook is triggered on each API Collection create request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. - OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] + OnCollectionCreateRequest() *hook.Hook[*CollectionRequestEvent] - // OnCollectionAfterCreateRequest hook is triggered after each - // successful API Collection create request. - OnCollectionAfterCreateRequest() *hook.Hook[*CollectionCreateEvent] - - // OnCollectionBeforeUpdateRequest hook is triggered before each API Collection - // update request (after request data load and before model persistence). + // OnCollectionUpdateRequest hook is triggered on each API Collection update request. // // Could be used to additionally validate the request data or implement // completely different persistence behavior. - OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] + OnCollectionUpdateRequest() *hook.Hook[*CollectionRequestEvent] - // OnCollectionAfterUpdateRequest hook is triggered after each - // successful API Collection update request. - OnCollectionAfterUpdateRequest() *hook.Hook[*CollectionUpdateEvent] - - // OnCollectionBeforeDeleteRequest hook is triggered before each API - // Collection delete request (after model load and before actual deletion). + // OnCollectionDeleteRequest hook is triggered on each API Collection delete request. // // Could be used to additionally validate the request data or implement // completely different delete behavior. - OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] + OnCollectionDeleteRequest() *hook.Hook[*CollectionRequestEvent] - // OnCollectionAfterDeleteRequest hook is triggered after each - // successful API Collection delete request. - OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] - - // OnCollectionsBeforeImportRequest hook is triggered before each API - // collections import request (after request data load and before the actual import). + // OnCollectionsBeforeImportRequest hook is triggered on each API + // collections import request. // // Could be used to additionally validate the imported collections or // to implement completely different import behavior. - OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent] + OnCollectionsImportRequest() *hook.Hook[*CollectionsImportRequestEvent] - // OnCollectionsAfterImportRequest hook is triggered after each - // successful API collections import request. - OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent] + // --------------------------------------------------------------- + // Batch API event hooks + // --------------------------------------------------------------- + + // OnBatchRequest hook is triggered on each API batch request. + // + // Could be used to additionally validate or modify the submitted batch requests. + OnBatchRequest() *hook.Hook[*BatchRequestEvent] } diff --git a/core/auth_origin_model.go b/core/auth_origin_model.go new file mode 100644 index 00000000..103ae936 --- /dev/null +++ b/core/auth_origin_model.go @@ -0,0 +1,239 @@ +package core + +import ( + "context" + "errors" + "slices" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/types" +) + +const CollectionNameAuthOrigins = "_authOrigins" + +var ( + _ Model = (*AuthOrigin)(nil) + _ PreValidator = (*AuthOrigin)(nil) + _ RecordProxy = (*AuthOrigin)(nil) +) + +// AuthOrigin defines a Record proxy for working with the authOrigins collection. +type AuthOrigin struct { + *Record +} + +// NewAuthOrigin instantiates and returns a new blank *AuthOrigin model. +// +// Example usage: +// +// origin := core.NewOrigin(app) +// origin.SetRecordRef(user.Id) +// origin.SetCollectionRef(user.Collection().Id) +// origin.SetFingerprint("...") +// app.Save(origin) +func NewAuthOrigin(app App) *AuthOrigin { + m := &AuthOrigin{} + + c, err := app.FindCachedCollectionByNameOrId(CollectionNameAuthOrigins) + if err != nil { + // this is just to make tests easier since authOrigins is a system collection and it is expected to be always accessible + // (note: the loaded record is further checked on AuthOrigin.PreValidate()) + c = NewBaseCollection("@___invalid___") + } + + m.Record = NewRecord(c) + + return m +} + +// PreValidate implements the [PreValidator] interface and checks +// whether the proxy is properly loaded. +func (m *AuthOrigin) PreValidate(ctx context.Context, app App) error { + if m.Record == nil || m.Record.Collection().Name != CollectionNameAuthOrigins { + return errors.New("missing or invalid AuthOrigin ProxyRecord") + } + + return nil +} + +// ProxyRecord returns the proxied Record model. +func (m *AuthOrigin) ProxyRecord() *Record { + return m.Record +} + +// SetProxyRecord loads the specified record model into the current proxy. +func (m *AuthOrigin) SetProxyRecord(record *Record) { + m.Record = record +} + +// CollectionRef returns the "collectionRef" field value. +func (m *AuthOrigin) CollectionRef() string { + return m.GetString("collectionRef") +} + +// SetCollectionRef updates the "collectionRef" record field value. +func (m *AuthOrigin) SetCollectionRef(collectionId string) { + m.Set("collectionRef", collectionId) +} + +// RecordRef returns the "recordRef" record field value. +func (m *AuthOrigin) RecordRef() string { + return m.GetString("recordRef") +} + +// SetRecordRef updates the "recordRef" record field value. +func (m *AuthOrigin) SetRecordRef(recordId string) { + m.Set("recordRef", recordId) +} + +// Fingerprint returns the "fingerprint" record field value. +func (m *AuthOrigin) Fingerprint() string { + return m.GetString("fingerprint") +} + +// SetFingerprint updates the "fingerprint" record field value. +func (m *AuthOrigin) SetFingerprint(fingerprint string) { + m.Set("fingerprint", fingerprint) +} + +// Created returns the "created" record field value. +func (m *AuthOrigin) Created() types.DateTime { + return m.GetDateTime("created") +} + +// Updated returns the "updated" record field value. +func (m *AuthOrigin) Updated() types.DateTime { + return m.GetDateTime("updated") +} + +func (app *BaseApp) registerAuthOriginHooks() { + recordRefHooks[*AuthOrigin](app, CollectionNameAuthOrigins, CollectionTypeAuth) + + // delete existing auth origins on password change + app.OnRecordUpdate().Bind(&hook.Handler[*RecordEvent]{ + Func: func(e *RecordEvent) error { + err := e.Next() + if err != nil || !e.Record.Collection().IsAuth() { + return err + } + + old := e.Record.Original().GetString(FieldNamePassword + ":hash") + new := e.Record.GetString(FieldNamePassword + ":hash") + if old != new { + err = e.App.DeleteAllAuthOriginsByRecord(e.Record) + if err != nil { + e.App.Logger().Warn( + "Failed to delete all previous auth origin fingerprints", + "error", err, + "recordId", e.Record.Id, + "collectionId", e.Record.Collection().Id, + ) + } + } + + return nil + }, + Priority: 99, + }) +} + +// ------------------------------------------------------------------- + +// recordRefHooks registers common hooks that are usually used with record proxies +// that have polymorphic record relations (aka. "collectionRef" and "recordRef" fields). +func recordRefHooks[T RecordProxy](app App, collectionName string, optCollectionTypes ...string) { + app.OnRecordValidate(collectionName).Bind(&hook.Handler[*RecordEvent]{ + Func: func(e *RecordEvent) error { + collectionId := e.Record.GetString("collectionRef") + err := validation.Validate(collectionId, validation.Required, validation.By(validateCollectionId(e.App, optCollectionTypes...))) + if err != nil { + return validation.Errors{"collectionRef": err} + } + + recordId := e.Record.GetString("recordRef") + err = validation.Validate(recordId, validation.Required, validation.By(validateRecordId(e.App, collectionId))) + if err != nil { + return validation.Errors{"recordRef": err} + } + + return e.Next() + }, + Priority: 99, + }) + + // delete on collection ref delete + app.OnCollectionDeleteExecute().Bind(&hook.Handler[*CollectionEvent]{ + Func: func(e *CollectionEvent) error { + if e.Collection.Name == collectionName || (len(optCollectionTypes) > 0 && !slices.Contains(optCollectionTypes, e.Collection.Type)) { + return e.Next() + } + + originalApp := e.App + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + if err := e.Next(); err != nil { + return err + } + + rels, err := txApp.FindAllRecords(collectionName, dbx.HashExp{"collectionRef": e.Collection.Id}) + if err != nil { + return err + } + + for _, mfa := range rels { + if err := txApp.Delete(mfa); err != nil { + return err + } + } + + return nil + }) + e.App = originalApp + + return txErr + }, + Priority: 99, + }) + + // delete on record ref delete + app.OnRecordDeleteExecute().Bind(&hook.Handler[*RecordEvent]{ + Func: func(e *RecordEvent) error { + if e.Record.Collection().Name == collectionName || + (len(optCollectionTypes) > 0 && !slices.Contains(optCollectionTypes, e.Record.Collection().Type)) { + return e.Next() + } + + originalApp := e.App + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + if err := e.Next(); err != nil { + return err + } + + rels, err := txApp.FindAllRecords(collectionName, dbx.HashExp{ + "collectionRef": e.Record.Collection().Id, + "recordRef": e.Record.Id, + }) + if err != nil { + return err + } + + for _, rel := range rels { + if err := txApp.Delete(rel); err != nil { + return err + } + } + + return nil + }) + e.App = originalApp + + return txErr + }, + Priority: 99, + }) +} diff --git a/core/auth_origin_model_test.go b/core/auth_origin_model_test.go new file mode 100644 index 00000000..2d06a2d3 --- /dev/null +++ b/core/auth_origin_model_test.go @@ -0,0 +1,332 @@ +package core_test + +import ( + "fmt" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewAuthOrigin(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + if origin.Collection().Name != core.CollectionNameAuthOrigins { + t.Fatalf("Expected record with %q collection, got %q", core.CollectionNameAuthOrigins, origin.Collection().Name) + } +} + +func TestAuthOriginProxyRecord(t *testing.T) { + t.Parallel() + + record := core.NewRecord(core.NewBaseCollection("test")) + record.Id = "test_id" + + origin := core.AuthOrigin{} + origin.SetProxyRecord(record) + + if origin.ProxyRecord() == nil || origin.ProxyRecord().Id != record.Id { + t.Fatalf("Expected proxy record with id %q, got %v", record.Id, origin.ProxyRecord()) + } +} + +func TestAuthOriginRecordRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + origin.SetRecordRef(testValue) + + if v := origin.RecordRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := origin.GetString("recordRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestAuthOriginCollectionRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + origin.SetCollectionRef(testValue) + + if v := origin.CollectionRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := origin.GetString("collectionRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestAuthOriginFingerprint(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + origin.SetFingerprint(testValue) + + if v := origin.Fingerprint(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := origin.GetString("fingerprint"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestAuthOriginCreated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + if v := origin.Created().String(); v != "" { + t.Fatalf("Expected empty created, got %q", v) + } + + now := types.NowDateTime() + origin.SetRaw("created", now) + + if v := origin.Created().String(); v != now.String() { + t.Fatalf("Expected %q created, got %q", now.String(), v) + } +} + +func TestAuthOriginUpdated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + origin := core.NewAuthOrigin(app) + + if v := origin.Updated().String(); v != "" { + t.Fatalf("Expected empty updated, got %q", v) + } + + now := types.NowDateTime() + origin.SetRaw("updated", now) + + if v := origin.Updated().String(); v != now.String() { + t.Fatalf("Expected %q updated, got %q", now.String(), v) + } +} + +func TestAuthOriginPreValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + originsCol, err := app.FindCollectionByNameOrId(core.CollectionNameAuthOrigins) + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + t.Run("no proxy record", func(t *testing.T) { + origin := &core.AuthOrigin{} + + if err := app.Validate(origin); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("non-AuthOrigin collection", func(t *testing.T) { + origin := &core.AuthOrigin{} + origin.SetProxyRecord(core.NewRecord(core.NewBaseCollection("invalid"))) + origin.SetRecordRef(user.Id) + origin.SetCollectionRef(user.Collection().Id) + origin.SetFingerprint("abc") + + if err := app.Validate(origin); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("AuthOrigin collection", func(t *testing.T) { + origin := &core.AuthOrigin{} + origin.SetProxyRecord(core.NewRecord(originsCol)) + origin.SetRecordRef(user.Id) + origin.SetCollectionRef(user.Collection().Id) + origin.SetFingerprint("abc") + + if err := app.Validate(origin); err != nil { + t.Fatalf("Expected nil validation error, got %v", err) + } + }) +} + +func TestAuthOriginValidateHook(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + origin func() *core.AuthOrigin + expectErrors []string + }{ + { + "empty", + func() *core.AuthOrigin { + return core.NewAuthOrigin(app) + }, + []string{"collectionRef", "recordRef", "fingerprint"}, + }, + { + "non-auth collection", + func() *core.AuthOrigin { + origin := core.NewAuthOrigin(app) + origin.SetCollectionRef(demo1.Collection().Id) + origin.SetRecordRef(demo1.Id) + origin.SetFingerprint("abc") + return origin + }, + []string{"collectionRef"}, + }, + { + "missing record id", + func() *core.AuthOrigin { + origin := core.NewAuthOrigin(app) + origin.SetCollectionRef(user.Collection().Id) + origin.SetRecordRef("missing") + origin.SetFingerprint("abc") + return origin + }, + []string{"recordRef"}, + }, + { + "valid ref", + func() *core.AuthOrigin { + origin := core.NewAuthOrigin(app) + origin.SetCollectionRef(user.Collection().Id) + origin.SetRecordRef(user.Id) + origin.SetFingerprint("abc") + return origin + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := app.Validate(s.origin()) + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestAuthOriginPasswordChangeDeletion(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + // no auth origin associated with it + user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + superuser2, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + client1, err := testApp.FindAuthRecordByEmail("clients", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + deletedIds []string + }{ + {user1, nil}, + {superuser2, []string{"5798yh833k6w6w0", "ic55o70g4f8pcl4", "dmy260k6ksjr4ib"}}, + {client1, []string{"9r2j0m74260ur8i"}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + deletedIds := []string{} + app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + s.record.SetPassword("new_password") + + err := app.Save(s.record) + if err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(s.deletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds) + } + + for _, id := range s.deletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + }) + } +} diff --git a/core/auth_origin_query.go b/core/auth_origin_query.go new file mode 100644 index 00000000..c8fd20d8 --- /dev/null +++ b/core/auth_origin_query.go @@ -0,0 +1,101 @@ +package core + +import ( + "errors" + + "github.com/pocketbase/dbx" +) + +// FindAllAuthOriginsByRecord returns all AuthOrigin models linked to the provided auth record (in DESC order). +func (app *BaseApp) FindAllAuthOriginsByRecord(authRecord *Record) ([]*AuthOrigin, error) { + result := []*AuthOrigin{} + + err := app.RecordQuery(CollectionNameAuthOrigins). + AndWhere(dbx.HashExp{ + "collectionRef": authRecord.Collection().Id, + "recordRef": authRecord.Id, + }). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindAllAuthOriginsByCollection returns all AuthOrigin models linked to the provided collection (in DESC order). +func (app *BaseApp) FindAllAuthOriginsByCollection(collection *Collection) ([]*AuthOrigin, error) { + result := []*AuthOrigin{} + + err := app.RecordQuery(CollectionNameAuthOrigins). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindAuthOriginById returns a single AuthOrigin model by its id. +func (app *BaseApp) FindAuthOriginById(id string) (*AuthOrigin, error) { + result := &AuthOrigin{} + + err := app.RecordQuery(CollectionNameAuthOrigins). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindAuthOriginByRecordAndFingerprint returns a single AuthOrigin model +// by its authRecord relation and fingerprint. +func (app *BaseApp) FindAuthOriginByRecordAndFingerprint(authRecord *Record, fingerprint string) (*AuthOrigin, error) { + result := &AuthOrigin{} + + err := app.RecordQuery(CollectionNameAuthOrigins). + AndWhere(dbx.HashExp{ + "collectionRef": authRecord.Collection().Id, + "recordRef": authRecord.Id, + "fingerprint": fingerprint, + }). + Limit(1). + One(result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteAllAuthOriginsByRecord deletes all AuthOrigin models associated with the provided record. +// +// Returns a combined error with the failed deletes. +func (app *BaseApp) DeleteAllAuthOriginsByRecord(authRecord *Record) error { + models, err := app.FindAllAuthOriginsByRecord(authRecord) + if err != nil { + return err + } + + var errs []error + for _, m := range models { + if err := app.Delete(m); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/core/auth_origin_query_test.go b/core/auth_origin_query_test.go new file mode 100644 index 00000000..eec036ae --- /dev/null +++ b/core/auth_origin_query_test.go @@ -0,0 +1,268 @@ +package core_test + +import ( + "fmt" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestFindAllAuthOriginsByRecord(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + client1, err := app.FindAuthRecordByEmail("clients", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + expected []string + }{ + {demo1, nil}, + {superuser2, []string{"5798yh833k6w6w0", "ic55o70g4f8pcl4", "dmy260k6ksjr4ib"}}, + {superuser4, nil}, + {client1, []string{"9r2j0m74260ur8i"}}, + } + + for _, s := range scenarios { + t.Run(s.record.Collection().Name+"_"+s.record.Id, func(t *testing.T) { + result, err := app.FindAllAuthOriginsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total origins %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindAllAuthOriginsByCollection(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + + clients, err := app.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + collection *core.Collection + expected []string + }{ + {demo1, nil}, + {superusers, []string{"5798yh833k6w6w0", "ic55o70g4f8pcl4", "dmy260k6ksjr4ib", "5f29jy38bf5zm3f"}}, + {clients, []string{"9r2j0m74260ur8i"}}, + } + + for _, s := range scenarios { + t.Run(s.collection.Name, func(t *testing.T) { + result, err := app.FindAllAuthOriginsByCollection(s.collection) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total origins %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindAuthOriginById(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + expectError bool + }{ + {"", true}, + {"84nmscqy84lsi1t", true}, // non-origin id + {"9r2j0m74260ur8i", false}, + } + + for _, s := range scenarios { + t.Run(s.id, func(t *testing.T) { + result, err := app.FindAuthOriginById(s.id) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if result.Id != s.id { + t.Fatalf("Expected record with id %q, got %q", s.id, result.Id) + } + }) + } +} + +func TestFindAuthOriginByRecordAndFingerprint(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + fingerprint string + expectError bool + }{ + {demo1, "6afbfe481c31c08c55a746cccb88ece0", true}, + {superuser2, "", true}, + {superuser2, "abc", true}, + {superuser2, "22bbbcbed36e25321f384ccf99f60057", false}, // fingerprint from different origin + {superuser2, "6afbfe481c31c08c55a746cccb88ece0", false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Id, s.fingerprint), func(t *testing.T) { + result, err := app.FindAuthOriginByRecordAndFingerprint(s.record, s.fingerprint) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if result.Fingerprint() != s.fingerprint { + t.Fatalf("Expected origin with fingerprint %q, got %q", s.fingerprint, result.Fingerprint()) + } + + if result.RecordRef() != s.record.Id || result.CollectionRef() != s.record.Collection().Id { + t.Fatalf("Expected record %q (%q), got %q (%q)", s.record.Id, s.record.Collection().Id, result.RecordRef(), result.CollectionRef()) + } + }) + } +} + +func TestDeleteAllAuthOriginsByRecord(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + client1, err := testApp.FindAuthRecordByEmail("clients", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + deletedIds []string + }{ + {demo1, nil}, // non-auth record + {superuser2, []string{"5798yh833k6w6w0", "ic55o70g4f8pcl4", "dmy260k6ksjr4ib"}}, + {superuser4, nil}, + {client1, []string{"9r2j0m74260ur8i"}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + deletedIds := []string{} + app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + err := app.DeleteAllAuthOriginsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(s.deletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds) + } + + for _, id := range s.deletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + }) + } +} diff --git a/core/base.go b/core/base.go index 8c201bc7..a1991012 100644 --- a/core/base.go +++ b/core/base.go @@ -4,10 +4,12 @@ import ( "context" "database/sql" "errors" + "fmt" "log" "log/slog" "os" "path/filepath" + "regexp" "runtime" "strings" "syscall" @@ -15,174 +17,174 @@ import ( "github.com/fatih/color" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/settings" + "github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/logger" "github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/routine" - "github.com/pocketbase/pocketbase/tools/security" "github.com/pocketbase/pocketbase/tools/store" "github.com/pocketbase/pocketbase/tools/subscriptions" "github.com/pocketbase/pocketbase/tools/types" - "github.com/spf13/cast" ) const ( - DefaultDataMaxOpenConns int = 120 - DefaultDataMaxIdleConns int = 20 - DefaultLogsMaxOpenConns int = 10 - DefaultLogsMaxIdleConns int = 2 + DefaultDataMaxOpenConns int = 250 + DefaultDataMaxIdleConns int = 15 + DefaultAuxMaxOpenConns int = 20 + DefaultAuxMaxIdleConns int = 3 + DefaultQueryTimeout time.Duration = 30 * time.Second - LocalStorageDirName string = "storage" - LocalBackupsDirName string = "backups" - LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap() + LocalStorageDirName string = "storage" + LocalBackupsDirName string = "backups" + LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap() + LocalAutocertCacheDirName string = ".autocert_cache" ) +// FilesManager defines an interface with common methods that files manager models should implement. +type FilesManager interface { + // BaseFilesPath returns the storage dir path used by the interface instance. + BaseFilesPath() string +} + +// DBConnectFunc defines a database connection initialization function. +type DBConnectFunc func(dbPath string) (*dbx.DB, error) + +// BaseAppConfig defines a BaseApp configuration option +type BaseAppConfig struct { + DBConnect DBConnectFunc + DataDir string + EncryptionEnv string + QueryTimeout time.Duration + DataMaxOpenConns int + DataMaxIdleConns int + AuxMaxOpenConns int + AuxMaxIdleConns int + IsDev bool +} + +// ensures that the BaseApp implements the App interface. var _ App = (*BaseApp)(nil) // BaseApp implements core.App and defines the base PocketBase app structure. type BaseApp struct { - // @todo consider introducing a mutex to allow safe concurrent config changes during runtime - - // configurable parameters - isDev bool - dataDir string - encryptionEnv string - dataMaxOpenConns int - dataMaxIdleConns int - logsMaxOpenConns int - logsMaxIdleConns int - - // internals + config *BaseAppConfig + txInfo *txAppInfo store *store.Store[any] - settings *settings.Settings - dao *daos.Dao - logsDao *daos.Dao + cron *cron.Cron + settings *Settings subscriptionsBroker *subscriptions.Broker logger *slog.Logger + concurrentDB dbx.Builder + nonconcurrentDB dbx.Builder + auxConcurrentDB dbx.Builder + auxNonconcurrentDB dbx.Builder // app event hooks - onBeforeBootstrap *hook.Hook[*BootstrapEvent] - onAfterBootstrap *hook.Hook[*BootstrapEvent] - onBeforeServe *hook.Hook[*ServeEvent] - onBeforeApiError *hook.Hook[*ApiErrorEvent] - onAfterApiError *hook.Hook[*ApiErrorEvent] - onTerminate *hook.Hook[*TerminateEvent] + onBootstrap *hook.Hook[*BootstrapEvent] + onServe *hook.Hook[*ServeEvent] + onTerminate *hook.Hook[*TerminateEvent] + onBackupCreate *hook.Hook[*BackupEvent] + onBackupRestore *hook.Hook[*BackupEvent] - // dao event hooks - onModelBeforeCreate *hook.Hook[*ModelEvent] - onModelAfterCreate *hook.Hook[*ModelEvent] - onModelBeforeUpdate *hook.Hook[*ModelEvent] - onModelAfterUpdate *hook.Hook[*ModelEvent] - onModelBeforeDelete *hook.Hook[*ModelEvent] - onModelAfterDelete *hook.Hook[*ModelEvent] + // db model hooks + onModelValidate *hook.Hook[*ModelEvent] + onModelCreate *hook.Hook[*ModelEvent] + onModelCreateExecute *hook.Hook[*ModelEvent] + onModelAfterCreateSuccess *hook.Hook[*ModelEvent] + onModelAfterCreateError *hook.Hook[*ModelErrorEvent] + onModelUpdate *hook.Hook[*ModelEvent] + onModelUpdateWrite *hook.Hook[*ModelEvent] + onModelAfterUpdateSuccess *hook.Hook[*ModelEvent] + onModelAfterUpdateError *hook.Hook[*ModelErrorEvent] + onModelDelete *hook.Hook[*ModelEvent] + onModelDeleteExecute *hook.Hook[*ModelEvent] + onModelAfterDeleteSuccess *hook.Hook[*ModelEvent] + onModelAfterDeleteError *hook.Hook[*ModelErrorEvent] + + // db record hooks + onRecordEnrich *hook.Hook[*RecordEnrichEvent] + onRecordValidate *hook.Hook[*RecordEvent] + onRecordCreate *hook.Hook[*RecordEvent] + onRecordCreateExecute *hook.Hook[*RecordEvent] + onRecordAfterCreateSuccess *hook.Hook[*RecordEvent] + onRecordAfterCreateError *hook.Hook[*RecordErrorEvent] + onRecordUpdate *hook.Hook[*RecordEvent] + onRecordUpdateExecute *hook.Hook[*RecordEvent] + onRecordAfterUpdateSuccess *hook.Hook[*RecordEvent] + onRecordAfterUpdateError *hook.Hook[*RecordErrorEvent] + onRecordDelete *hook.Hook[*RecordEvent] + onRecordDeleteExecute *hook.Hook[*RecordEvent] + onRecordAfterDeleteSuccess *hook.Hook[*RecordEvent] + onRecordAfterDeleteError *hook.Hook[*RecordErrorEvent] + + // db collection hooks + onCollectionValidate *hook.Hook[*CollectionEvent] + onCollectionCreate *hook.Hook[*CollectionEvent] + onCollectionCreateExecute *hook.Hook[*CollectionEvent] + onCollectionAfterCreateSuccess *hook.Hook[*CollectionEvent] + onCollectionAfterCreateError *hook.Hook[*CollectionErrorEvent] + onCollectionUpdate *hook.Hook[*CollectionEvent] + onCollectionUpdateExecute *hook.Hook[*CollectionEvent] + onCollectionAfterUpdateSuccess *hook.Hook[*CollectionEvent] + onCollectionAfterUpdateError *hook.Hook[*CollectionErrorEvent] + onCollectionDelete *hook.Hook[*CollectionEvent] + onCollectionDeleteExecute *hook.Hook[*CollectionEvent] + onCollectionAfterDeleteSuccess *hook.Hook[*CollectionEvent] + onCollectionAfterDeleteError *hook.Hook[*CollectionErrorEvent] // mailer event hooks - onMailerBeforeAdminResetPasswordSend *hook.Hook[*MailerAdminEvent] - onMailerAfterAdminResetPasswordSend *hook.Hook[*MailerAdminEvent] - onMailerBeforeRecordResetPasswordSend *hook.Hook[*MailerRecordEvent] - onMailerAfterRecordResetPasswordSend *hook.Hook[*MailerRecordEvent] - onMailerBeforeRecordVerificationSend *hook.Hook[*MailerRecordEvent] - onMailerAfterRecordVerificationSend *hook.Hook[*MailerRecordEvent] - onMailerBeforeRecordChangeEmailSend *hook.Hook[*MailerRecordEvent] - onMailerAfterRecordChangeEmailSend *hook.Hook[*MailerRecordEvent] + onMailerSend *hook.Hook[*MailerEvent] + onMailerRecordPasswordResetSend *hook.Hook[*MailerRecordEvent] + onMailerRecordVerificationSend *hook.Hook[*MailerRecordEvent] + onMailerRecordEmailChangeSend *hook.Hook[*MailerRecordEvent] + onMailerRecordOTPSend *hook.Hook[*MailerRecordEvent] + onMailerRecordAuthAlertSend *hook.Hook[*MailerRecordEvent] // realtime api event hooks - onRealtimeConnectRequest *hook.Hook[*RealtimeConnectEvent] - onRealtimeDisconnectRequest *hook.Hook[*RealtimeDisconnectEvent] - onRealtimeBeforeMessageSend *hook.Hook[*RealtimeMessageEvent] - onRealtimeAfterMessageSend *hook.Hook[*RealtimeMessageEvent] - onRealtimeBeforeSubscribeRequest *hook.Hook[*RealtimeSubscribeEvent] - onRealtimeAfterSubscribeRequest *hook.Hook[*RealtimeSubscribeEvent] + onRealtimeConnectRequest *hook.Hook[*RealtimeConnectRequestEvent] + onRealtimeMessageSend *hook.Hook[*RealtimeMessageEvent] + onRealtimeSubscribeRequest *hook.Hook[*RealtimeSubscribeRequestEvent] - // settings api event hooks - onSettingsListRequest *hook.Hook[*SettingsListEvent] - onSettingsBeforeUpdateRequest *hook.Hook[*SettingsUpdateEvent] - onSettingsAfterUpdateRequest *hook.Hook[*SettingsUpdateEvent] + // settings event hooks + onSettingsListRequest *hook.Hook[*SettingsListRequestEvent] + onSettingsUpdateRequest *hook.Hook[*SettingsUpdateRequestEvent] + onSettingsReload *hook.Hook[*SettingsReloadEvent] // file api event hooks - onFileDownloadRequest *hook.Hook[*FileDownloadEvent] - onFileBeforeTokenRequest *hook.Hook[*FileTokenEvent] - onFileAfterTokenRequest *hook.Hook[*FileTokenEvent] - - // admin api event hooks - onAdminsListRequest *hook.Hook[*AdminsListEvent] - onAdminViewRequest *hook.Hook[*AdminViewEvent] - onAdminBeforeCreateRequest *hook.Hook[*AdminCreateEvent] - onAdminAfterCreateRequest *hook.Hook[*AdminCreateEvent] - onAdminBeforeUpdateRequest *hook.Hook[*AdminUpdateEvent] - onAdminAfterUpdateRequest *hook.Hook[*AdminUpdateEvent] - onAdminBeforeDeleteRequest *hook.Hook[*AdminDeleteEvent] - onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent] - onAdminAuthRequest *hook.Hook[*AdminAuthEvent] - onAdminBeforeAuthWithPasswordRequest *hook.Hook[*AdminAuthWithPasswordEvent] - onAdminAfterAuthWithPasswordRequest *hook.Hook[*AdminAuthWithPasswordEvent] - onAdminBeforeAuthRefreshRequest *hook.Hook[*AdminAuthRefreshEvent] - onAdminAfterAuthRefreshRequest *hook.Hook[*AdminAuthRefreshEvent] - onAdminBeforeRequestPasswordResetRequest *hook.Hook[*AdminRequestPasswordResetEvent] - onAdminAfterRequestPasswordResetRequest *hook.Hook[*AdminRequestPasswordResetEvent] - onAdminBeforeConfirmPasswordResetRequest *hook.Hook[*AdminConfirmPasswordResetEvent] - onAdminAfterConfirmPasswordResetRequest *hook.Hook[*AdminConfirmPasswordResetEvent] + onFileDownloadRequest *hook.Hook[*FileDownloadRequestEvent] + onFileTokenRequest *hook.Hook[*FileTokenRequestEvent] // record auth API event hooks - onRecordAuthRequest *hook.Hook[*RecordAuthEvent] - onRecordBeforeAuthWithPasswordRequest *hook.Hook[*RecordAuthWithPasswordEvent] - onRecordAfterAuthWithPasswordRequest *hook.Hook[*RecordAuthWithPasswordEvent] - onRecordBeforeAuthWithOAuth2Request *hook.Hook[*RecordAuthWithOAuth2Event] - onRecordAfterAuthWithOAuth2Request *hook.Hook[*RecordAuthWithOAuth2Event] - onRecordBeforeAuthRefreshRequest *hook.Hook[*RecordAuthRefreshEvent] - onRecordAfterAuthRefreshRequest *hook.Hook[*RecordAuthRefreshEvent] - onRecordBeforeRequestPasswordResetRequest *hook.Hook[*RecordRequestPasswordResetEvent] - onRecordAfterRequestPasswordResetRequest *hook.Hook[*RecordRequestPasswordResetEvent] - onRecordBeforeConfirmPasswordResetRequest *hook.Hook[*RecordConfirmPasswordResetEvent] - onRecordAfterConfirmPasswordResetRequest *hook.Hook[*RecordConfirmPasswordResetEvent] - onRecordBeforeRequestVerificationRequest *hook.Hook[*RecordRequestVerificationEvent] - onRecordAfterRequestVerificationRequest *hook.Hook[*RecordRequestVerificationEvent] - onRecordBeforeConfirmVerificationRequest *hook.Hook[*RecordConfirmVerificationEvent] - onRecordAfterConfirmVerificationRequest *hook.Hook[*RecordConfirmVerificationEvent] - onRecordBeforeRequestEmailChangeRequest *hook.Hook[*RecordRequestEmailChangeEvent] - onRecordAfterRequestEmailChangeRequest *hook.Hook[*RecordRequestEmailChangeEvent] - onRecordBeforeConfirmEmailChangeRequest *hook.Hook[*RecordConfirmEmailChangeEvent] - onRecordAfterConfirmEmailChangeRequest *hook.Hook[*RecordConfirmEmailChangeEvent] - onRecordListExternalAuthsRequest *hook.Hook[*RecordListExternalAuthsEvent] - onRecordBeforeUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent] - onRecordAfterUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent] + onRecordAuthRequest *hook.Hook[*RecordAuthRequestEvent] + onRecordAuthWithPasswordRequest *hook.Hook[*RecordAuthWithPasswordRequestEvent] + onRecordAuthWithOAuth2Request *hook.Hook[*RecordAuthWithOAuth2RequestEvent] + onRecordAuthRefreshRequest *hook.Hook[*RecordAuthRefreshRequestEvent] + onRecordRequestPasswordResetRequest *hook.Hook[*RecordRequestPasswordResetRequestEvent] + onRecordConfirmPasswordResetRequest *hook.Hook[*RecordConfirmPasswordResetRequestEvent] + onRecordRequestVerificationRequest *hook.Hook[*RecordRequestVerificationRequestEvent] + onRecordConfirmVerificationRequest *hook.Hook[*RecordConfirmVerificationRequestEvent] + onRecordRequestEmailChangeRequest *hook.Hook[*RecordRequestEmailChangeRequestEvent] + onRecordConfirmEmailChangeRequest *hook.Hook[*RecordConfirmEmailChangeRequestEvent] + onRecordRequestOTPRequest *hook.Hook[*RecordCreateOTPRequestEvent] + onRecordAuthWithOTPRequest *hook.Hook[*RecordAuthWithOTPRequestEvent] // record crud API event hooks - onRecordsListRequest *hook.Hook[*RecordsListEvent] - onRecordViewRequest *hook.Hook[*RecordViewEvent] - onRecordBeforeCreateRequest *hook.Hook[*RecordCreateEvent] - onRecordAfterCreateRequest *hook.Hook[*RecordCreateEvent] - onRecordBeforeUpdateRequest *hook.Hook[*RecordUpdateEvent] - onRecordAfterUpdateRequest *hook.Hook[*RecordUpdateEvent] - onRecordBeforeDeleteRequest *hook.Hook[*RecordDeleteEvent] - onRecordAfterDeleteRequest *hook.Hook[*RecordDeleteEvent] + onRecordsListRequest *hook.Hook[*RecordsListRequestEvent] + onRecordViewRequest *hook.Hook[*RecordRequestEvent] + onRecordCreateRequest *hook.Hook[*RecordRequestEvent] + onRecordUpdateRequest *hook.Hook[*RecordRequestEvent] + onRecordDeleteRequest *hook.Hook[*RecordRequestEvent] // collection API event hooks - onCollectionsListRequest *hook.Hook[*CollectionsListEvent] - onCollectionViewRequest *hook.Hook[*CollectionViewEvent] - onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent] - onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent] - onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent] - onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent] - onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent] - onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent] - onCollectionsBeforeImportRequest *hook.Hook[*CollectionsImportEvent] - onCollectionsAfterImportRequest *hook.Hook[*CollectionsImportEvent] -} + onCollectionsListRequest *hook.Hook[*CollectionsListRequestEvent] + onCollectionViewRequest *hook.Hook[*CollectionRequestEvent] + onCollectionCreateRequest *hook.Hook[*CollectionRequestEvent] + onCollectionUpdateRequest *hook.Hook[*CollectionRequestEvent] + onCollectionDeleteRequest *hook.Hook[*CollectionRequestEvent] + onCollectionsImportRequest *hook.Hook[*CollectionsImportRequestEvent] -// BaseAppConfig defines a BaseApp configuration option -type BaseAppConfig struct { - IsDev bool - DataDir string - EncryptionEnv string - DataMaxOpenConns int // default to 500 - DataMaxIdleConns int // default 20 - LogsMaxOpenConns int // default to 100 - LogsMaxIdleConns int // default to 5 + onBatchRequest *hook.Hook[*BatchRequestEvent] } // NewBaseApp creates and returns a new BaseApp instance @@ -191,136 +193,162 @@ type BaseAppConfig struct { // To initialize the app, you need to call `app.Bootstrap()`. func NewBaseApp(config BaseAppConfig) *BaseApp { app := &BaseApp{ - isDev: config.IsDev, - dataDir: config.DataDir, - encryptionEnv: config.EncryptionEnv, - dataMaxOpenConns: config.DataMaxOpenConns, - dataMaxIdleConns: config.DataMaxIdleConns, - logsMaxOpenConns: config.LogsMaxOpenConns, - logsMaxIdleConns: config.LogsMaxIdleConns, + settings: newDefaultSettings(), store: store.New[any](nil), - settings: settings.New(), + cron: cron.New(), subscriptionsBroker: subscriptions.NewBroker(), - - // app event hooks - onBeforeBootstrap: &hook.Hook[*BootstrapEvent]{}, - onAfterBootstrap: &hook.Hook[*BootstrapEvent]{}, - onBeforeServe: &hook.Hook[*ServeEvent]{}, - onBeforeApiError: &hook.Hook[*ApiErrorEvent]{}, - onAfterApiError: &hook.Hook[*ApiErrorEvent]{}, - onTerminate: &hook.Hook[*TerminateEvent]{}, - - // dao event hooks - onModelBeforeCreate: &hook.Hook[*ModelEvent]{}, - onModelAfterCreate: &hook.Hook[*ModelEvent]{}, - onModelBeforeUpdate: &hook.Hook[*ModelEvent]{}, - onModelAfterUpdate: &hook.Hook[*ModelEvent]{}, - onModelBeforeDelete: &hook.Hook[*ModelEvent]{}, - onModelAfterDelete: &hook.Hook[*ModelEvent]{}, - - // mailer event hooks - onMailerBeforeAdminResetPasswordSend: &hook.Hook[*MailerAdminEvent]{}, - onMailerAfterAdminResetPasswordSend: &hook.Hook[*MailerAdminEvent]{}, - onMailerBeforeRecordResetPasswordSend: &hook.Hook[*MailerRecordEvent]{}, - onMailerAfterRecordResetPasswordSend: &hook.Hook[*MailerRecordEvent]{}, - onMailerBeforeRecordVerificationSend: &hook.Hook[*MailerRecordEvent]{}, - onMailerAfterRecordVerificationSend: &hook.Hook[*MailerRecordEvent]{}, - onMailerBeforeRecordChangeEmailSend: &hook.Hook[*MailerRecordEvent]{}, - onMailerAfterRecordChangeEmailSend: &hook.Hook[*MailerRecordEvent]{}, - - // realtime API event hooks - onRealtimeConnectRequest: &hook.Hook[*RealtimeConnectEvent]{}, - onRealtimeDisconnectRequest: &hook.Hook[*RealtimeDisconnectEvent]{}, - onRealtimeBeforeMessageSend: &hook.Hook[*RealtimeMessageEvent]{}, - onRealtimeAfterMessageSend: &hook.Hook[*RealtimeMessageEvent]{}, - onRealtimeBeforeSubscribeRequest: &hook.Hook[*RealtimeSubscribeEvent]{}, - onRealtimeAfterSubscribeRequest: &hook.Hook[*RealtimeSubscribeEvent]{}, - - // settings API event hooks - onSettingsListRequest: &hook.Hook[*SettingsListEvent]{}, - onSettingsBeforeUpdateRequest: &hook.Hook[*SettingsUpdateEvent]{}, - onSettingsAfterUpdateRequest: &hook.Hook[*SettingsUpdateEvent]{}, - - // file API event hooks - onFileDownloadRequest: &hook.Hook[*FileDownloadEvent]{}, - onFileBeforeTokenRequest: &hook.Hook[*FileTokenEvent]{}, - onFileAfterTokenRequest: &hook.Hook[*FileTokenEvent]{}, - - // admin API event hooks - onAdminsListRequest: &hook.Hook[*AdminsListEvent]{}, - onAdminViewRequest: &hook.Hook[*AdminViewEvent]{}, - onAdminBeforeCreateRequest: &hook.Hook[*AdminCreateEvent]{}, - onAdminAfterCreateRequest: &hook.Hook[*AdminCreateEvent]{}, - onAdminBeforeUpdateRequest: &hook.Hook[*AdminUpdateEvent]{}, - onAdminAfterUpdateRequest: &hook.Hook[*AdminUpdateEvent]{}, - onAdminBeforeDeleteRequest: &hook.Hook[*AdminDeleteEvent]{}, - onAdminAfterDeleteRequest: &hook.Hook[*AdminDeleteEvent]{}, - onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{}, - onAdminBeforeAuthWithPasswordRequest: &hook.Hook[*AdminAuthWithPasswordEvent]{}, - onAdminAfterAuthWithPasswordRequest: &hook.Hook[*AdminAuthWithPasswordEvent]{}, - onAdminBeforeAuthRefreshRequest: &hook.Hook[*AdminAuthRefreshEvent]{}, - onAdminAfterAuthRefreshRequest: &hook.Hook[*AdminAuthRefreshEvent]{}, - onAdminBeforeRequestPasswordResetRequest: &hook.Hook[*AdminRequestPasswordResetEvent]{}, - onAdminAfterRequestPasswordResetRequest: &hook.Hook[*AdminRequestPasswordResetEvent]{}, - onAdminBeforeConfirmPasswordResetRequest: &hook.Hook[*AdminConfirmPasswordResetEvent]{}, - onAdminAfterConfirmPasswordResetRequest: &hook.Hook[*AdminConfirmPasswordResetEvent]{}, - - // record auth API event hooks - onRecordAuthRequest: &hook.Hook[*RecordAuthEvent]{}, - onRecordBeforeAuthWithPasswordRequest: &hook.Hook[*RecordAuthWithPasswordEvent]{}, - onRecordAfterAuthWithPasswordRequest: &hook.Hook[*RecordAuthWithPasswordEvent]{}, - onRecordBeforeAuthWithOAuth2Request: &hook.Hook[*RecordAuthWithOAuth2Event]{}, - onRecordAfterAuthWithOAuth2Request: &hook.Hook[*RecordAuthWithOAuth2Event]{}, - onRecordBeforeAuthRefreshRequest: &hook.Hook[*RecordAuthRefreshEvent]{}, - onRecordAfterAuthRefreshRequest: &hook.Hook[*RecordAuthRefreshEvent]{}, - onRecordBeforeRequestPasswordResetRequest: &hook.Hook[*RecordRequestPasswordResetEvent]{}, - onRecordAfterRequestPasswordResetRequest: &hook.Hook[*RecordRequestPasswordResetEvent]{}, - onRecordBeforeConfirmPasswordResetRequest: &hook.Hook[*RecordConfirmPasswordResetEvent]{}, - onRecordAfterConfirmPasswordResetRequest: &hook.Hook[*RecordConfirmPasswordResetEvent]{}, - onRecordBeforeRequestVerificationRequest: &hook.Hook[*RecordRequestVerificationEvent]{}, - onRecordAfterRequestVerificationRequest: &hook.Hook[*RecordRequestVerificationEvent]{}, - onRecordBeforeConfirmVerificationRequest: &hook.Hook[*RecordConfirmVerificationEvent]{}, - onRecordAfterConfirmVerificationRequest: &hook.Hook[*RecordConfirmVerificationEvent]{}, - onRecordBeforeRequestEmailChangeRequest: &hook.Hook[*RecordRequestEmailChangeEvent]{}, - onRecordAfterRequestEmailChangeRequest: &hook.Hook[*RecordRequestEmailChangeEvent]{}, - onRecordBeforeConfirmEmailChangeRequest: &hook.Hook[*RecordConfirmEmailChangeEvent]{}, - onRecordAfterConfirmEmailChangeRequest: &hook.Hook[*RecordConfirmEmailChangeEvent]{}, - onRecordListExternalAuthsRequest: &hook.Hook[*RecordListExternalAuthsEvent]{}, - onRecordBeforeUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{}, - onRecordAfterUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{}, - - // record crud API event hooks - onRecordsListRequest: &hook.Hook[*RecordsListEvent]{}, - onRecordViewRequest: &hook.Hook[*RecordViewEvent]{}, - onRecordBeforeCreateRequest: &hook.Hook[*RecordCreateEvent]{}, - onRecordAfterCreateRequest: &hook.Hook[*RecordCreateEvent]{}, - onRecordBeforeUpdateRequest: &hook.Hook[*RecordUpdateEvent]{}, - onRecordAfterUpdateRequest: &hook.Hook[*RecordUpdateEvent]{}, - onRecordBeforeDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, - onRecordAfterDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, - - // collection API event hooks - onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{}, - onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{}, - onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, - onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, - onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, - onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, - onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, - onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, - onCollectionsBeforeImportRequest: &hook.Hook[*CollectionsImportEvent]{}, - onCollectionsAfterImportRequest: &hook.Hook[*CollectionsImportEvent]{}, + config: &config, } - app.registerDefaultHooks() + // apply config defaults + if app.config.DBConnect == nil { + app.config.DBConnect = dbConnect + } + if app.config.DataMaxOpenConns <= 0 { + app.config.DataMaxOpenConns = DefaultDataMaxOpenConns + } + if app.config.DataMaxIdleConns <= 0 { + app.config.DataMaxIdleConns = DefaultDataMaxIdleConns + } + if app.config.AuxMaxOpenConns <= 0 { + app.config.AuxMaxOpenConns = DefaultAuxMaxOpenConns + } + if app.config.AuxMaxIdleConns <= 0 { + app.config.AuxMaxIdleConns = DefaultAuxMaxIdleConns + } + if app.config.QueryTimeout <= 0 { + app.config.QueryTimeout = DefaultQueryTimeout + } + + app.initHooks() + app.registerBaseHooks() return app } -// IsBootstrapped checks if the application was initialized -// (aka. whether Bootstrap() was called). -func (app *BaseApp) IsBootstrapped() bool { - return app.dao != nil && app.logsDao != nil && app.settings != nil +// initHooks initializes all app hook handlers. +func (app *BaseApp) initHooks() { + // app event hooks + app.onBootstrap = &hook.Hook[*BootstrapEvent]{} + app.onServe = &hook.Hook[*ServeEvent]{} + app.onTerminate = &hook.Hook[*TerminateEvent]{} + app.onBackupCreate = &hook.Hook[*BackupEvent]{} + app.onBackupRestore = &hook.Hook[*BackupEvent]{} + + // db model hooks + app.onModelValidate = &hook.Hook[*ModelEvent]{} + app.onModelCreate = &hook.Hook[*ModelEvent]{} + app.onModelCreateExecute = &hook.Hook[*ModelEvent]{} + app.onModelAfterCreateSuccess = &hook.Hook[*ModelEvent]{} + app.onModelAfterCreateError = &hook.Hook[*ModelErrorEvent]{} + app.onModelUpdate = &hook.Hook[*ModelEvent]{} + app.onModelUpdateWrite = &hook.Hook[*ModelEvent]{} + app.onModelAfterUpdateSuccess = &hook.Hook[*ModelEvent]{} + app.onModelAfterUpdateError = &hook.Hook[*ModelErrorEvent]{} + app.onModelDelete = &hook.Hook[*ModelEvent]{} + app.onModelDeleteExecute = &hook.Hook[*ModelEvent]{} + app.onModelAfterDeleteSuccess = &hook.Hook[*ModelEvent]{} + app.onModelAfterDeleteError = &hook.Hook[*ModelErrorEvent]{} + + // db record hooks + app.onRecordEnrich = &hook.Hook[*RecordEnrichEvent]{} + app.onRecordValidate = &hook.Hook[*RecordEvent]{} + app.onRecordCreate = &hook.Hook[*RecordEvent]{} + app.onRecordCreateExecute = &hook.Hook[*RecordEvent]{} + app.onRecordAfterCreateSuccess = &hook.Hook[*RecordEvent]{} + app.onRecordAfterCreateError = &hook.Hook[*RecordErrorEvent]{} + app.onRecordUpdate = &hook.Hook[*RecordEvent]{} + app.onRecordUpdateExecute = &hook.Hook[*RecordEvent]{} + app.onRecordAfterUpdateSuccess = &hook.Hook[*RecordEvent]{} + app.onRecordAfterUpdateError = &hook.Hook[*RecordErrorEvent]{} + app.onRecordDelete = &hook.Hook[*RecordEvent]{} + app.onRecordDeleteExecute = &hook.Hook[*RecordEvent]{} + app.onRecordAfterDeleteSuccess = &hook.Hook[*RecordEvent]{} + app.onRecordAfterDeleteError = &hook.Hook[*RecordErrorEvent]{} + + // db collection hooks + app.onCollectionValidate = &hook.Hook[*CollectionEvent]{} + app.onCollectionCreate = &hook.Hook[*CollectionEvent]{} + app.onCollectionCreateExecute = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterCreateSuccess = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterCreateError = &hook.Hook[*CollectionErrorEvent]{} + app.onCollectionUpdate = &hook.Hook[*CollectionEvent]{} + app.onCollectionUpdateExecute = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterUpdateSuccess = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterUpdateError = &hook.Hook[*CollectionErrorEvent]{} + app.onCollectionDelete = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterDeleteSuccess = &hook.Hook[*CollectionEvent]{} + app.onCollectionDeleteExecute = &hook.Hook[*CollectionEvent]{} + app.onCollectionAfterDeleteError = &hook.Hook[*CollectionErrorEvent]{} + + // mailer event hooks + app.onMailerSend = &hook.Hook[*MailerEvent]{} + app.onMailerRecordPasswordResetSend = &hook.Hook[*MailerRecordEvent]{} + app.onMailerRecordVerificationSend = &hook.Hook[*MailerRecordEvent]{} + app.onMailerRecordEmailChangeSend = &hook.Hook[*MailerRecordEvent]{} + app.onMailerRecordOTPSend = &hook.Hook[*MailerRecordEvent]{} + app.onMailerRecordAuthAlertSend = &hook.Hook[*MailerRecordEvent]{} + + // realtime API event hooks + app.onRealtimeConnectRequest = &hook.Hook[*RealtimeConnectRequestEvent]{} + app.onRealtimeMessageSend = &hook.Hook[*RealtimeMessageEvent]{} + app.onRealtimeSubscribeRequest = &hook.Hook[*RealtimeSubscribeRequestEvent]{} + + // settings event hooks + app.onSettingsListRequest = &hook.Hook[*SettingsListRequestEvent]{} + app.onSettingsUpdateRequest = &hook.Hook[*SettingsUpdateRequestEvent]{} + app.onSettingsReload = &hook.Hook[*SettingsReloadEvent]{} + + // file API event hooks + app.onFileDownloadRequest = &hook.Hook[*FileDownloadRequestEvent]{} + app.onFileTokenRequest = &hook.Hook[*FileTokenRequestEvent]{} + + // record auth API event hooks + app.onRecordAuthRequest = &hook.Hook[*RecordAuthRequestEvent]{} + app.onRecordAuthWithPasswordRequest = &hook.Hook[*RecordAuthWithPasswordRequestEvent]{} + app.onRecordAuthWithOAuth2Request = &hook.Hook[*RecordAuthWithOAuth2RequestEvent]{} + app.onRecordAuthRefreshRequest = &hook.Hook[*RecordAuthRefreshRequestEvent]{} + app.onRecordRequestPasswordResetRequest = &hook.Hook[*RecordRequestPasswordResetRequestEvent]{} + app.onRecordConfirmPasswordResetRequest = &hook.Hook[*RecordConfirmPasswordResetRequestEvent]{} + app.onRecordRequestVerificationRequest = &hook.Hook[*RecordRequestVerificationRequestEvent]{} + app.onRecordConfirmVerificationRequest = &hook.Hook[*RecordConfirmVerificationRequestEvent]{} + app.onRecordRequestEmailChangeRequest = &hook.Hook[*RecordRequestEmailChangeRequestEvent]{} + app.onRecordConfirmEmailChangeRequest = &hook.Hook[*RecordConfirmEmailChangeRequestEvent]{} + app.onRecordRequestOTPRequest = &hook.Hook[*RecordCreateOTPRequestEvent]{} + app.onRecordAuthWithOTPRequest = &hook.Hook[*RecordAuthWithOTPRequestEvent]{} + + // record crud API event hooks + app.onRecordsListRequest = &hook.Hook[*RecordsListRequestEvent]{} + app.onRecordViewRequest = &hook.Hook[*RecordRequestEvent]{} + app.onRecordCreateRequest = &hook.Hook[*RecordRequestEvent]{} + app.onRecordUpdateRequest = &hook.Hook[*RecordRequestEvent]{} + app.onRecordDeleteRequest = &hook.Hook[*RecordRequestEvent]{} + + // collection API event hooks + app.onCollectionsListRequest = &hook.Hook[*CollectionsListRequestEvent]{} + app.onCollectionViewRequest = &hook.Hook[*CollectionRequestEvent]{} + app.onCollectionCreateRequest = &hook.Hook[*CollectionRequestEvent]{} + app.onCollectionUpdateRequest = &hook.Hook[*CollectionRequestEvent]{} + app.onCollectionDeleteRequest = &hook.Hook[*CollectionRequestEvent]{} + app.onCollectionsImportRequest = &hook.Hook[*CollectionsImportRequestEvent]{} + + 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. +func (app *BaseApp) UnsafeWithoutHooks() App { + clone := *app + + // reset all hook handlers + clone.initHooks() + + return &clone } // Logger returns the default app logger. @@ -334,157 +362,179 @@ func (app *BaseApp) Logger() *slog.Logger { return app.logger } +// IsTransactional checks if the current app instance is part of a transaction. +func (app *BaseApp) IsTransactional() bool { + return app.txInfo != nil +} + +// IsBootstrapped checks if the application was initialized +// (aka. whether Bootstrap() was called). +func (app *BaseApp) IsBootstrapped() bool { + return app.concurrentDB != nil && app.auxConcurrentDB != nil +} + // Bootstrap initializes the application // (aka. create data dir, open db connections, load settings, etc.). // // It will call ResetBootstrapState() if the application was already bootstrapped. func (app *BaseApp) Bootstrap() error { - event := &BootstrapEvent{app} + event := &BootstrapEvent{} + event.App = app - if err := app.OnBeforeBootstrap().Trigger(event); err != nil { - return err - } + return app.OnBootstrap().Trigger(event, func(e *BootstrapEvent) error { + // clear resources of previous core state (if any) + if err := app.ResetBootstrapState(); err != nil { + return err + } - // clear resources of previous core state (if any) - if err := app.ResetBootstrapState(); err != nil { - return err - } + // ensure that data dir exist + if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil { + return err + } - // ensure that data dir exist - if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil { - return err - } + if err := app.initDataDB(); err != nil { + return err + } - if err := app.initDataDB(); err != nil { - return err - } + if err := app.initAuxDB(); err != nil { + return err + } - if err := app.initLogsDB(); err != nil { - return err - } + if err := app.initLogger(); err != nil { + return err + } - if err := app.initLogger(); err != nil { - return err - } + if err := app.RunSystemMigrations(); err != nil { + return err + } - // we don't check for an error because the db migrations may have not been executed yet - app.RefreshSettings() + if err := app.ReloadCachedCollections(); err != nil { + return err + } - // cleanup the pb_data temp directory (if any) - os.RemoveAll(filepath.Join(app.DataDir(), LocalTempDirName)) + if err := app.ReloadSettings(); err != nil { + return err + } - return app.OnAfterBootstrap().Trigger(event) + // try to cleanup the pb_data temp directory (if any) + _ = os.RemoveAll(filepath.Join(app.DataDir(), LocalTempDirName)) + + return nil + }) } -// ResetBootstrapState takes care for releasing initialized app resources -// (eg. closing db connections). +type closer interface { + Close() error +} + +// ResetBootstrapState releases the initialized core app resources +// (closing db connections, stopping cron ticker, etc.). func (app *BaseApp) ResetBootstrapState() error { - if app.Dao() != nil { - if err := app.Dao().ConcurrentDB().(*dbx.DB).Close(); err != nil { - return err - } - if err := app.Dao().NonconcurrentDB().(*dbx.DB).Close(); err != nil { - return err - } + app.Cron().Stop() + + var errs []error + + dbs := []*dbx.Builder{ + &app.concurrentDB, + &app.nonconcurrentDB, + &app.auxConcurrentDB, + &app.auxNonconcurrentDB, } - if app.LogsDao() != nil { - if err := app.LogsDao().ConcurrentDB().(*dbx.DB).Close(); err != nil { - return err + for _, db := range dbs { + if db == nil { + continue } - if err := app.LogsDao().NonconcurrentDB().(*dbx.DB).Close(); err != nil { - return err + if v, ok := (*db).(closer); ok { + if err := v.Close(); err != nil { + errs = append(errs, err) + } } + *db = nil } - app.dao = nil - app.logsDao = nil + if len(errs) > 0 { + return errors.Join(errs...) + } return nil } -// Deprecated: -// This method may get removed in the near future. -// It is recommended to access the db instance from app.Dao().DB() or -// if you want more flexibility - app.Dao().ConcurrentDB() and app.Dao().NonconcurrentDB(). +// DB returns the default app data db instance (pb_data/data.db). +func (app *BaseApp) DB() dbx.Builder { + return app.concurrentDB +} + +// NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db). // -// DB returns the default app database instance. -func (app *BaseApp) DB() *dbx.DB { - if app.Dao() == nil { - return nil - } - - db, ok := app.Dao().DB().(*dbx.DB) - if !ok { - return nil - } - - return db -} - -// Dao returns the default app Dao instance. -func (app *BaseApp) Dao() *daos.Dao { - return app.dao -} - -// Deprecated: -// This method may get removed in the near future. -// It is recommended to access the logs db instance from app.LogsDao().DB() or -// if you want more flexibility - app.LogsDao().ConcurrentDB() and app.LogsDao().NonconcurrentDB(). +// The returned db instance is limited only to a single open connection, +// meaning that it can process only 1 db operation at a time (other operations will be queued up). // -// LogsDB returns the app logs database instance. -func (app *BaseApp) LogsDB() *dbx.DB { - if app.LogsDao() == nil { - return nil - } - - db, ok := app.LogsDao().DB().(*dbx.DB) - if !ok { - return nil - } - - return db +// This method is used mainly internally and in the tests to execute write +// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. +// +// For the majority of cases you would want to use the regular DB() method +// since it allows concurrent db read operations. +// +// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance. +func (app *BaseApp) NonconcurrentDB() dbx.Builder { + return app.nonconcurrentDB } -// LogsDao returns the app logs Dao instance. -func (app *BaseApp) LogsDao() *daos.Dao { - return app.logsDao +// AuxDB returns the default app auxiliary db instance (pb_data/aux.db). +func (app *BaseApp) AuxDB() dbx.Builder { + return app.auxConcurrentDB +} + +// AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/aux.db). +// +// The returned db instance is limited only to a single open connection, +// meaning that it can process only 1 db operation at a time (other operations will be queued up). +// +// This method is used mainly internally and in the tests to execute write +// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. +// +// For the majority of cases you would want to use the regular DB() method +// since it allows concurrent db read operations. +// +// In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance. +func (app *BaseApp) AuxNonconcurrentDB() dbx.Builder { + return app.auxNonconcurrentDB } // DataDir returns the app data directory path. func (app *BaseApp) DataDir() string { - return app.dataDir + return app.config.DataDir } // EncryptionEnv returns the name of the app secret env key -// (used for settings encryption). +// (currently used primarily for optional settings encryption but this may change in the future). func (app *BaseApp) EncryptionEnv() string { - return app.encryptionEnv + return app.config.EncryptionEnv } // IsDev returns whether the app is in dev mode. // // When enabled logs, executed sql statements, etc. are printed to the stderr. func (app *BaseApp) IsDev() bool { - return app.isDev + return app.config.IsDev } // Settings returns the loaded app settings. -func (app *BaseApp) Settings() *settings.Settings { +func (app *BaseApp) Settings() *Settings { return app.settings } -// Deprecated: Use app.Store() instead. -func (app *BaseApp) Cache() *store.Store[any] { - color.Yellow("app.Store() is soft-deprecated. Please replace it with app.Store().") - return app.Store() -} - -// Store returns the app internal runtime store. +// Store returns the app runtime store. func (app *BaseApp) Store() *store.Store[any] { return app.store } +// Cron returns the app cron instance. +func (app *BaseApp) Cron() *cron.Cron { + return app.cron +} + // SubscriptionsBroker returns the app realtime subscriptions broker instance. func (app *BaseApp) SubscriptionsBroker() *subscriptions.Broker { return app.subscriptionsBroker @@ -493,23 +543,88 @@ func (app *BaseApp) SubscriptionsBroker() *subscriptions.Broker { // NewMailClient creates and returns a new SMTP or Sendmail client // based on the current app settings. func (app *BaseApp) NewMailClient() mailer.Mailer { - if app.Settings().Smtp.Enabled { - return &mailer.SmtpClient{ - Host: app.Settings().Smtp.Host, - Port: app.Settings().Smtp.Port, - Username: app.Settings().Smtp.Username, - Password: app.Settings().Smtp.Password, - Tls: app.Settings().Smtp.Tls, - AuthMethod: app.Settings().Smtp.AuthMethod, - LocalName: app.Settings().Smtp.LocalName, + var client mailer.Mailer + + // init mailer client + if app.Settings().SMTP.Enabled { + client = &mailer.SMTPClient{ + Host: app.Settings().SMTP.Host, + Port: app.Settings().SMTP.Port, + Username: app.Settings().SMTP.Username, + Password: app.Settings().SMTP.Password, + TLS: app.Settings().SMTP.TLS, + AuthMethod: app.Settings().SMTP.AuthMethod, + LocalName: app.Settings().SMTP.LocalName, } + } else { + client = &mailer.Sendmail{} } - return &mailer.Sendmail{} + // register the app level hook + if h, ok := client.(mailer.SendInterceptor); ok { + h.OnSend().Bind(&hook.Handler[*mailer.SendEvent]{ + Id: "__pbMailerOnSend__", + Func: func(e *mailer.SendEvent) error { + appEvent := new(MailerEvent) + appEvent.App = app + appEvent.Mailer = client + appEvent.Message = e.Message + + return app.OnMailerSend().Trigger(appEvent, func(ae *MailerEvent) error { + e.Message = ae.Message + + // print the mail in the console to assist with the debugging + if app.IsDev() { + logDate := new(strings.Builder) + log.New(logDate, "", log.LstdFlags).Print() + + mailLog := new(strings.Builder) + mailLog.WriteString(strings.TrimSpace(logDate.String())) + mailLog.WriteString(" Mail sent\n") + fmt.Fprintf(mailLog, "├─ From: %v\n", ae.Message.From) + fmt.Fprintf(mailLog, "├─ To: %v\n", ae.Message.To) + fmt.Fprintf(mailLog, "├─ Cc: %v\n", ae.Message.Cc) + fmt.Fprintf(mailLog, "├─ Bcc: %v\n", ae.Message.Bcc) + fmt.Fprintf(mailLog, "├─ Subject: %v\n", ae.Message.Subject) + + if len(ae.Message.Attachments) > 0 { + attachmentKeys := make([]string, 0, len(ae.Message.Attachments)) + for k := range ae.Message.Attachments { + attachmentKeys = append(attachmentKeys, k) + } + fmt.Fprintf(mailLog, "├─ Attachments: %v\n", attachmentKeys) + } + + const indentation = " " + if ae.Message.Text != "" { + textParts := strings.Split(strings.TrimSpace(ae.Message.Text), "\n") + textIndented := indentation + strings.Join(textParts, "\n"+indentation) + fmt.Fprintf(mailLog, "└─ Text:\n%s", textIndented) + } else { + htmlParts := strings.Split(strings.TrimSpace(ae.Message.HTML), "\n") + htmlIndented := indentation + strings.Join(htmlParts, "\n"+indentation) + fmt.Fprintf(mailLog, "└─ HTML:\n%s", htmlIndented) + } + + color.HiBlack("%s", mailLog.String()) + } + + // send the email with the new mailer in case it was replaced + if client != ae.Mailer { + return ae.Mailer.Send(e.Message) + } + + return e.Next() + }) + }, + }) + } + + return client } // NewFilesystem creates a new local or S3 filesystem instance -// for managing regular app files (eg. collection uploads) +// for managing regular app files (ex. record uploads) // based on the current app settings. // // NB! Make sure to call Close() on the returned result @@ -564,502 +679,428 @@ func (app *BaseApp) Restart() error { return err } - return app.OnTerminate().Trigger(&TerminateEvent{ - App: app, - IsRestart: true, - }, func(e *TerminateEvent) error { - e.App.ResetBootstrapState() + event := &TerminateEvent{} + event.App = app + event.IsRestart = true + + return app.OnTerminate().Trigger(event, func(e *TerminateEvent) error { + _ = e.App.ResetBootstrapState() // attempt to restart the bootstrap process in case execve returns an error for some reason - defer e.App.Bootstrap() + defer func() { + if err := e.App.Bootstrap(); err != nil { + app.Logger().Error("Failed to rebootstrap the application after failed app.Restart()", "error", err) + } + }() return syscall.Exec(execPath, os.Args, os.Environ()) }) } -// RefreshSettings reinitializes and reloads the stored application settings. -func (app *BaseApp) RefreshSettings() error { - if app.settings == nil { - app.settings = settings.New() - } +// RunSystemMigrations applies all new migrations registered in the [core.SystemMigrations] list. +func (app *BaseApp) RunSystemMigrations() error { + _, err := NewMigrationsRunner(app, SystemMigrations).Up() + return err +} - encryptionKey := os.Getenv(app.EncryptionEnv()) +// RunAppMigrations applies all new migrations registered in the [core.AppMigrations] list. +func (app *BaseApp) RunAppMigrations() error { + _, err := NewMigrationsRunner(app, AppMigrations).Up() + return err +} - storedSettings, err := app.Dao().FindSettings(encryptionKey) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - - // no settings were previously stored - if storedSettings == nil { - return app.Dao().SaveSettings(app.settings, encryptionKey) - } - - // load the settings from the stored param into the app ones - if err := app.settings.Merge(storedSettings); err != nil { - return err - } - - // reload handler level (if initialized) - if app.Logger() != nil { - if h, ok := app.Logger().Handler().(*logger.BatchHandler); ok { - h.SetLevel(app.getLoggerMinLevel()) - } - } - - return nil +// RunAllMigrations applies all system and app migrations +// (aka. from both [core.SystemMigrations] and [core.AppMigrations]). +func (app *BaseApp) RunAllMigrations() error { + list := MigrationsList{} + list.Copy(SystemMigrations) + list.Copy(AppMigrations) + _, err := NewMigrationsRunner(app, list).Up() + return err } // ------------------------------------------------------------------- // App event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnBeforeBootstrap() *hook.Hook[*BootstrapEvent] { - return app.onBeforeBootstrap +func (app *BaseApp) OnBootstrap() *hook.Hook[*BootstrapEvent] { + return app.onBootstrap } -func (app *BaseApp) OnAfterBootstrap() *hook.Hook[*BootstrapEvent] { - return app.onAfterBootstrap -} - -func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] { - return app.onBeforeServe -} - -func (app *BaseApp) OnBeforeApiError() *hook.Hook[*ApiErrorEvent] { - return app.onBeforeApiError -} - -func (app *BaseApp) OnAfterApiError() *hook.Hook[*ApiErrorEvent] { - return app.onAfterApiError +func (app *BaseApp) OnServe() *hook.Hook[*ServeEvent] { + return app.onServe } func (app *BaseApp) OnTerminate() *hook.Hook[*TerminateEvent] { return app.onTerminate } -// ------------------------------------------------------------------- -// Dao event hooks -// ------------------------------------------------------------------- - -func (app *BaseApp) OnModelBeforeCreate(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelBeforeCreate, tags...) +func (app *BaseApp) OnBackupCreate() *hook.Hook[*BackupEvent] { + return app.onBackupCreate } -func (app *BaseApp) OnModelAfterCreate(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelAfterCreate, tags...) +func (app *BaseApp) OnBackupRestore() *hook.Hook[*BackupEvent] { + return app.onBackupRestore } -func (app *BaseApp) OnModelBeforeUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelBeforeUpdate, tags...) +// --------------------------------------------------------------- + +func (app *BaseApp) OnModelCreate(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelCreate, tags...) } -func (app *BaseApp) OnModelAfterUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelAfterUpdate, tags...) +func (app *BaseApp) OnModelCreateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelCreateExecute, tags...) } -func (app *BaseApp) OnModelBeforeDelete(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelBeforeDelete, tags...) +func (app *BaseApp) OnModelAfterCreateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelAfterCreateSuccess, tags...) } -func (app *BaseApp) OnModelAfterDelete(tags ...string) *hook.TaggedHook[*ModelEvent] { - return hook.NewTaggedHook(app.onModelAfterDelete, tags...) +func (app *BaseApp) OnModelAfterCreateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] { + return hook.NewTaggedHook(app.onModelAfterCreateError, tags...) +} + +func (app *BaseApp) OnModelUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelUpdate, tags...) +} + +func (app *BaseApp) OnModelUpdateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelUpdateWrite, tags...) +} + +func (app *BaseApp) OnModelAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelAfterUpdateSuccess, tags...) +} + +func (app *BaseApp) OnModelAfterUpdateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] { + return hook.NewTaggedHook(app.onModelAfterUpdateError, tags...) +} + +func (app *BaseApp) OnModelValidate(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelValidate, tags...) +} + +func (app *BaseApp) OnModelDelete(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelDelete, tags...) +} + +func (app *BaseApp) OnModelDeleteExecute(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelDeleteExecute, tags...) +} + +func (app *BaseApp) OnModelAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] { + return hook.NewTaggedHook(app.onModelAfterDeleteSuccess, tags...) +} + +func (app *BaseApp) OnModelAfterDeleteError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] { + return hook.NewTaggedHook(app.onModelAfterDeleteError, tags...) +} + +func (app *BaseApp) OnRecordEnrich(tags ...string) *hook.TaggedHook[*RecordEnrichEvent] { + return hook.NewTaggedHook(app.onRecordEnrich, tags...) +} + +func (app *BaseApp) OnRecordValidate(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordValidate, tags...) +} + +func (app *BaseApp) OnRecordCreate(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordCreate, tags...) +} + +func (app *BaseApp) OnRecordCreateExecute(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordCreateExecute, tags...) +} + +func (app *BaseApp) OnRecordAfterCreateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordAfterCreateSuccess, tags...) +} + +func (app *BaseApp) OnRecordAfterCreateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] { + return hook.NewTaggedHook(app.onRecordAfterCreateError, tags...) +} + +func (app *BaseApp) OnRecordUpdate(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordUpdate, tags...) +} + +func (app *BaseApp) OnRecordUpdateExecute(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordUpdateExecute, tags...) +} + +func (app *BaseApp) OnRecordAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordAfterUpdateSuccess, tags...) +} + +func (app *BaseApp) OnRecordAfterUpdateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] { + return hook.NewTaggedHook(app.onRecordAfterUpdateError, tags...) +} + +func (app *BaseApp) OnRecordDelete(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordDelete, tags...) +} + +func (app *BaseApp) OnRecordDeleteExecute(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordDeleteExecute, tags...) +} + +func (app *BaseApp) OnRecordAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] { + return hook.NewTaggedHook(app.onRecordAfterDeleteSuccess, tags...) +} + +func (app *BaseApp) OnRecordAfterDeleteError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] { + return hook.NewTaggedHook(app.onRecordAfterDeleteError, tags...) +} + +func (app *BaseApp) OnCollectionValidate(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionValidate, tags...) +} + +func (app *BaseApp) OnCollectionCreate(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionCreate, tags...) +} + +func (app *BaseApp) OnCollectionCreateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionCreateExecute, tags...) +} + +func (app *BaseApp) OnCollectionAfterCreateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionAfterCreateSuccess, tags...) +} + +func (app *BaseApp) OnCollectionAfterCreateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] { + return hook.NewTaggedHook(app.onCollectionAfterCreateError, tags...) +} + +func (app *BaseApp) OnCollectionUpdate(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionUpdate, tags...) +} + +func (app *BaseApp) OnCollectionUpdateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionUpdateExecute, tags...) +} + +func (app *BaseApp) OnCollectionAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionAfterUpdateSuccess, tags...) +} + +func (app *BaseApp) OnCollectionAfterUpdateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] { + return hook.NewTaggedHook(app.onCollectionAfterUpdateError, tags...) +} + +func (app *BaseApp) OnCollectionDelete(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionDelete, tags...) +} + +func (app *BaseApp) OnCollectionDeleteExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionDeleteExecute, tags...) +} + +func (app *BaseApp) OnCollectionAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] { + return hook.NewTaggedHook(app.onCollectionAfterDeleteSuccess, tags...) +} + +func (app *BaseApp) OnCollectionAfterDeleteError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] { + return hook.NewTaggedHook(app.onCollectionAfterDeleteError, tags...) } // ------------------------------------------------------------------- // Mailer event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] { - return app.onMailerBeforeAdminResetPasswordSend +func (app *BaseApp) OnMailerSend() *hook.Hook[*MailerEvent] { + return app.onMailerSend } -func (app *BaseApp) OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] { - return app.onMailerAfterAdminResetPasswordSend +func (app *BaseApp) OnMailerRecordPasswordResetSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { + return hook.NewTaggedHook(app.onMailerRecordPasswordResetSend, tags...) } -func (app *BaseApp) OnMailerBeforeRecordResetPasswordSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerBeforeRecordResetPasswordSend, tags...) +func (app *BaseApp) OnMailerRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { + return hook.NewTaggedHook(app.onMailerRecordVerificationSend, tags...) } -func (app *BaseApp) OnMailerAfterRecordResetPasswordSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerAfterRecordResetPasswordSend, tags...) +func (app *BaseApp) OnMailerRecordEmailChangeSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { + return hook.NewTaggedHook(app.onMailerRecordEmailChangeSend, tags...) } -func (app *BaseApp) OnMailerBeforeRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerBeforeRecordVerificationSend, tags...) +func (app *BaseApp) OnMailerRecordOTPSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { + return hook.NewTaggedHook(app.onMailerRecordOTPSend, tags...) } -func (app *BaseApp) OnMailerAfterRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerAfterRecordVerificationSend, tags...) -} - -func (app *BaseApp) OnMailerBeforeRecordChangeEmailSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerBeforeRecordChangeEmailSend, tags...) -} - -func (app *BaseApp) OnMailerAfterRecordChangeEmailSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { - return hook.NewTaggedHook(app.onMailerAfterRecordChangeEmailSend, tags...) +func (app *BaseApp) OnMailerRecordAuthAlertSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] { + return hook.NewTaggedHook(app.onMailerRecordAuthAlertSend, tags...) } // ------------------------------------------------------------------- // Realtime API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectEvent] { +func (app *BaseApp) OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectRequestEvent] { return app.onRealtimeConnectRequest } -func (app *BaseApp) OnRealtimeDisconnectRequest() *hook.Hook[*RealtimeDisconnectEvent] { - return app.onRealtimeDisconnectRequest +func (app *BaseApp) OnRealtimeMessageSend() *hook.Hook[*RealtimeMessageEvent] { + return app.onRealtimeMessageSend } -func (app *BaseApp) OnRealtimeBeforeMessageSend() *hook.Hook[*RealtimeMessageEvent] { - return app.onRealtimeBeforeMessageSend -} - -func (app *BaseApp) OnRealtimeAfterMessageSend() *hook.Hook[*RealtimeMessageEvent] { - return app.onRealtimeAfterMessageSend -} - -func (app *BaseApp) OnRealtimeBeforeSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] { - return app.onRealtimeBeforeSubscribeRequest -} - -func (app *BaseApp) OnRealtimeAfterSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] { - return app.onRealtimeAfterSubscribeRequest +func (app *BaseApp) OnRealtimeSubscribeRequest() *hook.Hook[*RealtimeSubscribeRequestEvent] { + return app.onRealtimeSubscribeRequest } // ------------------------------------------------------------------- // Settings API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnSettingsListRequest() *hook.Hook[*SettingsListEvent] { +func (app *BaseApp) OnSettingsListRequest() *hook.Hook[*SettingsListRequestEvent] { return app.onSettingsListRequest } -func (app *BaseApp) OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] { - return app.onSettingsBeforeUpdateRequest +func (app *BaseApp) OnSettingsUpdateRequest() *hook.Hook[*SettingsUpdateRequestEvent] { + return app.onSettingsUpdateRequest } -func (app *BaseApp) OnSettingsAfterUpdateRequest() *hook.Hook[*SettingsUpdateEvent] { - return app.onSettingsAfterUpdateRequest +func (app *BaseApp) OnSettingsReload() *hook.Hook[*SettingsReloadEvent] { + return app.onSettingsReload } // ------------------------------------------------------------------- // File API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadEvent] { +func (app *BaseApp) OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadRequestEvent] { return hook.NewTaggedHook(app.onFileDownloadRequest, tags...) } -func (app *BaseApp) OnFileBeforeTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenEvent] { - return hook.NewTaggedHook(app.onFileBeforeTokenRequest, tags...) -} - -func (app *BaseApp) OnFileAfterTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenEvent] { - return hook.NewTaggedHook(app.onFileAfterTokenRequest, tags...) -} - -// ------------------------------------------------------------------- -// Admin API event hooks -// ------------------------------------------------------------------- - -func (app *BaseApp) OnAdminsListRequest() *hook.Hook[*AdminsListEvent] { - return app.onAdminsListRequest -} - -func (app *BaseApp) OnAdminViewRequest() *hook.Hook[*AdminViewEvent] { - return app.onAdminViewRequest -} - -func (app *BaseApp) OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] { - return app.onAdminBeforeCreateRequest -} - -func (app *BaseApp) OnAdminAfterCreateRequest() *hook.Hook[*AdminCreateEvent] { - return app.onAdminAfterCreateRequest -} - -func (app *BaseApp) OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] { - return app.onAdminBeforeUpdateRequest -} - -func (app *BaseApp) OnAdminAfterUpdateRequest() *hook.Hook[*AdminUpdateEvent] { - return app.onAdminAfterUpdateRequest -} - -func (app *BaseApp) OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] { - return app.onAdminBeforeDeleteRequest -} - -func (app *BaseApp) OnAdminAfterDeleteRequest() *hook.Hook[*AdminDeleteEvent] { - return app.onAdminAfterDeleteRequest -} - -func (app *BaseApp) OnAdminAuthRequest() *hook.Hook[*AdminAuthEvent] { - return app.onAdminAuthRequest -} - -func (app *BaseApp) OnAdminBeforeAuthWithPasswordRequest() *hook.Hook[*AdminAuthWithPasswordEvent] { - return app.onAdminBeforeAuthWithPasswordRequest -} - -func (app *BaseApp) OnAdminAfterAuthWithPasswordRequest() *hook.Hook[*AdminAuthWithPasswordEvent] { - return app.onAdminAfterAuthWithPasswordRequest -} - -func (app *BaseApp) OnAdminBeforeAuthRefreshRequest() *hook.Hook[*AdminAuthRefreshEvent] { - return app.onAdminBeforeAuthRefreshRequest -} - -func (app *BaseApp) OnAdminAfterAuthRefreshRequest() *hook.Hook[*AdminAuthRefreshEvent] { - return app.onAdminAfterAuthRefreshRequest -} - -func (app *BaseApp) OnAdminBeforeRequestPasswordResetRequest() *hook.Hook[*AdminRequestPasswordResetEvent] { - return app.onAdminBeforeRequestPasswordResetRequest -} - -func (app *BaseApp) OnAdminAfterRequestPasswordResetRequest() *hook.Hook[*AdminRequestPasswordResetEvent] { - return app.onAdminAfterRequestPasswordResetRequest -} - -func (app *BaseApp) OnAdminBeforeConfirmPasswordResetRequest() *hook.Hook[*AdminConfirmPasswordResetEvent] { - return app.onAdminBeforeConfirmPasswordResetRequest -} - -func (app *BaseApp) OnAdminAfterConfirmPasswordResetRequest() *hook.Hook[*AdminConfirmPasswordResetEvent] { - return app.onAdminAfterConfirmPasswordResetRequest +func (app *BaseApp) OnFileTokenRequest() *hook.Hook[*FileTokenRequestEvent] { + return app.onFileTokenRequest } // ------------------------------------------------------------------- // Record auth API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnRecordAuthRequest(tags ...string) *hook.TaggedHook[*RecordAuthEvent] { +func (app *BaseApp) OnRecordAuthRequest(tags ...string) *hook.TaggedHook[*RecordAuthRequestEvent] { return hook.NewTaggedHook(app.onRecordAuthRequest, tags...) } -func (app *BaseApp) OnRecordBeforeAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordEvent] { - return hook.NewTaggedHook(app.onRecordBeforeAuthWithPasswordRequest, tags...) +func (app *BaseApp) OnRecordAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordRequestEvent] { + return hook.NewTaggedHook(app.onRecordAuthWithPasswordRequest, tags...) } -func (app *BaseApp) OnRecordAfterAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordEvent] { - return hook.NewTaggedHook(app.onRecordAfterAuthWithPasswordRequest, tags...) +func (app *BaseApp) OnRecordAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2RequestEvent] { + return hook.NewTaggedHook(app.onRecordAuthWithOAuth2Request, tags...) } -func (app *BaseApp) OnRecordBeforeAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2Event] { - return hook.NewTaggedHook(app.onRecordBeforeAuthWithOAuth2Request, tags...) +func (app *BaseApp) OnRecordAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshRequestEvent] { + return hook.NewTaggedHook(app.onRecordAuthRefreshRequest, tags...) } -func (app *BaseApp) OnRecordAfterAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2Event] { - return hook.NewTaggedHook(app.onRecordAfterAuthWithOAuth2Request, tags...) +func (app *BaseApp) OnRecordRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetRequestEvent] { + return hook.NewTaggedHook(app.onRecordRequestPasswordResetRequest, tags...) } -func (app *BaseApp) OnRecordBeforeAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshEvent] { - return hook.NewTaggedHook(app.onRecordBeforeAuthRefreshRequest, tags...) +func (app *BaseApp) OnRecordConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetRequestEvent] { + return hook.NewTaggedHook(app.onRecordConfirmPasswordResetRequest, tags...) } -func (app *BaseApp) OnRecordAfterAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshEvent] { - return hook.NewTaggedHook(app.onRecordAfterAuthRefreshRequest, tags...) +func (app *BaseApp) OnRecordRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationRequestEvent] { + return hook.NewTaggedHook(app.onRecordRequestVerificationRequest, tags...) } -func (app *BaseApp) OnRecordBeforeRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetEvent] { - return hook.NewTaggedHook(app.onRecordBeforeRequestPasswordResetRequest, tags...) +func (app *BaseApp) OnRecordConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationRequestEvent] { + return hook.NewTaggedHook(app.onRecordConfirmVerificationRequest, tags...) } -func (app *BaseApp) OnRecordAfterRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetEvent] { - return hook.NewTaggedHook(app.onRecordAfterRequestPasswordResetRequest, tags...) +func (app *BaseApp) OnRecordRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeRequestEvent] { + return hook.NewTaggedHook(app.onRecordRequestEmailChangeRequest, tags...) } -func (app *BaseApp) OnRecordBeforeConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetEvent] { - return hook.NewTaggedHook(app.onRecordBeforeConfirmPasswordResetRequest, tags...) +func (app *BaseApp) OnRecordConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeRequestEvent] { + return hook.NewTaggedHook(app.onRecordConfirmEmailChangeRequest, tags...) } -func (app *BaseApp) OnRecordAfterConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetEvent] { - return hook.NewTaggedHook(app.onRecordAfterConfirmPasswordResetRequest, tags...) +func (app *BaseApp) OnRecordRequestOTPRequest(tags ...string) *hook.TaggedHook[*RecordCreateOTPRequestEvent] { + return hook.NewTaggedHook(app.onRecordRequestOTPRequest, tags...) } -func (app *BaseApp) OnRecordBeforeRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationEvent] { - return hook.NewTaggedHook(app.onRecordBeforeRequestVerificationRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationEvent] { - return hook.NewTaggedHook(app.onRecordAfterRequestVerificationRequest, tags...) -} - -func (app *BaseApp) OnRecordBeforeConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationEvent] { - return hook.NewTaggedHook(app.onRecordBeforeConfirmVerificationRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationEvent] { - return hook.NewTaggedHook(app.onRecordAfterConfirmVerificationRequest, tags...) -} - -func (app *BaseApp) OnRecordBeforeRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeEvent] { - return hook.NewTaggedHook(app.onRecordBeforeRequestEmailChangeRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeEvent] { - return hook.NewTaggedHook(app.onRecordAfterRequestEmailChangeRequest, tags...) -} - -func (app *BaseApp) OnRecordBeforeConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeEvent] { - return hook.NewTaggedHook(app.onRecordBeforeConfirmEmailChangeRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeEvent] { - return hook.NewTaggedHook(app.onRecordAfterConfirmEmailChangeRequest, tags...) -} - -func (app *BaseApp) OnRecordListExternalAuthsRequest(tags ...string) *hook.TaggedHook[*RecordListExternalAuthsEvent] { - return hook.NewTaggedHook(app.onRecordListExternalAuthsRequest, tags...) -} - -func (app *BaseApp) OnRecordBeforeUnlinkExternalAuthRequest(tags ...string) *hook.TaggedHook[*RecordUnlinkExternalAuthEvent] { - return hook.NewTaggedHook(app.onRecordBeforeUnlinkExternalAuthRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterUnlinkExternalAuthRequest(tags ...string) *hook.TaggedHook[*RecordUnlinkExternalAuthEvent] { - return hook.NewTaggedHook(app.onRecordAfterUnlinkExternalAuthRequest, tags...) +func (app *BaseApp) OnRecordAuthWithOTPRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithOTPRequestEvent] { + return hook.NewTaggedHook(app.onRecordAuthWithOTPRequest, tags...) } // ------------------------------------------------------------------- // Record CRUD API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnRecordsListRequest(tags ...string) *hook.TaggedHook[*RecordsListEvent] { +func (app *BaseApp) OnRecordsListRequest(tags ...string) *hook.TaggedHook[*RecordsListRequestEvent] { return hook.NewTaggedHook(app.onRecordsListRequest, tags...) } -func (app *BaseApp) OnRecordViewRequest(tags ...string) *hook.TaggedHook[*RecordViewEvent] { +func (app *BaseApp) OnRecordViewRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] { return hook.NewTaggedHook(app.onRecordViewRequest, tags...) } -func (app *BaseApp) OnRecordBeforeCreateRequest(tags ...string) *hook.TaggedHook[*RecordCreateEvent] { - return hook.NewTaggedHook(app.onRecordBeforeCreateRequest, tags...) +func (app *BaseApp) OnRecordCreateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] { + return hook.NewTaggedHook(app.onRecordCreateRequest, tags...) } -func (app *BaseApp) OnRecordAfterCreateRequest(tags ...string) *hook.TaggedHook[*RecordCreateEvent] { - return hook.NewTaggedHook(app.onRecordAfterCreateRequest, tags...) +func (app *BaseApp) OnRecordUpdateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] { + return hook.NewTaggedHook(app.onRecordUpdateRequest, tags...) } -func (app *BaseApp) OnRecordBeforeUpdateRequest(tags ...string) *hook.TaggedHook[*RecordUpdateEvent] { - return hook.NewTaggedHook(app.onRecordBeforeUpdateRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterUpdateRequest(tags ...string) *hook.TaggedHook[*RecordUpdateEvent] { - return hook.NewTaggedHook(app.onRecordAfterUpdateRequest, tags...) -} - -func (app *BaseApp) OnRecordBeforeDeleteRequest(tags ...string) *hook.TaggedHook[*RecordDeleteEvent] { - return hook.NewTaggedHook(app.onRecordBeforeDeleteRequest, tags...) -} - -func (app *BaseApp) OnRecordAfterDeleteRequest(tags ...string) *hook.TaggedHook[*RecordDeleteEvent] { - return hook.NewTaggedHook(app.onRecordAfterDeleteRequest, tags...) +func (app *BaseApp) OnRecordDeleteRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] { + return hook.NewTaggedHook(app.onRecordDeleteRequest, tags...) } // ------------------------------------------------------------------- // Collection API event hooks // ------------------------------------------------------------------- -func (app *BaseApp) OnCollectionsListRequest() *hook.Hook[*CollectionsListEvent] { +func (app *BaseApp) OnCollectionsListRequest() *hook.Hook[*CollectionsListRequestEvent] { return app.onCollectionsListRequest } -func (app *BaseApp) OnCollectionViewRequest() *hook.Hook[*CollectionViewEvent] { +func (app *BaseApp) OnCollectionViewRequest() *hook.Hook[*CollectionRequestEvent] { return app.onCollectionViewRequest } -func (app *BaseApp) OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] { - return app.onCollectionBeforeCreateRequest +func (app *BaseApp) OnCollectionCreateRequest() *hook.Hook[*CollectionRequestEvent] { + return app.onCollectionCreateRequest } -func (app *BaseApp) OnCollectionAfterCreateRequest() *hook.Hook[*CollectionCreateEvent] { - return app.onCollectionAfterCreateRequest +func (app *BaseApp) OnCollectionUpdateRequest() *hook.Hook[*CollectionRequestEvent] { + return app.onCollectionUpdateRequest } -func (app *BaseApp) OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] { - return app.onCollectionBeforeUpdateRequest +func (app *BaseApp) OnCollectionDeleteRequest() *hook.Hook[*CollectionRequestEvent] { + return app.onCollectionDeleteRequest } -func (app *BaseApp) OnCollectionAfterUpdateRequest() *hook.Hook[*CollectionUpdateEvent] { - return app.onCollectionAfterUpdateRequest +func (app *BaseApp) OnCollectionsImportRequest() *hook.Hook[*CollectionsImportRequestEvent] { + return app.onCollectionsImportRequest } -func (app *BaseApp) OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] { - return app.onCollectionBeforeDeleteRequest -} - -func (app *BaseApp) OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] { - return app.onCollectionAfterDeleteRequest -} - -func (app *BaseApp) OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent] { - return app.onCollectionsBeforeImportRequest -} - -func (app *BaseApp) OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent] { - return app.onCollectionsAfterImportRequest +func (app *BaseApp) OnBatchRequest() *hook.Hook[*BatchRequestEvent] { + return app.onBatchRequest } // ------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------- -func (app *BaseApp) initLogsDB() error { - maxOpenConns := DefaultLogsMaxOpenConns - maxIdleConns := DefaultLogsMaxIdleConns - if app.logsMaxOpenConns > 0 { - maxOpenConns = app.logsMaxOpenConns - } - if app.logsMaxIdleConns > 0 { - maxIdleConns = app.logsMaxIdleConns - } - - concurrentDB, err := connectDB(filepath.Join(app.DataDir(), "logs.db")) - if err != nil { - return err - } - concurrentDB.DB().SetMaxOpenConns(maxOpenConns) - concurrentDB.DB().SetMaxIdleConns(maxIdleConns) - concurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute) - - nonconcurrentDB, err := connectDB(filepath.Join(app.DataDir(), "logs.db")) - if err != nil { - return err - } - nonconcurrentDB.DB().SetMaxOpenConns(1) - nonconcurrentDB.DB().SetMaxIdleConns(1) - nonconcurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute) - - app.logsDao = daos.NewMultiDB(concurrentDB, nonconcurrentDB) - - return nil -} - func (app *BaseApp) initDataDB() error { - maxOpenConns := DefaultDataMaxOpenConns - maxIdleConns := DefaultDataMaxIdleConns - if app.dataMaxOpenConns > 0 { - maxOpenConns = app.dataMaxOpenConns - } - if app.dataMaxIdleConns > 0 { - maxIdleConns = app.dataMaxIdleConns - } + dbPath := filepath.Join(app.DataDir(), "data.db") - concurrentDB, err := connectDB(filepath.Join(app.DataDir(), "data.db")) + concurrentDB, err := app.config.DBConnect(dbPath) if err != nil { return err } - concurrentDB.DB().SetMaxOpenConns(maxOpenConns) - concurrentDB.DB().SetMaxIdleConns(maxIdleConns) + concurrentDB.DB().SetMaxOpenConns(app.config.DataMaxOpenConns) + concurrentDB.DB().SetMaxIdleConns(app.config.DataMaxIdleConns) concurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute) - nonconcurrentDB, err := connectDB(filepath.Join(app.DataDir(), "data.db")) + nonconcurrentDB, err := app.config.DBConnect(dbPath) if err != nil { return err } @@ -1069,81 +1110,72 @@ func (app *BaseApp) initDataDB() error { if app.IsDev() { nonconcurrentDB.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { - color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql) + color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), normalizeSQLLog(sql)) } nonconcurrentDB.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql) + color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), normalizeSQLLog(sql)) } concurrentDB.QueryLogFunc = nonconcurrentDB.QueryLogFunc concurrentDB.ExecLogFunc = nonconcurrentDB.ExecLogFunc } - app.dao = app.createDaoWithHooks(concurrentDB, nonconcurrentDB) + app.concurrentDB = concurrentDB + app.nonconcurrentDB = nonconcurrentDB return nil } -func (app *BaseApp) createDaoWithHooks(concurrentDB, nonconcurrentDB dbx.Builder) *daos.Dao { - dao := daos.NewMultiDB(concurrentDB, nonconcurrentDB) +var sqlLogReplacements = map[string]string{ + "{{": "`", + "}}": "`", + "[[": "`", + "]]": "`", + "": "NULL", +} +var sqlLogPrefixedTableIdentifierPattern = regexp.MustCompile(`\[\[(.+)\.(.+)\]\]`) +var sqlLogPrefixedColumnIdentifierPattern = regexp.MustCompile(`\{\{(.+)\.(.+)\}\}`) - dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m +// normalizeSQLLog replaces common query builder charactes with their plain SQL version for easier debugging. +// The query is still not suitable for execution and should be used only for log and debug purposes +// (the normalization is done here to avoid breaking changes in dbx). +func normalizeSQLLog(sql string) string { + sql = sqlLogPrefixedTableIdentifierPattern.ReplaceAllString(sql, "`$1`.`$2`") - return app.OnModelBeforeCreate().Trigger(e, func(e *ModelEvent) error { - return action() - }) + sql = sqlLogPrefixedColumnIdentifierPattern.ReplaceAllString(sql, "`$1`.`$2`") + + for old, new := range sqlLogReplacements { + sql = strings.ReplaceAll(sql, old, new) } - dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m - - return app.OnModelAfterCreate().Trigger(e) - } - - dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m - - return app.OnModelBeforeUpdate().Trigger(e, func(e *ModelEvent) error { - return action() - }) - } - - dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m - - return app.OnModelAfterUpdate().Trigger(e) - } - - dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m - - return app.OnModelBeforeDelete().Trigger(e, func(e *ModelEvent) error { - return action() - }) - } - - dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - e := new(ModelEvent) - e.Dao = eventDao - e.Model = m - - return app.OnModelAfterDelete().Trigger(e) - } - - return dao + return sql } -func (app *BaseApp) registerDefaultHooks() { +func (app *BaseApp) initAuxDB() error { + dbPath := filepath.Join(app.DataDir(), "aux.db") + + concurrentDB, err := app.config.DBConnect(dbPath) + if err != nil { + return err + } + concurrentDB.DB().SetMaxOpenConns(app.config.AuxMaxOpenConns) + concurrentDB.DB().SetMaxIdleConns(app.config.AuxMaxIdleConns) + concurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute) + + nonconcurrentDB, err := app.config.DBConnect(dbPath) + if err != nil { + return err + } + nonconcurrentDB.DB().SetMaxOpenConns(1) + nonconcurrentDB.DB().SetMaxIdleConns(1) + nonconcurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute) + + app.auxConcurrentDB = concurrentDB + app.auxNonconcurrentDB = nonconcurrentDB + + return nil +} + +func (app *BaseApp) registerBaseHooks() { deletePrefix := func(prefix string) error { fs, err := app.NewFilesystem() if err != nil { @@ -1160,33 +1192,58 @@ func (app *BaseApp) registerDefaultHooks() { } // try to delete the storage files from deleted Collection, Records, etc. model - app.OnModelAfterDelete().Add(func(e *ModelEvent) error { - if m, ok := e.Model.(models.FilesManager); ok && m.BaseFilesPath() != "" { - // ensure that there is a trailing slash so that the list iterator could start walking from the prefix - // (https://github.com/pocketbase/pocketbase/discussions/5246#discussioncomment-10128955) - prefix := strings.TrimRight(m.BaseFilesPath(), "/") + "/" + app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: "__pbFilesManagerDelete__", + Func: func(e *ModelEvent) error { + if m, ok := e.Model.(FilesManager); ok && m.BaseFilesPath() != "" { + // ensure that there is a trailing slash so that the list iterator could start walking from the prefix dir + // (https://github.com/pocketbase/pocketbase/discussions/5246#discussioncomment-10128955) + prefix := strings.TrimRight(m.BaseFilesPath(), "/") + "/" - // run in the background for "optimistic" delete to avoid - // blocking the delete transaction - routine.FireAndForget(func() { - if err := deletePrefix(prefix); err != nil { - app.Logger().Error( - "Failed to delete storage prefix (non critical error; usually could happen because of S3 api limits)", - slog.String("prefix", prefix), - slog.String("error", err.Error()), - ) - } - }) - } + // run in the background for "optimistic" delete to avoid + // blocking the delete transaction + routine.FireAndForget(func() { + if err := deletePrefix(prefix); err != nil { + app.Logger().Error( + "Failed to delete storage prefix (non critical error; usually could happen because of S3 api limits)", + slog.String("prefix", prefix), + slog.String("error", err.Error()), + ) + } + }) + } - return nil + return e.Next() + }, + Priority: -99, }) - if err := app.initAutobackupHooks(); err != nil { - app.Logger().Error("Failed to init auto backup hooks", slog.String("error", err.Error())) - } + app.OnServe().Bind(&hook.Handler[*ServeEvent]{ + Id: "__pbCronStart__", + Func: func(e *ServeEvent) error { + app.Cron().Start() - registerCachedCollectionsAppHooks(app) + return e.Next() + }, + Priority: 999, + }) + + app.Cron().Add("__pbPragmaOptimize__", "0 0 * * *", func() { + _, execErr := app.DB().NewQuery("PRAGMA optimize").Execute() + if execErr != nil { + app.Logger().Warn("Failed to run periodic PRAGMA optimize", slog.String("error", execErr.Error())) + } + }) + + app.registerSettingsHooks() + app.registerAutobackupHooks() + app.registerCollectionHooks() + app.registerRecordHooks() + app.registerSuperuserHooks() + app.registerExternalAuthHooks() + app.registerMFAHooks() + app.registerOTPHooks() + app.registerAuthOriginHooks() } // getLoggerMinLevel returns the logger min level based on the @@ -1198,11 +1255,11 @@ func (app *BaseApp) registerDefaultHooks() { // practically all logs to the terminal. // In this case DB logs are still filtered but the checks for the min level are done // in the BatchOptions.BeforeAddFunc instead of the slog.Handler.Enabled() method. -func (app *BaseApp) getLoggerMinLevel() slog.Level { +func getLoggerMinLevel(app App) slog.Level { var minLevel slog.Level if app.IsDev() { - minLevel = -9999 + minLevel = -99999 } else if app.Settings() != nil { minLevel = slog.Level(app.Settings().Logs.MinLevel) } @@ -1216,7 +1273,7 @@ func (app *BaseApp) initLogger() error { done := make(chan bool) handler := logger.NewBatchHandler(logger.BatchOptions{ - Level: app.getLoggerMinLevel(), + Level: getLoggerMinLevel(app), BatchSize: 200, BeforeAddFunc: func(ctx context.Context, log *logger.Log) bool { if app.IsDev() { @@ -1239,19 +1296,17 @@ func (app *BaseApp) initLogger() error { // write the accumulated logs // (note: based on several local tests there is no significant performance difference between small number of separate write queries vs 1 big INSERT) - app.LogsDao().RunInTransaction(func(txDao *daos.Dao) error { - model := &models.Log{} + app.AuxRunInTransaction(func(txApp App) error { + model := &Log{} for _, l := range logs { model.MarkAsNew() - // note: using pseudorandom for a slightly better performance - model.Id = security.PseudorandomStringWithAlphabet(models.DefaultIdLength, models.DefaultIdAlphabet) + model.Id = GenerateDefaultRandomId() model.Level = int(l.Level) model.Message = l.Message model.Data = l.Data model.Created, _ = types.ParseDateTime(l.Time) - model.Updated = model.Created - if err := txDao.SaveLog(model); err != nil { + if err := txApp.AuxSave(model); err != nil { log.Println("Failed to write log", model, err) } } @@ -1259,21 +1314,6 @@ func (app *BaseApp) initLogger() error { return nil }) - // @todo replace with cron so that it doesn't rely on the logs write - // - // delete old logs - // --- - now := time.Now() - lastLogsDeletedAt := cast.ToTime(app.Store().Get("lastLogsDeletedAt")) - if now.Sub(lastLogsDeletedAt).Hours() >= 6 { - deleteErr := app.LogsDao().DeleteOldLogs(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays)) - if deleteErr == nil { - app.Store().Set("lastLogsDeletedAt", now) - } else { - log.Println("Logs delete failed", deleteErr) - } - } - return nil }, }) @@ -1293,15 +1333,66 @@ func (app *BaseApp) initLogger() error { app.logger = slog.New(handler) - app.OnTerminate().PreAdd(func(e *TerminateEvent) error { - // write all remaining logs before ticker.Stop to avoid races with ResetBootstrap user calls - handler.WriteAll(context.Background()) + // write all remaining logs before ticker.Stop to avoid races with ResetBootstrap user calls + app.OnTerminate().Bind(&hook.Handler[*TerminateEvent]{ + Id: "__pbAppLoggerOnTerminate__", + Func: func(e *TerminateEvent) error { + handler.WriteAll(context.Background()) - ticker.Stop() + ticker.Stop() - done <- true + done <- true - return nil + return e.Next() + }, + Priority: -999, + }) + + // reload log handler level (if initialized) + app.OnSettingsReload().Bind(&hook.Handler[*SettingsReloadEvent]{ + Id: "__pbAppLoggerOnSettingsReload__", + Func: func(e *SettingsReloadEvent) error { + err := e.Next() + if err != nil { + return err + } + + if e.App.Logger() != nil { + if h, ok := e.App.Logger().Handler().(*logger.BatchHandler); ok { + h.SetLevel(getLoggerMinLevel(e.App)) + } + } + + // try to clear old logs not matching the new settings + createdBefore := types.NowDateTime().AddDate(0, 0, -1*e.App.Settings().Logs.MaxDays) + expr := dbx.NewExp("[[created]] <= {:date} OR [[level]] < {:level}", dbx.Params{ + "date": createdBefore.String(), + "level": e.App.Settings().Logs.MinLevel, + }) + _, err = e.App.AuxNonconcurrentDB().Delete((&Log{}).TableName(), expr).Execute() + if err != nil { + e.App.Logger().Debug("Failed to cleanup old logs", "error", err) + } + + // no logs are allowed -> try to reclaim preserved disk space after the previous delete operation + if e.App.Settings().Logs.MaxDays == 0 { + err = e.App.AuxVacuum() + if err != nil { + e.App.Logger().Debug("Failed to VACUUM aux database", "error", err) + } + } + + return nil + }, + Priority: -999, + }) + + // cleanup old logs + app.Cron().Add("__pbLogsCleanup__", "0 */6 * * *", func() { + deleteErr := app.DeleteOldLogs(time.Now().AddDate(0, 0, -1*app.Settings().Logs.MaxDays)) + if deleteErr != nil { + app.Logger().Warn("Failed to delete old logs", "error", deleteErr) + } }) return nil diff --git a/core/base_backup.go b/core/base_backup.go index 65d6e201..2b272bd8 100644 --- a/core/base_backup.go +++ b/core/base_backup.go @@ -12,20 +12,16 @@ import ( "sort" "time" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/archive" - "github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/inflector" "github.com/pocketbase/pocketbase/tools/osutils" "github.com/pocketbase/pocketbase/tools/security" ) -// Deprecated: Replaced with StoreKeyActiveBackup. -const CacheKeyActiveBackup string = "@activeBackup" - -const StoreKeyActiveBackup string = "@activeBackup" +const ( + StoreKeyActiveBackup = "@activeBackup" +) // CreateBackup creates a new backup of the current app pb_data directory. // @@ -50,61 +46,67 @@ func (app *BaseApp) CreateBackup(ctx context.Context, name string) error { return errors.New("try again later - another backup/restore operation has already been started") } - if name == "" { - name = app.generateBackupName("pb_backup_") - } - app.Store().Set(StoreKeyActiveBackup, name) defer app.Store().Remove(StoreKeyActiveBackup) - // root dir entries to exclude from the backup generation - exclude := []string{LocalBackupsDirName, LocalTempDirName} + event := new(BackupEvent) + event.App = app + event.Context = ctx + event.Name = name + // default root dir entries to exclude from the backup generation + event.Exclude = []string{LocalBackupsDirName, LocalTempDirName, LocalAutocertCacheDirName} - // make sure that the special temp directory exists - // note: it needs to be inside the current pb_data to avoid "cross-device link" errors - localTempDir := filepath.Join(app.DataDir(), LocalTempDirName) - if err := os.MkdirAll(localTempDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create a temp dir: %w", err) - } + return app.OnBackupCreate().Trigger(event, func(e *BackupEvent) error { + // generate a default name if missing + if e.Name == "" { + e.Name = generateBackupName(e.App, "pb_backup_") + } - // Archive pb_data in a temp directory, exluding the "backups" and the temp dirs. - // - // Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection). - // --- - tempPath := filepath.Join(localTempDir, "pb_backup_"+security.PseudorandomString(4)) - createErr := app.Dao().RunInTransaction(func(dataTXDao *daos.Dao) error { - return app.LogsDao().RunInTransaction(func(logsTXDao *daos.Dao) error { - // @todo consider experimenting with temp switching the readonly pragma after the db interface change - return archive.Create(app.DataDir(), tempPath, exclude...) + // make sure that the special temp directory exists + // note: it needs to be inside the current pb_data to avoid "cross-device link" errors + localTempDir := filepath.Join(e.App.DataDir(), LocalTempDirName) + if err := os.MkdirAll(localTempDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create a temp dir: %w", err) + } + + // archive pb_data in a temp directory, exluding the "backups" and the temp dirs + // + // Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection). + // --- + tempPath := filepath.Join(localTempDir, "pb_backup_"+security.PseudorandomString(6)) + createErr := e.App.RunInTransaction(func(txApp App) error { + return txApp.AuxRunInTransaction(func(txApp App) error { + return archive.Create(txApp.DataDir(), tempPath, e.Exclude...) + }) }) + if createErr != nil { + return createErr + } + defer os.Remove(tempPath) + + // persist the backup in the backups filesystem + // --- + fsys, err := e.App.NewBackupsFilesystem() + if err != nil { + return err + } + defer fsys.Close() + + fsys.SetContext(e.Context) + + file, err := filesystem.NewFileFromPath(tempPath) + if err != nil { + return err + } + file.OriginalName = e.Name + file.Name = file.OriginalName + + if err := fsys.UploadFile(file, file.Name); err != nil { + return err + } + + return nil }) - if createErr != nil { - return createErr - } - defer os.Remove(tempPath) - - // Persist the backup in the backups filesystem. - // --- - fsys, err := app.NewBackupsFilesystem() - if err != nil { - return err - } - defer fsys.Close() - - fsys.SetContext(ctx) - - file, err := filesystem.NewFileFromPath(tempPath) - if err != nil { - return err - } - file.OriginalName = name - file.Name = file.OriginalName - - if err := fsys.UploadFile(file, file.Name); err != nil { - return err - } - - return nil } // RestoreBackup restores the backup with the specified name and restarts @@ -136,10 +138,6 @@ func (app *BaseApp) CreateBackup(ctx context.Context, name string) error { // If a failure occure during the restore process the dir changes are reverted. // If for whatever reason the revert is not possible, it panics. func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error { - if runtime.GOOS == "windows" { - return errors.New("restore is not supported on windows") - } - if app.Store().Has(StoreKeyActiveBackup) { return errors.New("try again later - another backup/restore operation has already been started") } @@ -147,131 +145,129 @@ func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error { app.Store().Set(StoreKeyActiveBackup, name) defer app.Store().Remove(StoreKeyActiveBackup) - fsys, err := app.NewBackupsFilesystem() - if err != nil { - return err - } - defer fsys.Close() + event := new(BackupEvent) + event.App = app + event.Context = ctx + event.Name = name + // default root dir entries to exclude from the backup restore + event.Exclude = []string{LocalBackupsDirName, LocalTempDirName, LocalAutocertCacheDirName} - fsys.SetContext(ctx) - - // fetch the backup file in a temp location - br, err := fsys.GetFile(name) - if err != nil { - return err - } - defer br.Close() - - // make sure that the special temp directory exists - // note: it needs to be inside the current pb_data to avoid "cross-device link" errors - localTempDir := filepath.Join(app.DataDir(), LocalTempDirName) - if err := os.MkdirAll(localTempDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create a temp dir: %w", err) - } - - // create a temp zip file from the blob.Reader and try to extract it - tempZip, err := os.CreateTemp(localTempDir, "pb_restore_zip") - if err != nil { - return err - } - defer os.Remove(tempZip.Name()) - - if _, err := io.Copy(tempZip, br); err != nil { - return err - } - - extractedDataDir := filepath.Join(localTempDir, "pb_restore_"+security.PseudorandomString(4)) - defer os.RemoveAll(extractedDataDir) - if err := archive.Extract(tempZip.Name(), extractedDataDir); err != nil { - return err - } - - // ensure that a database file exists - extractedDB := filepath.Join(extractedDataDir, "data.db") - if _, err := os.Stat(extractedDB); err != nil { - return fmt.Errorf("data.db file is missing or invalid: %w", err) - } - - // remove the extracted zip file since we no longer need it - // (this is in case the app restarts and the defer calls are not called) - if err := os.Remove(tempZip.Name()); err != nil { - app.Logger().Debug( - "[RestoreBackup] Failed to remove the temp zip backup file", - slog.String("file", tempZip.Name()), - slog.String("error", err.Error()), - ) - } - - // root dir entries to exclude from the backup restore - exclude := []string{LocalBackupsDirName, LocalTempDirName} - - // move the current pb_data content to a special temp location - // that will hold the old data between dirs replace - // (the temp dir will be automatically removed on the next app start) - oldTempDataDir := filepath.Join(localTempDir, "old_pb_data_"+security.PseudorandomString(4)) - if err := osutils.MoveDirContent(app.DataDir(), oldTempDataDir, exclude...); err != nil { - return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err) - } - - // move the extracted archive content to the app's pb_data - if err := osutils.MoveDirContent(extractedDataDir, app.DataDir(), exclude...); err != nil { - return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err) - } - - revertDataDirChanges := func() error { - if err := osutils.MoveDirContent(app.DataDir(), extractedDataDir, exclude...); err != nil { - return fmt.Errorf("failed to revert the extracted dir change: %w", err) + return app.OnBackupRestore().Trigger(event, func(e *BackupEvent) error { + if runtime.GOOS == "windows" { + return errors.New("restore is not supported on Windows") } - if err := osutils.MoveDirContent(oldTempDataDir, app.DataDir(), exclude...); err != nil { - return fmt.Errorf("failed to revert old pb_data dir change: %w", err) + fsys, err := e.App.NewBackupsFilesystem() + if err != nil { + return err + } + defer fsys.Close() + + fsys.SetContext(e.Context) + + // fetch the backup file in a temp location + br, err := fsys.GetFile(name) + if err != nil { + return err + } + defer br.Close() + + // make sure that the special temp directory exists + // note: it needs to be inside the current pb_data to avoid "cross-device link" errors + localTempDir := filepath.Join(e.App.DataDir(), LocalTempDirName) + if err := os.MkdirAll(localTempDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create a temp dir: %w", err) } - return nil - } + // create a temp zip file from the blob.Reader and try to extract it + tempZip, err := os.CreateTemp(localTempDir, "pb_restore_zip") + if err != nil { + return err + } + defer os.Remove(tempZip.Name()) - // restart the app - if err := app.Restart(); err != nil { - if revertErr := revertDataDirChanges(); revertErr != nil { - panic(revertErr) + if _, err := io.Copy(tempZip, br); err != nil { + return err } - return fmt.Errorf("failed to restart the app process: %w", err) - } + extractedDataDir := filepath.Join(localTempDir, "pb_restore_"+security.PseudorandomString(4)) + defer os.RemoveAll(extractedDataDir) + if err := archive.Extract(tempZip.Name(), extractedDataDir); err != nil { + return err + } - return nil -} + // ensure that a database file exists + extractedDB := filepath.Join(extractedDataDir, "data.db") + if _, err := os.Stat(extractedDB); err != nil { + return fmt.Errorf("data.db file is missing or invalid: %w", err) + } -// initAutobackupHooks registers the autobackup app serve hooks. -func (app *BaseApp) initAutobackupHooks() error { - c := cron.New() - isServe := false - - loadJob := func() { - c.Stop() - - // make sure that app.Settings() is always up to date - // - // @todo remove with the refactoring as core.App and daos.Dao will be one. - if err := app.RefreshSettings(); err != nil { - app.Logger().Debug( - "[Backup cron] Failed to get the latest app settings", + // remove the extracted zip file since we no longer need it + // (this is in case the app restarts and the defer calls are not called) + if err := os.Remove(tempZip.Name()); err != nil { + e.App.Logger().Debug( + "[RestoreBackup] Failed to remove the temp zip backup file", + slog.String("file", tempZip.Name()), slog.String("error", err.Error()), ) } + // move the current pb_data content to a special temp location + // that will hold the old data between dirs replace + // (the temp dir will be automatically removed on the next app start) + oldTempDataDir := filepath.Join(localTempDir, "old_pb_data_"+security.PseudorandomString(4)) + if err := osutils.MoveDirContent(e.App.DataDir(), oldTempDataDir, e.Exclude...); err != nil { + return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err) + } + + // move the extracted archive content to the app's pb_data + if err := osutils.MoveDirContent(extractedDataDir, e.App.DataDir(), e.Exclude...); err != nil { + return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err) + } + + revertDataDirChanges := func() error { + if err := osutils.MoveDirContent(e.App.DataDir(), extractedDataDir, e.Exclude...); err != nil { + return fmt.Errorf("failed to revert the extracted dir change: %w", err) + } + + if err := osutils.MoveDirContent(oldTempDataDir, e.App.DataDir(), e.Exclude...); err != nil { + return fmt.Errorf("failed to revert old pb_data dir change: %w", err) + } + + return nil + } + + // restart the app + if err := e.App.Restart(); err != nil { + if revertErr := revertDataDirChanges(); revertErr != nil { + panic(revertErr) + } + + return fmt.Errorf("failed to restart the app process: %w", err) + } + + return nil + }) +} + +// registerAutobackupHooks registers the autobackup app serve hooks. +func (app *BaseApp) registerAutobackupHooks() { + const jobId = "__auto_pb_backup__" + + loadJob := func() { rawSchedule := app.Settings().Backups.Cron - if rawSchedule == "" || !isServe || !app.IsBootstrapped() { + if rawSchedule == "" { + app.Cron().Remove(jobId) return } - c.Add("@autobackup", rawSchedule, func() { + app.Cron().Add(jobId, rawSchedule, func() { const autoPrefix = "@auto_pb_backup_" - name := app.generateBackupName(autoPrefix) + name := generateBackupName(app, autoPrefix) if err := app.CreateBackup(context.Background(), name); err != nil { - app.Logger().Debug( + app.Logger().Error( "[Backup cron] Failed to create backup", slog.String("name", name), slog.String("error", err.Error()), @@ -286,7 +282,7 @@ func (app *BaseApp) initAutobackupHooks() error { fsys, err := app.NewBackupsFilesystem() if err != nil { - app.Logger().Debug( + app.Logger().Error( "[Backup cron] Failed to initialize the backup filesystem", slog.String("error", err.Error()), ) @@ -296,7 +292,7 @@ func (app *BaseApp) initAutobackupHooks() error { files, err := fsys.List(autoPrefix) if err != nil { - app.Logger().Debug( + app.Logger().Error( "[Backup cron] Failed to list autogenerated backups", slog.String("error", err.Error()), ) @@ -317,7 +313,7 @@ func (app *BaseApp) initAutobackupHooks() error { for _, f := range toRemove { if err := fsys.Delete(f.Key); err != nil { - app.Logger().Debug( + app.Logger().Error( "[Backup cron] Failed to remove old autogenerated backup", slog.String("key", f.Key), slog.String("error", err.Error()), @@ -325,29 +321,11 @@ func (app *BaseApp) initAutobackupHooks() error { } } }) - - // restart the ticker - c.Start() } - // load on app serve - app.OnBeforeServe().Add(func(e *ServeEvent) error { - isServe = true - loadJob() - return nil - }) - - // stop the ticker on app termination - app.OnTerminate().Add(func(e *TerminateEvent) error { - c.Stop() - return nil - }) - - // reload on app settings change - app.OnModelAfterUpdate((&models.Param{}).TableName()).Add(func(e *ModelEvent) error { - p := e.Model.(*models.Param) - if p == nil || p.Key != models.ParamAppSettings { - return nil + app.OnBootstrap().BindFunc(func(e *BootstrapEvent) error { + if err := e.Next(); err != nil { + return err } loadJob() @@ -355,10 +333,18 @@ func (app *BaseApp) initAutobackupHooks() error { return nil }) - return nil + app.OnSettingsReload().BindFunc(func(e *SettingsReloadEvent) error { + if err := e.Next(); err != nil { + return err + } + + loadJob() + + return nil + }) } -func (app *BaseApp) generateBackupName(prefix string) string { +func generateBackupName(app App, prefix string) string { appName := inflector.Snakecase(app.Settings().Meta.AppName) if len(appName) > 50 { appName = appName[:50] diff --git a/core/base_backup_test.go b/core/base_backup_test.go index da4f93b5..496585ee 100644 --- a/core/base_backup_test.go +++ b/core/base_backup_test.go @@ -128,9 +128,9 @@ func verifyBackupContent(app core.App, path string) error { "data.db", "data.db-shm", "data.db-wal", - "logs.db", - "logs.db-shm", - "logs.db-wal", + "aux.db", + "aux.db-shm", + "aux.db-wal", ".gitignore", } diff --git a/core/base_settings_test.go b/core/base_settings_test.go deleted file mode 100644 index 4c38a829..00000000 --- a/core/base_settings_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package core_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestBaseAppRefreshSettings(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // cleanup all stored settings - if _, err := app.DB().NewQuery("DELETE from _params;").Execute(); err != nil { - t.Fatalf("Failed to delete all test settings: %v", err) - } - - // check if the new settings are saved in the db - app.ResetEventCalls() - if err := app.RefreshSettings(); err != nil { - t.Fatalf("Failed to refresh the settings after delete: %v", err) - } - testEventCalls(t, app, map[string]int{ - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - }) - param, err := app.Dao().FindParamByKey(models.ParamAppSettings) - if err != nil { - t.Fatalf("Expected new settings to be persisted, got %v", err) - } - - // change the db entry and refresh the app settings (ensure that there was no db update) - param.Value = types.JsonRaw([]byte(`{"example": 123}`)) - if err := app.Dao().SaveParam(param.Key, param.Value); err != nil { - t.Fatalf("Failed to update the test settings: %v", err) - } - app.ResetEventCalls() - if err := app.RefreshSettings(); err != nil { - t.Fatalf("Failed to refresh the app settings: %v", err) - } - testEventCalls(t, app, nil) - - // try to refresh again without doing any changes - app.ResetEventCalls() - if err := app.RefreshSettings(); err != nil { - t.Fatalf("Failed to refresh the app settings without change: %v", err) - } - testEventCalls(t, app, nil) -} - -func testEventCalls(t *testing.T, app *tests.TestApp, events map[string]int) { - if len(events) != len(app.EventCalls) { - t.Fatalf("Expected events doesn't match: \n%v, \ngot \n%v", events, app.EventCalls) - } - - for name, total := range events { - if v, ok := app.EventCalls[name]; !ok || v != total { - t.Fatalf("Expected events doesn't exist or match: \n%v, \ngot \n%v", events, app.EventCalls) - } - } -} diff --git a/core/base_test.go b/core/base_test.go index 6b9c59f5..a64a94b4 100644 --- a/core/base_test.go +++ b/core/base_test.go @@ -1,59 +1,56 @@ -package core +package core_test import ( "context" - "database/sql" - "fmt" "log/slog" "os" - "strings" "testing" "time" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/migrations/logs" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/list" + _ "unsafe" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/logger" "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/migrate" - "github.com/pocketbase/pocketbase/tools/types" ) func TestNewBaseApp(t *testing.T) { const testDataDir = "./pb_base_app_test_data_dir/" defer os.RemoveAll(testDataDir) - app := NewBaseApp(BaseAppConfig{ + app := core.NewBaseApp(core.BaseAppConfig{ DataDir: testDataDir, EncryptionEnv: "test_env", IsDev: true, }) - if app.dataDir != testDataDir { - t.Fatalf("expected dataDir %q, got %q", testDataDir, app.dataDir) + if app.DataDir() != testDataDir { + t.Fatalf("expected DataDir %q, got %q", testDataDir, app.DataDir()) } - if app.encryptionEnv != "test_env" { - t.Fatalf("expected encryptionEnv test_env, got %q", app.dataDir) + if app.EncryptionEnv() != "test_env" { + t.Fatalf("expected EncryptionEnv test_env, got %q", app.EncryptionEnv()) } - if !app.isDev { - t.Fatalf("expected isDev true, got %v", app.isDev) + if !app.IsDev() { + t.Fatalf("expected IsDev true, got %v", app.IsDev()) } - if app.store == nil { - t.Fatal("expected store to be set, got nil") + if app.Store() == nil { + t.Fatal("expected Store to be set, got nil") } - if app.settings == nil { - t.Fatal("expected settings to be set, got nil") + if app.Settings() == nil { + t.Fatal("expected Settings to be set, got nil") } - if app.subscriptionsBroker == nil { - t.Fatal("expected subscriptionsBroker to be set, got nil") + if app.SubscriptionsBroker() == nil { + t.Fatal("expected SubscriptionsBroker to be set, got nil") + } + + if app.Cron() == nil { + t.Fatal("expected Cron to be set, got nil") } } @@ -61,9 +58,8 @@ func TestBaseAppBootstrap(t *testing.T) { const testDataDir = "./pb_base_app_test_data_dir/" defer os.RemoveAll(testDataDir) - app := NewBaseApp(BaseAppConfig{ - DataDir: testDataDir, - EncryptionEnv: "pb_test_env", + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, }) defer app.ResetBootstrapState() @@ -83,72 +79,59 @@ func TestBaseAppBootstrap(t *testing.T) { t.Fatal("Expected test data directory to be created.") } - if app.dao == nil { - t.Fatal("Expected app.dao to be initialized, got nil.") + type nilCheck struct { + name string + value any + expectNil bool } - if app.dao.BeforeCreateFunc == nil { - t.Fatal("Expected app.dao.BeforeCreateFunc to be set, got nil.") + runNilChecks := func(checks []nilCheck) { + for _, check := range checks { + t.Run(check.name, func(t *testing.T) { + isNil := check.value == nil + if isNil != check.expectNil { + t.Fatalf("Expected isNil %v, got %v", check.expectNil, isNil) + } + }) + } } - if app.dao.AfterCreateFunc == nil { - t.Fatal("Expected app.dao.AfterCreateFunc to be set, got nil.") + nilChecksBeforeReset := []nilCheck{ + {"[before] concurrentDB", app.DB(), false}, + {"[before] nonconcurrentDB", app.NonconcurrentDB(), false}, + {"[before] auxConcurrentDB", app.AuxDB(), false}, + {"[before] auxNonconcurrentDB", app.AuxNonconcurrentDB(), false}, + {"[before] settings", app.Settings(), false}, + {"[before] logger", app.Logger(), false}, + {"[before] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false}, } - if app.dao.BeforeUpdateFunc == nil { - t.Fatal("Expected app.dao.BeforeUpdateFunc to be set, got nil.") - } - - if app.dao.AfterUpdateFunc == nil { - t.Fatal("Expected app.dao.AfterUpdateFunc to be set, got nil.") - } - - if app.dao.BeforeDeleteFunc == nil { - t.Fatal("Expected app.dao.BeforeDeleteFunc to be set, got nil.") - } - - if app.dao.AfterDeleteFunc == nil { - t.Fatal("Expected app.dao.AfterDeleteFunc to be set, got nil.") - } - - if app.logsDao == nil { - t.Fatal("Expected app.logsDao to be initialized, got nil.") - } - - if app.settings == nil { - t.Fatal("Expected app.settings to be initialized, got nil.") - } - - if app.logger == nil { - t.Fatal("Expected app.logger to be initialized, got nil.") - } - - if _, ok := app.logger.Handler().(*logger.BatchHandler); !ok { - t.Fatal("Expected app.logger handler to be initialized.") - } + runNilChecks(nilChecksBeforeReset) // reset if err := app.ResetBootstrapState(); err != nil { t.Fatal(err) } - if app.dao != nil { - t.Fatalf("Expected app.dao to be nil, got %v.", app.dao) + nilChecksAfterReset := []nilCheck{ + {"[after] concurrentDB", app.DB(), true}, + {"[after] nonconcurrentDB", app.NonconcurrentDB(), true}, + {"[after] auxConcurrentDB", app.AuxDB(), true}, + {"[after] auxNonconcurrentDB", app.AuxNonconcurrentDB(), true}, + {"[after] settings", app.Settings(), false}, + {"[after] logger", app.Logger(), false}, + {"[after] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false}, } - if app.logsDao != nil { - t.Fatalf("Expected app.logsDao to be nil, got %v.", app.logsDao) - } + runNilChecks(nilChecksAfterReset) } -func TestBaseAppGetters(t *testing.T) { +func TestNewBaseAppIsTransactional(t *testing.T) { const testDataDir = "./pb_base_app_test_data_dir/" defer os.RemoveAll(testDataDir) - app := NewBaseApp(BaseAppConfig{ - DataDir: testDataDir, - EncryptionEnv: "pb_test_env", - IsDev: true, + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, }) defer app.ResetBootstrapState() @@ -156,81 +139,58 @@ func TestBaseAppGetters(t *testing.T) { t.Fatal(err) } - if app.dao != app.Dao() { - t.Fatalf("Expected app.Dao %v, got %v", app.Dao(), app.dao) + if app.IsTransactional() { + t.Fatalf("Didn't expect the app to be transactional") } - if app.dao.ConcurrentDB() != app.DB() { - t.Fatalf("Expected app.DB %v, got %v", app.DB(), app.dao.ConcurrentDB()) - } + app.RunInTransaction(func(txApp core.App) error { + if !txApp.IsTransactional() { + t.Fatalf("Expected the app to be transactional") + } - if app.logsDao != app.LogsDao() { - t.Fatalf("Expected app.LogsDao %v, got %v", app.LogsDao(), app.logsDao) - } - - if app.logsDao.ConcurrentDB() != app.LogsDB() { - t.Fatalf("Expected app.LogsDB %v, got %v", app.LogsDB(), app.logsDao.ConcurrentDB()) - } - - if app.dataDir != app.DataDir() { - t.Fatalf("Expected app.DataDir %v, got %v", app.DataDir(), app.dataDir) - } - - if app.encryptionEnv != app.EncryptionEnv() { - t.Fatalf("Expected app.EncryptionEnv %v, got %v", app.EncryptionEnv(), app.encryptionEnv) - } - - if app.isDev != app.IsDev() { - t.Fatalf("Expected app.IsDev %v, got %v", app.IsDev(), app.isDev) - } - - if app.settings != app.Settings() { - t.Fatalf("Expected app.Settings %v, got %v", app.Settings(), app.settings) - } - - if app.store != app.Store() { - t.Fatalf("Expected app.Store %v, got %v", app.Store(), app.store) - } - - if app.logger != app.Logger() { - t.Fatalf("Expected app.Logger %v, got %v", app.Logger(), app.logger) - } - - if app.subscriptionsBroker != app.SubscriptionsBroker() { - t.Fatalf("Expected app.SubscriptionsBroker %v, got %v", app.SubscriptionsBroker(), app.subscriptionsBroker) - } - - if app.onBeforeServe != app.OnBeforeServe() || app.OnBeforeServe() == nil { - t.Fatalf("Getter app.OnBeforeServe does not match or nil (%v vs %v)", app.OnBeforeServe(), app.onBeforeServe) - } + return nil + }) } func TestBaseAppNewMailClient(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { - t.Fatal(err) - } - defer cleanup() + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, + EncryptionEnv: "pb_test_env", + }) + defer app.ResetBootstrapState() client1 := app.NewMailClient() - if val, ok := client1.(*mailer.Sendmail); !ok { - t.Fatalf("Expected mailer.Sendmail instance, got %v", val) + m1, ok := client1.(*mailer.Sendmail) + if !ok { + t.Fatalf("Expected mailer.Sendmail instance, got %v", m1) + } + if m1.OnSend() == nil || m1.OnSend().Length() == 0 { + t.Fatal("Expected OnSend hook to be registered") } - app.Settings().Smtp.Enabled = true + app.Settings().SMTP.Enabled = true client2 := app.NewMailClient() - if val, ok := client2.(*mailer.SmtpClient); !ok { - t.Fatalf("Expected mailer.SmtpClient instance, got %v", val) + m2, ok := client2.(*mailer.SMTPClient) + if !ok { + t.Fatalf("Expected mailer.SMTPClient instance, got %v", m2) + } + if m2.OnSend() == nil || m2.OnSend().Length() == 0 { + t.Fatal("Expected OnSend hook to be registered") } } func TestBaseAppNewFilesystem(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { - t.Fatal(err) - } - defer cleanup() + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, + }) + defer app.ResetBootstrapState() // local local, localErr := app.NewFilesystem() @@ -253,11 +213,13 @@ func TestBaseAppNewFilesystem(t *testing.T) { } func TestBaseAppNewBackupsFilesystem(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { - t.Fatal(err) - } - defer cleanup() + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, + }) + defer app.ResetBootstrapState() // local local, localErr := app.NewBackupsFilesystem() @@ -280,18 +242,22 @@ func TestBaseAppNewBackupsFilesystem(t *testing.T) { } func TestBaseAppLoggerWrites(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // reset + if err := app.DeleteOldLogs(time.Now()); err != nil { t.Fatal(err) } - defer cleanup() const logsThreshold = 200 - totalLogs := func(app App, t *testing.T) int { + totalLogs := func(app core.App, t *testing.T) int { var total int - err := app.LogsDao().LogQuery().Select("count(*)").Row(&total) + err := app.LogQuery().Select("count(*)").Row(&total) if err != nil { t.Fatalf("Failed to fetch total logs: %v", err) } @@ -338,106 +304,9 @@ func TestBaseAppLoggerWrites(t *testing.T) { t.Fatalf("Expected %d logs, got %d", logsThreshold+1, total) } }) - - t.Run("test batch logs delete", func(t *testing.T) { - app.Settings().Logs.MaxDays = 2 - - deleteQueries := 0 - - // reset - app.Store().Set("lastLogsDeletedAt", time.Now()) - if err := app.LogsDao().DeleteOldLogs(time.Now()); err != nil { - t.Fatal(err) - } - - db := app.LogsDao().NonconcurrentDB().(*dbx.DB) - db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - if strings.Contains(sql, "DELETE") { - deleteQueries++ - } - } - - // trigger batch write (A) - expectedLogs := logsThreshold - for i := 0; i < expectedLogs; i++ { - app.Logger().Error("testA") - } - - if total := totalLogs(app, t); total != expectedLogs { - t.Fatalf("[batch write A] Expected %d logs, got %d", expectedLogs, total) - } - - // mark the A inserted logs as 2-day expired - aExpiredDate, err := types.ParseDateTime(time.Now().AddDate(0, 0, -2)) - if err != nil { - t.Fatal(err) - } - _, err = app.LogsDao().NonconcurrentDB().NewQuery("UPDATE _logs SET created={:date}, updated={:date}").Bind(dbx.Params{ - "date": aExpiredDate.String(), - }).Execute() - if err != nil { - t.Fatalf("Failed to mock logs timestamp fields: %v", err) - } - - // simulate recently deleted logs - app.Store().Set("lastLogsDeletedAt", time.Now().Add(-5*time.Hour)) - - // trigger batch write (B) - for i := 0; i < logsThreshold; i++ { - app.Logger().Error("testB") - } - - expectedLogs = 2 * logsThreshold - - // note: even though there are expired logs it shouldn't perform the delete operation because of the lastLogsDeledAt time - if total := totalLogs(app, t); total != expectedLogs { - t.Fatalf("[batch write B] Expected %d logs, got %d", expectedLogs, total) - } - - // mark the B inserted logs as 1-day expired to ensure that they will not be deleted - bExpiredDate, err := types.ParseDateTime(time.Now().AddDate(0, 0, -1)) - if err != nil { - t.Fatal(err) - } - _, err = app.LogsDao().NonconcurrentDB().NewQuery("UPDATE _logs SET created={:date}, updated={:date} where message='testB'").Bind(dbx.Params{ - "date": bExpiredDate.String(), - }).Execute() - if err != nil { - t.Fatalf("Failed to mock logs timestamp fields: %v", err) - } - - // should trigger delete on the next batch write - app.Store().Set("lastLogsDeletedAt", time.Now().Add(-6*time.Hour)) - - // trigger batch write (C) - for i := 0; i < logsThreshold; i++ { - app.Logger().Error("testC") - } - - expectedLogs = 2 * logsThreshold // only B and C logs should remain - - if total := totalLogs(app, t); total != expectedLogs { - t.Fatalf("[batch write C] Expected %d logs, got %d", expectedLogs, total) - } - - if deleteQueries != 1 { - t.Fatalf("Expected DeleteOldLogs to be called %d, got %d", 1, deleteQueries) - } - }) } func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { - t.Fatal(err) - } - defer cleanup() - - handler, ok := app.Logger().Handler().(*logger.BatchHandler) - if !ok { - t.Fatalf("Expected BatchHandler, got %v", app.Logger().Handler()) - } - scenarios := []struct { name string isDev bool @@ -469,173 +338,35 @@ func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - app.isDev = s.isDev + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := core.NewBaseApp(core.BaseAppConfig{ + DataDir: testDataDir, + IsDev: s.isDev, + }) + defer app.ResetBootstrapState() + + if err := app.Bootstrap(); err != nil { + t.Fatal(err) + } + + handler, ok := app.Logger().Handler().(*logger.BatchHandler) + if !ok { + t.Fatalf("Expected BatchHandler, got %v", app.Logger().Handler()) + } app.Settings().Logs.MinLevel = s.level - if err := app.Dao().SaveSettings(app.Settings()); err != nil { + if err := app.Save(app.Settings()); err != nil { t.Fatalf("Failed to save settings: %v", err) } - if err := app.RefreshSettings(); err != nil { - t.Fatalf("Failed to refresh app settings: %v", err) - } - for level, enabled := range s.expectations { - if v := handler.Enabled(nil, slog.Level(level)); v != enabled { + if v := handler.Enabled(context.Background(), slog.Level(level)); v != enabled { t.Fatalf("Expected level %d Enabled() to be %v, got %v", level, enabled, v) } } }) } } - -func TestBaseAppLoggerLevelDevPrint(t *testing.T) { - app, cleanup, err := initTestBaseApp() - if err != nil { - t.Fatal(err) - } - defer cleanup() - - testLogLevel := 4 - - app.Settings().Logs.MinLevel = testLogLevel - if err := app.Dao().SaveSettings(app.Settings()); err != nil { - t.Fatal(err) - } - - scenarios := []struct { - name string - isDev bool - levels []int - printedLevels []int - persistedLevels []int - }{ - { - "dev mode", - true, - []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, - []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, - []int{testLogLevel, testLogLevel + 1}, - }, - { - "nondev mode", - false, - []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, - []int{}, - []int{testLogLevel, testLogLevel + 1}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - var printedLevels []int - var persistedLevels []int - - app.isDev = s.isDev - - // trigger slog handler min level refresh - if err := app.RefreshSettings(); err != nil { - t.Fatal(err) - } - - // track printed logs - originalPrintLog := printLog - defer func() { - printLog = originalPrintLog - }() - printLog = func(log *logger.Log) { - printedLevels = append(printedLevels, int(log.Level)) - } - - // track persisted logs - app.LogsDao().AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - l, ok := m.(*models.Log) - if ok { - persistedLevels = append(persistedLevels, l.Level) - } - return nil - } - - // write and persist logs - for _, l := range s.levels { - app.Logger().Log(nil, slog.Level(l), "test") - } - handler, ok := app.Logger().Handler().(*logger.BatchHandler) - if !ok { - t.Fatalf("Expected BatchHandler, got %v", app.Logger().Handler()) - } - if err := handler.WriteAll(nil); err != nil { - t.Fatalf("Failed to write all logs: %v", err) - } - - // check persisted log levels - if len(s.persistedLevels) != len(persistedLevels) { - t.Fatalf("Expected persisted levels \n%v\ngot\n%v", s.persistedLevels, persistedLevels) - } - for _, l := range persistedLevels { - if !list.ExistInSlice(l, s.persistedLevels) { - t.Fatalf("Missing expected persisted level %v in %v", l, persistedLevels) - } - } - - // check printed log levels - if len(s.printedLevels) != len(printedLevels) { - t.Fatalf("Expected printed levels \n%v\ngot\n%v", s.printedLevels, printedLevels) - } - for _, l := range printedLevels { - if !list.ExistInSlice(l, s.printedLevels) { - t.Fatalf("Missing expected printed level %v in %v", l, printedLevels) - } - } - }) - } -} - -// ------------------------------------------------------------------- - -// note: make sure to call `defer cleanup()` when the app is no longer needed. -func initTestBaseApp() (app *BaseApp, cleanup func(), err error) { - testDataDir, err := os.MkdirTemp("", "test_base_app") - if err != nil { - return nil, nil, err - } - - cleanup = func() { - os.RemoveAll(testDataDir) - } - - app = NewBaseApp(BaseAppConfig{ - DataDir: testDataDir, - }) - - initErr := func() error { - if err := app.Bootstrap(); err != nil { - return fmt.Errorf("bootstrap error: %w", err) - } - - logsRunner, err := migrate.NewRunner(app.LogsDB(), logs.LogsMigrations) - if err != nil { - return fmt.Errorf("logsRunner error: %w", err) - } - if _, err := logsRunner.Up(); err != nil { - return fmt.Errorf("logsRunner migrations execution error: %w", err) - } - - dataRunner, err := migrate.NewRunner(app.DB(), migrations.AppMigrations) - if err != nil { - return fmt.Errorf("logsRunner error: %w", err) - } - if _, err := dataRunner.Up(); err != nil { - return fmt.Errorf("dataRunner migrations execution error: %w", err) - } - - return nil - }() - if initErr != nil { - cleanup() - return nil, nil, initErr - } - - return app, cleanup, nil -} diff --git a/core/collection_import.go b/core/collection_import.go new file mode 100644 index 00000000..39a155b9 --- /dev/null +++ b/core/collection_import.go @@ -0,0 +1,194 @@ +package core + +import ( + "cmp" + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "slices" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/spf13/cast" +) + +// ImportCollectionsByMarshaledJSON is the same as [ImportCollections] +// but accept marshaled json array as import data (usually used for the autogenerated snapshots). +func (app *BaseApp) ImportCollectionsByMarshaledJSON(rawSliceOfMaps []byte, deleteMissing bool) error { + data := []map[string]any{} + + err := json.Unmarshal(rawSliceOfMaps, &data) + if err != nil { + return err + } + + return app.ImportCollections(data, deleteMissing) +} + +// ImportCollections imports the provided collections data in a single transaction. +// +// For existing matching collections, the imported data is unmarshaled on top of the existing model. +// +// NB! If deleteMissing is true, ALL NON-SYSTEM COLLECTIONS AND SCHEMA FIELDS, +// that are not present in the imported configuration, WILL BE DELETED +// (this includes their related records data). +func (app *BaseApp) ImportCollections(toImport []map[string]any, deleteMissing bool) error { + if len(toImport) == 0 { + // prevent accidentally deleting all collections + return errors.New("no collections to import") + } + + importedCollections := make([]*Collection, len(toImport)) + mappedImported := make(map[string]*Collection, len(toImport)) + + // normalize imported collections data to ensure that all + // collection fields are present and properly initialized + for i, data := range toImport { + var imported *Collection + + identifier := cast.ToString(data["id"]) + if identifier == "" { + identifier = cast.ToString(data["name"]) + } + + existing, err := app.FindCollectionByNameOrId(identifier) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + if existing != nil { + // refetch for deep copy + imported, err = app.FindCollectionByNameOrId(existing.Id) + if err != nil { + return err + } + + // ensure that the fields will be cleared + if data["fields"] == nil && deleteMissing { + data["fields"] = []map[string]any{} + } + + rawData, err := json.Marshal(data) + if err != nil { + return err + } + + // load the imported data + err = json.Unmarshal(rawData, imported) + if err != nil { + return err + } + + // extend with the existing fields if necessary + for _, f := range existing.Fields { + if !f.GetSystem() && deleteMissing { + continue + } + if imported.Fields.GetById(f.GetId()) == nil { + imported.Fields.Add(f) + } + } + } else { + imported = &Collection{} + + rawData, err := json.Marshal(data) + if err != nil { + return err + } + + // load the imported data + err = json.Unmarshal(rawData, imported) + if err != nil { + return err + } + } + + imported.IntegrityChecks(false) + + importedCollections[i] = imported + mappedImported[imported.Id] = imported + } + + // reorder views last since the view query could depend on some of the other collections + slices.SortStableFunc(importedCollections, func(a, b *Collection) int { + cmpA := -1 + if a.IsView() { + cmpA = 1 + } + + cmpB := -1 + if b.IsView() { + cmpB = 1 + } + + res := cmp.Compare(cmpA, cmpB) + if res == 0 { + res = a.Created.Compare(b.Created) + if res == 0 { + res = a.Updated.Compare(b.Updated) + } + } + return res + }) + + return app.RunInTransaction(func(txApp App) error { + existingCollections := []*Collection{} + if err := txApp.CollectionQuery().OrderBy("updated ASC").All(&existingCollections); err != nil { + return err + } + mappedExisting := make(map[string]*Collection, len(existingCollections)) + for _, existing := range existingCollections { + existing.IntegrityChecks(false) + mappedExisting[existing.Id] = existing + } + + // delete old collections not available in the new configuration + // (before saving the imports in case a deleted collection name is being reused) + if deleteMissing { + for _, existing := range existingCollections { + if mappedImported[existing.Id] != nil || existing.System { + continue // exist or system + } + + // delete collection + if err := txApp.Delete(existing); err != nil { + return err + } + } + } + + // upsert imported collections + for _, imported := range importedCollections { + if err := txApp.SaveNoValidate(imported); err != nil { + return fmt.Errorf("failed to save collection %q: %w", imported.Name, err) + } + } + + // run validations + for _, imported := range importedCollections { + original := mappedExisting[imported.Id] + if original == nil { + original = imported + } + + validator := newCollectionValidator( + context.Background(), + txApp, + imported, + original, + ) + if err := validator.run(); err != nil { + // serialize the validation error(s) + serializedErr, _ := json.MarshalIndent(err, "", " ") + + return validation.Errors{"collections": validation.NewError( + "validation_collections_import_failure", + fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", imported.Name, imported.Id, serializedErr), + )} + } + } + + return nil + }) +} diff --git a/core/collection_import_test.go b/core/collection_import_test.go new file mode 100644 index 00000000..74c1201f --- /dev/null +++ b/core/collection_import_test.go @@ -0,0 +1,476 @@ +package core_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestImportCollections(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + var regularCollections []*core.Collection + err := testApp.CollectionQuery().AndWhere(dbx.HashExp{"system": false}).All(®ularCollections) + if err != nil { + t.Fatal(err) + } + + var systemCollections []*core.Collection + err = testApp.CollectionQuery().AndWhere(dbx.HashExp{"system": true}).All(&systemCollections) + if err != nil { + t.Fatal(err) + } + + totalRegularCollections := len(regularCollections) + totalSystemCollections := len(systemCollections) + totalCollections := totalRegularCollections + totalSystemCollections + + scenarios := []struct { + name string + data []map[string]any + deleteMissing bool + expectError bool + expectCollectionsCount int + afterTestFunc func(testApp *tests.TestApp, resultCollections []*core.Collection) + }{ + { + name: "empty collections", + data: []map[string]any{}, + expectError: true, + expectCollectionsCount: totalCollections, + }, + { + name: "minimal collection import (with missing system fields)", + data: []map[string]any{ + {"name": "import_test1", "type": "auth"}, + { + "name": "import_test2", "fields": []map[string]any{ + {"name": "test", "type": "text"}, + }, + }, + }, + deleteMissing: false, + expectError: false, + expectCollectionsCount: totalCollections + 2, + }, + { + name: "minimal collection import (trigger collection model validations)", + data: []map[string]any{ + {"name": ""}, + { + "name": "import_test2", "fields": []map[string]any{ + {"name": "test", "type": "text"}, + }, + }, + }, + deleteMissing: false, + expectError: true, + expectCollectionsCount: totalCollections, + }, + { + name: "minimal collection import (trigger field settings validation)", + data: []map[string]any{ + {"name": "import_test", "fields": []map[string]any{{"name": "test", "type": "text", "min": -1}}}, + }, + deleteMissing: false, + expectError: true, + expectCollectionsCount: totalCollections, + }, + { + name: "new + update + delete (system collections delete should be ignored)", + data: []map[string]any{ + { + "id": "wsmn24bux7wo113", + "name": "demo", + "fields": []map[string]any{ + { + "id": "_2hlxbmp", + "name": "title", + "type": "text", + "system": false, + "required": true, + "min": 3, + "max": nil, + "pattern": "", + }, + }, + "indexes": []string{}, + }, + { + "name": "import1", + "fields": []map[string]any{ + { + "name": "active", + "type": "bool", + }, + }, + }, + }, + deleteMissing: true, + expectError: false, + expectCollectionsCount: totalSystemCollections + 2, + }, + { + name: "test with deleteMissing: false", + data: []map[string]any{ + { + // "id": "wsmn24bux7wo113", // test update with only name as identifier + "name": "demo1", + "fields": []map[string]any{ + { + "id": "_2hlxbmp", + "name": "title", + "type": "text", + "system": false, + "required": true, + "min": 3, + "max": nil, + "pattern": "", + }, + { + "id": "_2hlxbmp", + "name": "field_with_duplicate_id", + "type": "text", + "system": false, + "required": true, + "unique": false, + "min": 4, + "max": nil, + "pattern": "", + }, + { + "id": "abcd_import", + "name": "new_field", + "type": "text", + }, + }, + }, + { + "name": "new_import", + "fields": []map[string]any{ + { + "id": "abcd_import", + "name": "active", + "type": "bool", + }, + }, + }, + }, + deleteMissing: false, + expectError: false, + expectCollectionsCount: totalCollections + 1, + afterTestFunc: func(testApp *tests.TestApp, resultCollections []*core.Collection) { + expectedCollectionFields := map[string]int{ + core.CollectionNameAuthOrigins: 6, + "nologin": 10, + "demo1": 18, + "demo2": 5, + "demo3": 5, + "demo4": 16, + "demo5": 9, + "new_import": 2, + } + for name, expectedCount := range expectedCollectionFields { + collection, err := testApp.FindCollectionByNameOrId(name) + if err != nil { + t.Fatal(err) + } + + if totalFields := len(collection.Fields); totalFields != expectedCount { + t.Errorf("Expected %d %q fields, got %d", expectedCount, collection.Name, totalFields) + } + } + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + err := testApp.ImportCollections(s.data, s.deleteMissing) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + // check collections count + collections := []*core.Collection{} + if err := testApp.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + if len(collections) != s.expectCollectionsCount { + t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections)) + } + + if s.afterTestFunc != nil { + s.afterTestFunc(testApp, collections) + } + }) + } +} + +func TestImportCollectionsByMarshaledJSON(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + var regularCollections []*core.Collection + err := testApp.CollectionQuery().AndWhere(dbx.HashExp{"system": false}).All(®ularCollections) + if err != nil { + t.Fatal(err) + } + + var systemCollections []*core.Collection + err = testApp.CollectionQuery().AndWhere(dbx.HashExp{"system": true}).All(&systemCollections) + if err != nil { + t.Fatal(err) + } + + totalRegularCollections := len(regularCollections) + totalSystemCollections := len(systemCollections) + totalCollections := totalRegularCollections + totalSystemCollections + + scenarios := []struct { + name string + data string + deleteMissing bool + expectError bool + expectCollectionsCount int + afterTestFunc func(testApp *tests.TestApp, resultCollections []*core.Collection) + }{ + { + name: "invalid json array", + data: `{"test":123}`, + expectError: true, + expectCollectionsCount: totalCollections, + }, + { + name: "new + update + delete (system collections delete should be ignored)", + data: `[ + { + "id": "wsmn24bux7wo113", + "name": "demo", + "fields": [ + { + "id": "_2hlxbmp", + "name": "title", + "type": "text", + "system": false, + "required": true, + "min": 3, + "max": null, + "pattern": "" + } + ], + "indexes": [] + }, + { + "name": "import1", + "fields": [ + { + "name": "active", + "type": "bool" + } + ] + } + ]`, + deleteMissing: true, + expectError: false, + expectCollectionsCount: totalSystemCollections + 2, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + err := testApp.ImportCollectionsByMarshaledJSON([]byte(s.data), s.deleteMissing) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + // check collections count + collections := []*core.Collection{} + if err := testApp.CollectionQuery().All(&collections); err != nil { + t.Fatal(err) + } + if len(collections) != s.expectCollectionsCount { + t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections)) + } + + if s.afterTestFunc != nil { + s.afterTestFunc(testApp, collections) + } + }) + } +} + +func TestImportCollectionsUpdateRules(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + data map[string]any + deleteMissing bool + }{ + { + "extend existing by name (without deleteMissing)", + map[string]any{"name": "clients", "authToken": map[string]any{"duration": 100}, "fields": []map[string]any{{"name": "test", "type": "text"}}}, + false, + }, + { + "extend existing by id (without deleteMissing)", + map[string]any{"id": "v851q4r790rhknl", "authToken": map[string]any{"duration": 100}, "fields": []map[string]any{{"name": "test", "type": "text"}}}, + false, + }, + { + "extend with delete missing", + map[string]any{ + "id": "v851q4r790rhknl", + "authToken": map[string]any{"duration": 100}, + "fields": []map[string]any{{"name": "test", "type": "text"}}, + "passwordAuth": map[string]any{"identityFields": []string{"email"}}, + "indexes": []string{ + // min required system fields indexes + "CREATE UNIQUE INDEX `_v851q4r790rhknl_email_idx` ON `clients` (email) WHERE email != ''", + "CREATE UNIQUE INDEX `_v851q4r790rhknl_tokenKey_idx` ON `clients` (tokenKey)", + }, + }, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + beforeCollection, err := testApp.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + err = testApp.ImportCollections([]map[string]any{s.data}, s.deleteMissing) + if err != nil { + t.Fatal(err) + } + + afterCollection, err := testApp.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + if afterCollection.AuthToken.Duration != 100 { + t.Fatalf("Expected AuthToken duration to be %d, got %d", 100, afterCollection.AuthToken.Duration) + } + if beforeCollection.AuthToken.Secret != afterCollection.AuthToken.Secret { + t.Fatalf("Expected AuthToken secrets to remain the same, got\n%q\nVS\n%q", beforeCollection.AuthToken.Secret, afterCollection.AuthToken.Secret) + } + if beforeCollection.Name != afterCollection.Name { + t.Fatalf("Expected Name to remain the same, got\n%q\nVS\n%q", beforeCollection.Name, afterCollection.Name) + } + if beforeCollection.Id != afterCollection.Id { + t.Fatalf("Expected Id to remain the same, got\n%q\nVS\n%q", beforeCollection.Id, afterCollection.Id) + } + + if !s.deleteMissing { + totalExpectedFields := len(beforeCollection.Fields) + 1 + if v := len(afterCollection.Fields); v != totalExpectedFields { + t.Fatalf("Expected %d total fields, got %d", totalExpectedFields, v) + } + + if afterCollection.Fields.GetByName("test") == nil { + t.Fatalf("Missing new field %q", "test") + } + + // ensure that the old fields still exist + oldFields := beforeCollection.Fields.FieldNames() + for _, name := range oldFields { + if afterCollection.Fields.GetByName(name) == nil { + t.Fatalf("Missing expected old field %q", name) + } + } + } else { + totalExpectedFields := 1 + for _, f := range beforeCollection.Fields { + if f.GetSystem() { + totalExpectedFields++ + } + } + + if v := len(afterCollection.Fields); v != totalExpectedFields { + t.Fatalf("Expected %d total fields, got %d", totalExpectedFields, v) + } + + if afterCollection.Fields.GetByName("test") == nil { + t.Fatalf("Missing new field %q", "test") + } + + // ensure that the old system fields still exist + for _, f := range beforeCollection.Fields { + if f.GetSystem() && afterCollection.Fields.GetByName(f.GetName()) == nil { + t.Fatalf("Missing expected old field %q", f.GetName()) + } + } + } + }) + } +} + +func TestImportCollectionsCreateRules(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + err := testApp.ImportCollections([]map[string]any{ + {"name": "new_test", "type": "auth", "authToken": map[string]any{"duration": 123}, "fields": []map[string]any{{"name": "test", "type": "text"}}}, + }, false) + if err != nil { + t.Fatal(err) + } + + collection, err := testApp.FindCollectionByNameOrId("new_test") + if err != nil { + t.Fatal(err) + } + + raw, err := json.Marshal(collection) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + expectedParts := []string{ + `"name":"new_test"`, + `"fields":[`, + `"name":"id"`, + `"name":"email"`, + `"name":"tokenKey"`, + `"name":"password"`, + `"name":"test"`, + `"indexes":[`, + `CREATE UNIQUE INDEX`, + `"duration":123`, + } + + for _, part := range expectedParts { + if !strings.Contains(rawStr, part) { + t.Errorf("Missing %q in\n%s", part, rawStr) + } + } +} diff --git a/core/collection_model.go b/core/collection_model.go new file mode 100644 index 00000000..2abb2017 --- /dev/null +++ b/core/collection_model.go @@ -0,0 +1,949 @@ +package core + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pocketbase/pocketbase/tools/dbutils" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +var ( + _ Model = (*Collection)(nil) + _ DBExporter = (*Collection)(nil) + _ FilesManager = (*Collection)(nil) +) + +const ( + CollectionTypeBase = "base" + CollectionTypeAuth = "auth" + CollectionTypeView = "view" +) + +const systemHookIdCollection = "__pbCollectionSystemHook__" + +func (app *BaseApp) registerCollectionHooks() { + app.OnModelValidate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionValidate().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelCreate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionCreate().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelCreateExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionCreateExecute().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterCreateSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionAfterCreateSuccess().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterCreateError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelErrorEvent) error { + if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok { + return me.App.OnCollectionAfterCreateError().Trigger(ce, func(ce *CollectionErrorEvent) error { + syncModelErrorEventWithCollectionErrorEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelUpdate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionUpdate().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelUpdateExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionUpdateExecute().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionAfterUpdateSuccess().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterUpdateError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelErrorEvent) error { + if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok { + return me.App.OnCollectionAfterUpdateError().Trigger(ce, func(ce *CollectionErrorEvent) error { + syncModelErrorEventWithCollectionErrorEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelDelete().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionDelete().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelDeleteExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionDeleteExecute().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelEvent) error { + if ce, ok := newCollectionEventFromModelEvent(me); ok { + return me.App.OnCollectionAfterDeleteSuccess().Trigger(ce, func(ce *CollectionEvent) error { + syncModelEventWithCollectionEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterDeleteError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdCollection, + Func: func(me *ModelErrorEvent) error { + if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok { + return me.App.OnCollectionAfterDeleteError().Trigger(ce, func(ce *CollectionErrorEvent) error { + syncModelErrorEventWithCollectionErrorEvent(me, ce) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + // -------------------------------------------------------------- + + app.OnCollectionValidate().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionValidate, + Priority: 99, + }) + + app.OnCollectionCreate().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionSave, + Priority: -99, + }) + + app.OnCollectionUpdate().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionSave, + Priority: -99, + }) + + app.OnCollectionCreateExecute().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionSaveExecute, + // execute as latest as possible, aka. closer to the db action to minimize the transactions lock time + Priority: 99, + }) + + app.OnCollectionUpdateExecute().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionSaveExecute, + Priority: 99, // execute as latest as possible, aka. closer to the db action to minimize the transactions lock time + }) + + app.OnCollectionDeleteExecute().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdCollection, + Func: onCollectionDeleteExecute, + Priority: 99, // execute as latest as possible, aka. closer to the db action to minimize the transactions lock time + }) + + // reload cache on failure + // --- + onErrorReloadCachedCollections := func(ce *CollectionErrorEvent) error { + if err := ce.App.ReloadCachedCollections(); err != nil { + ce.App.Logger().Warn("Failed to reload collections cache", "error", err) + } + + return ce.Next() + } + app.OnCollectionAfterCreateError().Bind(&hook.Handler[*CollectionErrorEvent]{ + Id: systemHookIdCollection, + Func: onErrorReloadCachedCollections, + Priority: -99, + }) + app.OnCollectionAfterUpdateError().Bind(&hook.Handler[*CollectionErrorEvent]{ + Id: systemHookIdCollection, + Func: onErrorReloadCachedCollections, + Priority: -99, + }) + app.OnCollectionAfterDeleteError().Bind(&hook.Handler[*CollectionErrorEvent]{ + Id: systemHookIdCollection, + Func: onErrorReloadCachedCollections, + Priority: -99, + }) + // --- + + app.OnBootstrap().Bind(&hook.Handler[*BootstrapEvent]{ + Id: systemHookIdCollection, + Func: func(e *BootstrapEvent) error { + if err := e.Next(); err != nil { + return err + } + + if err := e.App.ReloadCachedCollections(); err != nil { + return fmt.Errorf("failed to load collections cache: %w", err) + } + + return nil + }, + Priority: 99, // execute as latest as possible + }) +} + +// @todo experiment eventually replacing the rules *string with a struct? +type baseCollection struct { + BaseModel + + disableIntegrityChecks bool + + ListRule *string `db:"listRule" json:"listRule" form:"listRule"` + ViewRule *string `db:"viewRule" json:"viewRule" form:"viewRule"` + CreateRule *string `db:"createRule" json:"createRule" form:"createRule"` + UpdateRule *string `db:"updateRule" json:"updateRule" form:"updateRule"` + DeleteRule *string `db:"deleteRule" json:"deleteRule" form:"deleteRule"` + + // RawOptions represents the raw serialized collection option loaded from the DB. + // NB! This field shouldn't be modified manually. It is automatically updated + // with the collection type specific option before save. + RawOptions types.JSONRaw `db:"options" json:"-" xml:"-" form:"-"` + + Name string `db:"name" json:"name" form:"name"` + 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"` +} + +// Collection defines the table, fields and various options related to a set of records. +type Collection struct { + baseCollection + collectionAuthOptions + collectionViewOptions +} + +// NewCollection initializes and returns a new Collection model with the specified type and name. +func NewCollection(typ, name string) *Collection { + switch typ { + case CollectionTypeAuth: + return NewAuthCollection(name) + case CollectionTypeView: + return NewViewCollection(name) + default: + return NewBaseCollection(name) + } +} + +// NewBaseCollection initializes and returns a new "base" Collection model. +func NewBaseCollection(name string) *Collection { + m := &Collection{} + m.Name = name + m.Type = CollectionTypeBase + m.initDefaultId() + m.initDefaultFields() + return m +} + +// NewViewCollection initializes and returns a new "view" Collection model. +func NewViewCollection(name string) *Collection { + m := &Collection{} + m.Name = name + m.Type = CollectionTypeView + m.initDefaultId() + m.initDefaultFields() + return m +} + +// NewAuthCollection initializes and returns a new "auth" Collection model. +func NewAuthCollection(name string) *Collection { + m := &Collection{} + m.Name = name + m.Type = CollectionTypeAuth + m.initDefaultId() + m.initDefaultFields() + m.setDefaultAuthOptions() + return m +} + +// TableName returns the Collection model SQL table name. +func (m *Collection) TableName() string { + return "_collections" +} + +// BaseFilesPath returns the storage dir path used by the collection. +func (m *Collection) BaseFilesPath() string { + return m.Id +} + +// IsBase checks if the current collection has "base" type. +func (m *Collection) IsBase() bool { + return m.Type == CollectionTypeBase +} + +// IsAuth checks if the current collection has "auth" type. +func (m *Collection) IsAuth() bool { + return m.Type == CollectionTypeAuth +} + +// IsView checks if the current collection has "view" type. +func (m *Collection) IsView() bool { + return m.Type == CollectionTypeView +} + +// IntegrityChecks toggles the current collection integrity checks (ex. checking references on delete). +func (m *Collection) IntegrityChecks(enable bool) { + m.disableIntegrityChecks = !enable +} + +// PostScan implements the [dbx.PostScanner] interface to auto unmarshal +// the raw serialized options into the concrete type specific fields. +func (m *Collection) PostScan() error { + if err := m.BaseModel.PostScan(); err != nil { + return err + } + + return m.unmarshalRawOptions() +} + +func (m *Collection) unmarshalRawOptions() error { + raw, err := m.RawOptions.MarshalJSON() + if err != nil { + return nil + } + + switch m.Type { + case CollectionTypeView: + return json.Unmarshal(raw, &m.collectionViewOptions) + case CollectionTypeAuth: + return json.Unmarshal(raw, &m.collectionAuthOptions) + } + + return nil +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +// +// For new/"blank" Collection models it replaces the model with a factory +// instance and then unmarshal the provided data one on top of it. +func (m *Collection) UnmarshalJSON(b []byte) error { + type alias *Collection + + // initialize the default fields + // (e.g. in case the collection was NOT created using the designated factories) + if m.IsNew() && m.Type == "" { + minimal := &struct { + Type string `json:"type"` + Name string `json:"name"` + }{} + if err := json.Unmarshal(b, minimal); err != nil { + return err + } + + blank := NewCollection(minimal.Type, minimal.Name) + *m = *blank + } + + return json.Unmarshal(b, alias(m)) +} + +// MarshalJSON implements the [json.Marshaler] interface. +// +// Note that non-type related fields are ignored from the serialization +// (ex. for "view" colections the "auth" fields are skipped). +func (m Collection) MarshalJSON() ([]byte, error) { + switch m.Type { + case CollectionTypeView: + return json.Marshal(struct { + baseCollection + collectionViewOptions + }{m.baseCollection, m.collectionViewOptions}) + case CollectionTypeAuth: + alias := struct { + baseCollection + collectionAuthOptions + }{m.baseCollection, m.collectionAuthOptions} + + // ensure that it is always returned as array + if alias.OAuth2.Providers == nil { + alias.OAuth2.Providers = []OAuth2ProviderConfig{} + } + + // hide secret keys from the serialization + alias.AuthToken.Secret = "" + alias.FileToken.Secret = "" + alias.PasswordResetToken.Secret = "" + alias.EmailChangeToken.Secret = "" + alias.VerificationToken.Secret = "" + for i := range alias.OAuth2.Providers { + alias.OAuth2.Providers[i].ClientSecret = "" + } + + return json.Marshal(alias) + default: + return json.Marshal(m.baseCollection) + } +} + +// String returns a string representation of the current collection. +func (m Collection) String() string { + raw, _ := json.Marshal(m) + return string(raw) +} + +// DBExport prepares and exports the current collection data for db persistence. +func (m *Collection) DBExport(app App) (map[string]any, error) { + result := map[string]any{ + "id": m.Id, + "type": m.Type, + "listRule": m.ListRule, + "viewRule": m.ViewRule, + "createRule": m.CreateRule, + "updateRule": m.UpdateRule, + "deleteRule": m.DeleteRule, + "name": m.Name, + "fields": m.Fields, + "indexes": m.Indexes, + "system": m.System, + "created": m.Created, + "updated": m.Updated, + "options": `{}`, + } + + switch m.Type { + case CollectionTypeView: + if raw, err := types.ParseJSONRaw(m.collectionViewOptions); err == nil { + result["options"] = raw + } else { + return nil, err + } + case CollectionTypeAuth: + if raw, err := types.ParseJSONRaw(m.collectionAuthOptions); err == nil { + result["options"] = raw + } else { + return nil, err + } + } + + return result, nil +} + +// GetIndex returns s single Collection index expression by its name. +func (m *Collection) GetIndex(name string) string { + for _, idx := range m.Indexes { + if strings.EqualFold(dbutils.ParseIndex(idx).IndexName, name) { + return idx + } + } + + return "" +} + +// AddIndex adds a new index into the current collection. +// +// If the collection has an existing index matching the new name it will be replaced with the new one. +func (m *Collection) AddIndex(name string, unique bool, columnsExpr string, optWhereExpr string) { + m.RemoveIndex(name) + + var idx strings.Builder + + idx.WriteString("CREATE ") + if unique { + idx.WriteString("UNIQUE ") + } + idx.WriteString("INDEX `") + idx.WriteString(name) + idx.WriteString("` ") + idx.WriteString("ON `") + idx.WriteString(m.Name) + idx.WriteString("` (") + idx.WriteString(columnsExpr) + idx.WriteString(")") + if optWhereExpr != "" { + idx.WriteString(" WHERE ") + idx.WriteString(optWhereExpr) + } + + m.Indexes = append(m.Indexes, idx.String()) +} + +// RemoveIndex removes a single index with the specified name from the current collection. +func (m *Collection) RemoveIndex(name string) { + for i, idx := range m.Indexes { + if strings.EqualFold(dbutils.ParseIndex(idx).IndexName, name) { + m.Indexes = append(m.Indexes[:i], m.Indexes[i+1:]...) + return + } + } +} + +// delete hook +// ------------------------------------------------------------------- + +func onCollectionDeleteExecute(e *CollectionEvent) error { + if e.Collection.System { + return fmt.Errorf("[%s] system collections cannot be deleted", e.Collection.Name) + } + + defer func() { + if err := e.App.ReloadCachedCollections(); err != nil { + e.App.Logger().Warn("Failed to reload collections cache", "error", err) + } + }() + + if !e.Collection.disableIntegrityChecks { + // ensure that there aren't any existing references. + // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + references, err := e.App.FindCollectionReferences(e.Collection, e.Collection.Id) + if err != nil { + return fmt.Errorf("[%s] failed to check collection references: %w", e.Collection.Name, err) + } + if total := len(references); total > 0 { + names := make([]string, 0, len(references)) + for ref := range references { + names = append(names, ref.Name) + } + return fmt.Errorf("[%s] failed to delete due to existing relation references: %s", e.Collection.Name, strings.Join(names, ", ")) + } + } + + originalApp := e.App + + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + // delete the related view or records table + if e.Collection.IsView() { + if err := txApp.DeleteView(e.Collection.Name); err != nil { + return err + } + } else { + if err := txApp.DeleteTable(e.Collection.Name); err != nil { + return err + } + } + + if !e.Collection.disableIntegrityChecks { + // trigger views resave to check for dependencies + if err := resaveViewsWithChangedFields(txApp, e.Collection.Id); err != nil { + return fmt.Errorf("[%s] failed to delete due to existing view dependency: %w", e.Collection.Name, err) + } + } + + // delete + return e.Next() + }) + + e.App = originalApp + + return txErr +} + +// save hook +// ------------------------------------------------------------------- + +func (c *Collection) initDefaultId() { + if c.Id == "" && c.Name != "" { + c.Id = "_pbc_" + crc32Checksum(c.Name) + } +} + +func (c *Collection) savePrepare() error { + if c.Type == "" { + c.Type = CollectionTypeBase + } + + if c.IsNew() { + c.initDefaultId() + c.Created = types.NowDateTime() + } + + c.Updated = types.NowDateTime() + + // recreate the fields list to ensure that all normalizations + // like default field id are applied + c.Fields = NewFieldsList(c.Fields...) + + c.initDefaultFields() + + if c.IsAuth() { + c.unsetMissingOAuth2MappedFields() + } + + return nil +} + +func onCollectionSave(e *CollectionEvent) error { + if err := e.Collection.savePrepare(); err != nil { + return err + } + + return e.Next() +} + +func onCollectionSaveExecute(e *CollectionEvent) error { + defer func() { + if err := e.App.ReloadCachedCollections(); err != nil { + e.App.Logger().Warn("Failed to reload collections cache", "error", err) + } + }() + + var oldCollection *Collection + if !e.Collection.IsNew() { + var err error + oldCollection, err = e.App.FindCachedCollectionByNameOrId(e.Collection.Id) + if err != nil { + return err + } + + // invalidate previously issued auth tokens on auth rule change + if oldCollection.AuthRule != e.Collection.AuthRule && + cast.ToString(oldCollection.AuthRule) != cast.ToString(e.Collection.AuthRule) { + e.Collection.AuthToken.Secret = security.RandomString(50) + } + } + + originalApp := e.App + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + isView := e.Collection.IsView() + + // ensures that the view collection shema is properly loaded + if isView { + query := e.Collection.ViewQuery + + // generate collection fields list from the query + viewFields, err := e.App.CreateViewFields(query) + if err != nil { + return err + } + + // delete old renamed view + if oldCollection != nil { + if err := e.App.DeleteView(oldCollection.Name); err != nil { + return err + } + } + + // wrap view query if necessary + query, err = normalizeViewQueryId(e.App, query) + if err != nil { + return fmt.Errorf("failed to normalize view query id: %w", err) + } + + // (re)create the view + if err := e.App.SaveView(e.Collection.Name, query); err != nil { + return err + } + + // updates newCollection.Fields based on the generated view table info and query + e.Collection.Fields = viewFields + } + + // save the Collection model + if err := e.Next(); err != nil { + return err + } + + // sync the changes with the related records table + if !isView { + if err := e.App.SyncRecordTableSchema(e.Collection, oldCollection); err != nil { + // note: don't wrap to allow propagating indexes validation.Errors + return err + } + } + + return nil + }) + e.App = originalApp + + if txErr != nil { + return txErr + } + + // trigger an update for all views with changed fields as a result of the current collection save + // (ignoring view errors to allow users to update the query from the UI) + resaveViewsWithChangedFields(e.App, e.Collection.Id) + + return nil +} + +func (m *Collection) initDefaultFields() { + switch m.Type { + case CollectionTypeBase: + m.initIdField() + case CollectionTypeAuth: + m.initIdField() + m.initPasswordField() + m.initTokenKeyField() + m.initEmailField() + m.initEmailVisibilityField() + m.initVerifiedField() + case CollectionTypeView: + // view fields are autogenerated + } +} + +func (m *Collection) initIdField() { + field, _ := m.Fields.GetByName(FieldNameId).(*TextField) + if field == nil { + // create default field + field = &TextField{ + Name: FieldNameId, + System: true, + PrimaryKey: true, + Required: true, + Min: 15, + Max: 15, + Pattern: `^[a-z0-9]+$`, + AutogeneratePattern: `[a-z0-9]{15}`, + } + + // prepend it + m.Fields = NewFieldsList(append([]Field{field}, m.Fields...)...) + } else { + // enforce system defaults + field.System = true + field.Required = true + field.PrimaryKey = true + field.Hidden = false + } +} + +func (m *Collection) initPasswordField() { + field, _ := m.Fields.GetByName(FieldNamePassword).(*PasswordField) + if field == nil { + // load default field + m.Fields.Add(&PasswordField{ + Name: FieldNamePassword, + System: true, + Hidden: true, + Required: true, + Min: 8, + }) + } else { + // enforce system defaults + field.System = true + field.Hidden = true + field.Required = true + } +} + +func (m *Collection) initTokenKeyField() { + field, _ := m.Fields.GetByName(FieldNameTokenKey).(*TextField) + if field == nil { + // load default field + m.Fields.Add(&TextField{ + Name: FieldNameTokenKey, + System: true, + Hidden: true, + Min: 30, + Max: 60, + Required: true, + AutogeneratePattern: `[a-zA-Z0-9]{50}`, + }) + } else { + // enforce system defaults + field.System = true + field.Hidden = true + field.Required = true + } + + // ensure that there is a unique index for the field + if !dbutils.HasSingleColumnUniqueIndex(FieldNameTokenKey, m.Indexes) { + m.Indexes = append(m.Indexes, fmt.Sprintf( + "CREATE UNIQUE INDEX `%s` ON `%s` (`%s`)", + m.fieldIndexName(FieldNameTokenKey), + m.Name, + FieldNameTokenKey, + )) + } +} + +func (m *Collection) initEmailField() { + field, _ := m.Fields.GetByName(FieldNameEmail).(*EmailField) + if field == nil { + // load default field + m.Fields.Add(&EmailField{ + Name: FieldNameEmail, + System: true, + Required: true, + }) + } else { + // enforce system defaults + field.System = true + field.Hidden = false // managed by the emailVisibility flag + } + + // ensure that there is a unique index for the email field + if !dbutils.HasSingleColumnUniqueIndex(FieldNameEmail, m.Indexes) { + m.Indexes = append(m.Indexes, fmt.Sprintf( + "CREATE UNIQUE INDEX `%s` ON `%s` (`%s`) WHERE `%s` != ''", + m.fieldIndexName(FieldNameEmail), + m.Name, + FieldNameEmail, + FieldNameEmail, + )) + } +} + +func (m *Collection) initEmailVisibilityField() { + field, _ := m.Fields.GetByName(FieldNameEmailVisibility).(*BoolField) + if field == nil { + // load default field + m.Fields.Add(&BoolField{ + Name: FieldNameEmailVisibility, + System: true, + }) + } else { + // enforce system defaults + field.System = true + } +} + +func (m *Collection) initVerifiedField() { + field, _ := m.Fields.GetByName(FieldNameVerified).(*BoolField) + if field == nil { + // load default field + m.Fields.Add(&BoolField{ + Name: FieldNameVerified, + System: true, + }) + } else { + // enforce system defaults + field.System = true + } +} + +func (m *Collection) fieldIndexName(field string) string { + name := "idx_" + field + "_" + + if m.Id != "" { + name += m.Id + } else if m.Name != "" { + name += m.Name + } else { + name += security.PseudorandomString(10) + } + + if len(name) > 64 { + return name[:64] + } + + return name +} diff --git a/core/collection_model_auth_options.go b/core/collection_model_auth_options.go new file mode 100644 index 00000000..75626f97 --- /dev/null +++ b/core/collection_model_auth_options.go @@ -0,0 +1,535 @@ +package core + +import ( + "strconv" + "strings" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +func (m *Collection) unsetMissingOAuth2MappedFields() { + if !m.IsAuth() { + return + } + + if m.OAuth2.MappedFields.Id != "" { + if m.Fields.GetByName(m.OAuth2.MappedFields.Id) == nil { + m.OAuth2.MappedFields.Id = "" + } + } + + if m.OAuth2.MappedFields.Name != "" { + if m.Fields.GetByName(m.OAuth2.MappedFields.Name) == nil { + m.OAuth2.MappedFields.Name = "" + } + } + + if m.OAuth2.MappedFields.Username != "" { + if m.Fields.GetByName(m.OAuth2.MappedFields.Username) == nil { + m.OAuth2.MappedFields.Username = "" + } + } + + if m.OAuth2.MappedFields.AvatarURL != "" { + if m.Fields.GetByName(m.OAuth2.MappedFields.AvatarURL) == nil { + m.OAuth2.MappedFields.AvatarURL = "" + } + } +} + +func (m *Collection) setDefaultAuthOptions() { + m.collectionAuthOptions = collectionAuthOptions{ + VerificationTemplate: defaultVerificationTemplate, + ResetPasswordTemplate: defaultResetPasswordTemplate, + ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate, + AuthRule: types.Pointer(""), + AuthAlert: AuthAlertConfig{ + Enabled: true, + EmailTemplate: defaultAuthAlertTemplate, + }, + PasswordAuth: PasswordAuthConfig{ + Enabled: true, + IdentityFields: []string{FieldNameEmail}, + }, + MFA: MFAConfig{ + Enabled: false, + Duration: 1800, // 30min + }, + OTP: OTPConfig{ + Enabled: false, + Duration: 180, // 3min + Length: 8, + EmailTemplate: defaultOTPTemplate, + }, + AuthToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 604800, // 7 days + }, + PasswordResetToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1800, // 30min + }, + EmailChangeToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1800, // 30min + }, + VerificationToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 259200, // 3days + }, + FileToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 180, // 3min + }, + } +} + +var _ optionsValidator = (*collectionAuthOptions)(nil) + +// collectionAuthOptions defines the options for the "auth" type collection. +type collectionAuthOptions struct { + // AuthRule could be used to specify additional record constraints + // applied after record authentication and right before returning the + // auth token response to the client. + // + // For example, to allow only verified users you could set it to + // "verified = true". + // + // Set it to empty string to allow any Auth collection record to authenticate. + // + // Set it to nil to disallow authentication altogether for the collection + // (that includes password, OAuth2, etc.). + AuthRule *string `form:"authRule" json:"authRule"` + + // ManageRule gives admin-like permissions to allow fully managing + // the auth record(s), eg. changing the password without requiring + // to enter the old one, directly updating the verified state and email, etc. + // + // This rule is executed in addition to the Create and Update API rules. + ManageRule *string `form:"manageRule" json:"manageRule"` + + // AuthAlert defines options related to the auth alerts on new device login. + AuthAlert AuthAlertConfig `form:"authAlert" json:"authAlert"` + + // OAuth2 specifies whether OAuth2 auth is enabled for the collection + // and which OAuth2 providers are allowed. + OAuth2 OAuth2Config `form:"oauth2" json:"oauth2"` + + PasswordAuth PasswordAuthConfig `form:"passwordAuth" json:"passwordAuth"` + + MFA MFAConfig `form:"mfa" json:"mfa"` + + OTP OTPConfig `form:"otp" json:"otp"` + + // Various token configurations + // --- + AuthToken TokenConfig `form:"authToken" json:"authToken"` + PasswordResetToken TokenConfig `form:"passwordResetToken" json:"passwordResetToken"` + EmailChangeToken TokenConfig `form:"emailChangeToken" json:"emailChangeToken"` + VerificationToken TokenConfig `form:"verificationToken" json:"verificationToken"` + FileToken TokenConfig `form:"fileToken" json:"fileToken"` + + // default email templates + // --- + VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"` + ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"` + ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"` +} + +func (o *collectionAuthOptions) validate(cv *collectionValidator) error { + err := validation.ValidateStruct(o, + validation.Field( + &o.AuthRule, + validation.By(cv.checkRule), + validation.By(cv.ensureNoSystemRuleChange(cv.original.AuthRule)), + ), + validation.Field( + &o.ManageRule, + validation.NilOrNotEmpty, + validation.By(cv.checkRule), + validation.By(cv.ensureNoSystemRuleChange(cv.original.ManageRule)), + ), + validation.Field(&o.AuthAlert), + validation.Field(&o.PasswordAuth), + validation.Field(&o.OAuth2), + validation.Field(&o.OTP), + validation.Field(&o.MFA), + validation.Field(&o.AuthToken), + validation.Field(&o.PasswordResetToken), + validation.Field(&o.EmailChangeToken), + validation.Field(&o.VerificationToken), + validation.Field(&o.FileToken), + validation.Field(&o.VerificationTemplate, validation.Required), + validation.Field(&o.ResetPasswordTemplate, validation.Required), + validation.Field(&o.ConfirmEmailChangeTemplate, validation.Required), + ) + if err != nil { + return err + } + + if o.MFA.Enabled { + // if MFA is enabled require at least 2 auth methods + // + // @todo maybe consider disabling the check because if custom auth methods + // are registered it may fail since we don't have mechanism to detect them at the moment + authsEnabled := 0 + if o.PasswordAuth.Enabled { + authsEnabled++ + } + if o.OAuth2.Enabled { + authsEnabled++ + } + if o.OTP.Enabled { + authsEnabled++ + } + if authsEnabled < 2 { + return validation.Errors{ + "mfa": validation.Errors{ + "enabled": validation.NewError("validation_mfa_not_enough_auths", "MFA requires at least 2 auth methods to be enabled."), + }, + } + } + + if o.MFA.Rule != "" { + mfaRuleValidators := []validation.RuleFunc{ + cv.checkRule, + cv.ensureNoSystemRuleChange(&cv.original.MFA.Rule), + } + + for _, validator := range mfaRuleValidators { + err := validator(&o.MFA.Rule) + if err != nil { + return validation.Errors{ + "mfa": validation.Errors{ + "rule": err, + }, + } + } + } + } + } + + // extra check to ensure that only unique identity fields are used + if o.PasswordAuth.Enabled { + err = validation.Validate(o.PasswordAuth.IdentityFields, validation.By(cv.checkFieldsForUniqueIndex)) + if err != nil { + return validation.Errors{ + "passwordAuth": validation.Errors{ + "identityFields": err, + }, + } + } + } + + return nil +} + +// ------------------------------------------------------------------- + +type EmailTemplate struct { + Subject string `form:"subject" json:"subject"` + Body string `form:"body" json:"body"` +} + +// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface. +func (t EmailTemplate) Validate() error { + return validation.ValidateStruct(&t, + validation.Field(&t.Subject, validation.Required), + validation.Field(&t.Body, validation.Required), + ) +} + +// Resolve replaces the placeholder parameters in the current email +// template and returns its components as ready-to-use strings. +func (t EmailTemplate) Resolve(placeholders map[string]any) (subject, body string) { + body = t.Body + subject = t.Subject + + for k, v := range placeholders { + vStr := cast.ToString(v) + + // replace subject placeholder params (if any) + subject = strings.ReplaceAll(subject, k, vStr) + + // replace body placeholder params (if any) + body = strings.ReplaceAll(body, k, vStr) + } + + return subject, body +} + +// ------------------------------------------------------------------- + +type AuthAlertConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + EmailTemplate EmailTemplate `form:"emailTemplate" json:"emailTemplate"` +} + +// Validate makes AuthAlertConfig validatable by implementing [validation.Validatable] interface. +func (c AuthAlertConfig) Validate() error { + return validation.ValidateStruct(&c, + // note: for now always run the email template validations even + // if not enabled since it could be used separately + validation.Field(&c.EmailTemplate), + ) +} + +// ------------------------------------------------------------------- + +type TokenConfig struct { + Secret string `form:"secret" json:"secret,omitempty"` + + // Duration specifies how long an issued token to be valid (in seconds) + Duration int64 `form:"duration" json:"duration"` +} + +// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface. +func (c TokenConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Secret, validation.Required, validation.Length(30, 255)), + validation.Field(&c.Duration, validation.Required, validation.Min(10), validation.Max(94670856)), // ~3y max + ) +} + +// DurationTime returns the current Duration as [time.Duration]. +func (c TokenConfig) DurationTime() time.Duration { + return time.Duration(c.Duration) * time.Second +} + +// ------------------------------------------------------------------- + +type OTPConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + + // Duration specifies how long the OTP to be valid (in seconds) + Duration int64 `form:"duration" json:"duration"` + + // Length specifies the auto generated password length. + Length int `form:"length" json:"length"` + + // EmailTemplate is the default OTP email template that will be send to the auth record. + // + // In addition to the system placeholders you can also make use of + // [core.EmailPlaceholderOTPId] and [core.EmailPlaceholderOTP]. + EmailTemplate EmailTemplate `form:"emailTemplate" json:"emailTemplate"` +} + +// Validate makes OTPConfig validatable by implementing [validation.Validatable] interface. +func (c OTPConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Duration, validation.When(c.Enabled, validation.Required, validation.Min(10), validation.Max(86400))), + validation.Field(&c.Length, validation.When(c.Enabled, validation.Required, validation.Min(4))), + // note: for now always run the email template validations even + // if not enabled since it could be used separately + validation.Field(&c.EmailTemplate), + ) +} + +// DurationTime returns the current Duration as [time.Duration]. +func (c OTPConfig) DurationTime() time.Duration { + return time.Duration(c.Duration) * time.Second +} + +// ------------------------------------------------------------------- + +type MFAConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + + // Duration specifies how long an issued MFA to be valid (in seconds) + Duration int64 `form:"duration" json:"duration"` + + // Rule is an optional field to restrict MFA only for the records that satisfy the rule. + // + // Leave it empty to enable MFA for everyone. + Rule string `form:"rule" json:"rule"` +} + +// Validate makes MFAConfig validatable by implementing [validation.Validatable] interface. +func (c MFAConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Duration, validation.When(c.Enabled, validation.Required, validation.Min(10), validation.Max(86400))), + ) +} + +// DurationTime returns the current Duration as [time.Duration]. +func (c MFAConfig) DurationTime() time.Duration { + return time.Duration(c.Duration) * time.Second +} + +// ------------------------------------------------------------------- + +type PasswordAuthConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + + // IdentityFields is a list of field names that could be used as + // identity during password authentication. + // + // Usually only fields that has single column UNIQUE index are accepted as values. + IdentityFields []string `form:"identityFields" json:"identityFields"` +} + +// Validate makes PasswordAuthConfig validatable by implementing [validation.Validatable] interface. +func (c PasswordAuthConfig) Validate() error { + // strip duplicated values + c.IdentityFields = list.ToUniqueStringSlice(c.IdentityFields) + + if !c.Enabled { + return nil // no need to validate + } + + return validation.ValidateStruct(&c, + validation.Field(&c.IdentityFields, validation.Required), + ) +} + +// ------------------------------------------------------------------- + +type OAuth2KnownFields struct { + Id string `form:"id" json:"id"` + Name string `form:"name" json:"name"` + Username string `form:"username" json:"username"` + AvatarURL string `form:"avatarURL" json:"avatarURL"` +} + +type OAuth2Config struct { + Providers []OAuth2ProviderConfig `form:"providers" json:"providers"` + + MappedFields OAuth2KnownFields `form:"mappedFields" json:"mappedFields"` + + Enabled bool `form:"enabled" json:"enabled"` +} + +// GetProviderConfig returns the first OAuth2ProviderConfig that matches the specified name. +// +// Returns false and zero config if no such provider is available in c.Providers. +func (c OAuth2Config) GetProviderConfig(name string) (config OAuth2ProviderConfig, exists bool) { + for _, p := range c.Providers { + if p.Name == name { + return p, true + } + } + return +} + +// Validate makes OAuth2Config validatable by implementing [validation.Validatable] interface. +func (c OAuth2Config) Validate() error { + if !c.Enabled { + return nil // no need to validate + } + + return validation.ValidateStruct(&c, + // note: don't require providers for now as they could be externally registered/removed + validation.Field(&c.Providers, validation.By(checkForDuplicatedProviders)), + ) +} + +func checkForDuplicatedProviders(value any) error { + configs, _ := value.([]OAuth2ProviderConfig) + + existing := map[string]struct{}{} + + for i, c := range configs { + if c.Name == "" { + continue // the name nonempty state is validated separately + } + if _, ok := existing[c.Name]; ok { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "name": validation.NewError("validation_duplicated_provider", "The provider "+c.Name+" is already registered."). + SetParams(map[string]any{"name": c.Name}), + }, + } + } + existing[c.Name] = struct{}{} + } + + return nil +} + +type OAuth2ProviderConfig struct { + // PKCE overwrites the default provider PKCE config option. + // + // This usually shouldn't be needed but some OAuth2 vendors, like the LinkedIn OIDC, + // may require manual adjustment due to returning error if extra parameters are added to the request + // (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"` +} + +// Validate makes OAuth2ProviderConfig validatable by implementing [validation.Validatable] interface. +func (c OAuth2ProviderConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Name, validation.Required, validation.By(checkProviderName)), + validation.Field(&c.ClientId, validation.Required), + validation.Field(&c.ClientSecret, validation.Required), + validation.Field(&c.AuthURL, is.URL), + validation.Field(&c.TokenURL, is.URL), + validation.Field(&c.UserInfoURL, is.URL), + ) +} + +func checkProviderName(value any) error { + name, _ := value.(string) + if name == "" { + return nil // nothing to check + } + + if _, err := auth.NewProviderByName(name); err != nil { + return validation.NewError("validation_missing_provider", "Invalid or missing provider with name "+name+"."). + SetParams(map[string]any{"name": name}) + } + + return nil +} + +// InitProvider returns a new auth.Provider instance loaded with the current OAuth2ProviderConfig options. +func (c OAuth2ProviderConfig) InitProvider() (auth.Provider, error) { + provider, err := auth.NewProviderByName(c.Name) + if err != nil { + return nil, err + } + + if c.ClientId != "" { + provider.SetClientId(c.ClientId) + } + + if c.ClientSecret != "" { + provider.SetClientSecret(c.ClientSecret) + } + + if c.AuthURL != "" { + provider.SetAuthURL(c.AuthURL) + } + + if c.UserInfoURL != "" { + provider.SetUserInfoURL(c.UserInfoURL) + } + + if c.TokenURL != "" { + provider.SetTokenURL(c.TokenURL) + } + + if c.DisplayName != "" { + provider.SetDisplayName(c.DisplayName) + } + + if c.PKCE != nil { + provider.SetPKCE(*c.PKCE) + } + + return provider, nil +} diff --git a/core/collection_model_auth_options_test.go b/core/collection_model_auth_options_test.go new file mode 100644 index 00000000..3a87869d --- /dev/null +++ b/core/collection_model_auth_options_test.go @@ -0,0 +1,1016 @@ +package core_test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestCollectionAuthOptionsValidate(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + collection func(app core.App) (*core.Collection, error) + expectedErrors []string + }{ + // authRule + { + name: "nil authRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.AuthRule = nil + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "empty authRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.AuthRule = types.Pointer("") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "invalid authRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.AuthRule = types.Pointer("missing != ''") + return c, nil + }, + expectedErrors: []string{"authRule"}, + }, + { + name: "valid authRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.AuthRule = types.Pointer("id != ''") + return c, nil + }, + expectedErrors: []string{}, + }, + + // manageRule + { + name: "nil manageRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ManageRule = nil + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "empty manageRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ManageRule = types.Pointer("") + return c, nil + }, + expectedErrors: []string{"manageRule"}, + }, + { + name: "invalid manageRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ManageRule = types.Pointer("missing != ''") + return c, nil + }, + expectedErrors: []string{"manageRule"}, + }, + { + name: "valid manageRule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ManageRule = types.Pointer("id != ''") + return c, nil + }, + expectedErrors: []string{}, + }, + + // passwordAuth + { + name: "trigger passwordAuth validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.PasswordAuth = core.PasswordAuthConfig{ + Enabled: true, + } + return c, nil + }, + expectedErrors: []string{"passwordAuth"}, + }, + { + name: "passwordAuth with non-unique identity fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add(&core.TextField{Name: "test"}) + c.PasswordAuth = core.PasswordAuthConfig{ + Enabled: true, + IdentityFields: []string{"email", "test"}, + } + return c, nil + }, + expectedErrors: []string{"passwordAuth"}, + }, + { + name: "passwordAuth with non-unique identity fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add(&core.TextField{Name: "test"}) + c.AddIndex("auth_test_idx", true, "test", "") + c.PasswordAuth = core.PasswordAuthConfig{ + Enabled: true, + IdentityFields: []string{"email", "test"}, + } + return c, nil + }, + expectedErrors: []string{}, + }, + + // oauth2 + { + name: "trigger oauth2 validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.OAuth2 = core.OAuth2Config{ + Enabled: true, + Providers: []core.OAuth2ProviderConfig{ + {Name: "missing"}, + }, + } + return c, nil + }, + expectedErrors: []string{"oauth2"}, + }, + + // otp + { + name: "trigger otp validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.OTP = core.OTPConfig{ + Enabled: true, + Duration: -10, + } + return c, nil + }, + expectedErrors: []string{"otp"}, + }, + + // mfa + { + name: "trigger mfa validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.MFA = core.MFAConfig{ + Enabled: true, + Duration: -10, + } + return c, nil + }, + expectedErrors: []string{"mfa"}, + }, + { + name: "mfa enabled with < 2 auth methods", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.MFA.Enabled = true + c.PasswordAuth.Enabled = true + c.OTP.Enabled = false + c.OAuth2.Enabled = false + return c, nil + }, + expectedErrors: []string{"mfa"}, + }, + { + name: "mfa enabled with >= 2 auth methods", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.MFA.Enabled = true + c.PasswordAuth.Enabled = true + c.OTP.Enabled = true + c.OAuth2.Enabled = false + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "mfa disabled with invalid rule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.PasswordAuth.Enabled = true + c.OTP.Enabled = true + c.MFA.Enabled = false + c.MFA.Rule = "invalid" + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "mfa enabled with invalid rule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.PasswordAuth.Enabled = true + c.OTP.Enabled = true + c.MFA.Enabled = true + c.MFA.Rule = "invalid" + return c, nil + }, + expectedErrors: []string{"mfa"}, + }, + { + name: "mfa enabled with valid rule", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.PasswordAuth.Enabled = true + c.OTP.Enabled = true + c.MFA.Enabled = true + c.MFA.Rule = "1=1" + return c, nil + }, + expectedErrors: []string{}, + }, + + // tokens + { + name: "trigger authToken validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.AuthToken.Secret = "" + return c, nil + }, + expectedErrors: []string{"authToken"}, + }, + { + name: "trigger passwordResetToken validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.PasswordResetToken.Secret = "" + return c, nil + }, + expectedErrors: []string{"passwordResetToken"}, + }, + { + name: "trigger emailChangeToken validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.EmailChangeToken.Secret = "" + return c, nil + }, + expectedErrors: []string{"emailChangeToken"}, + }, + { + name: "trigger verificationToken validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.VerificationToken.Secret = "" + return c, nil + }, + expectedErrors: []string{"verificationToken"}, + }, + { + name: "trigger fileToken validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.FileToken.Secret = "" + return c, nil + }, + expectedErrors: []string{"fileToken"}, + }, + + // templates + { + name: "trigger verificationTemplate validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.VerificationTemplate.Body = "" + return c, nil + }, + expectedErrors: []string{"verificationTemplate"}, + }, + { + name: "trigger resetPasswordTemplate validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ResetPasswordTemplate.Body = "" + return c, nil + }, + expectedErrors: []string{"resetPasswordTemplate"}, + }, + { + name: "trigger confirmEmailChangeTemplate validations", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.ConfirmEmailChangeTemplate.Body = "" + return c, nil + }, + expectedErrors: []string{"confirmEmailChangeTemplate"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := s.collection(app) + if err != nil { + t.Fatalf("Failed to retrieve test collection: %v", err) + } + + result := app.Validate(collection) + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestEmailTemplateValidate(t *testing.T) { + scenarios := []struct { + name string + template core.EmailTemplate + expectedErrors []string + }{ + { + "zero value", + core.EmailTemplate{}, + []string{"subject", "body"}, + }, + { + "non-empty data", + core.EmailTemplate{ + Subject: "a", + Body: "b", + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.template.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestEmailTemplateResolve(t *testing.T) { + template := core.EmailTemplate{ + Subject: "test_subject {PARAM3} {PARAM1}-{PARAM2} repeat-{PARAM1}", + Body: "test_body {PARAM3} {PARAM2}-{PARAM1} repeat-{PARAM2}", + } + + scenarios := []struct { + name string + placeholders map[string]any + template core.EmailTemplate + expectedSubject string + expectedBody string + }{ + { + "no placeholders", + nil, + template, + template.Subject, + template.Body, + }, + { + "no matching placeholders", + map[string]any{"{A}": "abc", "{B}": 456}, + template, + template.Subject, + template.Body, + }, + { + "at least one matching placeholder", + map[string]any{"{PARAM1}": "abc", "{PARAM2}": 456}, + template, + "test_subject {PARAM3} abc-456 repeat-abc", + "test_body {PARAM3} 456-abc repeat-456", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + subject, body := s.template.Resolve(s.placeholders) + + if subject != s.expectedSubject { + t.Fatalf("Expected subject\n%v\ngot\n%v", s.expectedSubject, subject) + } + + if body != s.expectedBody { + t.Fatalf("Expected body\n%v\ngot\n%v", s.expectedBody, body) + } + }) + } +} + +func TestTokenConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.TokenConfig + expectedErrors []string + }{ + { + "zero value", + core.TokenConfig{}, + []string{"secret", "duration"}, + }, + { + "invalid data", + core.TokenConfig{ + Secret: strings.Repeat("a", 29), + Duration: 9, + }, + []string{"secret", "duration"}, + }, + { + "valid data", + core.TokenConfig{ + Secret: strings.Repeat("a", 30), + Duration: 10, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestTokenConfigDurationTime(t *testing.T) { + scenarios := []struct { + config core.TokenConfig + expected time.Duration + }{ + {core.TokenConfig{}, 0 * time.Second}, + {core.TokenConfig{Duration: 1234}, 1234 * time.Second}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) { + result := s.config.DurationTime() + + if result != s.expected { + t.Fatalf("Expected duration %d, got %d", s.expected, result) + } + }) + } +} + +func TestAuthAlertConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.AuthAlertConfig + expectedErrors []string + }{ + { + "zero value (disabled)", + core.AuthAlertConfig{}, + []string{"emailTemplate"}, + }, + { + "zero value (enabled)", + core.AuthAlertConfig{Enabled: true}, + []string{"emailTemplate"}, + }, + { + "invalid template", + core.AuthAlertConfig{ + EmailTemplate: core.EmailTemplate{Body: "", Subject: "b"}, + }, + []string{"emailTemplate"}, + }, + { + "valid data", + core.AuthAlertConfig{ + EmailTemplate: core.EmailTemplate{Body: "a", Subject: "b"}, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestOTPConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.OTPConfig + expectedErrors []string + }{ + { + "zero value (disabled)", + core.OTPConfig{}, + []string{"emailTemplate"}, + }, + { + "zero value (enabled)", + core.OTPConfig{Enabled: true}, + []string{"duration", "length", "emailTemplate"}, + }, + { + "invalid length (< 3)", + core.OTPConfig{ + Enabled: true, + EmailTemplate: core.EmailTemplate{Body: "a", Subject: "b"}, + Duration: 100, + Length: 3, + }, + []string{"length"}, + }, + { + "invalid duration (< 10)", + core.OTPConfig{ + Enabled: true, + EmailTemplate: core.EmailTemplate{Body: "a", Subject: "b"}, + Duration: 9, + Length: 100, + }, + []string{"duration"}, + }, + { + "invalid duration (> 86400)", + core.OTPConfig{ + Enabled: true, + EmailTemplate: core.EmailTemplate{Body: "a", Subject: "b"}, + Duration: 86401, + Length: 100, + }, + []string{"duration"}, + }, + { + "invalid template (triggering EmailTemplate validations)", + core.OTPConfig{ + Enabled: true, + EmailTemplate: core.EmailTemplate{Body: "", Subject: "b"}, + Duration: 86400, + Length: 4, + }, + []string{"emailTemplate"}, + }, + { + "valid data", + core.OTPConfig{ + Enabled: true, + EmailTemplate: core.EmailTemplate{Body: "a", Subject: "b"}, + Duration: 86400, + Length: 4, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestOTPConfigDurationTime(t *testing.T) { + scenarios := []struct { + config core.OTPConfig + expected time.Duration + }{ + {core.OTPConfig{}, 0 * time.Second}, + {core.OTPConfig{Duration: 1234}, 1234 * time.Second}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) { + result := s.config.DurationTime() + + if result != s.expected { + t.Fatalf("Expected duration %d, got %d", s.expected, result) + } + }) + } +} + +func TestMFAConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.MFAConfig + expectedErrors []string + }{ + { + "zero value (disabled)", + core.MFAConfig{}, + []string{}, + }, + { + "zero value (enabled)", + core.MFAConfig{Enabled: true}, + []string{"duration"}, + }, + { + "invalid duration (< 10)", + core.MFAConfig{Enabled: true, Duration: 9}, + []string{"duration"}, + }, + { + "invalid duration (> 86400)", + core.MFAConfig{Enabled: true, Duration: 86401}, + []string{"duration"}, + }, + { + "valid data", + core.MFAConfig{Enabled: true, Duration: 86400}, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestMFAConfigDurationTime(t *testing.T) { + scenarios := []struct { + config core.MFAConfig + expected time.Duration + }{ + {core.MFAConfig{}, 0 * time.Second}, + {core.MFAConfig{Duration: 1234}, 1234 * time.Second}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) { + result := s.config.DurationTime() + + if result != s.expected { + t.Fatalf("Expected duration %d, got %d", s.expected, result) + } + }) + } +} + +func TestPasswordAuthConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.PasswordAuthConfig + expectedErrors []string + }{ + { + "zero value (disabled)", + core.PasswordAuthConfig{}, + []string{}, + }, + { + "zero value (enabled)", + core.PasswordAuthConfig{Enabled: true}, + []string{"identityFields"}, + }, + { + "empty values", + core.PasswordAuthConfig{Enabled: true, IdentityFields: []string{"", ""}}, + []string{"identityFields"}, + }, + { + "valid data", + core.PasswordAuthConfig{Enabled: true, IdentityFields: []string{"abc"}}, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestOAuth2ConfigGetProviderConfig(t *testing.T) { + scenarios := []struct { + name string + providerName string + config core.OAuth2Config + expectedExists bool + }{ + { + "zero value", + "gitlab", + core.OAuth2Config{}, + false, + }, + { + "empty config with valid provider", + "gitlab", + core.OAuth2Config{}, + false, + }, + { + "non-empty config with missing provider", + "gitlab", + core.OAuth2Config{Providers: []core.OAuth2ProviderConfig{{Name: "google"}, {Name: "github"}}}, + false, + }, + { + "config with existing provider", + "github", + core.OAuth2Config{Providers: []core.OAuth2ProviderConfig{{Name: "google"}, {Name: "github"}}}, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + config, exists := s.config.GetProviderConfig(s.providerName) + + if exists != s.expectedExists { + t.Fatalf("Expected exists %v, got %v", s.expectedExists, exists) + } + + if exists { + if config.Name != s.providerName { + t.Fatalf("Expected config with name %q, got %q", s.providerName, config.Name) + } + } else { + if config.Name != "" { + t.Fatalf("Expected empty config, got %v", config) + } + } + }) + } +} + +func TestOAuth2ConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.OAuth2Config + expectedErrors []string + }{ + { + "zero value (disabled)", + core.OAuth2Config{}, + []string{}, + }, + { + "zero value (enabled)", + core.OAuth2Config{Enabled: true}, + []string{}, + }, + { + "unknown provider", + core.OAuth2Config{Enabled: true, Providers: []core.OAuth2ProviderConfig{ + {Name: "missing", ClientId: "abc", ClientSecret: "456"}, + }}, + []string{"providers"}, + }, + { + "known provider with invalid data", + core.OAuth2Config{Enabled: true, Providers: []core.OAuth2ProviderConfig{ + {Name: "gitlab", ClientId: "abc", TokenURL: "!invalid!"}, + }}, + []string{"providers"}, + }, + { + "known provider with valid data", + core.OAuth2Config{Enabled: true, Providers: []core.OAuth2ProviderConfig{ + {Name: "gitlab", ClientId: "abc", ClientSecret: "456", TokenURL: "https://example.com"}, + }}, + []string{}, + }, + { + "known provider with valid data (duplicated)", + core.OAuth2Config{Enabled: true, Providers: []core.OAuth2ProviderConfig{ + {Name: "gitlab", ClientId: "abc1", ClientSecret: "1", TokenURL: "https://example1.com"}, + {Name: "google", ClientId: "abc2", ClientSecret: "2", TokenURL: "https://example2.com"}, + {Name: "gitlab", ClientId: "abc3", ClientSecret: "3", TokenURL: "https://example3.com"}, + }}, + []string{"providers"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestOAuth2ProviderConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.OAuth2ProviderConfig + expectedErrors []string + }{ + { + "zero value", + core.OAuth2ProviderConfig{}, + []string{"name", "clientId", "clientSecret"}, + }, + { + "minimum valid data", + core.OAuth2ProviderConfig{Name: "gitlab", ClientId: "abc", ClientSecret: "456"}, + []string{}, + }, + { + "non-existing provider", + core.OAuth2ProviderConfig{Name: "missing", ClientId: "abc", ClientSecret: "456"}, + []string{"name"}, + }, + { + "invalid urls", + core.OAuth2ProviderConfig{ + Name: "gitlab", + ClientId: "abc", + ClientSecret: "456", + AuthURL: "!invalid!", + TokenURL: "!invalid!", + UserInfoURL: "!invalid!", + }, + []string{"authURL", "tokenURL", "userInfoURL"}, + }, + { + "valid urls", + core.OAuth2ProviderConfig{ + Name: "gitlab", + ClientId: "abc", + ClientSecret: "456", + AuthURL: "https://example.com/a", + TokenURL: "https://example.com/b", + UserInfoURL: "https://example.com/c", + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestOAuth2ProviderConfigInitProvider(t *testing.T) { + scenarios := []struct { + name string + config core.OAuth2ProviderConfig + expectedConfig core.OAuth2ProviderConfig + expectedError bool + }{ + { + "empty config", + core.OAuth2ProviderConfig{}, + core.OAuth2ProviderConfig{}, + true, + }, + { + "missing provider", + core.OAuth2ProviderConfig{ + Name: "missing", + ClientId: "test_ClientId", + ClientSecret: "test_ClientSecret", + AuthURL: "test_AuthURL", + TokenURL: "test_TokenURL", + UserInfoURL: "test_UserInfoURL", + DisplayName: "test_DisplayName", + PKCE: types.Pointer(true), + }, + core.OAuth2ProviderConfig{ + Name: "missing", + ClientId: "test_ClientId", + ClientSecret: "test_ClientSecret", + AuthURL: "test_AuthURL", + TokenURL: "test_TokenURL", + UserInfoURL: "test_UserInfoURL", + DisplayName: "test_DisplayName", + PKCE: types.Pointer(true), + }, + true, + }, + { + "existing provider minimal", + core.OAuth2ProviderConfig{ + Name: "gitlab", + }, + core.OAuth2ProviderConfig{ + Name: "gitlab", + ClientId: "", + ClientSecret: "", + AuthURL: "https://gitlab.com/oauth/authorize", + TokenURL: "https://gitlab.com/oauth/token", + UserInfoURL: "https://gitlab.com/api/v4/user", + DisplayName: "GitLab", + PKCE: types.Pointer(true), + }, + false, + }, + { + "existing provider with all fields", + core.OAuth2ProviderConfig{ + Name: "gitlab", + ClientId: "test_ClientId", + ClientSecret: "test_ClientSecret", + AuthURL: "test_AuthURL", + TokenURL: "test_TokenURL", + UserInfoURL: "test_UserInfoURL", + DisplayName: "test_DisplayName", + PKCE: types.Pointer(true), + }, + core.OAuth2ProviderConfig{ + Name: "gitlab", + ClientId: "test_ClientId", + ClientSecret: "test_ClientSecret", + AuthURL: "test_AuthURL", + TokenURL: "test_TokenURL", + UserInfoURL: "test_UserInfoURL", + DisplayName: "test_DisplayName", + PKCE: types.Pointer(true), + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + provider, err := s.config.InitProvider() + + hasErr := err != nil + if hasErr != s.expectedError { + t.Fatalf("Expected hasErr %v, got %v", s.expectedError, hasErr) + } + + if hasErr { + if provider != nil { + t.Fatalf("Expected nil provider, got %v", provider) + } + return + } + + factory, ok := auth.Providers[s.expectedConfig.Name] + if !ok { + t.Fatalf("Missing factory for provider %q", s.expectedConfig.Name) + } + + expectedType := fmt.Sprintf("%T", factory()) + providerType := fmt.Sprintf("%T", provider) + if expectedType != providerType { + t.Fatalf("Expected provider instanceof %q, got %q", expectedType, providerType) + } + + if provider.ClientId() != s.expectedConfig.ClientId { + t.Fatalf("Expected ClientId %q, got %q", s.expectedConfig.ClientId, provider.ClientId()) + } + + if provider.ClientSecret() != s.expectedConfig.ClientSecret { + t.Fatalf("Expected ClientSecret %q, got %q", s.expectedConfig.ClientSecret, provider.ClientSecret()) + } + + if provider.AuthURL() != s.expectedConfig.AuthURL { + t.Fatalf("Expected AuthURL %q, got %q", s.expectedConfig.AuthURL, provider.AuthURL()) + } + + if provider.UserInfoURL() != s.expectedConfig.UserInfoURL { + t.Fatalf("Expected UserInfoURL %q, got %q", s.expectedConfig.UserInfoURL, provider.UserInfoURL()) + } + + if provider.TokenURL() != s.expectedConfig.TokenURL { + t.Fatalf("Expected TokenURL %q, got %q", s.expectedConfig.TokenURL, provider.TokenURL()) + } + + if provider.DisplayName() != s.expectedConfig.DisplayName { + t.Fatalf("Expected DisplayName %q, got %q", s.expectedConfig.DisplayName, provider.DisplayName()) + } + + if provider.PKCE() != *s.expectedConfig.PKCE { + t.Fatalf("Expected PKCE %v, got %v", *s.expectedConfig.PKCE, provider.PKCE()) + } + }) + } +} diff --git a/core/collection_model_auth_templates.go b/core/collection_model_auth_templates.go new file mode 100644 index 00000000..eede2714 --- /dev/null +++ b/core/collection_model_auth_templates.go @@ -0,0 +1,75 @@ +package core + +// Common settings placeholder tokens +const ( + EmailPlaceholderAppName string = "{APP_NAME}" + EmailPlaceholderAppURL string = "{APP_URL}" + EmailPlaceholderToken string = "{TOKEN}" + EmailPlaceholderOTP string = "{OTP}" + EmailPlaceholderOTPId string = "{OTP_ID}" +) + +var defaultVerificationTemplate = EmailTemplate{ + Subject: "Verify your " + EmailPlaceholderAppName + " email", + Body: `

Hello,

+

Thank you for joining us at ` + EmailPlaceholderAppName + `.

+

Click on the button below to verify your email address.

+

+ Verify +

+

+ Thanks,
+ ` + EmailPlaceholderAppName + ` team +

`, +} + +var defaultResetPasswordTemplate = EmailTemplate{ + Subject: "Reset your " + EmailPlaceholderAppName + " password", + Body: `

Hello,

+

Click on the button below to reset your password.

+

+ Reset password +

+

If you didn't ask to reset your password, you can ignore this email.

+

+ Thanks,
+ ` + EmailPlaceholderAppName + ` team +

`, +} + +var defaultConfirmEmailChangeTemplate = EmailTemplate{ + Subject: "Confirm your " + EmailPlaceholderAppName + " new email address", + Body: `

Hello,

+

Click on the button below to confirm your new email address.

+

+ Confirm new email +

+

If you didn't ask to change your email address, you can ignore this email.

+

+ Thanks,
+ ` + EmailPlaceholderAppName + ` team +

`, +} + +var defaultOTPTemplate = EmailTemplate{ + Subject: "OTP for " + EmailPlaceholderAppName, + Body: `

Hello,

+

Your one-time password is: ` + EmailPlaceholderOTP + `

+

If you didn't ask for the one-time password, you can ignore this email.

+

+ Thanks,
+ ` + EmailPlaceholderAppName + ` team +

`, +} + +var defaultAuthAlertTemplate = EmailTemplate{ + Subject: "Login from a new location", + Body: `

Hello,

+

We noticed a login to your ` + EmailPlaceholderAppName + ` account from a new location.

+

If this was you, you may disregard this email.

+

If this wasn't you, you should immediately change your ` + EmailPlaceholderAppName + ` account password to revoke access from all other locations.

+

+ Thanks,
+ ` + EmailPlaceholderAppName + ` team +

`, +} diff --git a/core/collection_model_base_options.go b/core/collection_model_base_options.go new file mode 100644 index 00000000..924312bf --- /dev/null +++ b/core/collection_model_base_options.go @@ -0,0 +1,11 @@ +package core + +var _ optionsValidator = (*collectionBaseOptions)(nil) + +// collectionBaseOptions defines the options for the "base" type collection. +type collectionBaseOptions struct { +} + +func (o *collectionBaseOptions) validate(cv *collectionValidator) error { + return nil +} diff --git a/core/collection_model_test.go b/core/collection_model_test.go new file mode 100644 index 00000000..879f583a --- /dev/null +++ b/core/collection_model_test.go @@ -0,0 +1,1335 @@ +package core_test + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/dbutils" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewCollection(t *testing.T) { + t.Parallel() + + scenarios := []struct { + typ string + name string + expected []string + }{ + { + "", + "", + []string{ + `"id":""`, + `"name":""`, + `"type":"base"`, + `"system":false`, + `"indexes":[]`, + `"fields":[{`, + `"name":"id"`, + `"type":"text"`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "unknown", + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"base"`, + `"system":false`, + `"indexes":[]`, + `"fields":[{`, + `"name":"id"`, + `"type":"text"`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "base", + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"base"`, + `"system":false`, + `"indexes":[]`, + `"fields":[{`, + `"name":"id"`, + `"type":"text"`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "view", + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"view"`, + `"indexes":[]`, + `"fields":[]`, + `"system":false`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "auth", + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"auth"`, + `"fields":[{`, + `"system":false`, + `"type":"text"`, + `"type":"email"`, + `"name":"id"`, + `"name":"email"`, + `"name":"password"`, + `"name":"tokenKey"`, + `"name":"emailVisibility"`, + `"name":"verified"`, + `idx_email`, + `idx_tokenKey`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + `"identityFields":["email"]`, + }, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.typ, s.name), func(t *testing.T) { + result := core.NewCollection(s.typ, s.name).String() + + for _, part := range s.expected { + if !strings.Contains(result, part) { + t.Fatalf("Missing part %q in\n%v", part, result) + } + } + }) + } +} + +func TestNewBaseCollection(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + expected []string + }{ + { + "", + []string{ + `"id":""`, + `"name":""`, + `"type":"base"`, + `"system":false`, + `"indexes":[]`, + `"fields":[{`, + `"name":"id"`, + `"type":"text"`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"base"`, + `"system":false`, + `"indexes":[]`, + `"fields":[{`, + `"name":"id"`, + `"type":"text"`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) { + result := core.NewBaseCollection(s.name).String() + + for _, part := range s.expected { + if !strings.Contains(result, part) { + t.Fatalf("Missing part %q in\n%v", part, result) + } + } + }) + } +} + +func TestNewViewCollection(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + expected []string + }{ + { + "", + []string{ + `"id":""`, + `"name":""`, + `"type":"view"`, + `"indexes":[]`, + `"fields":[]`, + `"system":false`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + { + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"view"`, + `"indexes":[]`, + `"fields":[]`, + `"system":false`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + }, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) { + result := core.NewViewCollection(s.name).String() + + for _, part := range s.expected { + if !strings.Contains(result, part) { + t.Fatalf("Missing part %q in\n%v", part, result) + } + } + }) + } +} + +func TestNewAuthCollection(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + expected []string + }{ + { + "", + []string{ + `"id":""`, + `"name":""`, + `"type":"auth"`, + `"fields":[{`, + `"system":false`, + `"type":"text"`, + `"type":"email"`, + `"name":"id"`, + `"name":"email"`, + `"name":"password"`, + `"name":"tokenKey"`, + `"name":"emailVisibility"`, + `"name":"verified"`, + `idx_email`, + `idx_tokenKey`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + `"identityFields":["email"]`, + }, + }, + { + "test", + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"auth"`, + `"fields":[{`, + `"system":false`, + `"type":"text"`, + `"type":"email"`, + `"name":"id"`, + `"name":"email"`, + `"name":"password"`, + `"name":"tokenKey"`, + `"name":"emailVisibility"`, + `"name":"verified"`, + `idx_email`, + `idx_tokenKey`, + `"listRule":null`, + `"viewRule":null`, + `"createRule":null`, + `"updateRule":null`, + `"deleteRule":null`, + `"identityFields":["email"]`, + }, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) { + result := core.NewAuthCollection(s.name).String() + + for _, part := range s.expected { + if !strings.Contains(result, part) { + t.Fatalf("Missing part %q in\n%v", part, result) + } + } + }) + } +} + +func TestCollectionTableName(t *testing.T) { + t.Parallel() + + c := core.NewBaseCollection("test") + if c.TableName() != "_collections" { + t.Fatalf("Expected tableName %q, got %q", "_collections", c.TableName()) + } +} + +func TestCollectionBaseFilesPath(t *testing.T) { + t.Parallel() + + c := core.Collection{} + + if c.BaseFilesPath() != "" { + t.Fatalf("Expected empty string, got %q", c.BaseFilesPath()) + } + + c.Id = "test" + + if c.BaseFilesPath() != c.Id { + t.Fatalf("Expected %q, got %q", c.Id, c.BaseFilesPath()) + } +} + +func TestCollectionIsBase(t *testing.T) { + t.Parallel() + + scenarios := []struct { + typ string + expected bool + }{ + {"unknown", false}, + {core.CollectionTypeBase, true}, + {core.CollectionTypeView, false}, + {core.CollectionTypeAuth, false}, + } + + for _, s := range scenarios { + t.Run(s.typ, func(t *testing.T) { + c := core.Collection{} + c.Type = s.typ + + if v := c.IsBase(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestCollectionIsView(t *testing.T) { + t.Parallel() + + scenarios := []struct { + typ string + expected bool + }{ + {"unknown", false}, + {core.CollectionTypeBase, false}, + {core.CollectionTypeView, true}, + {core.CollectionTypeAuth, false}, + } + + for _, s := range scenarios { + t.Run(s.typ, func(t *testing.T) { + c := core.Collection{} + c.Type = s.typ + + if v := c.IsView(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestCollectionIsAuth(t *testing.T) { + t.Parallel() + + scenarios := []struct { + typ string + expected bool + }{ + {"unknown", false}, + {core.CollectionTypeBase, false}, + {core.CollectionTypeView, false}, + {core.CollectionTypeAuth, true}, + } + + for _, s := range scenarios { + t.Run(s.typ, func(t *testing.T) { + c := core.Collection{} + c.Type = s.typ + + if v := c.IsAuth(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestCollectionPostScan(t *testing.T) { + t.Parallel() + + rawOptions := types.JSONRaw(`{ + "viewQuery":"select 1", + "authRule":"1=2" + }`) + + scenarios := []struct { + typ string + rawOptions types.JSONRaw + expected []string + }{ + { + core.CollectionTypeBase, + rawOptions, + []string{ + `lastSavedPK:"test"`, + `ViewQuery:""`, + `AuthRule:(*string)(nil)`, + }, + }, + { + core.CollectionTypeView, + rawOptions, + []string{ + `lastSavedPK:"test"`, + `ViewQuery:"select 1"`, + `AuthRule:(*string)(nil)`, + }, + }, + { + core.CollectionTypeAuth, + rawOptions, + []string{ + `lastSavedPK:"test"`, + `ViewQuery:""`, + `AuthRule:(*string)(0x`, + }, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) { + c := core.Collection{} + c.Id = "test" + c.Type = s.typ + c.RawOptions = s.rawOptions + + err := c.PostScan() + if err != nil { + t.Fatal(err) + } + + if c.IsNew() { + t.Fatal("Expected the collection to be marked as not new") + } + + rawModel := fmt.Sprintf("%#v", c) + + for _, part := range s.expected { + if !strings.Contains(rawModel, part) { + t.Fatalf("Missing part %q in\n%v", part, rawModel) + } + } + }) + } +} + +func TestCollectionUnmarshalJSON(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + raw string + collection func() *core.Collection + expectedCollection func() *core.Collection + }{ + { + "base new empty", + `{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`, + 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 + }, + }, + { + "view new empty", + `{"type":"view","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`, + 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 + }, + }, + { + "auth new empty", + `{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`, + 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 + }, + }, + { + "new but with set type (no default fields load)", + `{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`, + func() *core.Collection { + c := &core.Collection{} + 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 + }, + }, + { + "existing (no default fields load)", + `{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`, + func() *core.Collection { + 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 + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := s.collection() + + err := json.Unmarshal([]byte(s.raw), collection) + if err != nil { + t.Fatal(err) + } + + rawResult, err := json.Marshal(collection) + if err != nil { + t.Fatal(err) + } + rawResultStr := string(rawResult) + + rawExpected, err := json.Marshal(s.expectedCollection()) + if err != nil { + t.Fatal(err) + } + rawExpectedStr := string(rawExpected) + + if rawResultStr != rawExpectedStr { + t.Fatalf("Expected collection\n%s\ngot\n%s", rawExpectedStr, rawResultStr) + } + }) + } +} + +func TestCollectionSerialize(t *testing.T) { + scenarios := []struct { + name string + collection func() *core.Collection + expected []string + notExpected []string + }{ + { + "base", + func() *core.Collection { + c := core.NewCollection(core.CollectionTypeBase, "test") + c.ViewQuery = "1=1" + c.OAuth2.Providers = []core.OAuth2ProviderConfig{ + {Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"}, + {Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"}, + } + + return c + }, + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"base"`, + }, + []string{ + "verificationTemplate", + "manageRule", + "authRule", + "secret", + "oauth2", + "clientId", + "clientSecret", + "viewQuery", + }, + }, + { + "view", + func() *core.Collection { + c := core.NewCollection(core.CollectionTypeView, "test") + c.ViewQuery = "1=1" + c.OAuth2.Providers = []core.OAuth2ProviderConfig{ + {Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"}, + {Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"}, + } + + return c + }, + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"view"`, + `"viewQuery":"1=1"`, + }, + []string{ + "verificationTemplate", + "manageRule", + "authRule", + "secret", + "oauth2", + "clientId", + "clientSecret", + }, + }, + { + "auth", + func() *core.Collection { + c := core.NewCollection(core.CollectionTypeAuth, "test") + c.ViewQuery = "1=1" + c.OAuth2.Providers = []core.OAuth2ProviderConfig{ + {Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"}, + {Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"}, + } + + return c + }, + []string{ + `"id":"_pbc_3632233996"`, + `"name":"test"`, + `"type":"auth"`, + `"oauth2":{`, + `"providers":[{`, + `"clientId":"test_client_id1"`, + `"clientId":"test_client_id2"`, + }, + []string{ + "viewQuery", + "secret", + "clientSecret", + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := s.collection() + + raw, err := collection.MarshalJSON() + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != collection.String() { + t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", collection.String(), rawStr) + } + + for _, part := range s.expected { + if !strings.Contains(rawStr, part) { + t.Fatalf("Missing part %q in\n%v", part, rawStr) + } + } + + for _, part := range s.notExpected { + if strings.Contains(rawStr, part) { + t.Fatalf("Didn't expect part %q in\n%v", part, rawStr) + } + } + }) + } +} + +func TestCollectionDBExport(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + date, err := types.ParseDateTime("2024-07-01 01:02:03.456Z") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + typ string + expected string + }{ + { + "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"}`, + }, + { + 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"}`, + }, + { + 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"}`, + }, + { + 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"}`, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) { + c := core.Collection{} + c.Type = s.typ + c.Id = "test_id" + c.Name = "test_name" + c.System = true + c.ListRule = types.Pointer("1=1") + c.ViewRule = types.Pointer("1=2") + c.CreateRule = types.Pointer("1=3") + c.UpdateRule = types.Pointer("1=4") + c.DeleteRule = types.Pointer("1=5") + c.ManageRule = types.Pointer("1=6") + c.ViewRule = types.Pointer("1=7") + c.Created = date + 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.RawOptions = types.JSONRaw(`{"viewQuery": "select 2"}`) // should be ignored + + result, err := c.DBExport(app) + if err != nil { + t.Fatal(err) + } + + raw, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + if str := string(raw); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestCollectionIndexHelpers(t *testing.T) { + t.Parallel() + + checkIndexes := func(t *testing.T, indexes, expectedIndexes []string) { + if len(indexes) != len(expectedIndexes) { + t.Fatalf("Expected %d indexes, got %d\n%v", len(expectedIndexes), len(indexes), indexes) + } + + for _, idx := range expectedIndexes { + if !slices.Contains(indexes, idx) { + t.Fatalf("Missing index\n%v\nin\n%v", idx, indexes) + } + } + } + + c := core.NewBaseCollection("test") + checkIndexes(t, c.Indexes, nil) + + c.AddIndex("idx1", false, "colA,colB", "colA != 1") + c.AddIndex("idx2", true, "colA", "") + c.AddIndex("idx3", false, "colA", "") + c.AddIndex("idx3", false, "colB", "") // should overwrite the previous one + + idx1 := "CREATE INDEX `idx1` ON `test` (colA,colB) WHERE colA != 1" + idx2 := "CREATE UNIQUE INDEX `idx2` ON `test` (colA)" + idx3 := "CREATE INDEX `idx3` ON `test` (colB)" + + checkIndexes(t, c.Indexes, []string{idx1, idx2, idx3}) + + c.RemoveIndex("iDx2") // case-insensitive + c.RemoveIndex("missing") // noop + + checkIndexes(t, c.Indexes, []string{idx1, idx3}) + + expectedIndexes := map[string]string{ + "missing": "", + "idx1": idx1, + // the name is case insensitive + "iDX3": idx3, + } + for key, expectedIdx := range expectedIndexes { + idx := c.GetIndex(key) + if idx != expectedIdx { + t.Errorf("Expected index %q to be\n%v\ngot\n%v", key, expectedIdx, idx) + } + } +} + +// ------------------------------------------------------------------- + +func TestCollectionDelete(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + collection string + disableIntegrityChecks bool + expectError bool + }{ + { + name: "unsaved", + collection: "", + expectError: true, + }, + { + name: "system", + collection: core.CollectionNameSuperusers, + expectError: true, + }, + { + name: "base with references", + collection: "demo1", + expectError: true, + }, + { + name: "base with references with disabled integrity checks", + collection: "demo1", + disableIntegrityChecks: true, + expectError: false, + }, + { + name: "base without references", + collection: "demo1", + expectError: true, + }, + { + name: "view with reference", + collection: "view1", + expectError: true, + }, + { + name: "view with references with disabled integrity checks", + collection: "view1", + disableIntegrityChecks: true, + expectError: false, + }, + { + name: "view without references", + collection: "view2", + disableIntegrityChecks: true, + expectError: false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + var col *core.Collection + + if s.collection == "" { + col = core.NewBaseCollection("test") + } else { + var err error + col, err = app.FindCollectionByNameOrId(s.collection) + if err != nil { + t.Fatal(err) + } + } + + if s.disableIntegrityChecks { + col.IntegrityChecks(!s.disableIntegrityChecks) + } + + err := app.Delete(col) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + exists := app.HasTable(col.Name) + + if !col.IsNew() && exists != hasErr { + t.Fatalf("Expected HasTable %v, got %v", hasErr, exists) + } + + if !hasErr { + cache, _ := app.FindCachedCollectionByNameOrId(col.Id) + if cache != nil { + t.Fatal("Expected the collection to be removed from the cache.") + } + } + }) + } +} + +func TestCollectionSaveModel(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + collection func(app core.App) (*core.Collection, error) + expectError bool + expectColumns []string + }{ + // trigger validators + { + name: "create - trigger validators", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("!invalid") + c.Fields.Add(&core.TextField{Name: "example"}) + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: true, + }, + { + name: "update - trigger validators", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo5") + c.Name = "demo1" + c.Fields.Add(&core.TextField{Name: "example"}) + c.Fields.RemoveByName("file") + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: true, + }, + + // create + { + name: "create base collection", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new") + c.Type = "" // should be auto set to "base" + c.Fields.RemoveByName("id") // ensure that the default fields will be loaded + c.Fields.Add(&core.TextField{Name: "example"}) + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "example", + }, + }, + { + name: "create auth collection", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new") + c.Fields.RemoveByName("id") // ensure that the default fields will be loaded + c.Fields.RemoveByName("email") // ensure that the default fields will be loaded + c.Fields.Add(&core.TextField{Name: "example"}) + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "email", "tokenKey", "password", + "verified", "emailVisibility", "example", + }, + }, + { + name: "create view collection", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new") + c.Fields.Add(&core.TextField{Name: "ignored"}) // should be ignored + c.ViewQuery = "select 1 as id, 2 as example" + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "example", + }, + }, + + // update + { + name: "update base collection", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo5") + c.Fields.Add(&core.TextField{Name: "example"}) + c.Fields.RemoveByName("file") + c.Fields.GetByName("total").SetName("total_updated") + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "select_one", "select_many", "rel_one", "rel_many", + "total_updated", "created", "updated", "example", + }, + }, + { + name: "update auth collection", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("clients") + c.Fields.Add(&core.TextField{Name: "example"}) + c.Fields.RemoveByName("file") + c.Fields.GetByName("name").SetName("name_updated") + c.AddIndex("test_save_idx", false, "example", "") + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "email", "emailVisibility", "password", "tokenKey", + "verified", "username", "name_updated", "created", "updated", "example", + }, + }, + { + name: "update view collection", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("view2") + c.Fields.Add(&core.TextField{Name: "example"}) // should be ignored + c.ViewQuery = "select 1 as id, 2 as example" + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "example", + }, + }, + + // auth normalization + { + name: "unset missing oauth2 mapped fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new") + c.OAuth2.Enabled = true + // shouldn't fail + c.OAuth2.MappedFields = core.OAuth2KnownFields{ + Id: "missing", + Name: "missing", + Username: "missing", + AvatarURL: "missing", + } + return c, nil + }, + expectError: false, + expectColumns: []string{ + "id", "email", "emailVisibility", "password", "tokenKey", "verified", + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := s.collection(app) + if err != nil { + t.Fatalf("Failed to retrieve test collection: %v", err) + } + + saveErr := app.Save(collection) + + hasErr := saveErr != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", hasErr, s.expectError, saveErr) + } + + if hasErr { + return + } + + // the collection should always have an id after successful Save + if collection.Id == "" { + t.Fatal("Expected collection id to be set") + } + + // the timestamp fields should be non-empty after successful Save + if collection.Created.String() == "" { + t.Fatal("Expected collection created to be set") + } + if collection.Updated.String() == "" { + t.Fatal("Expected collection updated to be set") + } + + // check if the records table was synced + hasTable := app.HasTable(collection.Name) + if !hasTable { + t.Fatalf("Expected records table %s to be created", collection.Name) + } + + // check if the records table has the fields fields + columns, err := app.TableColumns(collection.Name) + if err != nil { + t.Fatal(err) + } + if len(columns) != len(s.expectColumns) { + t.Fatalf("Expected columns\n%v\ngot\n%v", s.expectColumns, columns) + } + for i, c := range columns { + if !slices.Contains(s.expectColumns, c) { + t.Fatalf("[%d] Didn't expect record column %q", i, c) + } + } + + // make sure that all collection indexes exists + indexes, err := app.TableIndexes(collection.Name) + if err != nil { + t.Fatal(err) + } + if len(indexes) != len(collection.Indexes) { + t.Fatalf("Expected %d indexes, got %d", len(collection.Indexes), len(indexes)) + } + for _, idx := range collection.Indexes { + parsed := dbutils.ParseIndex(idx) + if _, ok := indexes[parsed.IndexName]; !ok { + t.Fatalf("Missing index %q in\n%v", idx, indexes) + } + } + }) + } +} + +// indirect update of a field used in view should cause view(s) update +func TestCollectionSaveIndirectViewsUpdate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // update MaxSelect fields + { + relMany := collection.Fields.GetByName("rel_many").(*core.RelationField) + relMany.MaxSelect = 1 + + fileOne := collection.Fields.GetByName("file_one").(*core.FileField) + fileOne.MaxSelect = 10 + + if err := app.Save(collection); err != nil { + t.Fatal(err) + } + } + + // check view1 fields + { + view1, err := app.FindCollectionByNameOrId("view1") + if err != nil { + t.Fatal(err) + } + + relMany := view1.Fields.GetByName("rel_many").(*core.RelationField) + if relMany.MaxSelect != 1 { + t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect) + } + + fileOne := view1.Fields.GetByName("file_one").(*core.FileField) + if fileOne.MaxSelect != 10 { + t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOne.MaxSelect) + } + } + + // check view2 fields + { + view2, err := app.FindCollectionByNameOrId("view2") + if err != nil { + t.Fatal(err) + } + + relMany := view2.Fields.GetByName("rel_many").(*core.RelationField) + if relMany.MaxSelect != 1 { + t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect) + } + } +} + +func TestCollectionSaveViewWrapping(t *testing.T) { + t.Parallel() + + viewName := "test_wrapping" + + scenarios := []struct { + name string + query string + expected string + }{ + { + "no wrapping - text field", + "select text as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)", + }, + { + "no wrapping - id field", + "select text as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)", + }, + { + "no wrapping - relation field", + "select rel_one as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1)", + }, + { + "no wrapping - select field", + "select select_many as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1)", + }, + { + "no wrapping - email field", + "select email as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1)", + }, + { + "no wrapping - datetime field", + "select datetime as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1)", + }, + { + "no wrapping - url field", + "select url as id, bool from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1)", + }, + { + "wrapping - bool field", + "select bool as id, text as txt, url from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1))", + }, + { + "wrapping - bool field (different order)", + "select text as txt, url, bool as id from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,CAST(`id` as TEXT) `id` FROM (select text as txt, url, bool as id from demo1))", + }, + { + "wrapping - json field", + "select json as id, text, url from demo1", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`text`,`url` FROM (select json as id, text, url from demo1))", + }, + { + "wrapping - numeric id", + "select 1 as id", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select 1 as id))", + }, + { + "wrapping - expresion", + "select ('test') as id", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select ('test') as id))", + }, + { + "no wrapping - cast as text", + "select cast('test' as text) as id", + "CREATE VIEW `test_wrapping` AS SELECT * FROM (select cast('test' as text) as id)", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewViewCollection(viewName) + collection.ViewQuery = s.query + + err := app.Save(collection) + if err != nil { + t.Fatal(err) + } + + var sql string + + rowErr := app.DB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}"). + Bind(dbx.Params{"name": viewName}). + Row(&sql) + if rowErr != nil { + t.Fatalf("Failed to retrieve view sql: %v", rowErr) + } + + if sql != s.expected { + t.Fatalf("Expected query \n%v, \ngot \n%v", s.expected, sql) + } + }) + } +} diff --git a/core/collection_model_view_options.go b/core/collection_model_view_options.go new file mode 100644 index 00000000..ffe2859a --- /dev/null +++ b/core/collection_model_view_options.go @@ -0,0 +1,18 @@ +package core + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +var _ optionsValidator = (*collectionViewOptions)(nil) + +// collectionViewOptions defines the options for the "view" type collection. +type collectionViewOptions struct { + ViewQuery string `form:"viewQuery" json:"viewQuery"` +} + +func (o *collectionViewOptions) validate(cv *collectionValidator) error { + return validation.ValidateStruct(o, + validation.Field(&o.ViewQuery, validation.Required, validation.By(cv.checkViewQuery)), + ) +} diff --git a/core/collection_model_view_options_test.go b/core/collection_model_view_options_test.go new file mode 100644 index 00000000..c7da1eff --- /dev/null +++ b/core/collection_model_view_options_test.go @@ -0,0 +1,79 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestCollectionViewOptionsValidate(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + collection func(app core.App) (*core.Collection, error) + expectedErrors []string + }{ + { + name: "view with empty query", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new_auth") + return c, nil + }, + expectedErrors: []string{"fields", "viewQuery"}, + }, + { + name: "view with invalid query", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new_auth") + c.ViewQuery = "invalid" + return c, nil + }, + expectedErrors: []string{"fields", "viewQuery"}, + }, + { + name: "view with valid query but missing id", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new_auth") + c.ViewQuery = "select 1" + return c, nil + }, + expectedErrors: []string{"fields", "viewQuery"}, + }, + { + name: "view with valid query", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new_auth") + c.ViewQuery = "select demo1.id, text as example from demo1" + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "update view query ", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("view2") + c.ViewQuery = "select demo1.id, text as example from demo1" + return c, nil + }, + expectedErrors: []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := s.collection(app) + if err != nil { + t.Fatalf("Failed to retrieve test collection: %v", err) + } + + result := app.Validate(collection) + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} diff --git a/core/collection_query.go b/core/collection_query.go new file mode 100644 index 00000000..f5019899 --- /dev/null +++ b/core/collection_query.go @@ -0,0 +1,344 @@ +package core + +import ( + "bytes" + "database/sql" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/list" +) + +const StoreKeyCachedCollections = "pbAppCachedCollections" + +// CollectionQuery returns a new Collection select query. +func (app *BaseApp) CollectionQuery() *dbx.SelectQuery { + return app.ModelQuery(&Collection{}) +} + +// FindCollections finds all collections by the given type(s). +// +// If collectionTypes is not set, it returns all collections. +// +// Example: +// +// app.FindAllCollections() // all collections +// app.FindAllCollections("auth", "view") // only auth and view collections +func (app *BaseApp) FindAllCollections(collectionTypes ...string) ([]*Collection, error) { + collections := []*Collection{} + + q := app.CollectionQuery() + + types := list.NonzeroUniques(collectionTypes) + if len(types) > 0 { + q.AndWhere(dbx.In("type", list.ToInterfaceSlice(types)...)) + } + + err := q.OrderBy("created ASC").All(&collections) + if err != nil { + return nil, err + } + + return collections, nil +} + +// ReloadCachedCollections fetches all collections and caches them into the app store. +func (app *BaseApp) ReloadCachedCollections() error { + collections, err := app.FindAllCollections() + if err != nil { + return err + } + + app.Store().Set(StoreKeyCachedCollections, collections) + + return nil +} + +// FindCollectionByNameOrId finds a single collection by its name (case insensitive) or id. +func (app *BaseApp) FindCollectionByNameOrId(nameOrId string) (*Collection, error) { + m := &Collection{} + + err := app.CollectionQuery(). + AndWhere(dbx.NewExp("[[id]]={:id} OR LOWER([[name]])={:name}", dbx.Params{ + "id": nameOrId, + "name": strings.ToLower(nameOrId), + })). + Limit(1). + One(m) + if err != nil { + return nil, err + } + + return m, nil +} + +// FindCachedCollectionByNameOrId is similar to [App.FindCollectionByNameOrId] +// but retrieves the Collection from the app cache instead of making a db call. +// +// NB! This method is suitable for read-only Collection operations. +// +// Returns [sql.ErrNoRows] if no Collection is found for consistency +// with the [App.FindCollectionByNameOrId] method. +// +// If you plan making changes to the returned Collection model, +// use [App.FindCollectionByNameOrId] instead. +// +// Caveats: +// +// - The returned Collection should be used only for read-only operations. +// Avoid directly modifying the returned cached Collection as it will affect +// the global cached value even if you don't persist the changes in the database! +// - If you are updating a Collection in a transaction and then call this method before commit, +// it'll return the cached Collection state and not the one from the uncommitted transaction. +// - The cache is automatically updated on collections db change (create/update/delete). +// To manually reload the cache you can call [App.ReloadCachedCollections()] +func (app *BaseApp) FindCachedCollectionByNameOrId(nameOrId string) (*Collection, error) { + collections, _ := app.Store().Get(StoreKeyCachedCollections).([]*Collection) + if collections == nil { + // cache is not initialized yet (eg. run in a system migration) + return app.FindCollectionByNameOrId(nameOrId) + } + + for _, c := range collections { + if strings.EqualFold(c.Name, nameOrId) || c.Id == nameOrId { + return c, nil + } + } + + return nil, sql.ErrNoRows +} + +// IsCollectionNameUnique checks that there is no existing collection +// with the provided name (case insensitive!). +// +// Note: case insensitive check because the name is used also as +// table name for the records. +func (app *BaseApp) IsCollectionNameUnique(name string, excludeIds ...string) bool { + if name == "" { + return false + } + + query := app.CollectionQuery(). + Select("count(*)"). + AndWhere(dbx.NewExp("LOWER([[name]])={:name}", dbx.Params{"name": strings.ToLower(name)})). + Limit(1) + + if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { + query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) + } + + var exists bool + + return query.Row(&exists) == nil && !exists +} + +// FindCollectionReferences returns information for all relation fields +// referencing the provided collection. +// +// If the provided collection has reference to itself then it will be +// also included in the result. To exclude it, pass the collection id +// as the excludeIds argument. +func (app *BaseApp) FindCollectionReferences(collection *Collection, excludeIds ...string) (map[*Collection][]Field, error) { + collections := []*Collection{} + + query := app.CollectionQuery() + + if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { + query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) + } + + if err := query.All(&collections); err != nil { + return nil, err + } + + result := map[*Collection][]Field{} + + for _, c := range collections { + for _, rawField := range c.Fields { + f, ok := rawField.(*RelationField) + if ok && f.CollectionId == collection.Id { + result[c] = append(result[c], f) + } + } + } + + return result, nil +} + +// TruncateCollection deletes all records associated with the provided collection. +// +// The truncate operation is executed in a single transaction, +// aka. either everything is deleted or none. +// +// Note that this method will also trigger the records related +// cascade and file delete actions. +func (app *BaseApp) TruncateCollection(collection *Collection) error { + return app.RunInTransaction(func(txApp App) error { + records := make([]*Record, 0, 500) + + for { + err := txApp.RecordQuery(collection).Limit(500).All(&records) + if err != nil { + return err + } + + if len(records) == 0 { + return nil + } + + for _, record := range records { + err = txApp.Delete(record) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + } + + records = records[:0] + } + }) +} + +// ------------------------------------------------------------------- + +// saveViewCollection persists the provided View collection changes: +// - deletes the old related SQL view (if any) +// - creates a new SQL view with the latest newCollection.Options.Query +// - generates new feilds list based on newCollection.Options.Query +// - updates newCollection.Fields based on the generated view table info and query +// - saves the newCollection +// +// This method returns an error if newCollection is not a "view". +func saveViewCollection(app App, newCollection, oldCollection *Collection) error { + if !newCollection.IsView() { + return errors.New("not a view collection") + } + + return app.RunInTransaction(func(txApp App) error { + query := newCollection.ViewQuery + + // generate collection fields from the query + viewFields, err := txApp.CreateViewFields(query) + if err != nil { + return err + } + + // delete old renamed view + if oldCollection != nil { + if err := txApp.DeleteView(oldCollection.Name); err != nil { + return err + } + } + + // wrap view query if necessary + query, err = normalizeViewQueryId(txApp, query) + if err != nil { + return fmt.Errorf("failed to normalize view query id: %w", err) + } + + // (re)create the view + if err := txApp.SaveView(newCollection.Name, query); err != nil { + return err + } + + newCollection.Fields = viewFields + + return txApp.Save(newCollection) + }) +} + +// normalizeViewQueryId wraps (if necessary) the provided view query +// with a subselect to ensure that the id column is a text since +// currently we don't support non-string model ids +// (see https://github.com/pocketbase/pocketbase/issues/3110). +func normalizeViewQueryId(app App, query string) (string, error) { + query = strings.Trim(strings.TrimSpace(query), ";") + + info, err := getQueryTableInfo(app, query) + if err != nil { + return "", err + } + + for _, row := range info { + if strings.EqualFold(row.Name, FieldNameId) && strings.EqualFold(row.Type, "TEXT") { + return query, nil // no wrapping needed + } + } + + // raw parse to preserve the columns order + rawParsed := new(identifiersParser) + if err := rawParsed.parse(query); err != nil { + return "", err + } + + columns := make([]string, 0, len(rawParsed.columns)) + for _, col := range rawParsed.columns { + if col.alias == FieldNameId { + columns = append(columns, fmt.Sprintf("CAST([[%s]] as TEXT) [[%s]]", col.alias, col.alias)) + } else { + columns = append(columns, "[["+col.alias+"]]") + } + } + + query = fmt.Sprintf("SELECT %s FROM (%s)", strings.Join(columns, ","), query) + + return query, nil +} + +// resaveViewsWithChangedFields updates all view collections with changed fields. +func resaveViewsWithChangedFields(app App, excludeIds ...string) error { + collections, err := app.FindAllCollections(CollectionTypeView) + if err != nil { + return err + } + + return app.RunInTransaction(func(txApp App) error { + for _, collection := range collections { + if len(excludeIds) > 0 && list.ExistInSlice(collection.Id, excludeIds) { + continue + } + + // clone the existing fields for temp modifications + oldFields, err := collection.Fields.Clone() + if err != nil { + return err + } + + // generate new fields from the query + newFields, err := txApp.CreateViewFields(collection.ViewQuery) + if err != nil { + return err + } + + // unset the fields' ids to exclude from the comparison + for _, f := range oldFields { + f.SetId("") + } + for _, f := range newFields { + f.SetId("") + } + + encodedNewFields, err := json.Marshal(newFields) + if err != nil { + return err + } + + encodedOldFields, err := json.Marshal(oldFields) + if err != nil { + return err + } + + if bytes.EqualFold(encodedNewFields, encodedOldFields) { + continue // no changes + } + + if err := saveViewCollection(txApp, collection, nil); err != nil { + return err + } + } + + return nil + }) +} diff --git a/core/collection_query_test.go b/core/collection_query_test.go new file mode 100644 index 00000000..f612ff96 --- /dev/null +++ b/core/collection_query_test.go @@ -0,0 +1,381 @@ +package core_test + +import ( + "context" + "database/sql" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestCollectionQuery(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_collections}}.* FROM `_collections`" + + sql := app.CollectionQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestReloadCachedCollections(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + err := app.ReloadCachedCollections() + if err != nil { + t.Fatal(err) + } + + cached := app.Store().Get(core.StoreKeyCachedCollections) + + cachedCollections, ok := cached.([]*core.Collection) + if !ok { + t.Fatalf("Expected []*core.Collection, got %T", cached) + } + + collections, err := app.FindAllCollections() + if err != nil { + t.Fatalf("Failed to retrieve all collections: %v", err) + } + + if len(cachedCollections) != len(collections) { + t.Fatalf("Expected %d collections, got %d", len(collections), len(cachedCollections)) + } + + for _, c := range collections { + var exists bool + for _, cc := range cachedCollections { + if cc.Id == c.Id { + exists = true + break + } + } + if !exists { + t.Fatalf("The collections cache is missing collection %q", c.Name) + } + } +} + +func TestFindAllCollections(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionTypes []string + expectTotal int + }{ + {nil, 16}, + {[]string{}, 16}, + {[]string{""}, 16}, + {[]string{"unknown"}, 0}, + {[]string{"unknown", core.CollectionTypeAuth}, 4}, + {[]string{core.CollectionTypeAuth, core.CollectionTypeView}, 7}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, strings.Join(s.collectionTypes, "_")), func(t *testing.T) { + collections, err := app.FindAllCollections(s.collectionTypes...) + if err != nil { + t.Fatal(err) + } + + if len(collections) != s.expectTotal { + t.Fatalf("Expected %d collections, got %d", s.expectTotal, len(collections)) + } + + expectedTypes := list.NonzeroUniques(s.collectionTypes) + if len(expectedTypes) > 0 { + for _, c := range collections { + if !slices.Contains(expectedTypes, c.Type) { + t.Fatalf("Unexpected collection type %s\n%v", c.Type, c) + } + } + } + }) + } +} + +func TestFindCollectionByNameOrId(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + nameOrId string + expectError bool + }{ + {"", true}, + {"missing", true}, + {"wsmn24bux7wo113", false}, + {"demo1", false}, + {"DEMO1", false}, // case insensitive + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.nameOrId), func(t *testing.T) { + model, err := app.FindCollectionByNameOrId(s.nameOrId) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if model != nil && model.Id != s.nameOrId && !strings.EqualFold(model.Name, s.nameOrId) { + t.Fatalf("Expected model with identifier %s, got %v", s.nameOrId, model) + } + }) + } +} + +func TestFindCachedCollectionByNameOrId(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + totalQueries := 0 + app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + totalQueries++ + } + + run := func(withCache bool) { + scenarios := []struct { + nameOrId string + expectError bool + }{ + {"", true}, + {"missing", true}, + {"wsmn24bux7wo113", false}, + {"demo1", false}, + {"DEMO1", false}, // case insensitive + } + + var expectedTotalQueries int + + if withCache { + err := app.ReloadCachedCollections() + if err != nil { + t.Fatal(err) + } + } else { + app.Store().Reset(nil) + expectedTotalQueries = len(scenarios) + } + + totalQueries = 0 + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.nameOrId), func(t *testing.T) { + model, err := app.FindCachedCollectionByNameOrId(s.nameOrId) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if model != nil && model.Id != s.nameOrId && !strings.EqualFold(model.Name, s.nameOrId) { + t.Fatalf("Expected model with identifier %s, got %v", s.nameOrId, model) + } + }) + } + + if totalQueries != expectedTotalQueries { + t.Fatalf("Expected %d totalQueries, got %d", expectedTotalQueries, totalQueries) + } + } + + run(true) + + run(false) +} + +func TestIsCollectionNameUnique(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + excludeId string + expected bool + }{ + {"", "", false}, + {"demo1", "", false}, + {"Demo1", "", false}, + {"new", "", true}, + {"demo1", "wsmn24bux7wo113", true}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.name), func(t *testing.T) { + result := app.IsCollectionNameUnique(s.name, s.excludeId) + if result != s.expected { + t.Errorf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestFindCollectionReferences(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + result, err := app.FindCollectionReferences( + collection, + collection.Id, + // test whether "nonempty" exclude ids condition will be skipped + "", + "", + ) + if err != nil { + t.Fatal(err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 collection, got %d: %v", len(result), result) + } + + expectedFields := []string{ + "rel_one_no_cascade", + "rel_one_no_cascade_required", + "rel_one_cascade", + "rel_one_unique", + "rel_many_no_cascade", + "rel_many_no_cascade_required", + "rel_many_cascade", + "rel_many_unique", + } + + for col, fields := range result { + if col.Name != "demo4" { + t.Fatalf("Expected collection demo4, got %s", col.Name) + } + if len(fields) != len(expectedFields) { + t.Fatalf("Expected fields %v, got %v", expectedFields, fields) + } + for i, f := range fields { + if !slices.Contains(expectedFields, f.GetName()) { + t.Fatalf("[%d] Didn't expect field %v", i, f) + } + } + } +} + +func TestFindCollectionTruncate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + countFiles := func(collectionId string) (int, error) { + entries, err := os.ReadDir(filepath.Join(app.DataDir(), "storage", collectionId)) + return len(entries), err + } + + t.Run("truncate failure", func(t *testing.T) { + demo3, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + originalTotalRecords, err := app.CountRecords(demo3) + if err != nil { + t.Fatal(err) + } + + originalTotalFiles, err := countFiles(demo3.Id) + if err != nil { + t.Fatal(err) + } + + err = app.TruncateCollection(demo3) + if err == nil { + t.Fatalf("Expected truncate to fail due to cascade delete failed required constraint") + } + + // short delay to ensure that the file delete goroutine has been executed + time.Sleep(100 * time.Millisecond) + + totalRecords, err := app.CountRecords(demo3) + if err != nil { + t.Fatal(err) + } + + if totalRecords != originalTotalRecords { + t.Fatalf("Expected %d records, got %d", originalTotalRecords, totalRecords) + } + + totalFiles, err := countFiles(demo3.Id) + if err != nil { + t.Fatal(err) + } + if totalFiles != originalTotalFiles { + t.Fatalf("Expected %d files, got %d", originalTotalFiles, totalFiles) + } + }) + + t.Run("truncate success", func(t *testing.T) { + demo5, err := app.FindCollectionByNameOrId("demo5") + if err != nil { + t.Fatal(err) + } + + err = app.TruncateCollection(demo5) + if err != nil { + t.Fatal(err) + } + + // short delay to ensure that the file delete goroutine has been executed + time.Sleep(100 * time.Millisecond) + + total, err := app.CountRecords(demo5) + if err != nil { + t.Fatal(err) + } + if total != 0 { + t.Fatalf("Expected all records to be deleted, got %v", total) + } + + totalFiles, err := countFiles(demo5.Id) + if err != nil { + t.Fatal(err) + } + + if totalFiles != 0 { + t.Fatalf("Expected truncated record files to be deleted, got %d", totalFiles) + } + + // try to truncate again (shouldn't return an error) + err = app.TruncateCollection(demo5) + if err != nil { + t.Fatal(err) + } + }) +} diff --git a/core/collection_record_table_sync.go b/core/collection_record_table_sync.go new file mode 100644 index 00000000..ef6f42df --- /dev/null +++ b/core/collection_record_table_sync.go @@ -0,0 +1,346 @@ +package core + +import ( + "fmt" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/dbutils" + "github.com/pocketbase/pocketbase/tools/security" +) + +// SyncRecordTableSchema compares the two provided collections +// and applies the necessary related record table changes. +// +// If oldCollection is null, then only newCollection is used to create the record table. +// +// This method is automatically invoked as part of a collection create/update/delete operation. +func (app *BaseApp) SyncRecordTableSchema(newCollection *Collection, oldCollection *Collection) error { + if newCollection.IsView() { + return nil // nothing to sync since views don't have records table + } + + txErr := app.RunInTransaction(func(txApp App) error { + // create + // ----------------------------------------------------------- + if oldCollection == nil || !app.HasTable(oldCollection.Name) { + tableName := newCollection.Name + + fields := newCollection.Fields + + cols := make(map[string]string, len(fields)) + + // add fields definition + for _, field := range fields { + cols[field.GetName()] = field.ColumnType(app) + } + + // create table + if _, err := txApp.DB().CreateTable(tableName, cols).Execute(); err != nil { + return err + } + + return createCollectionIndexes(txApp, newCollection) + } + + // update + // ----------------------------------------------------------- + oldTableName := oldCollection.Name + newTableName := newCollection.Name + oldFields := oldCollection.Fields + newFields := newCollection.Fields + + needTableRename := !strings.EqualFold(oldTableName, newTableName) + + var needIndexesUpdate bool + if needTableRename || + oldFields.String() != newFields.String() || + oldCollection.Indexes.String() != newCollection.Indexes.String() { + needIndexesUpdate = true + } + + if needIndexesUpdate { + // drop old indexes (if any) + if err := dropCollectionIndexes(txApp, oldCollection); err != nil { + return err + } + } + + // check for renamed table + if needTableRename { + _, err := txApp.DB().RenameTable("{{"+oldTableName+"}}", "{{"+newTableName+"}}").Execute() + if err != nil { + return err + } + } + + // check for deleted columns + for _, oldField := range oldFields { + if f := newFields.GetById(oldField.GetId()); f != nil { + continue // exist + } + + _, err := txApp.DB().DropColumn(newTableName, oldField.GetName()).Execute() + if err != nil { + return fmt.Errorf("failed to drop column %s - %w", oldField.GetName(), err) + } + } + + // check for new or renamed columns + toRename := map[string]string{} + for _, field := range newFields { + oldField := oldFields.GetById(field.GetId()) + // Note: + // We are using a temporary column name when adding or renaming columns + // to ensure that there are no name collisions in case there is + // names switch/reuse of existing columns (eg. name, title -> title, name). + // This way we are always doing 1 more rename operation but it provides better less ambiguous experience. + + if oldField == nil { + tempName := field.GetName() + security.PseudorandomString(5) + toRename[tempName] = field.GetName() + + // add + _, err := txApp.DB().AddColumn(newTableName, tempName, field.ColumnType(txApp)).Execute() + if err != nil { + return fmt.Errorf("failed to add column %s - %w", field.GetName(), err) + } + } else if oldField.GetName() != field.GetName() { + tempName := field.GetName() + security.PseudorandomString(5) + toRename[tempName] = field.GetName() + + // rename + _, err := txApp.DB().RenameColumn(newTableName, oldField.GetName(), tempName).Execute() + if err != nil { + return fmt.Errorf("failed to rename column %s - %w", oldField.GetName(), err) + } + } + } + + // set the actual columns name + for tempName, actualName := range toRename { + _, err := txApp.DB().RenameColumn(newTableName, tempName, actualName).Execute() + if err != nil { + return err + } + } + + if err := normalizeSingleVsMultipleFieldChanges(txApp, newCollection, oldCollection); err != nil { + return err + } + + if needIndexesUpdate { + return createCollectionIndexes(txApp, newCollection) + } + + return nil + }) + if txErr != nil { + return txErr + } + + // run optimize per the SQLite recommendations + // (https://www.sqlite.org/pragma.html#pragma_optimize) + _, optimizeErr := app.DB().NewQuery("PRAGMA optimize").Execute() + if optimizeErr != nil { + return fmt.Errorf("failed to run optimize after the fields changes: %w", optimizeErr) + } + + return nil +} + +func normalizeSingleVsMultipleFieldChanges(app App, newCollection *Collection, oldCollection *Collection) error { + if newCollection.IsView() || oldCollection == nil { + return nil // view or not an update + } + + 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 + var isOldMultiple bool + if oldField := oldCollection.Fields.GetById(newField.GetId()); oldField != nil { + if mv, ok := oldField.(MultiValuer); ok { + isOldMultiple = mv.IsMultiple() + } + } + + var isNewMultiple bool + if mv, ok := newField.(MultiValuer); ok { + isNewMultiple = mv.IsMultiple() + } + + if isOldMultiple == isNewMultiple { + 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 + // ------------------------------------------------------- + + originalName := newField.GetName() + tempName := "_" + newField.GetName() + security.PseudorandomString(5) + + _, err := txApp.DB().AddColumn(newCollection.Name, tempName, newField.ColumnType(txApp)).Execute() + if err != nil { + return err + } + + var copyQuery *dbx.Query + + if !isOldMultiple && isNewMultiple { + // single -> multiple (convert to array) + copyQuery = txApp.DB().NewQuery(fmt.Sprintf( + `UPDATE {{%s}} set [[%s]] = ( + CASE + WHEN COALESCE([[%s]], '') = '' + THEN '[]' + ELSE ( + CASE + WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' + THEN [[%s]] + ELSE json_array([[%s]]) + END + ) + END + )`, + newCollection.Name, + tempName, + originalName, + originalName, + originalName, + originalName, + originalName, + )) + } else { + // multiple -> single (keep only the last element) + // + // note: for file fields the actual file objects are not + // deleted allowing additional custom handling via migration + copyQuery = txApp.DB().NewQuery(fmt.Sprintf( + `UPDATE {{%s}} set [[%s]] = ( + CASE + WHEN COALESCE([[%s]], '[]') = '[]' + THEN '' + ELSE ( + CASE + WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' + THEN COALESCE(json_extract([[%s]], '$[#-1]'), '') + ELSE [[%s]] + END + ) + END + )`, + newCollection.Name, + tempName, + originalName, + originalName, + originalName, + originalName, + originalName, + )) + } + + // copy the normalized values + if _, err := copyQuery.Execute(); err != nil { + return err + } + + // drop the original column + if _, err := txApp.DB().DropColumn(newCollection.Name, originalName).Execute(); 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 + } + } + + // revert the pragma and reload the schema + _, revertErr := txApp.DB().NewQuery("PRAGMA writable_schema = RESET").Execute() + + return revertErr + }) +} + +func dropCollectionIndexes(app App, collection *Collection) error { + if collection.IsView() { + return nil // views don't have indexes + } + + return app.RunInTransaction(func(txApp App) error { + for _, raw := range collection.Indexes { + parsed := dbutils.ParseIndex(raw) + + if !parsed.IsValid() { + continue + } + + if _, err := app.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil { + return err + } + } + + return nil + }) +} + +func createCollectionIndexes(app App, collection *Collection) error { + if collection.IsView() { + return nil // views don't have indexes + } + + return app.RunInTransaction(func(txApp App) error { + // upsert new indexes + // + // note: we are returning validation errors because the indexes cannot be + // easily validated in a form, aka. before persisting the related + // collection record table changes + errs := validation.Errors{} + for i, idx := range collection.Indexes { + parsed := dbutils.ParseIndex(idx) + + // ensure that the index is always for the current collection + parsed.TableName = collection.Name + + if !parsed.IsValid() { + errs[strconv.Itoa(i)] = validation.NewError( + "validation_invalid_index_expression", + "Invalid CREATE INDEX expression.", + ) + continue + } + + if _, err := txApp.DB().NewQuery(parsed.Build()).Execute(); err != nil { + errs[strconv.Itoa(i)] = validation.NewError( + "validation_invalid_index_expression", + fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()), + ).SetParams(map[string]any{ + "indexName": parsed.IndexName, + }) + continue + } + } + + if len(errs) > 0 { + return validation.Errors{"indexes": errs} + } + + return nil + }) +} diff --git a/daos/record_table_sync_test.go b/core/collection_record_table_sync_test.go similarity index 51% rename from daos/record_table_sync_test.go rename to core/collection_record_table_sync_test.go index e092f167..bc72da86 100644 --- a/daos/record_table_sync_test.go +++ b/core/collection_record_table_sync_test.go @@ -1,4 +1,4 @@ -package daos_test +package core_test import ( "bytes" @@ -6,8 +6,7 @@ import ( "testing" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/types" @@ -19,73 +18,55 @@ func TestSyncRecordTableSchema(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - oldCollection, err := app.Dao().FindCollectionByNameOrId("demo2") + oldCollection, err := app.FindCollectionByNameOrId("demo2") if err != nil { t.Fatal(err) } - updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo2") + updatedCollection, err := app.FindCollectionByNameOrId("demo2") if err != nil { t.Fatal(err) } updatedCollection.Name = "demo_renamed" - updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("active").Id) - updatedCollection.Schema.AddField( - &schema.SchemaField{ - Name: "new_field", - Type: schema.FieldTypeEmail, - }, - ) - updatedCollection.Schema.AddField( - &schema.SchemaField{ - Id: updatedCollection.Schema.GetFieldByName("title").Id, - Name: "title_renamed", - Type: schema.FieldTypeEmail, - }, - ) - updatedCollection.Indexes = types.JsonArray[string]{"create index idx_title_renamed on anything (title_renamed)"} + updatedCollection.Fields.RemoveByName("active") + updatedCollection.Fields.Add(&core.EmailField{ + Name: "new_field", + }) + updatedCollection.Fields.Add(&core.EmailField{ + Id: updatedCollection.Fields.GetByName("title").GetId(), + Name: "title_renamed", + }) + updatedCollection.Indexes = types.JSONArray[string]{"create index idx_title_renamed on anything (title_renamed)"} + + baseCol := core.NewBaseCollection("new_base") + baseCol.Fields.Add(&core.TextField{Name: "test"}) + + authCol := core.NewAuthCollection("new_auth") + authCol.Fields.Add(&core.TextField{Name: "test"}) + authCol.AddIndex("idx_auth_test", false, "email, id", "") scenarios := []struct { name string - newCollection *models.Collection - oldCollection *models.Collection + newCollection *core.Collection + oldCollection *core.Collection expectedColumns []string expectedIndexesCount int }{ { "new base collection", - &models.Collection{ - Name: "new_table", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }, - ), - }, + baseCol, nil, - []string{"id", "created", "updated", "test"}, + []string{"id", "test"}, 0, }, { "new auth collection", - &models.Collection{ - Name: "new_table_auth", - Type: models.CollectionTypeAuth, - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }, - ), - Indexes: types.JsonArray[string]{"create index idx_auth_test on anything (email, username)"}, - }, + authCol, nil, []string{ - "id", "created", "updated", "test", - "username", "email", "verified", "emailVisibility", - "tokenKey", "passwordHash", "lastResetSentAt", "lastVerificationSentAt", "lastLoginAlertSentAt", + "id", "test", "email", "verified", + "emailVisibility", "tokenKey", "password", }, - 4, + 3, }, { "no changes", @@ -104,32 +85,33 @@ func TestSyncRecordTableSchema(t *testing.T) { } for _, s := range scenarios { - err := app.Dao().SyncRecordTableSchema(s.newCollection, s.oldCollection) - if err != nil { - t.Errorf("[%s] %v", s.name, err) - continue - } - - if !app.Dao().HasTable(s.newCollection.Name) { - t.Errorf("[%s] Expected table %s to exist", s.name, s.newCollection.Name) - } - - cols, _ := app.Dao().TableColumns(s.newCollection.Name) - if len(cols) != len(s.expectedColumns) { - t.Errorf("[%s] Expected columns %v, got %v", s.name, s.expectedColumns, cols) - } - - for _, c := range cols { - if !list.ExistInSlice(c, s.expectedColumns) { - t.Errorf("[%s] Couldn't find column %s in %v", s.name, c, s.expectedColumns) + t.Run(s.name, func(t *testing.T) { + err := app.SyncRecordTableSchema(s.newCollection, s.oldCollection) + if err != nil { + t.Fatal(err) } - } - indexes, _ := app.Dao().TableIndexes(s.newCollection.Name) + if !app.HasTable(s.newCollection.Name) { + t.Fatalf("Expected table %s to exist", s.newCollection.Name) + } - if totalIndexes := len(indexes); totalIndexes != s.expectedIndexesCount { - t.Errorf("[%s] Expected %d indexes, got %d:\n%v", s.name, s.expectedIndexesCount, totalIndexes, indexes) - } + cols, _ := app.TableColumns(s.newCollection.Name) + if len(cols) != len(s.expectedColumns) { + t.Fatalf("Expected columns %v, got %v", s.expectedColumns, cols) + } + + for _, col := range cols { + if !list.ExistInSlice(col, s.expectedColumns) { + t.Fatalf("Couldn't find column %s in %v", col, s.expectedColumns) + } + } + + indexes, _ := app.TableIndexes(s.newCollection.Name) + + if totalIndexes := len(indexes); totalIndexes != s.expectedIndexesCount { + t.Fatalf("Expected %d indexes, got %d:\n%v", s.expectedIndexesCount, totalIndexes, indexes) + } + }) } } @@ -139,69 +121,41 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - collection, err := app.Dao().FindCollectionByNameOrId("demo1") + collection, err := app.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } // mock field changes - { - selectOneField := collection.Schema.GetFieldByName("select_one") - opt := selectOneField.Options.(*schema.SelectOptions) - opt.MaxSelect = 2 - } - { - selectManyField := collection.Schema.GetFieldByName("select_many") - opt := selectManyField.Options.(*schema.SelectOptions) - opt.MaxSelect = 1 - } - { - fileOneField := collection.Schema.GetFieldByName("file_one") - opt := fileOneField.Options.(*schema.FileOptions) - opt.MaxSelect = 2 - } - { - fileManyField := collection.Schema.GetFieldByName("file_many") - opt := fileManyField.Options.(*schema.FileOptions) - opt.MaxSelect = 1 - } - { - relOneField := collection.Schema.GetFieldByName("rel_one") - opt := relOneField.Options.(*schema.RelationOptions) - opt.MaxSelect = types.Pointer(2) - } - { - relManyField := collection.Schema.GetFieldByName("rel_many") - opt := relManyField.Options.(*schema.RelationOptions) - opt.MaxSelect = types.Pointer(1) - } - { - // new multivaluer field to check whether the array normalization - // will be applied for already inserted data - collection.Schema.AddField(&schema.SchemaField{ - Name: "new_multiple", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"a", "b", "c"}, - MaxSelect: 3, - }, - }) - } + collection.Fields.GetByName("select_one").(*core.SelectField).MaxSelect = 2 + collection.Fields.GetByName("select_many").(*core.SelectField).MaxSelect = 1 + collection.Fields.GetByName("file_one").(*core.FileField).MaxSelect = 2 + collection.Fields.GetByName("file_many").(*core.FileField).MaxSelect = 1 + collection.Fields.GetByName("rel_one").(*core.RelationField).MaxSelect = 2 + collection.Fields.GetByName("rel_many").(*core.RelationField).MaxSelect = 1 - if err := app.Dao().SaveCollection(collection); err != nil { + // new multivaluer field to check whether the array normalization + // will be applied for already inserted data + collection.Fields.Add(&core.SelectField{ + Name: "new_multiple", + Values: []string{"a", "b", "c"}, + MaxSelect: 3, + }) + + if err := app.Save(collection); err != nil { t.Fatal(err) } // ensures that the writable schema was reverted to its expected default var writableSchema bool - app.Dao().DB().NewQuery("PRAGMA writable_schema").Row(&writableSchema) + app.DB().NewQuery("PRAGMA writable_schema").Row(&writableSchema) if writableSchema == true { t.Fatalf("Expected writable_schema to be OFF, got %v", writableSchema) } // check whether the columns DEFAULT definition was updated // --------------------------------------------------------------- - tableInfo, err := app.Dao().TableInfo(collection.Name) + tableInfo, err := app.TableInfo(collection.Name) if err != nil { t.Fatal(err) } @@ -217,7 +171,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { } for col, dflt := range tableInfoExpectations { t.Run("check default for "+col, func(t *testing.T) { - var row *models.TableInfoRow + var row *core.TableInfoRow for _, r := range tableInfo { if r.Name == col { row = r @@ -228,7 +182,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { t.Fatalf("Missing info for column %q", col) } - if v := row.DefaultValue.String(); v != dflt { + if v := row.DefaultValue.String; v != dflt { t.Fatalf("Expected default value %q, got %q", dflt, v) } }) @@ -292,7 +246,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { t.Run("check fields for record "+s.recordId, func(t *testing.T) { result := new(fieldsExpectation) - err := app.Dao().DB().Select( + err := app.DB().Select( "select_one", "select_many", "file_one", diff --git a/core/collection_validate.go b/core/collection_validate.go new file mode 100644 index 00000000..9da95f69 --- /dev/null +++ b/core/collection_validate.go @@ -0,0 +1,658 @@ +package core + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/dbutils" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/types" +) + +var collectionNameRegex = regexp.MustCompile(`^\w+$`) + +func onCollectionValidate(e *CollectionEvent) error { + var original *Collection + if !e.Collection.IsNew() { + original = &Collection{} + if err := e.App.ModelQuery(original).Model(e.Collection.LastSavedPK(), original); err != nil { + return fmt.Errorf("failed to fetch old collection state: %w", err) + } + } + + validator := newCollectionValidator( + e.Context, + e.App, + e.Collection, + original, + ) + + return validator.run() +} + +func newCollectionValidator(ctx context.Context, app App, new, original *Collection) *collectionValidator { + validator := &collectionValidator{ + ctx: ctx, + app: app, + new: new, + original: original, + } + + // load old/original collection + if validator.original == nil { + validator.original = NewCollection(validator.new.Type, "") + } + + return validator +} + +type collectionValidator struct { + original *Collection + new *Collection + app App + ctx context.Context +} + +type optionsValidator interface { + validate(cv *collectionValidator) error +} + +func (validator *collectionValidator) run() error { + // generate fields from the query (overwriting any explicit user defined fields) + if validator.new.IsView() { + validator.new.Fields, _ = validator.app.CreateViewFields(validator.new.ViewQuery) + } + + // validate base fields + baseErr := validation.ValidateStruct(validator.new, + validation.Field( + &validator.new.Id, + validation.Required, + validation.When( + validator.original.IsNew(), + validation.Length(1, 100), + validation.Match(DefaultIdRegex), + validation.By(validators.UniqueId(validator.app.DB(), validator.new.TableName())), + ).Else( + validation.By(validators.Equal(validator.original.Id)), + ), + ), + validation.Field( + &validator.new.System, + validation.By(validator.ensureNoSystemFlagChange), + ), + validation.Field( + &validator.new.Type, + validation.Required, + validation.In( + CollectionTypeBase, + CollectionTypeAuth, + CollectionTypeView, + ), + validation.By(validator.ensureNoTypeChange), + ), + validation.Field( + &validator.new.Name, + validation.Required, + validation.Length(1, 255), + validation.By(checkForVia), + validation.Match(collectionNameRegex), + validation.By(validator.ensureNoSystemNameChange), + validation.By(validator.checkUniqueName), + ), + validation.Field( + &validator.new.Fields, + validation.By(validator.checkFieldDuplicates), + validation.By(validator.checkMinFields), + validation.When( + !validator.new.IsView(), + validation.By(validator.ensureNoSystemFieldsChange), + validation.By(validator.ensureNoFieldsTypeChange), + ), + validation.When(validator.new.IsAuth(), validation.By(validator.checkReservedAuthKeys)), + validation.By(validator.checkFieldValidators), + ), + validation.Field( + &validator.new.ListRule, + validation.By(validator.checkRule), + validation.By(validator.ensureNoSystemRuleChange(validator.original.ListRule)), + ), + validation.Field( + &validator.new.ViewRule, + validation.By(validator.checkRule), + validation.By(validator.ensureNoSystemRuleChange(validator.original.ViewRule)), + ), + validation.Field( + &validator.new.CreateRule, + validation.When(validator.new.IsView(), validation.Nil), + validation.By(validator.checkRule), + validation.By(validator.ensureNoSystemRuleChange(validator.original.CreateRule)), + ), + validation.Field( + &validator.new.UpdateRule, + validation.When(validator.new.IsView(), validation.Nil), + validation.By(validator.checkRule), + validation.By(validator.ensureNoSystemRuleChange(validator.original.UpdateRule)), + ), + validation.Field( + &validator.new.DeleteRule, + validation.When(validator.new.IsView(), validation.Nil), + validation.By(validator.checkRule), + validation.By(validator.ensureNoSystemRuleChange(validator.original.DeleteRule)), + ), + validation.Field(&validator.new.Indexes, validation.By(validator.checkIndexes)), + ) + + optionsErr := validator.validateOptions() + + return validators.JoinValidationErrors(baseErr, optionsErr) +} + +func (validator *collectionValidator) checkUniqueName(value any) error { + v, _ := value.(string) + + // ensure unique collection name + if !validator.app.IsCollectionNameUnique(v, validator.original.Id) { + return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") + } + + // ensure that the collection name doesn't collide with the id of any collection + dummyCollection := &Collection{} + if validator.app.ModelQuery(dummyCollection).Model(v, dummyCollection) == nil { + return validation.NewError("validation_collection_name_id_duplicate", "The name must not match an existing collection id.") + } + + // ensure that there is no existing internal table with the provided name + if validator.original.Name != v && // has changed + validator.app.IsCollectionNameUnique(v) && // is not a collection (in case it was presaved) + validator.app.HasTable(v) { + return validation.NewError("validation_collection_name_invalid", "The name shouldn't match with an existing internal table.") + } + + return nil +} + +func (validator *collectionValidator) ensureNoSystemNameChange(value any) error { + v, _ := value.(string) + + if !validator.original.IsNew() && validator.original.System && v != validator.original.Name { + return validation.NewError("validation_collection_system_name_change", "System collection name cannot be changed.") + } + + return nil +} + +func (validator *collectionValidator) ensureNoSystemFlagChange(value any) error { + v, _ := value.(bool) + + if !validator.original.IsNew() && v != validator.original.System { + return validation.NewError("validation_collection_system_flag_change", "System collection state cannot be changed.") + } + + return nil +} + +func (validator *collectionValidator) ensureNoTypeChange(value any) error { + v, _ := value.(string) + + if !validator.original.IsNew() && v != validator.original.Type { + return validation.NewError("validation_collection_type_change", "Collection type cannot be changed.") + } + + return nil +} + +func (validator *collectionValidator) ensureNoFieldsTypeChange(value any) error { + v, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + errs := validation.Errors{} + + for i, field := range v { + oldField := validator.original.Fields.GetById(field.GetId()) + + if oldField != nil && oldField.Type() != field.Type() { + errs[strconv.Itoa(i)] = validation.NewError( + "validation_field_type_change", + "Field type cannot be changed.", + ) + } + } + if len(errs) > 0 { + return errs + } + + return nil +} + +func (validator *collectionValidator) checkFieldDuplicates(value any) error { + fields, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + totalFields := len(fields) + ids := make([]string, 0, totalFields) + names := make([]string, 0, totalFields) + + for i, field := range fields { + if list.ExistInSlice(field.GetId(), ids) { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "id": validation.NewError( + "validation_duplicated_field_id", + fmt.Sprintf("Duplicated or invalid field id %q", field.GetId()), + ), + }, + } + } + + // field names are used as db columns and should be case insensitive + nameLower := strings.ToLower(field.GetName()) + + if list.ExistInSlice(nameLower, names) { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "name": validation.NewError( + "validation_duplicated_field_name", + fmt.Sprintf("Duplicated or invalid field name %q", field.GetName()), + ).SetParams(map[string]any{ + "fieldName": field.GetName(), + }), + }, + } + } + + ids = append(ids, field.GetId()) + names = append(names, nameLower) + } + + return nil +} + +func (validator *collectionValidator) checkFieldValidators(value any) error { + fields, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + errs := validation.Errors{} + + for i, field := range fields { + if err := field.ValidateSettings(validator.ctx, validator.app, validator.new); err != nil { + errs[strconv.Itoa(i)] = err + } + } + + if len(errs) > 0 { + return errs + } + + return nil +} + +func (cv *collectionValidator) checkViewQuery(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + if _, err := cv.app.CreateViewFields(v); err != nil { + return validation.NewError( + "validation_invalid_view_query", + fmt.Sprintf("Invalid query - %s", err.Error()), + ) + } + + return nil +} + +var reservedAuthKeys = []string{"passwordConfirm", "oldPassword"} + +func (cv *collectionValidator) checkReservedAuthKeys(value any) error { + fields, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + if !cv.new.IsAuth() { + return nil // not an auth collection + } + + errs := validation.Errors{} + for i, field := range fields { + if list.ExistInSlice(field.GetName(), reservedAuthKeys) { + errs[strconv.Itoa(i)] = validation.Errors{ + "name": validation.NewError( + "validation_reserved_field_name", + "The field name is reserved and cannot be used.", + ), + } + } + } + if len(errs) > 0 { + return errs + } + + return nil +} + +func (cv *collectionValidator) checkMinFields(value any) error { + fields, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + if len(fields) == 0 { + return validation.ErrRequired + } + + // all collections must have an "id" PK field + idField, _ := fields.GetByName(FieldNameId).(*TextField) + if idField == nil || !idField.PrimaryKey { + return validation.NewError("validation_missing_primary_key", `Missing or invalid "id" PK field.`) + } + + switch cv.new.Type { + case CollectionTypeAuth: + passwordField, _ := fields.GetByName(FieldNamePassword).(*PasswordField) + if passwordField == nil { + return validation.NewError("validation_missing_password_field", `System "password" field is required.`) + } + if !passwordField.Hidden || !passwordField.System { + return validation.Errors{FieldNamePassword: ErrMustBeSystemAndHidden} + } + + tokenKeyField, _ := fields.GetByName(FieldNameTokenKey).(*TextField) + if tokenKeyField == nil { + return validation.NewError("validation_missing_tokenKey_field", `System "tokenKey" field is required.`) + } + if !tokenKeyField.Hidden || !tokenKeyField.System { + return validation.Errors{FieldNameTokenKey: ErrMustBeSystemAndHidden} + } + + emailField, _ := fields.GetByName(FieldNameEmail).(*EmailField) + if emailField == nil { + return validation.NewError("validation_missing_email_field", `System "email" field is required.`) + } + if !emailField.System { + return validation.Errors{FieldNameEmail: ErrMustBeSystem} + } + + emailVisibilityField, _ := fields.GetByName(FieldNameEmailVisibility).(*BoolField) + if emailVisibilityField == nil { + return validation.NewError("validation_missing_emailVisibility_field", `System "emailVisibility" field is required.`) + } + if !emailVisibilityField.System { + return validation.Errors{FieldNameEmailVisibility: ErrMustBeSystem} + } + + verifiedField, _ := fields.GetByName(FieldNameVerified).(*BoolField) + if verifiedField == nil { + return validation.NewError("validation_missing_verified_field", `System "verified" field is required.`) + } + if !verifiedField.System { + return validation.Errors{FieldNameVerified: ErrMustBeSystem} + } + + return nil + } + + return nil +} + +func (validator *collectionValidator) ensureNoSystemFieldsChange(value any) error { + fields, ok := value.(FieldsList) + if !ok { + return validators.ErrUnsupportedValueType + } + + for _, oldField := range validator.original.Fields { + if !oldField.GetSystem() { + continue + } + + newField := fields.GetById(oldField.GetId()) + + if newField == nil || oldField.GetName() != newField.GetName() { + return validation.NewError("validation_system_field_change", "System fields cannot be deleted or renamed.") + } + } + + return nil +} + +func (cv *collectionValidator) checkFieldsForUniqueIndex(value any) error { + names, ok := value.([]string) + if !ok { + return validators.ErrUnsupportedValueType + } + + if len(names) == 0 { + return nil // nothing to check + } + + for _, name := range names { + field := cv.new.Fields.GetByName(name) + if field == nil { + return validation.NewError("validation_missing_field", fmt.Sprintf("Invalid or missing field %q", name)). + SetParams(map[string]any{"fieldName": name}) + } + + if !dbutils.HasSingleColumnUniqueIndex(name, cv.new.Indexes) { + return validation.NewError("validation_missing_unique_constraint", fmt.Sprintf("The field %q doesn't have a UNIQUE constraint.", name)). + SetParams(map[string]any{"fieldName": name}) + } + } + + return nil +} + +// note: value could be either *string or string +func (validator *collectionValidator) checkRule(value any) error { + var vStr string + + v, ok := value.(*string) + if ok { + if v != nil { + vStr = *v + } + } else { + vStr, ok = value.(string) + } + if !ok { + return validators.ErrUnsupportedValueType + } + + if vStr == "" { + return nil // nothing to check + } + + r := NewRecordFieldResolver(validator.app, validator.new, nil, true) + _, err := search.FilterData(vStr).BuildExpr(r) + if err != nil { + return validation.NewError("validation_invalid_rule", "Invalid rule. Raw error: "+err.Error()) + } + + return nil +} + +func (validator *collectionValidator) ensureNoSystemRuleChange(oldRule *string) validation.RuleFunc { + return func(value any) error { + if validator.original.IsNew() || !validator.original.System { + return nil // not an update of a system collection + } + + rule, ok := value.(*string) + if !ok { + return validators.ErrUnsupportedValueType + } + + if (rule == nil && oldRule == nil) || + (rule != nil && oldRule != nil && *rule == *oldRule) { + return nil + } + + return validation.NewError("validation_collection_system_rule_change", "System collection API rule cannot be changed.") + } +} + +func (cv *collectionValidator) checkIndexes(value any) error { + indexes, _ := value.(types.JSONArray[string]) + + if cv.new.IsView() && len(indexes) > 0 { + return validation.NewError( + "validation_indexes_not_supported", + "View collections don't support indexes.", + ) + } + + indexNames := make(map[string]struct{}, len(indexes)) + + for i, rawIndex := range indexes { + parsed := dbutils.ParseIndex(rawIndex) + + // always set a table name because it is ignored anyway in order to keep it in sync with the collection name + parsed.TableName = "validator" + + if !parsed.IsValid() { + return validation.Errors{ + strconv.Itoa(i): validation.NewError( + "validation_invalid_index_expression", + "Invalid CREATE INDEX expression.", + ), + } + } + + _, isDuplicated := indexNames[strings.ToLower(parsed.IndexName)] + if isDuplicated { + return validation.Errors{ + strconv.Itoa(i): validation.NewError( + "validation_duplicated_index_name", + "The index name must be unique.", + ), + } + } + + // ensure that the index name is not used in another collection + var usedTblName string + _ = cv.app.DB().Select("tbl_name"). + From("sqlite_master"). + AndWhere(dbx.HashExp{"type": "index"}). + AndWhere(dbx.NewExp("LOWER([[tbl_name]])!=LOWER({:oldName})", dbx.Params{"oldName": cv.original.Name})). + AndWhere(dbx.NewExp("LOWER([[tbl_name]])!=LOWER({:newName})", dbx.Params{"newName": cv.new.Name})). + AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:indexName})", dbx.Params{"indexName": parsed.IndexName})). + Limit(1). + Row(&usedTblName) + if usedTblName != "" { + return validation.Errors{ + strconv.Itoa(i): validation.NewError( + "validation_existing_index_name", + "The index name is already used in "+usedTblName+" collection.", + ), + } + } + + // note: we don't check the index table name because it is always + // overwritten by the SyncRecordTableSchema to allow + // easier partial modifications (eg. changing only the collection name). + // if !strings.EqualFold(parsed.TableName, form.Name) { + // return validation.Errors{ + // strconv.Itoa(i): validation.NewError( + // "validation_invalid_index_table", + // fmt.Sprintf("The index table must be the same as the collection name."), + // ), + // } + // } + + indexNames[strings.ToLower(parsed.IndexName)] = struct{}{} + } + + // ensure that indexes on system fields are not deleted or changed + if !cv.original.IsNew() { + OLD_INDEXES_LOOP: + for _, oldIndex := range cv.original.Indexes { + oldParsed := dbutils.ParseIndex(oldIndex) + + for _, column := range oldParsed.Columns { + for _, f := range cv.original.Fields { + if !f.GetSystem() || !strings.EqualFold(column.Name, f.GetName()) { + continue + } + + var exists bool + + for i, newIndex := range cv.new.Indexes { + newParsed := dbutils.ParseIndex(newIndex) + if !strings.EqualFold(newParsed.IndexName, oldParsed.IndexName) { + 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 { + return validation.NewError( + "validation_missing_system_index", + fmt.Sprintf("Missing system index %q.", oldParsed.IndexName), + ).SetParams(map[string]any{"name": oldParsed.IndexName}) + } + + continue OLD_INDEXES_LOOP + } + } + } + } + + // 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 + if cv.new.IsAuth() { + requiredNames := []string{FieldNameTokenKey, FieldNameEmail} + for _, name := range requiredNames { + if !dbutils.HasSingleColumnUniqueIndex(name, indexes) { + return validation.NewError( + "validation_missing_required_unique_index", + `Missing required unique index for field "`+name+`".`, + ) + } + } + } + + return nil +} + +func (validator *collectionValidator) validateOptions() error { + switch validator.new.Type { + case CollectionTypeAuth: + return validator.new.collectionAuthOptions.validate(validator) + case CollectionTypeView: + return validator.new.collectionViewOptions.validate(validator) + } + + return nil +} diff --git a/core/collection_validate_test.go b/core/collection_validate_test.go new file mode 100644 index 00000000..72410376 --- /dev/null +++ b/core/collection_validate_test.go @@ -0,0 +1,813 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestCollectionValidate(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + collection func(app core.App) (*core.Collection, error) + expectedErrors []string + }{ + { + name: "empty collection", + collection: func(app core.App) (*core.Collection, error) { + return &core.Collection{}, nil + }, + expectedErrors: []string{ + "id", "name", "type", "fields", // no default fields because the type is unknown + }, + }, + { + name: "unknown type with all invalid fields", + collection: func(app core.App) (*core.Collection, error) { + c := &core.Collection{} + c.Id = "invalid_id ?!@#$" + c.Name = "invalid_name ?!@#$" + c.Type = "invalid_type" + c.ListRule = types.Pointer("missing = '123'") + c.ViewRule = types.Pointer("missing = '123'") + c.CreateRule = types.Pointer("missing = '123'") + c.UpdateRule = types.Pointer("missing = '123'") + c.DeleteRule = types.Pointer("missing = '123'") + c.Indexes = []string{"create index '' on '' ()"} + + // type specific fields + c.ViewQuery = "invalid" // should be ignored + c.AuthRule = types.Pointer("missing = '123'") // should be ignored + + return c, nil + }, + expectedErrors: []string{ + "id", "name", "type", "indexes", + "listRule", "viewRule", "createRule", "updateRule", "deleteRule", + "fields", // no default fields because the type is unknown + }, + }, + { + name: "base with invalid fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("invalid_name ?!@#$") + c.Indexes = []string{"create index '' on '' ()"} + + // type specific fields + c.ViewQuery = "invalid" // should be ignored + c.AuthRule = types.Pointer("missing = '123'") // should be ignored + + return c, nil + }, + expectedErrors: []string{"name", "indexes"}, + }, + { + name: "view with invalid fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("invalid_name ?!@#$") + c.Indexes = []string{"create index '' on '' ()"} + + // type specific fields + c.ViewQuery = "invalid" + c.AuthRule = types.Pointer("missing = '123'") // should be ignored + + return c, nil + }, + expectedErrors: []string{"indexes", "name", "fields", "viewQuery"}, + }, + { + name: "auth with invalid fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("invalid_name ?!@#$") + c.Indexes = []string{"create index '' on '' ()"} + + // type specific fields + c.ViewQuery = "invalid" // should be ignored + c.AuthRule = types.Pointer("missing = '123'") + + return c, nil + }, + expectedErrors: []string{"indexes", "name", "authRule"}, + }, + + // type checks + { + name: "empty type", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Type = "" + return c, nil + }, + expectedErrors: []string{"type"}, + }, + { + name: "unknown type", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Type = "unknown" + return c, nil + }, + expectedErrors: []string{"type"}, + }, + { + name: "base type", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "view type", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("test") + c.ViewQuery = "select 1 as id" + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "auth type", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("test") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "changing type", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("users") + c.Type = core.CollectionTypeBase + return c, nil + }, + expectedErrors: []string{"type"}, + }, + + // system checks + { + name: "change from system to regular", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + c.System = false + return c, nil + }, + expectedErrors: []string{"system"}, + }, + { + name: "change from regular to system", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + c.System = true + return c, nil + }, + expectedErrors: []string{"system"}, + }, + { + name: "create system", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_system") + c.System = true + return c, nil + }, + expectedErrors: []string{}, + }, + + // id checks + { + name: "empty id", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Id = "" + return c, nil + }, + expectedErrors: []string{"id"}, + }, + { + name: "invalid id", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Id = "!invalid" + return c, nil + }, + expectedErrors: []string{"id"}, + }, + { + name: "existing id", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Id = "_pb_users_auth_" + return c, nil + }, + expectedErrors: []string{"id"}, + }, + { + name: "changing id", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo3") + c.Id = "anything" + return c, nil + }, + expectedErrors: []string{"id"}, + }, + { + name: "valid id", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test") + c.Id = "anything" + return c, nil + }, + expectedErrors: []string{}, + }, + + // name checks + { + name: "empty name", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("") + c.Id = "test" + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "invalid name", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("!invalid") + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "name with _via_", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("a_via_b") + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "create with existing collection name", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("demo1") + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "create with existing internal table name", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("_collections") + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "update with existing collection name", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("users") + c.Name = "demo1" + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "update with existing internal table name", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("users") + c.Name = "_collections" + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "system collection name change", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + c.Name = "superusers_new" + return c, nil + }, + expectedErrors: []string{"name"}, + }, + { + name: "create with valid name", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_col") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "update with valid name", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + c.Name = "demo1_new" + return c, nil + }, + expectedErrors: []string{}, + }, + + // rule checks + { + name: "invalid base rules", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new") + c.ListRule = types.Pointer("!invalid") + c.ViewRule = types.Pointer("missing = 123") + c.CreateRule = types.Pointer("id = 123 && missing = 456") + c.UpdateRule = types.Pointer("(id = 123") + c.DeleteRule = types.Pointer("missing = 123") + return c, nil + }, + expectedErrors: []string{"listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, + }, + { + name: "valid base rules", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new") + c.Fields.Add(&core.TextField{Name: "f1"}) // dummy field to ensure that new fields can be referenced + c.ListRule = types.Pointer("") + c.ViewRule = types.Pointer("f1 = 123") + c.CreateRule = types.Pointer("id = 123 && f1 = 456") + c.UpdateRule = types.Pointer("(id = 123)") + c.DeleteRule = types.Pointer("f1 = 123") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "view with non-nil create/update/delete rules", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new") + c.ViewQuery = "select 1 as id, 'text' as f1" + c.ListRule = types.Pointer("id = 123") + c.ViewRule = types.Pointer("f1 = 456") + c.CreateRule = types.Pointer("") + c.UpdateRule = types.Pointer("") + c.DeleteRule = types.Pointer("") + return c, nil + }, + expectedErrors: []string{"createRule", "updateRule", "deleteRule"}, + }, + { + name: "view with nil create/update/delete rules", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewViewCollection("new") + c.ViewQuery = "select 1 as id, 'text' as f1" + c.ListRule = types.Pointer("id = 1") + c.ViewRule = types.Pointer("f1 = 456") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "changing api rules", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("users") + c.Fields.Add(&core.TextField{Name: "f1"}) // dummy field to ensure that new fields can be referenced + c.ListRule = types.Pointer("id = 1") + c.ViewRule = types.Pointer("f1 = 456") + c.CreateRule = types.Pointer("id = 123 && f1 = 456") + c.UpdateRule = types.Pointer("(id = 123)") + c.DeleteRule = types.Pointer("f1 = 123") + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "changing system collection api rules", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + c.ListRule = types.Pointer("1 = 1") + c.ViewRule = types.Pointer("1 = 1") + c.CreateRule = types.Pointer("1 = 1") + c.UpdateRule = types.Pointer("1 = 1") + c.DeleteRule = types.Pointer("1 = 1") + c.ManageRule = types.Pointer("1 = 1") + c.AuthRule = types.Pointer("1 = 1") + return c, nil + }, + expectedErrors: []string{ + "listRule", "viewRule", "createRule", "updateRule", + "deleteRule", "manageRule", "authRule", + }, + }, + + // indexes checks + { + name: "invalid index expression", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + c.Indexes = []string{ + "create index invalid", + "create index idx_test_demo2 on anything (text)", // the name of table shouldn't matter + } + return c, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "index name used in other table", + 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 `__pb_USERS_auth__username_idx` on anything (text)", // should be case-insensitive + } + return c, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "duplicated index names", + 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_demo1 on anything (text)", + } + return c, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "try to add index to a view collection", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("view1") + c.Indexes = []string{"create index idx_test_view1 on view1 (id)"} + return c, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "replace old with new indexes", + 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 anything (text)", // the name of table shouldn't matter + } + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "old + new indexes", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + c.Indexes = []string{ + "CREATE INDEX `_wsmn24bux7wo113_created_idx` ON `demo1` (`created`)", + "create index idx_test_demo1 on anything (id)", + } + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "index for missing field", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + c.Indexes = []string{ + "create index idx_test_demo1 on anything (missing)", // still valid because it is checked on db persist + } + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "auth collection with missing required unique indexes", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Indexes = []string{} + return c, nil + }, + expectedErrors: []string{"indexes", "passwordAuth"}, + }, + { + name: "auth collection with non-unique required indexes", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Indexes = []string{ + "create index test_idx1 on new_auth (tokenKey)", + "create index test_idx2 on new_auth (email)", + } + return c, nil + }, + expectedErrors: []string{"indexes", "passwordAuth"}, + }, + { + name: "auth collection with unique required indexes", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Indexes = []string{ + "create unique index test_idx1 on new_auth (tokenKey)", + "create unique index test_idx2 on new_auth (email)", + } + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "removing index on system field", + collection: func(app core.App) (*core.Collection, error) { + demo2, err := app.FindCollectionByNameOrId("demo2") + if err != nil { + return nil, err + } + + // mark the title field as system + demo2.Fields.GetByName("title").SetSystem(true) + if err = app.Save(demo2); err != nil { + return nil, err + } + + // refresh + demo2, err = app.FindCollectionByNameOrId("demo2") + if err != nil { + return nil, err + } + + demo2.RemoveIndex("idx_unique_demo2_title") + + return demo2, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "changing index on system field", + collection: func(app core.App) (*core.Collection, error) { + demo2, err := app.FindCollectionByNameOrId("demo2") + if err != nil { + return nil, err + } + + // mark the title field as system + demo2.Fields.GetByName("title").SetSystem(true) + if err = app.Save(demo2); err != nil { + return nil, err + } + + // refresh + demo2, err = app.FindCollectionByNameOrId("demo2") + if err != nil { + return nil, err + } + + // replace the index with a partial one + demo2.RemoveIndex("idx_unique_demo2_title") + demo2.AddIndex("idx_unique_demo2_title", true, "title", "1 = 1") + + return demo2, nil + }, + expectedErrors: []string{"indexes"}, + }, + { + name: "changing index on non-system field", + collection: func(app core.App) (*core.Collection, error) { + demo2, err := app.FindCollectionByNameOrId("demo2") + if err != nil { + return nil, err + } + + // replace the index with a partial one + demo2.RemoveIndex("idx_demo2_active") + demo2.AddIndex("idx_demo2_active", true, "active", "1 = 1") + + return demo2, nil + }, + expectedErrors: []string{}, + }, + + // fields list checks + { + name: "empty fields", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_auth") + c.Fields = nil // the minimum fields should auto added + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "no id primay key field", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_auth") + c.Fields = core.NewFieldsList( + &core.TextField{Name: "id"}, + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with id primay key field", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_auth") + c.Fields = core.NewFieldsList( + &core.TextField{Name: "id", PrimaryKey: true, Required: true}, + ) + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "duplicated field names", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("new_auth") + c.Fields = core.NewFieldsList( + &core.TextField{Name: "id", PrimaryKey: true, Required: true}, + &core.TextField{Id: "f1", Name: "Test"}, // case-insensitive + &core.BoolField{Id: "f2", Name: "test"}, + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "changing field type", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("demo1") + f := c.Fields.GetByName("text") + c.Fields.Add(&core.BoolField{Id: f.GetId(), Name: f.GetName()}) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "renaming system field", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId(core.CollectionNameAuthOrigins) + f := c.Fields.GetByName("fingerprint") + f.SetName("fingerprint_new") + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "deleting system field", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId(core.CollectionNameAuthOrigins) + c.Fields.RemoveByName("fingerprint") + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "invalid field setting", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test_new") + c.Fields.Add(&core.TextField{Name: "f1", Min: -10}) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "valid field setting", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewBaseCollection("test_new") + c.Fields.Add(&core.TextField{Name: "f1", Min: 10}) + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "fields view changes should be ignored", + collection: func(app core.App) (*core.Collection, error) { + c, _ := app.FindCollectionByNameOrId("view1") + c.Fields = nil + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "with reserved auth only field name (passwordConfirm)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "passwordConfirm"}, + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with reserved auth only field name (oldPassword)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "oldPassword"}, + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with invalid password auth field options (1)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "password", System: true, Hidden: true}, // should be PasswordField + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with valid password auth field options (2)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.PasswordField{Name: "password", System: true, Hidden: true}, + ) + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "with invalid tokenKey auth field options (1)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "tokenKey", System: true}, // should be also hidden + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with valid tokenKey auth field options (2)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "tokenKey", System: true, Hidden: true}, + ) + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "with invalid email auth field options (1)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "email", System: true}, // should be EmailField + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with valid email auth field options (2)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.EmailField{Name: "email", System: true}, + ) + return c, nil + }, + expectedErrors: []string{}, + }, + { + name: "with invalid verified auth field options (1)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.TextField{Name: "verified", System: true}, // should be BoolField + ) + return c, nil + }, + expectedErrors: []string{"fields"}, + }, + { + name: "with valid verified auth field options (2)", + collection: func(app core.App) (*core.Collection, error) { + c := core.NewAuthCollection("new_auth") + c.Fields.Add( + &core.BoolField{Name: "verified", System: true}, + ) + return c, nil + }, + expectedErrors: []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := s.collection(app) + if err != nil { + t.Fatalf("Failed to retrieve test collection: %v", err) + } + + result := app.Validate(collection) + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} diff --git a/core/collections_cache.go b/core/collections_cache.go deleted file mode 100644 index e4ca3d43..00000000 --- a/core/collections_cache.go +++ /dev/null @@ -1,72 +0,0 @@ -package core - -// ------------------------------------------------------------------- -// This is a small optimization ported from the [ongoing refactoring branch](https://github.com/pocketbase/pocketbase/discussions/4355). -// -// @todo remove after the refactoring is finalized. -// ------------------------------------------------------------------- - -import ( - "strings" - - "github.com/pocketbase/pocketbase/models" -) - -const storeCachedCollectionsKey = "@cachedCollectionsContext" - -func registerCachedCollectionsAppHooks(app App) { - collectionsChangeFunc := func(e *ModelEvent) error { - if _, ok := e.Model.(*models.Collection); !ok { - return nil - } - - _ = ReloadCachedCollections(app) - - return nil - } - app.OnModelAfterCreate().Add(collectionsChangeFunc) - app.OnModelAfterUpdate().Add(collectionsChangeFunc) - app.OnModelAfterDelete().Add(collectionsChangeFunc) - app.OnBeforeServe().Add(func(e *ServeEvent) error { - _ = ReloadCachedCollections(e.App) - return nil - }) -} - -func ReloadCachedCollections(app App) error { - collections := []*models.Collection{} - - err := app.Dao().CollectionQuery().All(&collections) - if err != nil { - return err - } - - app.Store().Set(storeCachedCollectionsKey, collections) - - return nil -} - -func FindCachedCollectionByNameOrId(app App, nameOrId string) (*models.Collection, error) { - // retrieve from the app cache - // --- - collections, _ := app.Store().Get(storeCachedCollectionsKey).([]*models.Collection) - for _, c := range collections { - if strings.EqualFold(c.Name, nameOrId) || c.Id == nameOrId { - return c, nil - } - } - - // retrieve from the database - // --- - found, err := app.Dao().FindCollectionByNameOrId(nameOrId) - if err != nil { - return nil, err - } - - err = ReloadCachedCollections(app) - if err != nil { - app.Logger().Warn("Failed to reload collections cache", "error", err) - } - - return found, nil -} diff --git a/core/db.go b/core/db.go new file mode 100644 index 00000000..12df9b5f --- /dev/null +++ b/core/db.go @@ -0,0 +1,503 @@ +package core + +import ( + "context" + "errors" + "fmt" + "hash/crc32" + "regexp" + "slices" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +const ( + idColumn string = "id" + + // DefaultIdLength is the default length of the generated model id. + DefaultIdLength int = 15 + + // DefaultIdAlphabet is the default characters set used for generating the model id. + DefaultIdAlphabet string = "abcdefghijklmnopqrstuvwxyz0123456789" +) + +// DefaultIdRegex specifies the default regex pattern for an id value. +var DefaultIdRegex = regexp.MustCompile(`^\w+$`) + +// DBExporter defines an interface for custom DB data export. +// Usually used as part of [App.Save]. +type DBExporter interface { + // DBExport returns a key-value map with the data to be used when saving the struct in the database. + DBExport(app App) (map[string]any, error) +} + +// PreValidator defines an optional model interface for registering a +// function that will run BEFORE firing the validation hooks (see [App.ValidateWithContext]). +type PreValidator interface { + // PreValidate defines a function that runs BEFORE the validation hooks. + PreValidate(ctx context.Context, app App) error +} + +// PostValidator defines an optional model interface for registering a +// function that will run AFTER executing the validation hooks (see [App.ValidateWithContext]). +type PostValidator interface { + // PostValidate defines a function that runs AFTER the successful + // execution of the validation hooks. + PostValidate(ctx context.Context, app App) error +} + +// GenerateDefaultRandomId generates a default random id string +// (note: the generated random string is not intended for security purposes). +func GenerateDefaultRandomId() string { + return security.PseudorandomStringWithAlphabet(DefaultIdLength, DefaultIdAlphabet) +} + +// crc32Checksum generates a stringified crc32 checksum from the provided plain string. +func crc32Checksum(str string) string { + return strconv.Itoa(int(crc32.ChecksumIEEE([]byte(str)))) +} + +// ModelQuery creates a new preconfigured select app.DB() query with preset +// SELECT, FROM and other common fields based on the provided model. +func (app *BaseApp) ModelQuery(m Model) *dbx.SelectQuery { + return app.modelQuery(app.DB(), m) +} + +// AuxModelQuery creates a new preconfigured select app.AuxDB() query with preset +// SELECT, FROM and other common fields based on the provided model. +func (app *BaseApp) AuxModelQuery(m Model) *dbx.SelectQuery { + return app.modelQuery(app.AuxDB(), m) +} + +func (app *BaseApp) modelQuery(db dbx.Builder, m Model) *dbx.SelectQuery { + tableName := m.TableName() + + return db. + Select("{{" + tableName + "}}.*"). + From(tableName). + WithBuildHook(func(query *dbx.Query) { + query.WithExecHook(execLockRetry(app.config.QueryTimeout, defaultMaxLockRetries)) + }) +} + +// Delete deletes the specified model from the regular app database. +func (app *BaseApp) Delete(model Model) error { + return app.DeleteWithContext(context.Background(), model) +} + +// Delete deletes the specified model from the regular app database +// (the context could be used to limit the query execution). +func (app *BaseApp) DeleteWithContext(ctx context.Context, model Model) error { + return app.delete(ctx, model, false) +} + +// AuxDelete deletes the specified model from the auxiliary database. +func (app *BaseApp) AuxDelete(model Model) error { + return app.AuxDeleteWithContext(context.Background(), model) +} + +// AuxDeleteWithContext deletes the specified model from the auxiliary database +// (the context could be used to limit the query execution). +func (app *BaseApp) AuxDeleteWithContext(ctx context.Context, model Model) error { + return app.delete(ctx, model, true) +} + +func (app *BaseApp) delete(ctx context.Context, model Model, isForAuxDB bool) error { + event := new(ModelEvent) + event.App = app + event.Type = ModelEventTypeDelete + event.Context = ctx + event.Model = model + + deleteErr := app.OnModelDelete().Trigger(event, func(e *ModelEvent) error { + pk := cast.ToString(e.Model.LastSavedPK()) + + if cast.ToString(pk) == "" { + return errors.New("the model can be deleted only if it is existing and has a non-empty primary key") + } + + // db write + return e.App.OnModelDeleteExecute().Trigger(event, func(e *ModelEvent) error { + var db dbx.Builder + if isForAuxDB { + db = e.App.AuxNonconcurrentDB() + } else { + db = e.App.NonconcurrentDB() + } + + return baseLockRetry(func(attempt int) error { + _, err := db.Delete(e.Model.TableName(), dbx.HashExp{ + idColumn: pk, + }).WithContext(e.Context).Execute() + + return err + }, defaultMaxLockRetries) + }) + }) + if deleteErr != nil { + hookErr := app.OnModelAfterDeleteError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: deleteErr, + }) + if hookErr != nil { + return errors.Join(deleteErr, hookErr) + } + + return deleteErr + } + + if app.txInfo != nil { + // execute later after the transaction has completed + app.txInfo.onAfterFunc(func(txErr error) error { + if app.txInfo != nil && app.txInfo.parent != nil { + event.App = app.txInfo.parent + } + + if txErr != nil { + return app.OnModelAfterDeleteError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: txErr, + }) + } + + return app.OnModelAfterDeleteSuccess().Trigger(event) + }) + } else if err := event.App.OnModelAfterDeleteSuccess().Trigger(event); err != nil { + return err + } + + return nil +} + +// Save validates and saves the specified model into the regular app database. +// +// If you don't want to run validations, use [App.SaveNoValidate()]. +func (app *BaseApp) Save(model Model) error { + return app.SaveWithContext(context.Background(), model) +} + +// SaveWithContext is the same as [App.Save()] but allows specifying a context to limit the db execution. +// +// If you don't want to run validations, use [App.SaveNoValidateWithContext()]. +func (app *BaseApp) SaveWithContext(ctx context.Context, model Model) error { + return app.save(ctx, model, true, false) +} + +// SaveNoValidate saves the specified model into the regular app database without performing validations. +// +// If you want to also run validations before persisting, use [App.Save()]. +func (app *BaseApp) SaveNoValidate(model Model) error { + return app.SaveNoValidateWithContext(context.Background(), model) +} + +// SaveNoValidateWithContext is the same as [App.SaveNoValidate()] +// but allows specifying a context to limit the db execution. +// +// If you want to also run validations before persisting, use [App.SaveWithContext()]. +func (app *BaseApp) SaveNoValidateWithContext(ctx context.Context, model Model) error { + return app.save(ctx, model, false, false) +} + +// AuxSave validates and saves the specified model into the auxiliary app database. +// +// If you don't want to run validations, use [App.AuxSaveNoValidate()]. +func (app *BaseApp) AuxSave(model Model) error { + return app.AuxSaveWithContext(context.Background(), model) +} + +// AuxSaveWithContext is the same as [App.AuxSave()] but allows specifying a context to limit the db execution. +// +// If you don't want to run validations, use [App.AuxSaveNoValidateWithContext()]. +func (app *BaseApp) AuxSaveWithContext(ctx context.Context, model Model) error { + return app.save(ctx, model, true, true) +} + +// AuxSaveNoValidate saves the specified model into the auxiliary app database without performing validations. +// +// If you want to also run validations before persisting, use [App.AuxSave()]. +func (app *BaseApp) AuxSaveNoValidate(model Model) error { + return app.AuxSaveNoValidateWithContext(context.Background(), model) +} + +// AuxSaveNoValidateWithContext is the same as [App.AuxSaveNoValidate()] +// but allows specifying a context to limit the db execution. +// +// If you want to also run validations before persisting, use [App.AuxSaveWithContext()]. +func (app *BaseApp) AuxSaveNoValidateWithContext(ctx context.Context, model Model) error { + return app.save(ctx, model, false, true) +} + +// Validate triggers the OnModelValidate hook for the specified model. +func (app *BaseApp) Validate(model Model) error { + return app.ValidateWithContext(context.Background(), model) +} + +// ValidateWithContext is the same as Validate but allows specifying the ModelEvent context. +func (app *BaseApp) ValidateWithContext(ctx context.Context, model Model) error { + if m, ok := model.(PreValidator); ok { + if err := m.PreValidate(ctx, app); err != nil { + return err + } + } + + event := new(ModelEvent) + event.App = app + event.Context = ctx + event.Type = ModelEventTypeValidate + event.Model = model + + return event.App.OnModelValidate().Trigger(event, func(e *ModelEvent) error { + if m, ok := e.Model.(PostValidator); ok { + if err := m.PostValidate(ctx, e.App); err != nil { + return err + } + } + + return e.Next() + }) +} + +// ------------------------------------------------------------------- + +func (app *BaseApp) save(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error { + if model.IsNew() { + return app.create(ctx, model, withValidations, isForAuxDB) + } + + return app.update(ctx, model, withValidations, isForAuxDB) +} + +func (app *BaseApp) create(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error { + event := new(ModelEvent) + event.App = app + event.Context = ctx + event.Type = ModelEventTypeCreate + event.Model = model + + saveErr := app.OnModelCreate().Trigger(event, func(e *ModelEvent) error { + // run validations (if any) + if withValidations { + validateErr := e.App.ValidateWithContext(e.Context, e.Model) + if validateErr != nil { + return validateErr + } + } + + // db write + return e.App.OnModelCreateExecute().Trigger(event, func(e *ModelEvent) error { + var db dbx.Builder + if isForAuxDB { + db = e.App.AuxNonconcurrentDB() + } else { + db = e.App.NonconcurrentDB() + } + + dbErr := baseLockRetry(func(attempt int) error { + if m, ok := e.Model.(DBExporter); ok { + data, err := m.DBExport(e.App) + if err != nil { + return err + } + + // manually add the id to the data if missing + if _, ok := data[idColumn]; !ok { + data[idColumn] = e.Model.PK() + } + + if cast.ToString(data[idColumn]) == "" { + return errors.New("empty primary key is not allowed when using the DBExporter interface") + } + + _, err = db.Insert(e.Model.TableName(), data).WithContext(e.Context).Execute() + + return err + } + + return db.Model(e.Model).WithContext(e.Context).Insert() + }, defaultMaxLockRetries) + if dbErr != nil { + return dbErr + } + + e.Model.MarkAsNotNew() + + return nil + }) + }) + if saveErr != nil { + event.Model.MarkAsNew() // reset "new" state + + hookErr := app.OnModelAfterCreateError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: saveErr, + }) + if hookErr != nil { + return errors.Join(saveErr, hookErr) + } + + return saveErr + } + + if app.txInfo != nil { + // execute later after the transaction has completed + app.txInfo.onAfterFunc(func(txErr error) error { + if app.txInfo != nil && app.txInfo.parent != nil { + event.App = app.txInfo.parent + } + + if txErr != nil { + event.Model.MarkAsNew() // reset "new" state + + return app.OnModelAfterCreateError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: txErr, + }) + } + + return app.OnModelAfterCreateSuccess().Trigger(event) + }) + } else if err := event.App.OnModelAfterCreateSuccess().Trigger(event); err != nil { + return err + } + + return nil +} + +func (app *BaseApp) update(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error { + event := new(ModelEvent) + event.App = app + event.Context = ctx + event.Type = ModelEventTypeUpdate + event.Model = model + + saveErr := app.OnModelUpdate().Trigger(event, func(e *ModelEvent) error { + // run validations (if any) + if withValidations { + validateErr := e.App.ValidateWithContext(e.Context, e.Model) + if validateErr != nil { + return validateErr + } + } + + // db write + return e.App.OnModelUpdateExecute().Trigger(event, func(e *ModelEvent) error { + var db dbx.Builder + if isForAuxDB { + db = e.App.AuxNonconcurrentDB() + } else { + db = e.App.NonconcurrentDB() + } + + return baseLockRetry(func(attempt int) error { + if m, ok := e.Model.(DBExporter); ok { + data, err := m.DBExport(e.App) + if err != nil { + return err + } + + // note: for now disallow primary key change for consistency with dbx.ModelQuery.Update() + if data[idColumn] != e.Model.LastSavedPK() { + return errors.New("primary key change is not allowed") + } + + _, err = db.Update(e.Model.TableName(), data, dbx.HashExp{ + idColumn: e.Model.LastSavedPK(), + }).WithContext(e.Context).Execute() + + return err + } + + return db.Model(e.Model).WithContext(e.Context).Update() + }, defaultMaxLockRetries) + }) + }) + if saveErr != nil { + hookErr := app.OnModelAfterUpdateError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: saveErr, + }) + if hookErr != nil { + return errors.Join(saveErr, hookErr) + } + + return saveErr + } + + if app.txInfo != nil { + // execute later after the transaction has completed + app.txInfo.onAfterFunc(func(txErr error) error { + if app.txInfo != nil && app.txInfo.parent != nil { + event.App = app.txInfo.parent + } + + if txErr != nil { + return app.OnModelAfterUpdateError().Trigger(&ModelErrorEvent{ + ModelEvent: *event, + Error: txErr, + }) + } + + return app.OnModelAfterUpdateSuccess().Trigger(event) + }) + } else if err := event.App.OnModelAfterUpdateSuccess().Trigger(event); err != nil { + return err + } + + return nil +} + +func validateCollectionId(app App, optTypes ...string) validation.RuleFunc { + return func(value any) error { + id, _ := value.(string) + if id == "" { + return nil + } + + collection := &Collection{} + if err := app.ModelQuery(collection).Model(id, collection); err != nil { + return validation.NewError("validation_invalid_collection_id", "Missing or invalid collection.") + } + + if len(optTypes) > 0 && !slices.Contains(optTypes, collection.Type) { + return validation.NewError( + "validation_invalid_collection_type", + fmt.Sprintf("Invalid collection type - must be %s.", strings.Join(optTypes, ", ")), + ).SetParams(map[string]any{"types": optTypes}) + } + + return nil + } +} + +func validateRecordId(app App, collectionNameOrId string) validation.RuleFunc { + return func(value any) error { + id, _ := value.(string) + if id == "" { + return nil + } + + collection, err := app.FindCachedCollectionByNameOrId(collectionNameOrId) + if err != nil { + return validation.NewError("validation_invalid_collection", "Missing or invalid collection.") + } + + var exists bool + + rowErr := app.DB().Select("(1)"). + From(collection.Name). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + Row(&exists) + + if rowErr != nil || !exists { + return validation.NewError("validation_invalid_record", "Missing or invalid record.") + } + + return nil + } +} diff --git a/core/db_cgo.go b/core/db_connect_cgo.go similarity index 95% rename from core/db_cgo.go rename to core/db_connect_cgo.go index 25d46083..c7a7bd84 100644 --- a/core/db_cgo.go +++ b/core/db_connect_cgo.go @@ -40,7 +40,7 @@ func init() { dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"] } -func connectDB(dbPath string) (*dbx.DB, error) { +func dbConnect(dbPath string) (*dbx.DB, error) { db, err := dbx.Open("pb_sqlite3", dbPath) if err != nil { return nil, err diff --git a/core/db_nocgo.go b/core/db_connect_nocgo.go similarity index 92% rename from core/db_nocgo.go rename to core/db_connect_nocgo.go index 98fe8c3a..4e784b03 100644 --- a/core/db_nocgo.go +++ b/core/db_connect_nocgo.go @@ -7,7 +7,7 @@ import ( _ "modernc.org/sqlite" ) -func connectDB(dbPath string) (*dbx.DB, error) { +func dbConnect(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. diff --git a/core/db_model.go b/core/db_model.go new file mode 100644 index 00000000..9649e1ae --- /dev/null +++ b/core/db_model.go @@ -0,0 +1,59 @@ +package core + +// Model defines an interface with common methods that all db models should have. +// +// Note: for simplicity composite pk are not supported. +type Model interface { + TableName() string + PK() any + LastSavedPK() any + IsNew() bool + MarkAsNew() + MarkAsNotNew() +} + +// BaseModel defines a base struct that is intended to be embedded into other custom models. +type BaseModel struct { + lastSavedPK string + + // Id is the primary key of the model. + // It is usually autogenerated by the parent model implementation. + Id string `db:"id" json:"id" form:"id" xml:"id"` +} + +// LastSavedPK returns the last saved primary key of the model. +// +// Its value is updated to the latest PK value after MarkAsNotNew() or PostScan() calls. +func (m *BaseModel) LastSavedPK() any { + return m.lastSavedPK +} + +func (m *BaseModel) PK() any { + return m.Id +} + +// IsNew indicates what type of db query (insert or update) +// should be used with the model instance. +func (m *BaseModel) IsNew() bool { + return m.lastSavedPK == "" +} + +// MarkAsNew clears the pk field and marks the current model as "new" +// (aka. forces m.IsNew() to be true). +func (m *BaseModel) MarkAsNew() { + m.lastSavedPK = "" +} + +// MarkAsNew set the pk field to the Id value and marks the current model +// as NOT "new" (aka. forces m.IsNew() to be false). +func (m *BaseModel) MarkAsNotNew() { + m.lastSavedPK = m.Id +} + +// PostScan implements the [dbx.PostScanner] interface. +// +// It is usually executed right after the model is populated with the db row values. +func (m *BaseModel) PostScan() error { + m.MarkAsNotNew() + return nil +} diff --git a/core/db_model_test.go b/core/db_model_test.go new file mode 100644 index 00000000..1771a778 --- /dev/null +++ b/core/db_model_test.go @@ -0,0 +1,70 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" +) + +func TestBaseModel(t *testing.T) { + id := "test_id" + + m := core.BaseModel{Id: id} + + if m.PK() != id { + t.Fatalf("[before PostScan] Expected PK %q, got %q", "", m.PK()) + } + + if m.LastSavedPK() != "" { + t.Fatalf("[before PostScan] Expected LastSavedPK %q, got %q", "", m.LastSavedPK()) + } + + if !m.IsNew() { + t.Fatalf("[before PostScan] Expected IsNew %v, got %v", true, m.IsNew()) + } + + if err := m.PostScan(); err != nil { + t.Fatal(err) + } + + if m.PK() != id { + t.Fatalf("[after PostScan] Expected PK %q, got %q", "", m.PK()) + } + + if m.LastSavedPK() != id { + t.Fatalf("[after PostScan] Expected LastSavedPK %q, got %q", id, m.LastSavedPK()) + } + + if m.IsNew() { + t.Fatalf("[after PostScan] Expected IsNew %v, got %v", false, m.IsNew()) + } + + m.MarkAsNew() + + if m.PK() != id { + t.Fatalf("[after MarkAsNew] Expected PK %q, got %q", id, m.PK()) + } + + if m.LastSavedPK() != "" { + t.Fatalf("[after MarkAsNew] Expected LastSavedPK %q, got %q", "", m.LastSavedPK()) + } + + if !m.IsNew() { + t.Fatalf("[after MarkAsNew] Expected IsNew %v, got %v", true, m.IsNew()) + } + + // mark as not new without id + m.MarkAsNotNew() + + if m.PK() != id { + t.Fatalf("[after MarkAsNotNew] Expected PK %q, got %q", id, m.PK()) + } + + if m.LastSavedPK() != id { + t.Fatalf("[after MarkAsNotNew] Expected LastSavedPK %q, got %q", id, m.LastSavedPK()) + } + + if m.IsNew() { + t.Fatalf("[after MarkAsNotNew] Expected IsNew %v, got %v", false, m.IsNew()) + } +} diff --git a/daos/base_retry.go b/core/db_retry.go similarity index 85% rename from daos/base_retry.go rename to core/db_retry.go index 8be2409a..161a1e72 100644 --- a/daos/base_retry.go +++ b/core/db_retry.go @@ -1,4 +1,4 @@ -package daos +package core import ( "context" @@ -12,7 +12,10 @@ import ( ) // default retries intervals (in ms) -var defaultRetryIntervals = []int{100, 250, 350, 500, 700, 1000} +var defaultRetryIntervals = []int{50, 100, 150, 200, 300, 400, 500, 700, 1000} + +// default max retry attempts +const defaultMaxLockRetries = 12 func execLockRetry(timeout time.Duration, maxRetries int) dbx.ExecHookFunc { return func(q *dbx.Query, op func() error) error { @@ -45,7 +48,7 @@ Retry: if err != nil && attempt <= maxRetries && - // we are checking the err message to handle both the cgo and noncgo errors + // we are checking the plain error text to handle both cgo and noncgo errors strings.Contains(err.Error(), "database is locked") { // wait and retry time.Sleep(getDefaultRetryInterval(attempt)) diff --git a/core/db_retry_test.go b/core/db_retry_test.go new file mode 100644 index 00000000..85fb80b7 --- /dev/null +++ b/core/db_retry_test.go @@ -0,0 +1,65 @@ +package core + +import ( + "errors" + "fmt" + "testing" +) + +func TestGetDefaultRetryInterval(t *testing.T) { + t.Parallel() + + if i := getDefaultRetryInterval(-1); i.Milliseconds() != 1000 { + t.Fatalf("Expected 1000ms, got %v", i) + } + + if i := getDefaultRetryInterval(999); i.Milliseconds() != 1000 { + t.Fatalf("Expected 1000ms, got %v", i) + } + + if i := getDefaultRetryInterval(3); i.Milliseconds() != 200 { + t.Fatalf("Expected 500ms, got %v", i) + } +} + +func TestBaseLockRetry(t *testing.T) { + t.Parallel() + + scenarios := []struct { + err error + failUntilAttempt int + expectedAttempts int + }{ + {nil, 3, 1}, + {errors.New("test"), 3, 1}, + {errors.New("database is locked"), 3, 3}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.err), func(t *testing.T) { + lastAttempt := 0 + + err := baseLockRetry(func(attempt int) error { + lastAttempt = attempt + + if attempt < s.failUntilAttempt { + return s.err + } + + return nil + }, s.failUntilAttempt+2) + + if lastAttempt != s.expectedAttempts { + t.Errorf("Expected lastAttempt to be %d, got %d", s.expectedAttempts, lastAttempt) + } + + if s.failUntilAttempt == s.expectedAttempts && err != nil { + t.Fatalf("Expected nil, got err %v", err) + } + + if s.failUntilAttempt != s.expectedAttempts && s.err != nil && err == nil { + t.Fatalf("Expected error %q, got nil", s.err) + } + }) + } +} diff --git a/daos/table.go b/core/db_table.go similarity index 53% rename from daos/table.go rename to core/db_table.go index 1c2c2ac3..04183b79 100644 --- a/daos/table.go +++ b/core/db_table.go @@ -1,17 +1,17 @@ -package daos +package core import ( + "database/sql" "fmt" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" ) // HasTable checks if a table (or view) with the provided name exists (case insensitive). -func (dao *Dao) HasTable(tableName string) bool { +func (app *BaseApp) HasTable(tableName string) bool { var exists bool - err := dao.DB().Select("count(*)"). + err := app.DB().Select("(1)"). From("sqlite_schema"). AndWhere(dbx.HashExp{"type": []any{"table", "view"}}). AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:tableName})", dbx.Params{"tableName": tableName})). @@ -22,21 +22,33 @@ func (dao *Dao) HasTable(tableName string) bool { } // TableColumns returns all column names of a single table by its name. -func (dao *Dao) TableColumns(tableName string) ([]string, error) { +func (app *BaseApp) TableColumns(tableName string) ([]string, error) { columns := []string{} - err := dao.DB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})"). + err := app.DB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})"). Bind(dbx.Params{"tableName": tableName}). Column(&columns) return columns, err } -// TableInfo returns the `table_info` pragma result for the specified table. -func (dao *Dao) TableInfo(tableName string) ([]*models.TableInfoRow, error) { - info := []*models.TableInfoRow{} +type TableInfoRow struct { + // the `db:"pk"` tag has special semantic so we cannot rename + // the original field without specifying a custom mapper + PK int - err := dao.DB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})"). + Index int `db:"cid"` + Name string `db:"name"` + Type string `db:"type"` + NotNull bool `db:"notnull"` + DefaultValue sql.NullString `db:"dflt_value"` +} + +// TableInfo returns the "table_info" pragma result for the specified table. +func (app *BaseApp) TableInfo(tableName string) ([]*TableInfoRow, error) { + info := []*TableInfoRow{} + + err := app.DB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})"). Bind(dbx.Params{"tableName": tableName}). All(&info) if err != nil { @@ -55,13 +67,13 @@ func (dao *Dao) TableInfo(tableName string) ([]*models.TableInfoRow, error) { // TableIndexes returns a name grouped map with all non empty index of the specified table. // // Note: This method doesn't return an error on nonexisting table. -func (dao *Dao) TableIndexes(tableName string) (map[string]string, error) { +func (app *BaseApp) TableIndexes(tableName string) (map[string]string, error) { indexes := []struct { Name string Sql string }{} - err := dao.DB().Select("name", "sql"). + err := app.DB().Select("name", "sql"). From("sqlite_master"). AndWhere(dbx.NewExp("sql is not null")). AndWhere(dbx.HashExp{ @@ -86,10 +98,10 @@ func (dao *Dao) TableIndexes(tableName string) (map[string]string, error) { // // This method is a no-op if a table with the provided name doesn't exist. // -// Be aware that this method is vulnerable to SQL injection and the +// NB! Be aware that this method is vulnerable to SQL injection and the // "tableName" argument must come only from trusted input! -func (dao *Dao) DeleteTable(tableName string) error { - _, err := dao.DB().NewQuery(fmt.Sprintf( +func (app *BaseApp) DeleteTable(tableName string) error { + _, err := app.DB().NewQuery(fmt.Sprintf( "DROP TABLE IF EXISTS {{%s}}", tableName, )).Execute() @@ -97,10 +109,20 @@ func (dao *Dao) DeleteTable(tableName string) error { return err } -// Vacuum executes VACUUM on the current dao.DB() instance in order to -// reclaim unused db disk space. -func (dao *Dao) Vacuum() error { - _, err := dao.DB().NewQuery("VACUUM").Execute() +// Vacuum executes VACUUM on the current app.DB() instance +// in order to reclaim unused data db disk space. +func (app *BaseApp) Vacuum() error { + return app.vacuum(app.DB()) +} + +// AuxVacuum executes VACUUM on the current app.AuxDB() instance +// in order to reclaim unused auxiliary db disk space. +func (app *BaseApp) AuxVacuum() error { + return app.vacuum(app.AuxDB()) +} + +func (app *BaseApp) vacuum(db dbx.Builder) error { + _, err := db.NewQuery("VACUUM").Execute() return err } diff --git a/core/db_table_test.go b/core/db_table_test.go new file mode 100644 index 00000000..cec3c0d2 --- /dev/null +++ b/core/db_table_test.go @@ -0,0 +1,225 @@ +package core_test + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "slices" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestHasTable(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected bool + }{ + {"", false}, + {"test", false}, + {core.CollectionNameSuperusers, true}, + {"demo3", true}, + {"DEMO3", true}, // table names are case insensitives by default + {"view1", true}, // view + } + + for _, s := range scenarios { + t.Run(s.tableName, func(t *testing.T) { + result := app.HasTable(s.tableName) + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestTableColumns(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected []string + }{ + {"", nil}, + {"_params", []string{"id", "value", "created", "updated"}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.tableName), func(t *testing.T) { + columns, _ := app.TableColumns(s.tableName) + + if len(columns) != len(s.expected) { + t.Fatalf("Expected columns %v, got %v", s.expected, columns) + } + + for _, c := range columns { + if !slices.Contains(s.expected, c) { + t.Errorf("Didn't expect column %s", c) + } + } + }) + } +} + +func TestTableInfo(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected string + }{ + {"", "null"}, + {"missing", "null"}, + { + "_params", + `[{"PK":0,"Index":0,"Name":"created","Type":"TEXT","NotNull":true,"DefaultValue":{"String":"''","Valid":true}},{"PK":1,"Index":1,"Name":"id","Type":"TEXT","NotNull":true,"DefaultValue":{"String":"'r'||lower(hex(randomblob(7)))","Valid":true}},{"PK":0,"Index":2,"Name":"updated","Type":"TEXT","NotNull":true,"DefaultValue":{"String":"''","Valid":true}},{"PK":0,"Index":3,"Name":"value","Type":"JSON","NotNull":false,"DefaultValue":{"String":"NULL","Valid":true}}]`, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.tableName), func(t *testing.T) { + rows, _ := app.TableInfo(s.tableName) + + raw, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + + if str := string(raw); str != s.expected { + t.Fatalf("Expected\n%s\ngot\n%s", s.expected, str) + } + }) + } +} + +func TestTableIndexes(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected []string + }{ + {"", nil}, + {"missing", nil}, + { + core.CollectionNameSuperusers, + []string{"idx_email__pbc_3323866339", "idx_tokenKey__pbc_3323866339"}, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.tableName), func(t *testing.T) { + indexes, _ := app.TableIndexes(s.tableName) + + if len(indexes) != len(s.expected) { + t.Fatalf("Expected %d indexes, got %d\n%v", len(s.expected), len(indexes), indexes) + } + + for _, name := range s.expected { + if v, ok := indexes[name]; !ok || v == "" { + t.Fatalf("Expected non-empty index %q in \n%v", name, indexes) + } + } + }) + } +} + +func TestDeleteTable(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expectError bool + }{ + {"", true}, + {"test", false}, // missing tables are ignored + {"_admins", false}, + {"demo3", false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.tableName), func(t *testing.T) { + err := app.DeleteTable(s.tableName) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } + }) + } +} + +func TestVacuum(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + calledQueries := []string{} + app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + calledQueries = append(calledQueries, sql) + } + app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + calledQueries = append(calledQueries, sql) + } + + if err := app.Vacuum(); err != nil { + t.Fatal(err) + } + + if total := len(calledQueries); total != 1 { + t.Fatalf("Expected 1 query, got %d", total) + } + + if calledQueries[0] != "VACUUM" { + t.Fatalf("Expected VACUUM query, got %s", calledQueries[0]) + } +} + +func TestAuxVacuum(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + calledQueries := []string{} + app.AuxDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + calledQueries = append(calledQueries, sql) + } + app.AuxDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + calledQueries = append(calledQueries, sql) + } + + if err := app.AuxVacuum(); err != nil { + t.Fatal(err) + } + + if total := len(calledQueries); total != 1 { + t.Fatalf("Expected 1 query, got %d", total) + } + + if calledQueries[0] != "VACUUM" { + t.Fatalf("Expected VACUUM query, got %s", calledQueries[0]) + } +} diff --git a/core/db_test.go b/core/db_test.go new file mode 100644 index 00000000..2dc30998 --- /dev/null +++ b/core/db_test.go @@ -0,0 +1,113 @@ +package core_test + +import ( + "context" + "errors" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestGenerateDefaultRandomId(t *testing.T) { + t.Parallel() + + id1 := core.GenerateDefaultRandomId() + id2 := core.GenerateDefaultRandomId() + + if id1 == id2 { + t.Fatalf("Expected id1 and id2 to differ, got %q", id1) + } + + if l := len(id1); l != 15 { + t.Fatalf("Expected id1 length %d, got %d", 15, l) + } + + if l := len(id2); l != 15 { + t.Fatalf("Expected id2 length %d, got %d", 15, l) + } +} + +func TestModelQuery(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + modelsQuery := app.ModelQuery(&core.Collection{}) + logsModelQuery := app.AuxModelQuery(&core.Collection{}) + + if app.DB() == modelsQuery.Info().Builder { + t.Fatalf("ModelQuery() is not using app.DB()") + } + + if app.AuxDB() == logsModelQuery.Info().Builder { + t.Fatalf("AuxModelQuery() is not using app.AuxDB()") + } + + expectedSQL := "SELECT {{_collections}}.* FROM `_collections`" + for i, q := range []*dbx.SelectQuery{modelsQuery, logsModelQuery} { + sql := q.Build().SQL() + if sql != expectedSQL { + t.Fatalf("[%d] Expected select\n%s\ngot\n%s", i, expectedSQL, sql) + } + } +} + +func TestValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + u := &mockSuperusers{} + + testErr := errors.New("test") + + app.OnModelValidate().BindFunc(func(e *core.ModelEvent) error { + return testErr + }) + + err := app.Validate(u) + if err != testErr { + t.Fatalf("Expected error %v, got %v", testErr, err) + } +} + +func TestValidateWithContext(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + u := &mockSuperusers{} + + testErr := errors.New("test") + + app.OnModelValidate().BindFunc(func(e *core.ModelEvent) error { + if v := e.Context.Value("test"); v != 123 { + t.Fatalf("Expected 'test' context value %#v, got %#v", 123, v) + } + return testErr + }) + + //nolint:staticcheck + ctx := context.WithValue(context.Background(), "test", 123) + + err := app.ValidateWithContext(ctx, u) + if err != testErr { + t.Fatalf("Expected error %v, got %v", testErr, err) + } +} + +// ------------------------------------------------------------------- + +type mockSuperusers struct { + core.BaseModel + Email string `db:"email"` +} + +func (m *mockSuperusers) TableName() string { + return core.CollectionNameSuperusers +} diff --git a/core/db_tx.go b/core/db_tx.go new file mode 100644 index 00000000..53ef4f2b --- /dev/null +++ b/core/db_tx.go @@ -0,0 +1,105 @@ +package core + +import ( + "errors" + "fmt" + "sync" + + "github.com/pocketbase/dbx" +) + +// RunInTransaction wraps fn into a transaction for the regular app database. +// +// It is safe to nest RunInTransaction calls as long as you use the callback's txApp. +func (app *BaseApp) RunInTransaction(fn func(txApp App) error) error { + return app.runInTransaction(app.NonconcurrentDB(), fn, false) +} + +// AuxRunInTransaction wraps fn into a transaction for the auxiliary app database. +// +// It is safe to nest RunInTransaction calls as long as you use the callback's txApp. +func (app *BaseApp) AuxRunInTransaction(fn func(txApp App) error) error { + return app.runInTransaction(app.AuxNonconcurrentDB(), fn, true) +} + +func (app *BaseApp) runInTransaction(db dbx.Builder, fn func(txApp App) error, isForAuxDB bool) error { + switch txOrDB := db.(type) { + case *dbx.Tx: + // run as part of the already existing transaction + return fn(app) + case *dbx.DB: + var txApp *BaseApp + txErr := txOrDB.Transactional(func(tx *dbx.Tx) error { + txApp = app.createTxApp(tx, isForAuxDB) + return fn(txApp) + }) + + // execute all after event calls on transaction complete + if txApp != nil && txApp.txInfo != nil { + afterFuncErr := txApp.txInfo.runAfterFuncs(txErr) + if afterFuncErr != nil { + return errors.Join(txErr, afterFuncErr) + } + } + + return txErr + default: + return errors.New("failed to start transaction (unknown db type)") + } +} + +// createTxApp shallow clones the current app and assigns a new tx state. +func (app *BaseApp) createTxApp(tx *dbx.Tx, isForAuxDB bool) *BaseApp { + clone := *app + + if isForAuxDB { + clone.auxConcurrentDB = tx + clone.auxNonconcurrentDB = tx + } else { + clone.concurrentDB = tx + clone.nonconcurrentDB = tx + } + + clone.txInfo = &txAppInfo{ + parent: app, + isForAuxDB: isForAuxDB, + } + + return &clone +} + +type txAppInfo struct { + parent *BaseApp + afterFuncs []func(txErr error) error + mu sync.Mutex + isForAuxDB bool +} + +func (a *txAppInfo) onAfterFunc(fn func(txErr error) error) { + a.mu.Lock() + defer a.mu.Unlock() + + a.afterFuncs = append(a.afterFuncs, fn) +} + +// note: can be called only once because txAppInfo is cleared +func (a *txAppInfo) runAfterFuncs(txErr error) error { + a.mu.Lock() + defer a.mu.Unlock() + + var errs []error + + for _, call := range a.afterFuncs { + if err := call(txErr); err != nil { + errs = append(errs, err) + } + } + + a.afterFuncs = nil + + if len(errs) > 0 { + return fmt.Errorf("transaction afterFunc errors: %w", errors.Join(errs...)) + } + + return nil +} diff --git a/core/db_tx_test.go b/core/db_tx_test.go new file mode 100644 index 00000000..848a86b3 --- /dev/null +++ b/core/db_tx_test.go @@ -0,0 +1,235 @@ +package core_test + +import ( + "errors" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRunInTransaction(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + t.Run("failed nested transaction", func(t *testing.T) { + app.RunInTransaction(func(txApp core.App) error { + superuser, _ := txApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + + return txApp.RunInTransaction(func(tx2Dao core.App) error { + if err := tx2Dao.Delete(superuser); err != nil { + t.Fatal(err) + } + return errors.New("test error") + }) + }) + + // superuser should still exist + superuser, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if superuser == nil { + t.Fatal("Expected superuser test@example.com to not be deleted") + } + }) + + t.Run("successful nested transaction", func(t *testing.T) { + app.RunInTransaction(func(txApp core.App) error { + superuser, _ := txApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + + return txApp.RunInTransaction(func(tx2Dao core.App) error { + return tx2Dao.Delete(superuser) + }) + }) + + // superuser should have been deleted + superuser, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if superuser != nil { + t.Fatalf("Expected superuser test@example.com to be deleted, found %v", superuser) + } + }) +} + +func TestTransactionHooksCallsOnFailure(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + createHookCalls := 0 + updateHookCalls := 0 + deleteHookCalls := 0 + afterCreateHookCalls := 0 + afterUpdateHookCalls := 0 + afterDeleteHookCalls := 0 + + app.OnModelCreate().BindFunc(func(e *core.ModelEvent) error { + createHookCalls++ + return e.Next() + }) + + app.OnModelUpdate().BindFunc(func(e *core.ModelEvent) error { + updateHookCalls++ + return e.Next() + }) + + app.OnModelDelete().BindFunc(func(e *core.ModelEvent) error { + deleteHookCalls++ + return e.Next() + }) + + app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error { + afterCreateHookCalls++ + return e.Next() + }) + + app.OnModelAfterUpdateSuccess().BindFunc(func(e *core.ModelEvent) error { + afterUpdateHookCalls++ + return e.Next() + }) + + app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error { + afterDeleteHookCalls++ + return e.Next() + }) + + existingModel, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + + app.RunInTransaction(func(txApp1 core.App) error { + return txApp1.RunInTransaction(func(txApp2 core.App) error { + // test create + // --- + newModel := core.NewRecord(existingModel.Collection()) + newModel.SetEmail("test_new1@example.com") + newModel.SetPassword("1234567890") + if err := txApp2.Save(newModel); err != nil { + t.Fatal(err) + } + + // test update (twice) + // --- + if err := txApp2.Save(existingModel); err != nil { + t.Fatal(err) + } + if err := txApp2.Save(existingModel); err != nil { + t.Fatal(err) + } + + // test delete + // --- + if err := txApp2.Delete(newModel); err != nil { + t.Fatal(err) + } + + return errors.New("test_tx_error") + }) + }) + + if createHookCalls != 1 { + t.Errorf("Expected createHookCalls to be called 1 time, got %d", createHookCalls) + } + if updateHookCalls != 2 { + t.Errorf("Expected updateHookCalls to be called 2 times, got %d", updateHookCalls) + } + if deleteHookCalls != 1 { + t.Errorf("Expected deleteHookCalls to be called 1 time, got %d", deleteHookCalls) + } + if afterCreateHookCalls != 0 { + t.Errorf("Expected afterCreateHookCalls to be called 0 times, got %d", afterCreateHookCalls) + } + if afterUpdateHookCalls != 0 { + t.Errorf("Expected afterUpdateHookCalls to be called 0 times, got %d", afterUpdateHookCalls) + } + if afterDeleteHookCalls != 0 { + t.Errorf("Expected afterDeleteHookCalls to be called 0 times, got %d", afterDeleteHookCalls) + } +} + +func TestTransactionHooksCallsOnSuccess(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + createHookCalls := 0 + updateHookCalls := 0 + deleteHookCalls := 0 + afterCreateHookCalls := 0 + afterUpdateHookCalls := 0 + afterDeleteHookCalls := 0 + + app.OnModelCreate().BindFunc(func(e *core.ModelEvent) error { + createHookCalls++ + return e.Next() + }) + + app.OnModelUpdate().BindFunc(func(e *core.ModelEvent) error { + updateHookCalls++ + return e.Next() + }) + + app.OnModelDelete().BindFunc(func(e *core.ModelEvent) error { + deleteHookCalls++ + return e.Next() + }) + + app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error { + afterCreateHookCalls++ + return e.Next() + }) + + app.OnModelAfterUpdateSuccess().BindFunc(func(e *core.ModelEvent) error { + afterUpdateHookCalls++ + return e.Next() + }) + + app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error { + afterDeleteHookCalls++ + return e.Next() + }) + + existingModel, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + + app.RunInTransaction(func(txApp1 core.App) error { + return txApp1.RunInTransaction(func(txApp2 core.App) error { + // test create + // --- + newModel := core.NewRecord(existingModel.Collection()) + newModel.SetEmail("test_new1@example.com") + newModel.SetPassword("1234567890") + if err := txApp2.Save(newModel); err != nil { + t.Fatal(err) + } + + // test update (twice) + // --- + if err := txApp2.Save(existingModel); err != nil { + t.Fatal(err) + } + if err := txApp2.Save(existingModel); err != nil { + t.Fatal(err) + } + + // test delete + // --- + if err := txApp2.Delete(newModel); err != nil { + t.Fatal(err) + } + + return nil + }) + }) + + if createHookCalls != 1 { + t.Errorf("Expected createHookCalls to be called 1 time, got %d", createHookCalls) + } + if updateHookCalls != 2 { + t.Errorf("Expected updateHookCalls to be called 2 times, got %d", updateHookCalls) + } + if deleteHookCalls != 1 { + t.Errorf("Expected deleteHookCalls to be called 1 time, got %d", deleteHookCalls) + } + if afterCreateHookCalls != 1 { + t.Errorf("Expected afterCreateHookCalls to be called 1 time, got %d", afterCreateHookCalls) + } + if afterUpdateHookCalls != 2 { + t.Errorf("Expected afterUpdateHookCalls to be called 2 times, got %d", afterUpdateHookCalls) + } + if afterDeleteHookCalls != 1 { + t.Errorf("Expected afterDeleteHookCalls to be called 1 time, got %d", afterDeleteHookCalls) + } +} diff --git a/core/event_request.go b/core/event_request.go new file mode 100644 index 00000000..caf9ef34 --- /dev/null +++ b/core/event_request.go @@ -0,0 +1,195 @@ +package core + +import ( + "maps" + "net/netip" + "strings" + "sync" + + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/pocketbase/pocketbase/tools/router" +) + +// Common request store keys used by the middlewares and api handlers. +const ( + RequestEventKeyInfoContext = "infoContext" +) + +// RequestEvent defines the PocketBase router handler event. +type RequestEvent struct { + App App + + cachedRequestInfo *RequestInfo + + Auth *Record + + router.Event + + mu sync.Mutex +} + +// RealIP returns the "real" IP address from the configured trusted proxy headers. +// +// If Settings.TrustedProxy is not configured or the found IP is empty, +// it fallbacks to e.RemoteIP(). +// +// NB! +// Be careful when used in a security critical context as it relies on +// the trusted proxy to be properly configured and your app to be accessible only through it. +// If you are not sure, use e.RemoteIP(). +func (e *RequestEvent) RealIP() string { + settings := e.App.Settings() + + for _, h := range settings.TrustedProxy.Headers { + headerValues := e.Request.Header.Values(h) + if len(headerValues) == 0 { + continue + } + + // extract the last header value as it is expected to be the one controlled by the proxy + ipsList := headerValues[len(headerValues)-1] + if ipsList == "" { + continue + } + + ips := strings.Split(ipsList, ",") + + if settings.TrustedProxy.UseLeftmostIP { + for _, ip := range ips { + parsed, err := netip.ParseAddr(strings.TrimSpace(ip)) + if err == nil { + return parsed.StringExpanded() + } + } + } else { + for i := len(ips) - 1; i >= 0; i-- { + parsed, err := netip.ParseAddr(strings.TrimSpace(ips[i])) + if err == nil { + return parsed.StringExpanded() + } + } + } + } + + return e.RemoteIP() +} + +// HasSuperuserAuth checks whether the current RequestEvent has superuser authentication loaded. +func (e *RequestEvent) HasSuperuserAuth() bool { + return e.Auth != nil && e.Auth.IsSuperuser() +} + +// RequestInfo parses the current request into RequestInfo instance. +// +// Note that the returned result is cached to avoid copying the request data multiple times +// but the auth state and other common store items are always refreshed in case they were changed my another handler. +func (e *RequestEvent) RequestInfo() (*RequestInfo, error) { + e.mu.Lock() + defer e.mu.Unlock() + + if e.cachedRequestInfo != nil { + e.cachedRequestInfo.Auth = e.Auth + + infoCtx, _ := e.Get(RequestEventKeyInfoContext).(string) + if infoCtx != "" { + e.cachedRequestInfo.Context = infoCtx + } else { + e.cachedRequestInfo.Context = RequestInfoContextDefault + } + } else { + // (re)init e.cachedRequestInfo based on the current request event + if err := e.initRequestInfo(); err != nil { + return nil, err + } + } + + return e.cachedRequestInfo, nil +} + +func (e *RequestEvent) initRequestInfo() error { + infoCtx, _ := e.Get(RequestEventKeyInfoContext).(string) + if infoCtx == "" { + infoCtx = RequestInfoContextDefault + } + + info := &RequestInfo{ + Context: infoCtx, + Method: e.Request.Method, + Query: map[string]string{}, + Headers: map[string]string{}, + Body: map[string]any{}, + } + + if err := e.BindBody(&info.Body); err != nil { + return err + } + + // extract the first value of all query params + query := e.Request.URL.Query() + for k, v := range query { + if len(v) > 0 { + info.Query[k] = v[0] + } + } + + // extract the first value of all headers and normalizes the keys + // ("X-Token" is converted to "x_token") + for k, v := range e.Request.Header { + if len(v) > 0 { + info.Headers[inflector.Snakecase(k)] = v[0] + } + } + + info.Auth = e.Auth + + e.cachedRequestInfo = info + + return nil +} + +// ------------------------------------------------------------------- + +const ( + RequestInfoContextDefault = "default" + RequestInfoContextExpand = "expand" + RequestInfoContextRealtime = "realtime" + RequestInfoContextProtectedFile = "protectedFile" + RequestInfoContextOAuth2 = "oauth2" + RequestInfoContextBatch = "batch" +) + +// RequestInfo defines a HTTP request data struct, usually used +// as part of the `@request.*` filter resolver. +// +// The Query and Headers fields contains only the first value for each found entry. +type RequestInfo struct { + Query map[string]string `json:"query"` + Headers map[string]string `json:"headers"` + Body map[string]any `json:"body"` + Auth *Record `json:"auth"` + Method string `json:"method"` + Context string `json:"context"` +} + +// HasSuperuserAuth checks whether the current RequestInfo instance +// has superuser authentication loaded. +func (info *RequestInfo) HasSuperuserAuth() bool { + return info.Auth != nil && info.Auth.IsSuperuser() +} + +// Clone creates a new shallow copy of the current RequestInfo and its Auth record (if any). +func (info *RequestInfo) Clone() *RequestInfo { + clone := &RequestInfo{ + Method: info.Method, + Context: info.Context, + Query: maps.Clone(info.Query), + Body: maps.Clone(info.Body), + Headers: maps.Clone(info.Headers), + } + + if info.Auth != nil { + clone.Auth = info.Auth.Fresh() + } + + return clone +} diff --git a/core/event_request_batch.go b/core/event_request_batch.go new file mode 100644 index 00000000..1f24e075 --- /dev/null +++ b/core/event_request_batch.go @@ -0,0 +1,31 @@ +package core + +import ( + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type BatchRequestEvent struct { + *RequestEvent + + Batch []*InternalRequest +} + +type InternalRequest struct { + // note: for uploading files the value must be either *filesystem.File or []*filesystem.File + Body map[string]any `form:"body" json:"body"` + + Headers map[string]string `form:"headers" json:"headers"` + + Method string `form:"method" json:"method"` + + URL string `form:"url" json:"url"` +} + +func (br InternalRequest) Validate() error { + return validation.ValidateStruct(&br, + validation.Field(&br.Method, validation.Required, validation.In(http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)), + validation.Field(&br.URL, validation.Required, validation.Length(0, 2000)), + ) +} diff --git a/core/event_request_batch_test.go b/core/event_request_batch_test.go new file mode 100644 index 00000000..6feaf40d --- /dev/null +++ b/core/event_request_batch_test.go @@ -0,0 +1,74 @@ +package core_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestInternalRequestValidate(t *testing.T) { + scenarios := []struct { + name string + request core.InternalRequest + expectedErrors []string + }{ + { + "empty struct", + core.InternalRequest{}, + []string{"method", "url"}, + }, + + // method + { + "GET method", + core.InternalRequest{URL: "test", Method: http.MethodGet}, + []string{}, + }, + { + "POST method", + core.InternalRequest{URL: "test", Method: http.MethodPost}, + []string{}, + }, + { + "PUT method", + core.InternalRequest{URL: "test", Method: http.MethodPut}, + []string{}, + }, + { + "PATCH method", + core.InternalRequest{URL: "test", Method: http.MethodPatch}, + []string{}, + }, + { + "DELETE method", + core.InternalRequest{URL: "test", Method: http.MethodDelete}, + []string{}, + }, + { + "unknown method", + core.InternalRequest{URL: "test", Method: "unknown"}, + []string{"method"}, + }, + + // url + { + "url <= 2000", + core.InternalRequest{URL: strings.Repeat("a", 2000), Method: http.MethodGet}, + []string{}, + }, + { + "url > 2000", + core.InternalRequest{URL: strings.Repeat("a", 2001), Method: http.MethodGet}, + []string{"url"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + tests.TestValidationErrors(t, s.request.Validate(), s.expectedErrors) + }) + } +} diff --git a/core/event_request_test.go b/core/event_request_test.go new file mode 100644 index 00000000..41108025 --- /dev/null +++ b/core/event_request_test.go @@ -0,0 +1,334 @@ +package core_test + +import ( + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestEventRequestRealIP(t *testing.T) { + t.Parallel() + + headers := map[string][]string{ + "CF-Connecting-IP": {"1.2.3.4", "1.1.1.1"}, + "Fly-Client-IP": {"1.2.3.4", "1.1.1.2"}, + "X-Real-IP": {"1.2.3.4", "1.1.1.3,1.1.1.4"}, + "X-Forward-For": {"1.2.3.4", "invalid,1.1.1.5,1.1.1.6,invalid"}, + } + + scenarios := []struct { + name string + headers map[string][]string + trustedHeaders []string + useLeftmostIP bool + expected string + }{ + { + "no trusted headers", + headers, + nil, + false, + "127.0.0.1", + }, + { + "non-matching trusted header", + headers, + []string{"header1", "header2"}, + false, + "127.0.0.1", + }, + { + "trusted X-Real-IP (rightmost)", + headers, + []string{"header1", "x-real-ip", "x-forward-for"}, + false, + "1.1.1.4", + }, + { + "trusted X-Real-IP (leftmost)", + headers, + []string{"header1", "x-real-ip", "x-forward-for"}, + true, + "1.1.1.3", + }, + { + "trusted X-Forward-For (rightmost)", + headers, + []string{"header1", "x-forward-for"}, + false, + "1.1.1.6", + }, + { + "trusted X-Forward-For (leftmost)", + headers, + []string{"header1", "x-forward-for"}, + true, + "1.1.1.5", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, err := tests.NewTestApp() + if err != nil { + t.Fatal(err) + } + defer app.Cleanup() + + app.Settings().TrustedProxy.Headers = s.trustedHeaders + app.Settings().TrustedProxy.UseLeftmostIP = s.useLeftmostIP + + event := core.RequestEvent{} + event.App = app + + event.Request, err = http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + event.Request.RemoteAddr = "127.0.0.1:80" // fallback + + for k, values := range s.headers { + for _, v := range values { + event.Request.Header.Add(k, v) + } + } + + result := event.RealIP() + + if result != s.expected { + t.Fatalf("Expected ip %q, got %q", s.expected, result) + } + }) + } +} + +func TestEventRequestHasSuperUserAuth(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + record *core.Record + expected bool + }{ + {"nil record", nil, false}, + {"regular user record", user, false}, + {"superuser record", superuser, true}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + e := core.RequestEvent{} + e.Auth = s.record + + result := e.HasSuperuserAuth() + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRequestEventRequestInfo(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + userCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + user1 := core.NewRecord(userCol) + user1.Id = "user1" + user1.SetEmail("test1@example.com") + + user2 := core.NewRecord(userCol) + user2.Id = "user2" + user2.SetEmail("test2@example.com") + + testBody := `{"a":123,"b":"test"}` + + event := core.RequestEvent{} + event.Request, err = http.NewRequest("POST", "/test?q1=123&q2=456", strings.NewReader(testBody)) + if err != nil { + t.Fatal(err) + } + event.Request.Header.Add("content-type", "application/json") + event.Request.Header.Add("x-test", "test") + event.Set(core.RequestEventKeyInfoContext, "test") + event.Auth = user1 + + t.Run("init", func(t *testing.T) { + info, err := event.RequestInfo() + if err != nil { + t.Fatalf("Failed to resolve request info: %v", err) + } + + raw, err := json.Marshal(info) + if err != nil { + t.Fatalf("Failed to serialize request info: %v", err) + } + rawStr := string(raw) + + expected := `{"query":{"q1":"123","q2":"456"},"headers":{"content_type":"application/json","x_test":"test"},"body":{"a":123,"b":"test"},"auth":{"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users","created":"","emailVisibility":false,"file":[],"id":"user1","name":"","rel":"","updated":"","username":"","verified":false},"method":"POST","context":"test"}` + + if expected != rawStr { + t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr) + } + }) + + t.Run("change user and context", func(t *testing.T) { + event.Set(core.RequestEventKeyInfoContext, "test2") + event.Auth = user2 + + info, err := event.RequestInfo() + if err != nil { + t.Fatalf("Failed to resolve request info: %v", err) + } + + raw, err := json.Marshal(info) + if err != nil { + t.Fatalf("Failed to serialize request info: %v", err) + } + rawStr := string(raw) + + expected := `{"query":{"q1":"123","q2":"456"},"headers":{"content_type":"application/json","x_test":"test"},"body":{"a":123,"b":"test"},"auth":{"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users","created":"","emailVisibility":false,"file":[],"id":"user2","name":"","rel":"","updated":"","username":"","verified":false},"method":"POST","context":"test2"}` + + if expected != rawStr { + t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr) + } + }) +} + +func TestRequestInfoHasSuperuserAuth(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if err != nil { + t.Fatal(err) + } + + event := core.RequestEvent{} + event.Request, err = http.NewRequest("POST", "/test?q1=123&q2=456", strings.NewReader(`{"a":123,"b":"test"}`)) + if err != nil { + t.Fatal(err) + } + event.Request.Header.Add("content-type", "application/json") + + scenarios := []struct { + name string + record *core.Record + expected bool + }{ + {"nil record", nil, false}, + {"regular user record", user, false}, + {"superuser record", superuser, true}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + event.Auth = s.record + + info, err := event.RequestInfo() + if err != nil { + t.Fatalf("Failed to resolve request info: %v", err) + } + + result := info.HasSuperuserAuth() + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRequestInfoClone(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + userCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + user := core.NewRecord(userCol) + user.Id = "user1" + user.SetEmail("test1@example.com") + + event := core.RequestEvent{} + event.Request, err = http.NewRequest("POST", "/test?q1=123&q2=456", strings.NewReader(`{"a":123,"b":"test"}`)) + if err != nil { + t.Fatal(err) + } + event.Request.Header.Add("content-type", "application/json") + event.Auth = user + + info, err := event.RequestInfo() + if err != nil { + t.Fatalf("Failed to resolve request info: %v", err) + } + + clone := info.Clone() + + // modify the clone fields to ensure that it is a shallow copy + clone.Headers["new_header"] = "test" + clone.Query["new_query"] = "test" + clone.Body["new_body"] = "test" + clone.Auth.Id = "user2" // should be a Fresh copy of the record + + // check the original data + // --- + originalRaw, err := json.Marshal(info) + if err != nil { + t.Fatalf("Failed to serialize original request info: %v", err) + } + originalRawStr := string(originalRaw) + + expectedRawStr := `{"query":{"q1":"123","q2":"456"},"headers":{"content_type":"application/json"},"body":{"a":123,"b":"test"},"auth":{"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users","created":"","emailVisibility":false,"file":[],"id":"user1","name":"","rel":"","updated":"","username":"","verified":false},"method":"POST","context":"default"}` + if expectedRawStr != originalRawStr { + t.Fatalf("Expected original info\n%v\ngot\n%v", expectedRawStr, originalRawStr) + } + + // check the clone data + // --- + cloneRaw, err := json.Marshal(clone) + if err != nil { + t.Fatalf("Failed to serialize clone request info: %v", err) + } + cloneRawStr := string(cloneRaw) + + expectedCloneStr := `{"query":{"new_query":"test","q1":"123","q2":"456"},"headers":{"content_type":"application/json","new_header":"test"},"body":{"a":123,"b":"test","new_body":"test"},"auth":{"avatar":"","collectionId":"_pb_users_auth_","collectionName":"users","created":"","emailVisibility":false,"file":[],"id":"user2","name":"","rel":"","updated":"","username":"","verified":false},"method":"POST","context":"default"}` + if expectedCloneStr != cloneRawStr { + t.Fatalf("Expected clone info\n%v\ngot\n%v", expectedCloneStr, cloneRawStr) + } +} diff --git a/core/events.go b/core/events.go index d12e0e6e..12af18fc 100644 --- a/core/events.go +++ b/core/events.go @@ -1,49 +1,62 @@ package core import ( + "context" "net/http" "time" - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/models/settings" "github.com/pocketbase/pocketbase/tools/auth" - "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/subscriptions" "golang.org/x/crypto/acme/autocert" ) -var ( - _ hook.Tagger = (*BaseModelEvent)(nil) - _ hook.Tagger = (*BaseCollectionEvent)(nil) -) - -type BaseModelEvent struct { - Model models.Model +type HookTagger interface { + HookTags() []string } -func (e *BaseModelEvent) Tags() []string { +// ------------------------------------------------------------------- + +type baseModelEventData struct { + Model Model +} + +func (e *baseModelEventData) Tags() []string { if e.Model == nil { return nil } - if r, ok := e.Model.(*models.Record); ok && r.Collection() != nil { - return []string{r.Collection().Id, r.Collection().Name} + if ht, ok := e.Model.(HookTagger); ok { + return ht.HookTags() } return []string{e.Model.TableName()} } -type BaseCollectionEvent struct { - Collection *models.Collection +// ------------------------------------------------------------------- + +type baseRecordEventData struct { + Record *Record } -func (e *BaseCollectionEvent) Tags() []string { +func (e *baseRecordEventData) Tags() []string { + if e.Record == nil { + return nil + } + + return e.Record.HookTags() +} + +// ------------------------------------------------------------------- + +type baseCollectionEventData struct { + Collection *Collection +} + +func (e *baseCollectionEventData) Tags() []string { if e.Collection == nil { return nil } @@ -62,356 +75,466 @@ func (e *BaseCollectionEvent) Tags() []string { } // ------------------------------------------------------------------- -// Serve events data +// App events data // ------------------------------------------------------------------- type BootstrapEvent struct { + hook.Event App App } type TerminateEvent struct { + hook.Event App App IsRestart bool } +type BackupEvent struct { + hook.Event + App App + Context context.Context + Name string // the name of the backup to create/restore. + Exclude []string // list of dir entries to exclude from the backup create/restore. +} + type ServeEvent struct { + hook.Event App App - Router *echo.Echo + Router *router.Router[*RequestEvent] Server *http.Server CertManager *autocert.Manager } -type ApiErrorEvent struct { - HttpContext echo.Context - Error error +// ------------------------------------------------------------------- +// Settings events data +// ------------------------------------------------------------------- + +type SettingsListRequestEvent struct { + hook.Event + *RequestEvent + + Settings *Settings } -// ------------------------------------------------------------------- -// Model DAO events data -// ------------------------------------------------------------------- +type SettingsUpdateRequestEvent struct { + hook.Event + *RequestEvent -type ModelEvent struct { - BaseModelEvent + OldSettings *Settings + NewSettings *Settings +} - Dao *daos.Dao +type SettingsReloadEvent struct { + hook.Event + App App } // ------------------------------------------------------------------- // Mailer events data // ------------------------------------------------------------------- +type MailerEvent struct { + hook.Event + App App + + Mailer mailer.Mailer + Message *mailer.Message +} + type MailerRecordEvent struct { - BaseCollectionEvent - - MailClient mailer.Mailer - Message *mailer.Message - Record *models.Record - Meta map[string]any -} - -type MailerAdminEvent struct { - MailClient mailer.Mailer - Message *mailer.Message - Admin *models.Admin - Meta map[string]any + MailerEvent + baseRecordEventData + Meta map[string]any } // ------------------------------------------------------------------- -// Realtime API events data +// Model events data // ------------------------------------------------------------------- -type RealtimeConnectEvent struct { - HttpContext echo.Context - Client subscriptions.Client - IdleTimeout time.Duration +const ( + ModelEventTypeCreate = "create" + ModelEventTypeUpdate = "update" + ModelEventTypeDelete = "delete" + ModelEventTypeValidate = "validate" +) + +type ModelEvent struct { + hook.Event + App App + baseModelEventData + Context context.Context + + // Could be any of the ModelEventType* constants, like: + // - create + // - update + // - delete + // - validate + Type string } -type RealtimeDisconnectEvent struct { - HttpContext echo.Context - Client subscriptions.Client -} - -type RealtimeMessageEvent struct { - HttpContext echo.Context - Client subscriptions.Client - Message *subscriptions.Message -} - -type RealtimeSubscribeEvent struct { - HttpContext echo.Context - Client subscriptions.Client - Subscriptions []string +type ModelErrorEvent struct { + ModelEvent + Error error } // ------------------------------------------------------------------- -// Settings API events data +// Record events data // ------------------------------------------------------------------- -type SettingsListEvent struct { - HttpContext echo.Context - RedactedSettings *settings.Settings +type RecordEvent struct { + hook.Event + App App + baseRecordEventData + Context context.Context + + // Could be any of the ModelEventType* constants, like: + // - create + // - update + // - delete + // - validate + Type string } -type SettingsUpdateEvent struct { - HttpContext echo.Context - OldSettings *settings.Settings - NewSettings *settings.Settings +type RecordErrorEvent struct { + RecordEvent + Error error +} + +func syncModelEventWithRecordEvent(me *ModelEvent, re *RecordEvent) { + me.App = re.App + me.Context = re.Context + me.Type = re.Type + + // @todo enable if after profiling doesn't have significant impact + // skip for now to avoid excessive checks and assume that the + // Model and the Record fields still points to the same instance + // + // if _, ok := me.Model.(*Record); ok { + // me.Model = re.Record + // } else if proxy, ok := me.Model.(RecordProxy); ok { + // proxy.SetProxyRecord(re.Record) + // } +} + +func newRecordEventFromModelEvent(me *ModelEvent) (*RecordEvent, bool) { + record, ok := me.Model.(*Record) + if !ok { + proxy, ok := me.Model.(RecordProxy) + if !ok { + return nil, false + } + record = proxy.ProxyRecord() + } + + re := new(RecordEvent) + re.App = me.App + re.Context = me.Context + re.Type = me.Type + re.Record = record + + return re, true +} + +func newRecordErrorEventFromModelErrorEvent(me *ModelErrorEvent) (*RecordErrorEvent, bool) { + recordEvent, ok := newRecordEventFromModelEvent(&me.ModelEvent) + if !ok { + return nil, false + } + + re := new(RecordErrorEvent) + re.RecordEvent = *recordEvent + re.Error = me.Error + + return re, true +} + +func syncModelErrorEventWithRecordErrorEvent(me *ModelErrorEvent, re *RecordErrorEvent) { + syncModelEventWithRecordEvent(&me.ModelEvent, &re.RecordEvent) + me.Error = re.Error } // ------------------------------------------------------------------- -// Record CRUD API events data +// Collection events data // ------------------------------------------------------------------- -type RecordsListEvent struct { - BaseCollectionEvent +type CollectionEvent struct { + hook.Event + App App + baseCollectionEventData + Context context.Context - HttpContext echo.Context - Records []*models.Record - Result *search.Result + // Could be any of the ModelEventType* constants, like: + // - create + // - update + // - delete + // - validate + Type string } -type RecordViewEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record +type CollectionErrorEvent struct { + CollectionEvent + Error error } -type RecordCreateEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record - UploadedFiles map[string][]*filesystem.File +func syncModelEventWithCollectionEvent(me *ModelEvent, ce *CollectionEvent) { + me.App = ce.App + me.Context = ce.Context + me.Type = ce.Type + me.Model = ce.Collection } -type RecordUpdateEvent struct { - BaseCollectionEvent +func newCollectionEventFromModelEvent(me *ModelEvent) (*CollectionEvent, bool) { + record, ok := me.Model.(*Collection) + if !ok { + return nil, false + } - HttpContext echo.Context - Record *models.Record - UploadedFiles map[string][]*filesystem.File + ce := new(CollectionEvent) + ce.App = me.App + ce.Context = me.Context + ce.Type = me.Type + ce.Collection = record + + return ce, true } -type RecordDeleteEvent struct { - BaseCollectionEvent +func newCollectionErrorEventFromModelErrorEvent(me *ModelErrorEvent) (*CollectionErrorEvent, bool) { + collectionevent, ok := newCollectionEventFromModelEvent(&me.ModelEvent) + if !ok { + return nil, false + } - HttpContext echo.Context - Record *models.Record + ce := new(CollectionErrorEvent) + ce.CollectionEvent = *collectionevent + ce.Error = me.Error + + return ce, true } -// ------------------------------------------------------------------- -// Auth Record API events data -// ------------------------------------------------------------------- - -type RecordAuthEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record - Token string - Meta any -} - -type RecordAuthWithPasswordEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record - Identity string - Password string -} - -type RecordAuthWithOAuth2Event struct { - BaseCollectionEvent - - HttpContext echo.Context - ProviderName string - ProviderClient auth.Provider - Record *models.Record - OAuth2User *auth.AuthUser - IsNewRecord bool -} - -type RecordAuthRefreshEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordRequestPasswordResetEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordConfirmPasswordResetEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordRequestVerificationEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordConfirmVerificationEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordRequestEmailChangeEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordConfirmEmailChangeEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record -} - -type RecordListExternalAuthsEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record - ExternalAuths []*models.ExternalAuth -} - -type RecordUnlinkExternalAuthEvent struct { - BaseCollectionEvent - - HttpContext echo.Context - Record *models.Record - ExternalAuth *models.ExternalAuth -} - -// ------------------------------------------------------------------- -// Admin API events data -// ------------------------------------------------------------------- - -type AdminsListEvent struct { - HttpContext echo.Context - Admins []*models.Admin - Result *search.Result -} - -type AdminViewEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminCreateEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminUpdateEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminDeleteEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminAuthEvent struct { - HttpContext echo.Context - Admin *models.Admin - Token string -} - -type AdminAuthWithPasswordEvent struct { - HttpContext echo.Context - Admin *models.Admin - Identity string - Password string -} - -type AdminAuthRefreshEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminRequestPasswordResetEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -type AdminConfirmPasswordResetEvent struct { - HttpContext echo.Context - Admin *models.Admin -} - -// ------------------------------------------------------------------- -// Collection API events data -// ------------------------------------------------------------------- - -type CollectionsListEvent struct { - HttpContext echo.Context - Collections []*models.Collection - Result *search.Result -} - -type CollectionViewEvent struct { - BaseCollectionEvent - - HttpContext echo.Context -} - -type CollectionCreateEvent struct { - BaseCollectionEvent - - HttpContext echo.Context -} - -type CollectionUpdateEvent struct { - BaseCollectionEvent - - HttpContext echo.Context -} - -type CollectionDeleteEvent struct { - BaseCollectionEvent - - HttpContext echo.Context -} - -type CollectionsImportEvent struct { - HttpContext echo.Context - Collections []*models.Collection +func syncModelErrorEventWithCollectionErrorEvent(me *ModelErrorEvent, ce *CollectionErrorEvent) { + syncModelEventWithCollectionEvent(&me.ModelEvent, &ce.CollectionEvent) + me.Error = ce.Error } // ------------------------------------------------------------------- // File API events data // ------------------------------------------------------------------- -type FileTokenEvent struct { - BaseModelEvent +type FileTokenRequestEvent struct { + hook.Event + *RequestEvent - HttpContext echo.Context - Token string + Token string } -type FileDownloadEvent struct { - BaseCollectionEvent +type FileDownloadRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData - HttpContext echo.Context - Record *models.Record - FileField *schema.SchemaField - ServedPath string - ServedName string + Record *Record + FileField *FileField + ServedPath string + ServedName string +} + +// ------------------------------------------------------------------- +// Collection API events data +// ------------------------------------------------------------------- + +type CollectionsListRequestEvent struct { + hook.Event + *RequestEvent + + Collections []*Collection + Result *search.Result +} + +type CollectionsImportRequestEvent struct { + hook.Event + *RequestEvent + + CollectionsData []map[string]any + DeleteMissing bool +} + +type CollectionRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData +} + +// ------------------------------------------------------------------- +// Realtime API events data +// ------------------------------------------------------------------- + +type RealtimeConnectRequestEvent struct { + hook.Event + *RequestEvent + + Client subscriptions.Client + + // note: modifying it after the connect has no effect + IdleTimeout time.Duration +} + +type RealtimeMessageEvent struct { + hook.Event + *RequestEvent + + Client subscriptions.Client + Message *subscriptions.Message +} + +type RealtimeSubscribeRequestEvent struct { + hook.Event + *RequestEvent + + Client subscriptions.Client + Subscriptions []string +} + +// ------------------------------------------------------------------- +// Record CRUD API events data +// ------------------------------------------------------------------- + +type RecordsListRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + // @todo consider removing and maybe add as generic to the search.Result? + Records []*Record + Result *search.Result +} + +type RecordRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordEnrichEvent struct { + hook.Event + App App + baseRecordEventData + + RequestInfo *RequestInfo +} + +// ------------------------------------------------------------------- +// Auth Record API events data +// ------------------------------------------------------------------- + +type RecordCreateOTPRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + Password string +} + +type RecordAuthWithOTPRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + OTP *OTP +} + +type RecordAuthRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + Token string + Meta any + AuthMethod string +} + +type RecordAuthWithPasswordRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + Identity string + IdentityField string + Password string +} + +type RecordAuthWithOAuth2RequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + ProviderName string + ProviderClient auth.Provider + Record *Record + OAuth2User *auth.AuthUser + CreateData map[string]any + IsNewRecord bool +} + +type RecordAuthRefreshRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordRequestPasswordResetRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordConfirmPasswordResetRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordRequestVerificationRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordConfirmVerificationRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record +} + +type RecordRequestEmailChangeRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + NewEmail string +} + +type RecordConfirmEmailChangeRequestEvent struct { + hook.Event + *RequestEvent + baseCollectionEventData + + Record *Record + NewEmail string } diff --git a/core/events_test.go b/core/events_test.go deleted file mode 100644 index d4ee5ebc..00000000 --- a/core/events_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package core_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/list" -) - -func TestBaseCollectionEventTags(t *testing.T) { - c1 := new(models.Collection) - - c2 := new(models.Collection) - c2.Id = "a" - - c3 := new(models.Collection) - c3.Name = "b" - - c4 := new(models.Collection) - c4.Id = "a" - c4.Name = "b" - - scenarios := []struct { - collection *models.Collection - expectedTags []string - }{ - {c1, []string{}}, - {c2, []string{"a"}}, - {c3, []string{"b"}}, - {c4, []string{"a", "b"}}, - } - - for i, s := range scenarios { - event := new(core.BaseCollectionEvent) - event.Collection = s.collection - - tags := event.Tags() - - if len(s.expectedTags) != len(tags) { - t.Fatalf("[%d] Expected %v tags, got %v", i, s.expectedTags, tags) - } - - for _, tag := range s.expectedTags { - if !list.ExistInSlice(tag, tags) { - t.Fatalf("[%d] Expected %v tags, got %v", i, s.expectedTags, tags) - } - } - } -} - -func TestModelEventTags(t *testing.T) { - m1 := new(models.Admin) - - c := new(models.Collection) - c.Id = "a" - c.Name = "b" - m2 := models.NewRecord(c) - - scenarios := []struct { - model models.Model - expectedTags []string - }{ - {m1, []string{"_admins"}}, - {m2, []string{"a", "b"}}, - } - - for i, s := range scenarios { - event := new(core.ModelEvent) - event.Model = s.model - - tags := event.Tags() - - if len(s.expectedTags) != len(tags) { - t.Fatalf("[%d] Expected %v tags, got %v", i, s.expectedTags, tags) - } - - for _, tag := range s.expectedTags { - if !list.ExistInSlice(tag, tags) { - t.Fatalf("[%d] Expected %v tags, got %v", i, s.expectedTags, tags) - } - } - } -} diff --git a/core/external_auth_model.go b/core/external_auth_model.go new file mode 100644 index 00000000..3623dfa7 --- /dev/null +++ b/core/external_auth_model.go @@ -0,0 +1,140 @@ +package core + +import ( + "context" + "errors" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/types" +) + +var ( + _ Model = (*ExternalAuth)(nil) + _ PreValidator = (*ExternalAuth)(nil) + _ RecordProxy = (*ExternalAuth)(nil) +) + +const CollectionNameExternalAuths = "_externalAuths" + +// ExternalAuth defines a Record proxy for working with the externalAuths collection. +type ExternalAuth struct { + *Record +} + +// NewExternalAuth instantiates and returns a new blank *ExternalAuth model. +// +// Example usage: +// +// ea := core.NewExternalAuth(app) +// ea.SetRecordRef(user.Id) +// ea.SetCollectionRef(user.Collection().Id) +// ea.SetProvider("google") +// ea.SetProviderId("...") +// app.Save(ea) +func NewExternalAuth(app App) *ExternalAuth { + m := &ExternalAuth{} + + c, err := app.FindCachedCollectionByNameOrId(CollectionNameExternalAuths) + if err != nil { + // this is just to make tests easier since it is a system collection and it is expected to be always accessible + // (note: the loaded record is further checked on ExternalAuth.PreValidate()) + c = NewBaseCollection("@__invalid__") + } + + m.Record = NewRecord(c) + + return m +} + +// PreValidate implements the [PreValidator] interface and checks +// whether the proxy is properly loaded. +func (m *ExternalAuth) PreValidate(ctx context.Context, app App) error { + if m.Record == nil || m.Record.Collection().Name != CollectionNameExternalAuths { + return errors.New("missing or invalid ExternalAuth ProxyRecord") + } + + return nil +} + +// ProxyRecord returns the proxied Record model. +func (m *ExternalAuth) ProxyRecord() *Record { + return m.Record +} + +// SetProxyRecord loads the specified record model into the current proxy. +func (m *ExternalAuth) SetProxyRecord(record *Record) { + m.Record = record +} + +// CollectionRef returns the "collectionRef" field value. +func (m *ExternalAuth) CollectionRef() string { + return m.GetString("collectionRef") +} + +// SetCollectionRef updates the "collectionRef" record field value. +func (m *ExternalAuth) SetCollectionRef(collectionId string) { + m.Set("collectionRef", collectionId) +} + +// RecordRef returns the "recordRef" record field value. +func (m *ExternalAuth) RecordRef() string { + return m.GetString("recordRef") +} + +// SetRecordRef updates the "recordRef" record field value. +func (m *ExternalAuth) SetRecordRef(recordId string) { + m.Set("recordRef", recordId) +} + +// Provider returns the "provider" record field value. +func (m *ExternalAuth) Provider() string { + return m.GetString("provider") +} + +// SetProvider updates the "provider" record field value. +func (m *ExternalAuth) SetProvider(provider string) { + m.Set("provider", provider) +} + +// Provider returns the "providerId" record field value. +func (m *ExternalAuth) ProviderId() string { + return m.GetString("providerId") +} + +// SetProvider updates the "providerId" record field value. +func (m *ExternalAuth) SetProviderId(providerId string) { + m.Set("providerId", providerId) +} + +// Created returns the "created" record field value. +func (m *ExternalAuth) Created() types.DateTime { + return m.GetDateTime("created") +} + +// Updated returns the "updated" record field value. +func (m *ExternalAuth) Updated() types.DateTime { + return m.GetDateTime("updated") +} + +func (app *BaseApp) registerExternalAuthHooks() { + recordRefHooks[*ExternalAuth](app, CollectionNameExternalAuths, CollectionTypeAuth) + + app.OnRecordValidate(CollectionNameExternalAuths).Bind(&hook.Handler[*RecordEvent]{ + Func: func(e *RecordEvent) error { + providerNames := make([]any, 0, len(auth.Providers)) + for name := range auth.Providers { + providerNames = append(providerNames, name) + } + + provider := e.Record.GetString("provider") + if err := validation.Validate(provider, validation.Required, validation.In(providerNames...)); err != nil { + return validation.Errors{"provider": err} + } + + return e.Next() + }, + Priority: 99, + }) +} diff --git a/core/external_auth_model_test.go b/core/external_auth_model_test.go new file mode 100644 index 00000000..512f6771 --- /dev/null +++ b/core/external_auth_model_test.go @@ -0,0 +1,310 @@ +package core_test + +import ( + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewExternalAuth(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + if ea.Collection().Name != core.CollectionNameExternalAuths { + t.Fatalf("Expected record with %q collection, got %q", core.CollectionNameExternalAuths, ea.Collection().Name) + } +} + +func TestExternalAuthProxyRecord(t *testing.T) { + t.Parallel() + + record := core.NewRecord(core.NewBaseCollection("test")) + record.Id = "test_id" + + ea := core.ExternalAuth{} + ea.SetProxyRecord(record) + + if ea.ProxyRecord() == nil || ea.ProxyRecord().Id != record.Id { + t.Fatalf("Expected proxy record with id %q, got %v", record.Id, ea.ProxyRecord()) + } +} + +func TestExternalAuthRecordRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + ea.SetRecordRef(testValue) + + if v := ea.RecordRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := ea.GetString("recordRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestExternalAuthCollectionRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + ea.SetCollectionRef(testValue) + + if v := ea.CollectionRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := ea.GetString("collectionRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestExternalAuthProvider(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + ea.SetProvider(testValue) + + if v := ea.Provider(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := ea.GetString("provider"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestExternalAuthProviderId(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + ea.SetProviderId(testValue) + + if v := ea.ProviderId(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := ea.GetString("providerId"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestExternalAuthCreated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + if v := ea.Created().String(); v != "" { + t.Fatalf("Expected empty created, got %q", v) + } + + now := types.NowDateTime() + ea.SetRaw("created", now) + + if v := ea.Created().String(); v != now.String() { + t.Fatalf("Expected %q created, got %q", now.String(), v) + } +} + +func TestExternalAuthUpdated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + ea := core.NewExternalAuth(app) + + if v := ea.Updated().String(); v != "" { + t.Fatalf("Expected empty updated, got %q", v) + } + + now := types.NowDateTime() + ea.SetRaw("updated", now) + + if v := ea.Updated().String(); v != now.String() { + t.Fatalf("Expected %q updated, got %q", now.String(), v) + } +} + +func TestExternalAuthPreValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + externalAuthsCol, err := app.FindCollectionByNameOrId(core.CollectionNameExternalAuths) + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + t.Run("no proxy record", func(t *testing.T) { + externalAuth := &core.ExternalAuth{} + + if err := app.Validate(externalAuth); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("non-ExternalAuth collection", func(t *testing.T) { + externalAuth := &core.ExternalAuth{} + externalAuth.SetProxyRecord(core.NewRecord(core.NewBaseCollection("invalid"))) + externalAuth.SetRecordRef(user.Id) + externalAuth.SetCollectionRef(user.Collection().Id) + externalAuth.SetProvider("gitlab") + externalAuth.SetProviderId("test123") + + if err := app.Validate(externalAuth); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("ExternalAuth collection", func(t *testing.T) { + externalAuth := &core.ExternalAuth{} + externalAuth.SetProxyRecord(core.NewRecord(externalAuthsCol)) + externalAuth.SetRecordRef(user.Id) + externalAuth.SetCollectionRef(user.Collection().Id) + externalAuth.SetProvider("gitlab") + externalAuth.SetProviderId("test123") + + if err := app.Validate(externalAuth); err != nil { + t.Fatalf("Expected nil validation error, got %v", err) + } + }) +} + +func TestExternalAuthValidateHook(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + externalAuth func() *core.ExternalAuth + expectErrors []string + }{ + { + "empty", + func() *core.ExternalAuth { + return core.NewExternalAuth(app) + }, + []string{"collectionRef", "recordRef", "provider", "providerId"}, + }, + { + "non-auth collection", + func() *core.ExternalAuth { + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(demo1.Collection().Id) + ea.SetRecordRef(demo1.Id) + ea.SetProvider("gitlab") + ea.SetProviderId("test123") + return ea + }, + []string{"collectionRef"}, + }, + { + "disabled provider", + func() *core.ExternalAuth { + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(user.Collection().Id) + ea.SetRecordRef("missing") + ea.SetProvider("apple") + ea.SetProviderId("test123") + return ea + }, + []string{"recordRef"}, + }, + { + "missing record id", + func() *core.ExternalAuth { + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(user.Collection().Id) + ea.SetRecordRef("missing") + ea.SetProvider("gitlab") + ea.SetProviderId("test123") + return ea + }, + []string{"recordRef"}, + }, + { + "valid ref", + func() *core.ExternalAuth { + ea := core.NewExternalAuth(app) + ea.SetCollectionRef(user.Collection().Id) + ea.SetRecordRef(user.Id) + ea.SetProvider("gitlab") + ea.SetProviderId("test123") + return ea + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := app.Validate(s.externalAuth()) + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/external_auth_query.go b/core/external_auth_query.go new file mode 100644 index 00000000..9331cfde --- /dev/null +++ b/core/external_auth_query.go @@ -0,0 +1,61 @@ +package core + +import ( + "github.com/pocketbase/dbx" +) + +// FindAllExternalAuthsByRecord returns all ExternalAuth models +// linked to the provided auth record. +func (app *BaseApp) FindAllExternalAuthsByRecord(authRecord *Record) ([]*ExternalAuth, error) { + auths := []*ExternalAuth{} + + err := app.RecordQuery(CollectionNameExternalAuths). + AndWhere(dbx.HashExp{ + "collectionRef": authRecord.Collection().Id, + "recordRef": authRecord.Id, + }). + OrderBy("created DESC"). + All(&auths) + + if err != nil { + return nil, err + } + + return auths, nil +} + +// FindAllExternalAuthsByCollection returns all ExternalAuth models +// linked to the provided auth collection. +func (app *BaseApp) FindAllExternalAuthsByCollection(collection *Collection) ([]*ExternalAuth, error) { + auths := []*ExternalAuth{} + + err := app.RecordQuery(CollectionNameExternalAuths). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + OrderBy("created DESC"). + All(&auths) + + if err != nil { + return nil, err + } + + return auths, nil +} + +// FindFirstExternalAuthByExpr returns the first available (the most recent created) +// ExternalAuth model that satisfies the non-nil expression. +func (app *BaseApp) FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalAuth, error) { + model := &ExternalAuth{} + + err := app.RecordQuery(CollectionNameExternalAuths). + AndWhere(dbx.Not(dbx.HashExp{"providerId": ""})). // exclude empty providerIds + AndWhere(expr). + OrderBy("created DESC"). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} diff --git a/core/external_auth_query_test.go b/core/external_auth_query_test.go new file mode 100644 index 00000000..eaf51d00 --- /dev/null +++ b/core/external_auth_query_test.go @@ -0,0 +1,176 @@ +package core_test + +import ( + "fmt" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestFindAllExternalAuthsByRecord(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser1, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if err != nil { + t.Fatal(err) + } + + user1, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + user2, err := app.FindAuthRecordByEmail("users", "test2@example.com") + if err != nil { + t.Fatal(err) + } + + user3, err := app.FindAuthRecordByEmail("users", "test3@example.com") + if err != nil { + t.Fatal(err) + } + + client1, err := app.FindAuthRecordByEmail("clients", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + expected []string + }{ + {demo1, nil}, + {superuser1, nil}, + {client1, []string{"f1z5b3843pzc964"}}, + {user1, []string{"clmflokuq1xl341", "dlmflokuq1xl342"}}, + {user2, nil}, + {user3, []string{"5eto7nmys833164"}}, + } + + for _, s := range scenarios { + t.Run(s.record.Collection().Name+"_"+s.record.Id, func(t *testing.T) { + result, err := app.FindAllExternalAuthsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total models %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindAllExternalAuthsByCollection(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + + clients, err := app.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + users, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + collection *core.Collection + expected []string + }{ + {demo1, nil}, + {superusers, nil}, + {clients, []string{ + "f1z5b3843pzc964", + }}, + {users, []string{ + "5eto7nmys833164", + "clmflokuq1xl341", + "dlmflokuq1xl342", + }}, + } + + for _, s := range scenarios { + t.Run(s.collection.Name, func(t *testing.T) { + result, err := app.FindAllExternalAuthsByCollection(s.collection) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total models %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindFirstExternalAuthByExpr(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + expr dbx.Expression + expectedId string + }{ + {dbx.HashExp{"collectionRef": "invalid"}, ""}, + {dbx.HashExp{"collectionRef": "_pb_users_auth_"}, "5eto7nmys833164"}, + {dbx.HashExp{"collectionRef": "_pb_users_auth_", "provider": "gitlab"}, "dlmflokuq1xl342"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%v", i, s.expr.Build(app.DB().(*dbx.DB), dbx.Params{})), func(t *testing.T) { + result, err := app.FindFirstExternalAuthByExpr(s.expr) + + hasErr := err != nil + expectErr := s.expectedId == "" + if hasErr != expectErr { + t.Fatalf("Expected hasErr %v, got %v", expectErr, hasErr) + } + + if hasErr { + return + } + + if result.Id != s.expectedId { + t.Errorf("Expected id %q, got %q", s.expectedId, result.Id) + } + }) + } +} diff --git a/core/field.go b/core/field.go new file mode 100644 index 00000000..e4645d0b --- /dev/null +++ b/core/field.go @@ -0,0 +1,250 @@ +package core + +import ( + "context" + "database/sql/driver" + "regexp" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/list" +) + +var fieldNameRegex = regexp.MustCompile(`^\w+$`) + +// Commonly used field names. +const ( + FieldNameId = "id" + FieldNameCollectionId = "collectionId" + FieldNameCollectionName = "collectionName" + FieldNameExpand = "expand" + FieldNameEmail = "email" + FieldNameEmailVisibility = "emailVisibility" + FieldNameVerified = "verified" + FieldNameTokenKey = "tokenKey" + FieldNamePassword = "password" +) + +// SystemFields returns special internal field names that are usually readonly. +var SystemDynamicFieldNames = []string{ + FieldNameCollectionId, + FieldNameCollectionName, + FieldNameExpand, +} + +// Common RecordInterceptor action names. +const ( + InterceptorActionValidate = "validate" + InterceptorActionDelete = "delete" + InterceptorActionDeleteExecute = "deleteExecute" + InterceptorActionAfterDelete = "afterDelete" + InterceptorActionAfterDeleteError = "afterDeleteError" + InterceptorActionCreate = "create" + InterceptorActionCreateExecute = "createExecute" + InterceptorActionAfterCreate = "afterCreate" + InterceptorActionAfterCreateError = "afterCreateFailure" + InterceptorActionUpdate = "update" + InterceptorActionUpdateExecute = "updateExecute" + InterceptorActionAfterUpdate = "afterUpdate" + InterceptorActionAfterUpdateError = "afterUpdateError" +) + +// Common field errors. +var ( + ErrUnknownField = validation.NewError("validation_unknown_field", "Unknown or invalid field.") + ErrInvalidFieldValue = validation.NewError("validation_invalid_field_value", "Invalid field value.") + ErrMustBeSystemAndHidden = validation.NewError("validation_must_be_system_and_hidden", `The field must be marked as "System" and "Hidden".`) + ErrMustBeSystem = validation.NewError("validation_must_be_system", `The field must be marked as "System".`) +) + +// FieldFactoryFunc defines a simple function to construct a specific Field instance. +type FieldFactoryFunc func() Field + +// Fields holds all available collection fields. +var Fields = map[string]FieldFactoryFunc{} + +// Field defines a common interface that all Collection fields should implement. +type Field interface { + // note: the getters has an explicit "Get" prefix to avoid conflicts with their related field members + + // GetId returns the field id. + GetId() string + + // SetId changes the field id. + SetId(id string) + + // GetName returns the field name. + GetName() string + + // SetName changes the field name. + SetName(name string) + + // GetSystem returns the field system flag state. + GetSystem() bool + + // SetSystem changes the field system flag state. + SetSystem(system bool) + + // GetHidden returns the field hidden flag state. + GetHidden() bool + + // SetHidden changes the field hidden flag state. + SetHidden(hidden bool) + + // Type returns the unique type of the field. + Type() string + + // ColumnType returns the DB column definition of the field. + ColumnType(app App) string + + // PrepareValue returns a properly formatted field value based on the provided raw one. + // + // This method is also called on record construction to initialize its default field value. + PrepareValue(record *Record, raw any) (any, error) + + // ValidateSettings validates the current field value associated with the provided record. + ValidateValue(ctx context.Context, app App, record *Record) error + + // ValidateSettings validates the current field settings. + ValidateSettings(ctx context.Context, app App, collection *Collection) error +} + +// MaxBodySizeCalculator defines an optional field interface for +// specifying the max size of a field value. +type MaxBodySizeCalculator interface { + // CalculateMaxBodySize returns the approximate max body size of a field value. + CalculateMaxBodySize() int64 +} + +type ( + SetterFunc func(record *Record, raw any) + + // SetterFinder defines a field interface for registering custom field value setters. + SetterFinder interface { + // FindSetter returns a single field value setter function + // by performing pattern-like field matching using the specified key. + // + // The key is usually just the field name but it could also + // contains "modifier" characters based on which you can perform custom set operations + // (ex. "users+" could be mapped to a function that will append new user to the existing field value). + // + // Return nil if you want to fallback to the default field value setter. + FindSetter(key string) SetterFunc + } +) + +type ( + GetterFunc func(record *Record) any + + // GetterFinder defines a field interface for registering custom field value getters. + GetterFinder interface { + // FindGetter returns a single field value getter function + // by performing pattern-like field matching using the specified key. + // + // The key is usually just the field name but it could also + // contains "modifier" characters based on which you can perform custom get operations + // (ex. "description:excerpt" could be mapped to a function that will return an excerpt of the current field value). + // + // Return nil if you want to fallback to the default field value setter. + FindGetter(key string) GetterFunc + } +) + +// DriverValuer defines a Field interface for exporting and formatting +// a field value for the database. +type DriverValuer interface { + // DriverValue exports a single field value for persistence in the database. + DriverValue(record *Record) (driver.Value, error) +} + +// MultiValuer defines a field interface that every multi-valued (eg. with MaxSelect) field has. +type MultiValuer interface { + // IsMultiple checks whether the field is configured to support multiple or single values. + IsMultiple() bool +} + +// RecordInterceptor defines a field interface for reacting to various +// Record related operations (create, delete, validate, etc.). +type RecordInterceptor interface { + // Interceptor is invoked when a specific record action occurs + // allowing you to perform extra validations and normalization + // (ex. uploading or deleting files). + // + // Note that users must call actionFunc() manually if they want to + // execute the specific record action. + Intercept( + ctx context.Context, + app App, + record *Record, + actionName string, + actionFunc func() error, + ) error +} + +// DefaultFieldIdValidationRule performs base validation on a field id value. +func DefaultFieldIdValidationRule(value any) error { + v, ok := value.(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + rules := []validation.Rule{ + validation.Required, + validation.Length(1, 255), + } + + for _, r := range rules { + if err := r.Validate(v); err != nil { + return err + } + } + + return nil +} + +// exclude special filter and system literals +var excludeNames = append([]any{ + "null", "true", "false", "_rowid_", +}, list.ToInterfaceSlice(SystemDynamicFieldNames)...) + +// DefaultFieldIdValidationRule performs base validation on a field name value. +func DefaultFieldNameValidationRule(value any) error { + v, ok := value.(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + rules := []validation.Rule{ + validation.Required, + validation.Length(1, 255), + validation.Match(fieldNameRegex), + validation.NotIn(excludeNames...), + validation.By(checkForVia), + } + + for _, r := range rules { + if err := r.Validate(v); err != nil { + return err + } + } + + return nil +} + +func checkForVia(value any) error { + v, _ := value.(string) + if v == "" { + return nil + } + + if strings.Contains(strings.ToLower(v), "_via_") { + return validation.NewError("validation_found_via", `The value cannot contain "_via_".`) + } + + return nil +} + +func noopSetter(record *Record, raw any) { + // do nothing +} diff --git a/core/field_autodate.go b/core/field_autodate.go new file mode 100644 index 00000000..9b54211a --- /dev/null +++ b/core/field_autodate.go @@ -0,0 +1,176 @@ +package core + +import ( + "context" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + Fields[FieldTypeAutodate] = func() Field { + return &AutodateField{} + } +} + +const FieldTypeAutodate = "autodate" + +var ( + _ Field = (*AutodateField)(nil) + _ SetterFinder = (*AutodateField)(nil) + _ RecordInterceptor = (*AutodateField)(nil) +) + +// AutodateField defines an "autodate" type field, aka. +// field which datetime value could be auto set on record create/update. +// +// 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"` + + // --- + + // OnCreate auto sets the current datetime as field value on record create. + OnCreate bool `form:"onCreate" json:"onCreate"` + + // OnUpdate auto sets the current datetime as field value on record update. + OnUpdate bool `form:"onUpdate" json:"onUpdate"` +} + +// Type implements [Field.Type] interface method. +func (f *AutodateField) Type() string { + return FieldTypeAutodate +} + +// GetId implements [Field.GetId] interface method. +func (f *AutodateField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *AutodateField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *AutodateField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *AutodateField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *AutodateField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *AutodateField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *AutodateField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *AutodateField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *AutodateField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" // note: sqlite doesn't allow adding new columns with non-constant defaults +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *AutodateField) PrepareValue(record *Record, raw any) (any, error) { + val, _ := types.ParseDateTime(raw) + return val, nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *AutodateField) ValidateValue(ctx context.Context, app App, record *Record) error { + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *AutodateField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + oldOnCreate := f.OnCreate + oldOnUpdate := f.OnUpdate + + oldCollection, _ := app.FindCollectionByNameOrId(collection.Id) + if oldCollection != nil { + oldField, ok := oldCollection.Fields.GetById(f.Id).(*AutodateField) + if ok && oldField != nil { + oldOnCreate = oldField.OnCreate + oldOnUpdate = oldField.OnUpdate + } + } + + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field( + &f.OnCreate, + validation.When(f.System, validation.By(validators.Equal(oldOnCreate))), + validation.Required.Error("either onCreate or onUpdate must be enabled").When(!f.OnUpdate), + ), + validation.Field( + &f.OnUpdate, + validation.When(f.System, validation.By(validators.Equal(oldOnUpdate))), + validation.Required.Error("either onCreate or onUpdate must be enabled").When(!f.OnCreate), + ), + ) +} + +// FindSetter implements the [SetterFinder] interface. +func (f *AutodateField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + // return noopSetter to disallow updating the value with record.Set() + return noopSetter + default: + return nil + } +} + +// Intercept implements the [RecordInterceptor] interface. +func (f *AutodateField) Intercept( + ctx context.Context, + app App, + record *Record, + actionName string, + actionFunc func() error, +) error { + switch actionName { + case InterceptorActionCreate: + // ignore for custom date manually set with record.SetRaw() + if f.OnCreate && !f.hasBeenManuallyChanged(record) { + record.SetRaw(f.Name, types.NowDateTime()) + } + case InterceptorActionUpdate: + // ignore for custom date manually set with record.SetRaw() + if f.OnUpdate && !f.hasBeenManuallyChanged(record) { + record.SetRaw(f.Name, types.NowDateTime()) + } + } + + return actionFunc() +} + +func (f *AutodateField) hasBeenManuallyChanged(record *Record) bool { + vNew, _ := record.GetRaw(f.Name).(types.DateTime) + vOld, _ := record.Original().GetRaw(f.Name).(types.DateTime) + + return vNew.String() != vOld.String() +} diff --git a/core/field_autodate_test.go b/core/field_autodate_test.go new file mode 100644 index 00000000..94fe0ec9 --- /dev/null +++ b/core/field_autodate_test.go @@ -0,0 +1,349 @@ +package core_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestAutodateFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeAutodate) +} + +func TestAutodateFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.AutodateField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestAutodateFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.AutodateField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"invalid", ""}, + {"2024-01-01 00:11:22.345Z", "2024-01-01 00:11:22.345Z"}, + {time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC), "2024-01-02 03:04:05.000Z"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vDate, ok := v.(types.DateTime) + if !ok { + t.Fatalf("Expected types.DateTime instance, got %T", v) + } + + if vDate.String() != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestAutodateFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.AutodateField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.AutodateField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + false, + }, + { + "missing field value", + &core.AutodateField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("abc", true) + return record + }, + false, + }, + { + "existing field value", + &core.AutodateField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.NowDateTime()) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestAutodateFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeAutodate) + testDefaultFieldNameValidation(t, core.FieldTypeAutodate) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + field func() *core.AutodateField + expectErrors []string + }{ + { + "empty onCreate and onUpdate", + func() *core.AutodateField { + return &core.AutodateField{ + Id: "test", + Name: "test", + } + }, + []string{"onCreate", "onUpdate"}, + }, + { + "with onCreate", + func() *core.AutodateField { + return &core.AutodateField{ + Id: "test", + Name: "test", + OnCreate: true, + } + }, + []string{}, + }, + { + "with onUpdate", + func() *core.AutodateField { + return &core.AutodateField{ + Id: "test", + Name: "test", + OnUpdate: true, + } + }, + []string{}, + }, + { + "change of a system autodate field", + func() *core.AutodateField { + created := superusers.Fields.GetByName("created").(*core.AutodateField) + created.OnCreate = !created.OnCreate + created.OnUpdate = !created.OnUpdate + return created + }, + []string{"onCreate", "onUpdate"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, superusers) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestAutodateFieldFindSetter(t *testing.T) { + field := &core.AutodateField{Name: "test"} + + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(field) + + initialDate, err := types.ParseDateTime("2024-01-02 03:04:05.789Z") + if err != nil { + t.Fatal(err) + } + + record := core.NewRecord(collection) + record.SetRaw("test", initialDate) + + t.Run("no matching setter", func(t *testing.T) { + f := field.FindSetter("abc") + if f != nil { + t.Fatal("Expected nil setter") + } + }) + + t.Run("matching setter", func(t *testing.T) { + f := field.FindSetter("test") + if f == nil { + t.Fatal("Expected non-nil setter") + } + + f(record, types.NowDateTime()) // should be ignored + + if v := record.GetString("test"); v != "2024-01-02 03:04:05.789Z" { + t.Fatalf("Expected no value change, got %q", v) + } + }) +} + +func TestAutodateFieldIntercept(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + initialDate, err := types.ParseDateTime("2024-01-02 03:04:05.789Z") + if err != nil { + t.Fatal(err) + } + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + actionName string + field *core.AutodateField + record func() *core.Record + expected string + }{ + { + "non-matching action", + "test", + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true}, + func() *core.Record { + return core.NewRecord(collection) + }, + "", + }, + { + "create with zero value (disabled onCreate)", + core.InterceptorActionCreate, + &core.AutodateField{Name: "test", OnCreate: false, OnUpdate: true}, + func() *core.Record { + return core.NewRecord(collection) + }, + "", + }, + { + "create with zero value", + core.InterceptorActionCreate, + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true}, + func() *core.Record { + return core.NewRecord(collection) + }, + "{NOW}", + }, + { + "create with non-zero value", + core.InterceptorActionCreate, + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", initialDate) + return record + }, + initialDate.String(), + }, + { + "update with zero value (disabled onUpdate)", + core.InterceptorActionUpdate, + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: false}, + func() *core.Record { + return core.NewRecord(collection) + }, + "", + }, + { + "update with zero value", + core.InterceptorActionUpdate, + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true}, + func() *core.Record { + return core.NewRecord(collection) + }, + "{NOW}", + }, + { + "update with non-zero value", + core.InterceptorActionUpdate, + &core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", initialDate) + return record + }, + initialDate.String(), + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + actionCalls := 0 + record := s.record() + + now := types.NowDateTime().String() + err := s.field.Intercept(context.Background(), app, record, s.actionName, func() error { + actionCalls++ + return nil + }) + if err != nil { + t.Fatal(err) + } + + if actionCalls != 1 { + t.Fatalf("Expected actionCalls %d, got %d", 1, actionCalls) + } + + expected := cutMilliseconds(strings.ReplaceAll(s.expected, "{NOW}", now)) + + v := cutMilliseconds(record.GetString(s.field.GetName())) + if v != expected { + t.Fatalf("Expected value %q, got %q", expected, v) + } + }) + } +} + +func cutMilliseconds(datetime string) string { + if len(datetime) > 19 { + return datetime[:19] + } + return datetime +} diff --git a/core/field_bool.go b/core/field_bool.go new file mode 100644 index 00000000..cd4e3786 --- /dev/null +++ b/core/field_bool.go @@ -0,0 +1,110 @@ +package core + +import ( + "context" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeBool] = func() Field { + return &BoolField{} + } +} + +const FieldTypeBool = "bool" + +var _ Field = (*BoolField)(nil) + +// BoolField defines "bool" type field to store a single true/false value. +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"` + + // --- + + // Required will require the field value to be always "true". + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *BoolField) Type() string { + return FieldTypeBool +} + +// GetId implements [Field.GetId] interface method. +func (f *BoolField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *BoolField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *BoolField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *BoolField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *BoolField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *BoolField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *BoolField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *BoolField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *BoolField) ColumnType(app App) string { + return "BOOLEAN DEFAULT FALSE NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *BoolField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToBool(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *BoolField) ValidateValue(ctx context.Context, app App, record *Record) error { + v, ok := record.GetRaw(f.Name).(bool) + if !ok { + return validators.ErrUnsupportedValueType + } + + if f.Required { + return validation.Required.Validate(v) + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *BoolField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + ) +} diff --git a/core/field_bool_test.go b/core/field_bool_test.go new file mode 100644 index 00000000..6706099b --- /dev/null +++ b/core/field_bool_test.go @@ -0,0 +1,150 @@ +package core_test + +import ( + "context" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestBoolFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeBool) +} + +func TestBoolFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.BoolField{} + + expected := "BOOLEAN DEFAULT FALSE NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestBoolFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.BoolField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected bool + }{ + {"", false}, + {"f", false}, + {"t", true}, + {1, true}, + {0, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + if v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestBoolFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.BoolField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.BoolField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "missing field value (non-required)", + &core.BoolField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("abc", true) + return record + }, + true, // because of failed nil.(bool) cast + }, + { + "missing field value (required)", + &core.BoolField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("abc", true) + return record + }, + true, + }, + { + "false field value (non-required)", + &core.BoolField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", false) + return record + }, + false, + }, + { + "false field value (required)", + &core.BoolField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", false) + return record + }, + true, + }, + { + "true field value (required)", + &core.BoolField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", true) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestBoolFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeBool) + testDefaultFieldNameValidation(t, core.FieldTypeBool) +} diff --git a/core/field_date.go b/core/field_date.go new file mode 100644 index 00000000..5bb67cf3 --- /dev/null +++ b/core/field_date.go @@ -0,0 +1,160 @@ +package core + +import ( + "context" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + Fields[FieldTypeDate] = func() Field { + return &DateField{} + } +} + +const FieldTypeDate = "date" + +var _ Field = (*DateField)(nil) + +// DateField defines "date" type field to store a single [types.DateTime] value. +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"` + + // --- + + // Min specifies the min allowed field value. + // + // Leave it empty to skip the validator. + Min types.DateTime `form:"min" json:"min"` + + // Max specifies the max allowed field value. + // + // Leave it empty to skip the validator. + Max types.DateTime `form:"max" json:"max"` + + // Required will require the field value to be non-zero [types.DateTime]. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *DateField) Type() string { + return FieldTypeDate +} + +// GetId implements [Field.GetId] interface method. +func (f *DateField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *DateField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *DateField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *DateField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *DateField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *DateField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *DateField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *DateField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *DateField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *DateField) PrepareValue(record *Record, raw any) (any, error) { + // ignore scan errors since the format may change between versions + // and to allow running db adjusting migrations + val, _ := types.ParseDateTime(raw) + return val, nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *DateField) ValidateValue(ctx context.Context, app App, record *Record) error { + val, ok := record.GetRaw(f.Name).(types.DateTime) + if !ok { + return validators.ErrUnsupportedValueType + } + + if val.IsZero() { + if f.Required { + return validation.ErrRequired + } + return nil // nothing to check + } + + if !f.Min.IsZero() { + if err := validation.Min(f.Min.Time()).Validate(val.Time()); err != nil { + return err + } + } + + if !f.Max.IsZero() { + if err := validation.Max(f.Max.Time()).Validate(val.Time()); err != nil { + return err + } + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *DateField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.Max, validation.By(f.checkRange(f.Min, f.Max))), + ) +} + +func (f *DateField) checkRange(min types.DateTime, max types.DateTime) validation.RuleFunc { + return func(value any) error { + v, _ := value.(types.DateTime) + if v.IsZero() { + return nil // nothing to check + } + + dr := validation.Date(types.DefaultDateLayout) + + if !min.IsZero() { + dr.Min(min.Time()) + } + + if !max.IsZero() { + dr.Max(max.Time()) + } + + return dr.Validate(v.String()) + } +} diff --git a/core/field_date_test.go b/core/field_date_test.go new file mode 100644 index 00000000..ea95b191 --- /dev/null +++ b/core/field_date_test.go @@ -0,0 +1,229 @@ +package core_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestDateFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeDate) +} + +func TestDateFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.DateField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestDateFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.DateField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"invalid", ""}, + {"2024-01-01 00:11:22.345Z", "2024-01-01 00:11:22.345Z"}, + {time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC), "2024-01-02 03:04:05.000Z"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vDate, ok := v.(types.DateTime) + if !ok { + t.Fatalf("Expected types.DateTime instance, got %T", v) + } + + if vDate.String() != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestDateFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.DateField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.DateField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.DateField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.DateTime{}) + return record + }, + false, + }, + { + "zero field value (required)", + &core.DateField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.DateTime{}) + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.DateField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.NowDateTime()) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestDateFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeDate) + testDefaultFieldNameValidation(t, core.FieldTypeDate) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.DateField + expectErrors []string + }{ + { + "zero Min/Max", + func() *core.DateField { + return &core.DateField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "non-empty Min with empty Max", + func() *core.DateField { + return &core.DateField{ + Id: "test", + Name: "test", + Min: types.NowDateTime(), + } + }, + []string{}, + }, + { + "empty Min non-empty Max", + func() *core.DateField { + return &core.DateField{ + Id: "test", + Name: "test", + Max: types.NowDateTime(), + } + }, + []string{}, + }, + { + "Min = Max", + func() *core.DateField { + date := types.NowDateTime() + return &core.DateField{ + Id: "test", + Name: "test", + Min: date, + Max: date, + } + }, + []string{}, + }, + { + "Min > Max", + func() *core.DateField { + min := types.NowDateTime() + max := min.Add(-5 * time.Second) + return &core.DateField{ + Id: "test", + Name: "test", + Min: min, + Max: max, + } + }, + []string{}, + }, + { + "Min < Max", + func() *core.DateField { + max := types.NowDateTime() + min := max.Add(-5 * time.Second) + return &core.DateField{ + Id: "test", + Name: "test", + Min: min, + Max: max, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/field_editor.go b/core/field_editor.go new file mode 100644 index 00000000..be75677f --- /dev/null +++ b/core/field_editor.go @@ -0,0 +1,149 @@ +package core + +import ( + "context" + "fmt" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeEditor] = func() Field { + return &EditorField{} + } +} + +const FieldTypeEditor = "editor" + +const DefaultEditorFieldMaxSize int64 = 5 << 20 + +var ( + _ Field = (*EditorField)(nil) + _ MaxBodySizeCalculator = (*EditorField)(nil) +) + +// EditorField defines "editor" type field to store HTML formatted text. +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"` + + // --- + + // MaxSize specifies the maximum size of the allowed field value (in bytes). + // + // If zero, a default limit of ~5MB is applied. + MaxSize int64 `form:"maxSize" json:"maxSize"` + + // ConvertURLs is usually used to instruct the editor whether to + // apply url conversion (eg. stripping the domain name in case the + // urls are using the same domain as the one where the editor is loaded). + // + // (see also https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls) + ConvertURLs bool `form:"convertURLs" json:"convertURLs"` + + // Required will require the field value to be non-empty string. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *EditorField) Type() string { + return FieldTypeEditor +} + +// GetId implements [Field.GetId] interface method. +func (f *EditorField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *EditorField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *EditorField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *EditorField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *EditorField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *EditorField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *EditorField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *EditorField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *EditorField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *EditorField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToString(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *EditorField) ValidateValue(ctx context.Context, app App, record *Record) error { + val, ok := record.GetRaw(f.Name).(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + if f.Required { + if err := validation.Required.Validate(val); err != nil { + return err + } + } + + maxSize := f.CalculateMaxBodySize() + + if int64(len(val)) > maxSize { + return validation.NewError( + "validation_content_size_limit", + fmt.Sprintf("The maximum allowed content size is %v bytes", maxSize), + ).SetParams(map[string]any{"maxSize": maxSize}) + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *EditorField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.MaxSize, validation.Min(0)), + ) +} + +// CalculateMaxBodySize implements the [MaxBodySizeCalculator] interface. +func (f *EditorField) CalculateMaxBodySize() int64 { + if f.MaxSize <= 0 { + return DefaultEditorFieldMaxSize + } + + return f.MaxSize +} diff --git a/core/field_editor_test.go b/core/field_editor_test.go new file mode 100644 index 00000000..3cd1a388 --- /dev/null +++ b/core/field_editor_test.go @@ -0,0 +1,241 @@ +package core_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestEditorFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeEditor) +} + +func TestEditorFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.EditorField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestEditorFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.EditorField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + {123.456, "123.456"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vStr, ok := v.(string) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + + if vStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, v) + } + }) + } +} + +func TestEditorFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.EditorField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.EditorField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.EditorField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "zero field value (required)", + &core.EditorField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.EditorField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abc") + return record + }, + false, + }, + { + "> default MaxSize", + &core.EditorField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", strings.Repeat("a", 1+(5<<20))) + return record + }, + true, + }, + { + "> MaxSize", + &core.EditorField{Name: "test", Required: true, MaxSize: 5}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abcdef") + return record + }, + true, + }, + { + "<= MaxSize", + &core.EditorField{Name: "test", Required: true, MaxSize: 5}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abcde") + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestEditorFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeEditor) + testDefaultFieldNameValidation(t, core.FieldTypeEditor) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.EditorField + expectErrors []string + }{ + { + "< 0 MaxSize", + func() *core.EditorField { + return &core.EditorField{ + Id: "test", + Name: "test", + MaxSize: -1, + } + }, + []string{"maxSize"}, + }, + { + "= 0 MaxSize", + func() *core.EditorField { + return &core.EditorField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "> 0 MaxSize", + func() *core.EditorField { + return &core.EditorField{ + Id: "test", + Name: "test", + MaxSize: 1, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestEditorFieldCalculateMaxBodySize(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + field *core.EditorField + expected int64 + }{ + {&core.EditorField{}, core.DefaultEditorFieldMaxSize}, + {&core.EditorField{MaxSize: 10}, 10}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.field.MaxSize), func(t *testing.T) { + result := s.field.CalculateMaxBodySize() + + if result != s.expected { + t.Fatalf("Expected %d, got %d", s.expected, result) + } + }) + } +} diff --git a/core/field_email.go b/core/field_email.go new file mode 100644 index 00000000..f189aee5 --- /dev/null +++ b/core/field_email.go @@ -0,0 +1,153 @@ +package core + +import ( + "context" + "slices" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeEmail] = func() Field { + return &EmailField{} + } +} + +const FieldTypeEmail = "email" + +var _ Field = (*EmailField)(nil) + +// EmailField defines "email" type field for storing single email string address. +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"` + + // --- + + // ExceptDomains will require the email domain to NOT be included in the listed ones. + // + // This validator can be set only if OnlyDomains is empty. + ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` + + // OnlyDomains will require the email domain to be included in the listed ones. + // + // This validator can be set only if ExceptDomains is empty. + OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` + + // Required will require the field value to be non-empty email string. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *EmailField) Type() string { + return FieldTypeEmail +} + +// GetId implements [Field.GetId] interface method. +func (f *EmailField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *EmailField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *EmailField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *EmailField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *EmailField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *EmailField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *EmailField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *EmailField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *EmailField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *EmailField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToString(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *EmailField) ValidateValue(ctx context.Context, app App, record *Record) error { + val, ok := record.GetRaw(f.Name).(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + if f.Required { + if err := validation.Required.Validate(val); err != nil { + return err + } + } + + if val == "" { + return nil // nothing to check + } + + if err := is.EmailFormat.Validate(val); err != nil { + return err + } + + domain := val[strings.LastIndex(val, "@")+1:] + + // only domains check + if len(f.OnlyDomains) > 0 && !slices.Contains(f.OnlyDomains, domain) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") + } + + // except domains check + if len(f.ExceptDomains) > 0 && slices.Contains(f.ExceptDomains, domain) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *EmailField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field( + &f.ExceptDomains, + validation.When(len(f.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &f.OnlyDomains, + validation.When(len(f.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + ) +} diff --git a/core/field_email_test.go b/core/field_email_test.go new file mode 100644 index 00000000..600f2c2c --- /dev/null +++ b/core/field_email_test.go @@ -0,0 +1,271 @@ +package core_test + +import ( + "context" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestEmailFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeEmail) +} + +func TestEmailFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.EmailField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestEmailFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.EmailField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + {123.456, "123.456"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vStr, ok := v.(string) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + + if vStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, v) + } + }) + } +} + +func TestEmailFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.EmailField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.EmailField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.EmailField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "zero field value (required)", + &core.EmailField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.EmailField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "test@example.com") + return record + }, + false, + }, + { + "invalid email", + &core.EmailField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "invalid") + return record + }, + true, + }, + { + "failed onlyDomains", + &core.EmailField{Name: "test", OnlyDomains: []string{"example.org", "example.net"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "test@example.com") + return record + }, + true, + }, + { + "success onlyDomains", + &core.EmailField{Name: "test", OnlyDomains: []string{"example.org", "example.com"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "test@example.com") + return record + }, + false, + }, + { + "failed exceptDomains", + &core.EmailField{Name: "test", ExceptDomains: []string{"example.org", "example.com"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "test@example.com") + return record + }, + true, + }, + { + "success exceptDomains", + &core.EmailField{Name: "test", ExceptDomains: []string{"example.org", "example.net"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "test@example.com") + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestEmailFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeEmail) + testDefaultFieldNameValidation(t, core.FieldTypeEmail) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.EmailField + expectErrors []string + }{ + { + "zero minimal", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "both onlyDomains and exceptDomains", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com"}, + ExceptDomains: []string{"example.org"}, + } + }, + []string{"onlyDomains", "exceptDomains"}, + }, + { + "invalid onlyDomains", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com", "invalid"}, + } + }, + []string{"onlyDomains"}, + }, + { + "valid onlyDomains", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com", "example.org"}, + } + }, + []string{}, + }, + { + "invalid exceptDomains", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + ExceptDomains: []string{"example.com", "invalid"}, + } + }, + []string{"exceptDomains"}, + }, + { + "valid exceptDomains", + func() *core.EmailField { + return &core.EmailField{ + Id: "test", + Name: "test", + ExceptDomains: []string{"example.com", "example.org"}, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/field_file.go b/core/field_file.go new file mode 100644 index 00000000..55cc6cb8 --- /dev/null +++ b/core/field_file.go @@ -0,0 +1,792 @@ +package core + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeFile] = func() Field { + return &FileField{} + } +} + +const FieldTypeFile = "file" + +const DefaultFileFieldMaxSize int64 = 5 << 20 + +var looseFilenameRegex = regexp.MustCompile(`^[^\./\\][^/\\]+$`) + +const ( + deletedFilesPrefix = internalCustomFieldKeyPrefix + "_deletedFilesPrefix_" + uploadedFilesPrefix = internalCustomFieldKeyPrefix + "_uploadedFilesPrefix_" +) + +var ( + _ Field = (*FileField)(nil) + _ MultiValuer = (*FileField)(nil) + _ DriverValuer = (*FileField)(nil) + _ GetterFinder = (*FileField)(nil) + _ SetterFinder = (*FileField)(nil) + _ RecordInterceptor = (*FileField)(nil) + _ MaxBodySizeCalculator = (*FileField)(nil) +) + +// FileField defines "file" type field for managing record file(s). +// +// Only the file name is stored as part of the record value. +// New files (aka. files to upload) are expected to be of *filesytem.File. +// +// If MaxSelect is not set or <= 1, then the field value is expected to be a single record id. +// +// If MaxSelect is > 1, then the field value is expected to be a slice of record ids. +// +// --- +// +// The following additional setter keys are available: +// +// - "fieldName+" - append one or more files to the existing record one. For example: +// +// // []string{"old1.txt", "old2.txt", "new1_ajkvass.txt", "new2_klhfnwd.txt"} +// record.Set("documents+", []*filesystem.File{new1, new2}) +// +// - "+fieldName" - prepend one or more files to the existing record one. For example: +// +// // []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: +// +// // []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"` + + // --- + + // MaxSize specifies the maximum size of a single uploaded file (in bytes). + // + // If zero, a default limit of 5MB is applied. + MaxSize int64 `form:"maxSize" json:"maxSize"` + + // MaxSelect specifies the max allowed files. + // + // For multiple files the value must be > 1, otherwise fallbacks to single (default). + MaxSelect int `form:"maxSelect" json:"maxSelect"` + + // MimeTypes specifies an optional list of the allowed file mime types. + // + // Leave it empty to disable the validator. + MimeTypes []string `form:"mimeTypes" json:"mimeTypes"` + + // Thumbs specifies an optional list of the supported thumbs for image based files. + // + // Each entry must be in one of the following formats: + // + // - WxH (eg. 100x300) - crop to WxH viewbox (from center) + // - WxHt (eg. 100x300t) - crop to WxH viewbox (from top) + // - WxHb (eg. 100x300b) - crop to WxH viewbox (from bottom) + // - WxHf (eg. 100x300f) - fit inside a WxH viewbox (without cropping) + // - 0xH (eg. 0x300) - resize to H height preserving the aspect ratio + // - Wx0 (eg. 100x0) - resize to W width preserving the aspect ratio + Thumbs []string `form:"thumbs" json:"thumbs"` + + // Protected will require the users to provide a special file token to access the file. + // + // Note that by default all files are publicly accessible. + // + // For the majority of the cases this is fine because by default + // all file names have random part appended to their name which + // need to be known by the user before accessing the file. + Protected bool `form:"protected" json:"protected"` + + // Required will require the field value to have at least one file. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *FileField) Type() string { + return FieldTypeFile +} + +// GetId implements [Field.GetId] interface method. +func (f *FileField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *FileField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *FileField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *FileField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *FileField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *FileField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *FileField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *FileField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// IsMultiple implements MultiValuer interface and checks whether the +// current field options support multiple values. +func (f *FileField) IsMultiple() bool { + return f.MaxSelect > 1 +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *FileField) ColumnType(app App) string { + if f.IsMultiple() { + return "JSON DEFAULT '[]' NOT NULL" + } + + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *FileField) PrepareValue(record *Record, raw any) (any, error) { + return f.normalizeValue(raw), nil +} + +// DriverValue implements the [DriverValuer] interface. +func (f *FileField) DriverValue(record *Record) (driver.Value, error) { + files := f.toSliceValue(record.GetRaw(f.Name)) + + if f.IsMultiple() { + ja := make(types.JSONArray[string], len(files)) + for i, v := range files { + ja[i] = f.getFileName(v) + } + return ja, nil + } + + if len(files) == 0 { + return "", nil + } + + return f.getFileName(files[len(files)-1]), nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *FileField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.MaxSelect, validation.Min(0)), + validation.Field(&f.MaxSize, validation.Min(0)), + validation.Field(&f.Thumbs, validation.Each( + validation.NotIn("0x0", "0x0t", "0x0b", "0x0f"), + validation.Match(filesystem.ThumbSizeRegex), + )), + ) +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *FileField) ValidateValue(ctx context.Context, app App, record *Record) error { + files := f.toSliceValue(record.GetRaw(f.Name)) + if len(files) == 0 { + if f.Required { + return validation.ErrRequired + } + return nil // nothing to check + } + + // validate existing and disallow new plain string filenames submission + // (new files must be *filesystem.File) + // --- + oldExistingStrings := f.toSliceValue(f.getLatestOldValue(app, record)) + existingStrings := list.ToInterfaceSlice(f.extractPlainStrings(files)) + addedStrings := f.excludeFiles(existingStrings, oldExistingStrings) + + if len(addedStrings) > 0 { + return validation.NewError("validation_invalid_file", "Invalid files:"+strings.Join(cast.ToStringSlice(addedStrings), ", ")). + SetParams(map[string]any{"invalidFiles": addedStrings}) + } + + maxSelect := f.maxSelect() + if len(files) > maxSelect { + return validation.NewError("validation_too_many_files", fmt.Sprintf("The maximum allowed files is %d", maxSelect)). + SetParams(map[string]any{"maxSelect": maxSelect}) + } + + // validate uploaded + // --- + uploads := f.extractUploadableFiles(files) + for _, upload := range uploads { + // loosely check the filename just in case it was manually changed after the normalization + err := validation.Length(1, 150).Validate(upload.Name) + if err != nil { + return err + } + err = validation.Match(looseFilenameRegex).Validate(upload.Name) + if err != nil { + return err + } + + // check size + err = validators.UploadedFileSize(f.maxSize())(upload) + if err != nil { + return err + } + + // check type + if len(f.MimeTypes) > 0 { + err = validators.UploadedFileMimeType(f.MimeTypes)(upload) + if err != nil { + return err + } + } + } + + return nil +} + +func (f *FileField) maxSize() int64 { + if f.MaxSize <= 0 { + return DefaultFileFieldMaxSize + } + + return f.MaxSize +} + +func (f *FileField) maxSelect() int { + if f.MaxSelect <= 1 { + return 1 + } + + return f.MaxSelect +} + +// CalculateMaxBodySize implements the [MaxBodySizeCalculator] interface. +func (f *FileField) CalculateMaxBodySize() int64 { + return f.maxSize() * int64(f.maxSelect()) +} + +// Interceptors +// ------------------------------------------------------------------- + +// Intercept implements the [RecordInterceptor] interface. +// +// note: files delete after records deletion is handled globally by the app FileManager hook +func (f *FileField) Intercept( + ctx context.Context, + app App, + record *Record, + actionName string, + actionFunc func() error, +) error { + switch actionName { + case InterceptorActionCreateExecute, InterceptorActionUpdateExecute: + oldValue := f.getLatestOldValue(app, record) + + err := f.processFilesToUpload(ctx, app, record) + if err != nil { + return err + } + + err = actionFunc() + if err != nil { + return errors.Join(err, f.afterRecordExecuteFailure(newContextIfInvalid(ctx), app, record)) + } + + f.rememberFilesToDelete(app, record, oldValue) + + f.afterRecordExecuteSuccess(newContextIfInvalid(ctx), app, record) + + return nil + case InterceptorActionAfterCreateError, InterceptorActionAfterUpdateError: + // when in transaction we assume that the error was handled by afterRecordExecuteFailure + _, insideTransaction := app.DB().(*dbx.Tx) + if insideTransaction { + return actionFunc() + } + + failedToDelete, deleteErr := f.deleteNewlyUploadedFiles(newContextIfInvalid(ctx), app, record) + if deleteErr != nil { + app.Logger().Warn( + "Failed to cleanup all new files after record commit failure", + "error", deleteErr, + "failedToDelete", failedToDelete, + ) + } + + record.SetRaw(deletedFilesPrefix+f.Name, nil) + + if record.IsNew() { + // try to delete the record directory if there are no other files + // + // note: executed only on create failure to avoid accidentally + // deleting a concurrently updating directory due to the + // eventual consistent nature of some storage providers + err := f.deleteEmptyRecordDir(newContextIfInvalid(ctx), app, record) + if err != nil { + app.Logger().Warn("Failed to delete empty dir after new record commit failure", "error", err) + } + } + + return actionFunc() + case InterceptorActionAfterCreate, InterceptorActionAfterUpdate: + record.SetRaw(uploadedFilesPrefix+f.Name, nil) + + err := f.processFilesToDelete(ctx, app, record) + if err != nil { + return err + } + + return actionFunc() + default: + return actionFunc() + } +} +func (f *FileField) getLatestOldValue(app App, record *Record) any { + if !record.IsNew() { + latestOriginal, err := app.FindRecordById(record.Collection(), record.Id) + if err == nil { + return latestOriginal.GetRaw(f.Name) + } + } + + return record.Original().GetRaw(f.Name) +} + +func (f *FileField) afterRecordExecuteSuccess(ctx context.Context, app App, record *Record) { + uploaded, _ := record.GetRaw(uploadedFilesPrefix + f.Name).([]*filesystem.File) + + // replace the uploaded file objects with their plain string names + newValue := f.toSliceValue(record.GetRaw(f.Name)) + for i, v := range newValue { + if file, ok := v.(*filesystem.File); ok { + uploaded = append(uploaded, file) + newValue[i] = file.Name + } + } + f.setValue(record, newValue) + + record.SetRaw(uploadedFilesPrefix+f.Name, uploaded) +} + +func (f *FileField) afterRecordExecuteFailure(ctx context.Context, app App, record *Record) error { + uploaded := f.extractUploadableFiles(f.toSliceValue(record.GetRaw(f.Name))) + + toDelete := make([]string, len(uploaded)) + for i, file := range uploaded { + toDelete[i] = file.Name + } + + // delete previously uploaded files + failedToDelete, deleteErr := f.deleteFilesByNamesList(ctx, app, record, list.ToUniqueStringSlice(toDelete)) + + if len(failedToDelete) > 0 { + app.Logger().Warn( + "Failed to cleanup the new uploaded file after record db write failure", + "error", deleteErr, + "failedToDelete", failedToDelete, + ) + } + + return deleteErr +} + +func (f *FileField) deleteEmptyRecordDir(ctx context.Context, app App, record *Record) error { + fsys, err := app.NewFilesystem() + if err != nil { + return err + } + defer fsys.Close() + fsys.SetContext(newContextIfInvalid(ctx)) + + dir := record.BaseFilesPath() + + if !fsys.IsEmptyDir(dir) { + return nil // no-op + } + + err = fsys.Delete(dir) + if err != nil && !errors.Is(err, filesystem.ErrNotFound) { + return err + } + + return nil +} + +func (f *FileField) processFilesToDelete(ctx context.Context, app App, record *Record) error { + markedForDelete, _ := record.GetRaw(deletedFilesPrefix + f.Name).([]string) + if len(markedForDelete) == 0 { + return nil + } + + old := list.ToInterfaceSlice(markedForDelete) + new := list.ToInterfaceSlice(f.extractPlainStrings(f.toSliceValue(record.GetRaw(f.Name)))) + diff := f.excludeFiles(old, new) + + toDelete := make([]string, len(diff)) + for i, del := range diff { + toDelete[i] = f.getFileName(del) + } + + failedToDelete, err := f.deleteFilesByNamesList(ctx, app, record, list.ToUniqueStringSlice(toDelete)) + + record.SetRaw(deletedFilesPrefix+f.Name, failedToDelete) + + return err +} + +func (f *FileField) rememberFilesToDelete(app App, record *Record, oldValue any) { + old := list.ToInterfaceSlice(f.extractPlainStrings(f.toSliceValue(oldValue))) + new := list.ToInterfaceSlice(f.extractPlainStrings(f.toSliceValue(record.GetRaw(f.Name)))) + diff := f.excludeFiles(old, new) + + toDelete, _ := record.GetRaw(deletedFilesPrefix + f.Name).([]string) + + for _, del := range diff { + toDelete = append(toDelete, f.getFileName(del)) + } + + record.SetRaw(deletedFilesPrefix+f.Name, toDelete) +} + +func (f *FileField) processFilesToUpload(ctx context.Context, app App, record *Record) error { + uploads := f.extractUploadableFiles(f.toSliceValue(record.GetRaw(f.Name))) + if len(uploads) == 0 { + return nil + } + + if record.Id == "" { + return errors.New("uploading files requires the record to have a valid nonempty id") + } + + fsys, err := app.NewFilesystem() + if err != nil { + return err + } + defer fsys.Close() + fsys.SetContext(ctx) + + var failed []error // list of upload errors + var succeeded []string // list of uploaded file names + + for _, upload := range uploads { + path := record.BaseFilesPath() + "/" + upload.Name + if err := fsys.UploadFile(upload, path); err == nil { + succeeded = append(succeeded, upload.Name) + } else { + failed = append(failed, fmt.Errorf("%q: %w", upload.Name, err)) + break // for now stop on the first error since we currently don't allow partial uploads + } + } + + if len(failed) > 0 { + // cleanup - try to delete the successfully uploaded files (if any) + _, cleanupErr := f.deleteFilesByNamesList(newContextIfInvalid(ctx), app, record, succeeded) + + failed = append(failed, cleanupErr) + + return fmt.Errorf("failed to upload all files: %w", errors.Join(failed...)) + } + + return nil +} + +func (f *FileField) deleteNewlyUploadedFiles(ctx context.Context, app App, record *Record) ([]string, error) { + uploaded, _ := record.GetRaw(uploadedFilesPrefix + f.Name).([]*filesystem.File) + if len(uploaded) == 0 { + return nil, nil + } + + names := make([]string, len(uploaded)) + for i, file := range uploaded { + names[i] = file.Name + } + + failed, err := f.deleteFilesByNamesList(ctx, app, record, list.ToUniqueStringSlice(names)) + if err != nil { + return failed, err + } + + record.SetRaw(uploadedFilesPrefix+f.Name, nil) + + return nil, nil +} + +// deleteFiles deletes a list of record files by their names. +// Returns the failed/remaining files. +func (f *FileField) deleteFilesByNamesList(ctx context.Context, app App, record *Record, filenames []string) ([]string, error) { + if len(filenames) == 0 { + return nil, nil // nothing to delete + } + + if record.Id == "" { + return filenames, errors.New("the record doesn't have an id") + } + + fsys, err := app.NewFilesystem() + if err != nil { + return filenames, err + } + defer fsys.Close() + fsys.SetContext(ctx) + + var failures []error + + for i := len(filenames) - 1; i >= 0; i-- { + filename := filenames[i] + if filename == "" || strings.ContainsAny(filename, "/\\") { + continue // empty or not a plain filename + } + + path := record.BaseFilesPath() + "/" + filename + + err := fsys.Delete(path) + if err != nil && !errors.Is(err, filesystem.ErrNotFound) { + // store the delete error + failures = append(failures, fmt.Errorf("file %d (%q): %w", i, filename, err)) + } else { + // remove the deleted file from the list + filenames = append(filenames[:i], filenames[i+1:]...) + + // try to delete the related file thumbs (if any) + thumbsErr := fsys.DeletePrefix(record.BaseFilesPath() + "/thumbs_" + filename + "/") + if len(thumbsErr) > 0 { + app.Logger().Warn("Failed to delete file thumbs", "error", errors.Join(thumbsErr...)) + } + } + } + + if len(failures) > 0 { + return filenames, fmt.Errorf("failed to delete all files: %w", errors.Join(failures...)) + } + + return nil, nil +} + +// newContextIfInvalid returns a new Background context if the provided one was cancelled. +func newContextIfInvalid(ctx context.Context) context.Context { + if ctx.Err() == nil { + return ctx + } + + return context.Background() +} + +// ------------------------------------------------------------------- + +// FindGetter implements the [GetterFinder] interface. +func (f *FileField) FindGetter(key string) GetterFunc { + switch key { + case f.Name: + return func(record *Record) any { + return record.GetRaw(f.Name) + } + case f.Name + ":uploaded": + return func(record *Record) any { + return f.extractUploadableFiles(f.toSliceValue(record.GetRaw(f.Name))) + } + default: + return nil + } +} + +// ------------------------------------------------------------------- + +// FindSetter implements the [SetterFinder] interface. +func (f *FileField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return f.setValue + case "+" + f.Name: + return f.prependValue + case f.Name + "+": + return f.appendValue + case f.Name + "-": + return f.subtractValue + default: + return nil + } +} + +func (f *FileField) setValue(record *Record, raw any) { + val := f.normalizeValue(raw) + + record.SetRaw(f.Name, val) +} + +func (f *FileField) prependValue(record *Record, toPrepend any) { + files := f.toSliceValue(record.GetRaw(f.Name)) + prepends := f.toSliceValue(toPrepend) + + if len(prepends) > 0 { + files = append(prepends, files...) + } + + f.setValue(record, files) +} + +func (f *FileField) appendValue(record *Record, toAppend any) { + files := f.toSliceValue(record.GetRaw(f.Name)) + appends := f.toSliceValue(toAppend) + + if len(appends) > 0 { + files = append(files, appends...) + } + + f.setValue(record, files) +} + +func (f *FileField) subtractValue(record *Record, toRemove any) { + files := f.excludeFiles( + f.toSliceValue(record.GetRaw(f.Name)), + f.toSliceValue(toRemove), + ) + + f.setValue(record, files) +} + +func (f *FileField) normalizeValue(raw any) any { + files := f.toSliceValue(raw) + + if f.IsMultiple() { + return files + } + + if len(files) > 0 { + return files[len(files)-1] // the last selected + } + + return "" +} + +func (f *FileField) toSliceValue(raw any) []any { + var result []any + + switch value := raw.(type) { + case nil: + // nothing to cast + case *filesystem.File: + result = append(result, value) + case filesystem.File: + result = append(result, &value) + case []*filesystem.File: + for _, v := range value { + result = append(result, v) + } + case []filesystem.File: + for _, v := range value { + result = append(result, &v) + } + case []any: + for _, v := range value { + casted := f.toSliceValue(v) + if len(casted) == 1 { + result = append(result, casted[0]) + } + } + default: + result = list.ToInterfaceSlice(list.ToUniqueStringSlice(value)) + } + + return f.uniqueFiles(result) +} + +func (f *FileField) uniqueFiles(files []any) []any { + existing := make(map[string]struct{}, len(files)) + result := make([]any, 0, len(files)) + + for _, fv := range files { + name := f.getFileName(fv) + if _, ok := existing[name]; !ok { + result = append(result, fv) + existing[name] = struct{}{} + } + } + + return result +} + +func (f *FileField) extractPlainStrings(files []any) []string { + result := []string{} + + for _, raw := range files { + if f, ok := raw.(string); ok { + result = append(result, f) + } + } + + return result +} + +func (f *FileField) extractUploadableFiles(files []any) []*filesystem.File { + result := []*filesystem.File{} + + for _, raw := range files { + if upload, ok := raw.(*filesystem.File); ok { + result = append(result, upload) + } + } + + return result +} + +func (f *FileField) excludeFiles(base []any, toExclude []any) []any { + result := make([]any, 0, len(base)) + +SUBTRACT_LOOP: + for _, fv := range base { + for _, exclude := range toExclude { + if f.getFileName(exclude) == f.getFileName(fv) { + continue SUBTRACT_LOOP // skip + } + } + + result = append(result, fv) + } + + return result +} + +func (f *FileField) getFileName(file any) string { + switch v := file.(type) { + case string: + return v + case *filesystem.File: + return v.Name + default: + return "" + } +} diff --git a/core/field_file_test.go b/core/field_file_test.go new file mode 100644 index 00000000..dc94dcb3 --- /dev/null +++ b/core/field_file_test.go @@ -0,0 +1,1108 @@ +package core_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestFileFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeFile) +} + +func TestFileFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field *core.FileField + expected string + }{ + { + "single (zero)", + &core.FileField{}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "single", + &core.FileField{MaxSelect: 1}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "multiple", + &core.FileField{MaxSelect: 2}, + "JSON DEFAULT '[]' NOT NULL", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.ColumnType(app); v != s.expected { + t.Fatalf("Expected\n%q\ngot\n%q", s.expected, v) + } + }) + } +} + +func TestFileFieldIsMultiple(t *testing.T) { + scenarios := []struct { + name string + field *core.FileField + expected bool + }{ + { + "zero", + &core.FileField{}, + false, + }, + { + "single", + &core.FileField{MaxSelect: 1}, + false, + }, + { + "multiple", + &core.FileField{MaxSelect: 2}, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.IsMultiple(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestFileFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record := core.NewRecord(core.NewBaseCollection("test")) + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "test1.txt") + if err != nil { + t.Fatal(err) + } + f1Raw, err := json.Marshal(f1) + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + raw any + field *core.FileField + expected string + }{ + // single + {nil, &core.FileField{MaxSelect: 1}, `""`}, + {"", &core.FileField{MaxSelect: 1}, `""`}, + {123, &core.FileField{MaxSelect: 1}, `"123"`}, + {"a", &core.FileField{MaxSelect: 1}, `"a"`}, + {`["a"]`, &core.FileField{MaxSelect: 1}, `"a"`}, + {*f1, &core.FileField{MaxSelect: 1}, string(f1Raw)}, + {f1, &core.FileField{MaxSelect: 1}, string(f1Raw)}, + {[]string{}, &core.FileField{MaxSelect: 1}, `""`}, + {[]string{"a", "b"}, &core.FileField{MaxSelect: 1}, `"b"`}, + + // multiple + {nil, &core.FileField{MaxSelect: 2}, `[]`}, + {"", &core.FileField{MaxSelect: 2}, `[]`}, + {123, &core.FileField{MaxSelect: 2}, `["123"]`}, + {"a", &core.FileField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.FileField{MaxSelect: 2}, `["a"]`}, + {[]any{f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, + {[]*filesystem.File{f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, + {[]filesystem.File{*f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, + {[]string{}, &core.FileField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.FileField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + v, err := s.field.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestFileFieldDriverValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + raw any + field *core.FileField + expected string + }{ + // single + {nil, &core.FileField{MaxSelect: 1}, `""`}, + {"", &core.FileField{MaxSelect: 1}, `""`}, + {123, &core.FileField{MaxSelect: 1}, `"123"`}, + {"a", &core.FileField{MaxSelect: 1}, `"a"`}, + {`["a"]`, &core.FileField{MaxSelect: 1}, `"a"`}, + {f1, &core.FileField{MaxSelect: 1}, `"` + f1.Name + `"`}, + {[]string{}, &core.FileField{MaxSelect: 1}, `""`}, + {[]string{"a", "b"}, &core.FileField{MaxSelect: 1}, `"b"`}, + + // multiple + {nil, &core.FileField{MaxSelect: 2}, `[]`}, + {"", &core.FileField{MaxSelect: 2}, `[]`}, + {123, &core.FileField{MaxSelect: 2}, `["123"]`}, + {"a", &core.FileField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.FileField{MaxSelect: 2}, `["a"]`}, + {[]any{"a", f1}, &core.FileField{MaxSelect: 2}, `["a","` + f1.Name + `"]`}, + {[]string{}, &core.FileField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.FileField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + record := core.NewRecord(core.NewBaseCollection("test")) + record.SetRaw(s.field.GetName(), s.raw) + + v, err := s.field.DriverValue(record) + if err != nil { + t.Fatal(err) + } + + if s.field.IsMultiple() { + _, ok := v.(types.JSONArray[string]) + if !ok { + t.Fatalf("Expected types.JSONArray value, got %T", v) + } + } else { + _, ok := v.(string) + if !ok { + t.Fatalf("Expected string value, got %T", v) + } + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestFileFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "test1.txt") + if err != nil { + t.Fatal(err) + } + + f2, err := filesystem.NewFileFromBytes([]byte("test"), "test2.txt") + if err != nil { + t.Fatal(err) + } + + f3, err := filesystem.NewFileFromBytes([]byte("test_abc"), "test3.txt") + if err != nil { + t.Fatal(err) + } + + f4, err := filesystem.NewFileFromBytes(make([]byte, core.DefaultFileFieldMaxSize+1), "test4.txt") + if err != nil { + t.Fatal(err) + } + + f5, err := filesystem.NewFileFromBytes(make([]byte, core.DefaultFileFieldMaxSize), "test5.txt") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + field *core.FileField + record func() *core.Record + expectError bool + }{ + // single + { + "zero field value (not required)", + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "zero field value (required)", + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1, Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "new plain filename", // new files must be *filesystem.File + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "a") + return record + }, + true, + }, + { + "new file", + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", f1) + return record + }, + false, + }, + { + "new files > MaxSelect", + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2}) + return record + }, + true, + }, + { + "new files <= MaxSelect", + &core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2}) + return record + }, + false, + }, + { + "> default MaxSize", + &core.FileField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", f4) + return record + }, + true, + }, + { + "<= default MaxSize", + &core.FileField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", f5) + return record + }, + false, + }, + { + "> MaxSize", + &core.FileField{Name: "test", MaxSize: 4, MaxSelect: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2, f3}) // f3=8 + return record + }, + true, + }, + { + "<= MaxSize", + &core.FileField{Name: "test", MaxSize: 8, MaxSelect: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2, f3}) + return record + }, + false, + }, + { + "non-matching MimeType", + &core.FileField{Name: "test", MaxSize: 999, MaxSelect: 3, MimeTypes: []string{"a", "b"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2}) + return record + }, + true, + }, + { + "matching MimeType", + &core.FileField{Name: "test", MaxSize: 999, MaxSelect: 3, MimeTypes: []string{"text/plain", "b"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []any{f1, f2}) + return record + }, + false, + }, + { + "existing files > MaxSelect", + &core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 2}, + func() *core.Record { + record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") // 5 files + return record + }, + true, + }, + { + "existing files should ignore the MaxSize and Mimetypes checks", + &core.FileField{Name: "file_many", MaxSize: 1, MaxSelect: 5, MimeTypes: []string{"a", "b"}}, + func() *core.Record { + record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") + return record + }, + false, + }, + { + "existing + new file > MaxSelect (5+2)", + &core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 6}, + func() *core.Record { + record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") + record.Set("file_many+", []any{f1, f2}) + return record + }, + true, + }, + { + "existing + new file <= MaxSelect (5+2)", + &core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 7}, + func() *core.Record { + record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") + record.Set("file_many+", []any{f1, f2}) + return record + }, + false, + }, + { + "existing + new filename", + &core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 99}, + func() *core.Record { + record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") + record.Set("file_many+", "test123.png") + return record + }, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestFileFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeFile) + testDefaultFieldNameValidation(t, core.FieldTypeFile) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field func() *core.FileField + expectErrors []string + }{ + { + "zero minimal", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "0x0 thumb", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSelect: 1, + Thumbs: []string{"100x200", "0x0"}, + } + }, + []string{"thumbs"}, + }, + { + "0x0t thumb", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSize: 1, + MaxSelect: 1, + Thumbs: []string{"100x200", "0x0t"}, + } + }, + []string{"thumbs"}, + }, + { + "0x0b thumb", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSize: 1, + MaxSelect: 1, + Thumbs: []string{"100x200", "0x0b"}, + } + }, + []string{"thumbs"}, + }, + { + "0x0f thumb", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSize: 1, + MaxSelect: 1, + Thumbs: []string{"100x200", "0x0f"}, + } + }, + []string{"thumbs"}, + }, + { + "invalid format", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSize: 1, + MaxSelect: 1, + Thumbs: []string{"100x200", "100x"}, + } + }, + []string{"thumbs"}, + }, + { + "valid thumbs", + func() *core.FileField { + return &core.FileField{ + Id: "test", + Name: "test", + MaxSize: 1, + MaxSelect: 1, + Thumbs: []string{"100x200", "100x40", "100x200"}, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + field := s.field() + + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(field) + + errs := field.ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestFileFieldCalculateMaxBodySize(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + field *core.FileField + expected int64 + }{ + {&core.FileField{}, core.DefaultFileFieldMaxSize}, + {&core.FileField{MaxSelect: 2}, 2 * core.DefaultFileFieldMaxSize}, + {&core.FileField{MaxSize: 10}, 10}, + {&core.FileField{MaxSize: 10, MaxSelect: 1}, 10}, + {&core.FileField{MaxSize: 10, MaxSelect: 2}, 20}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d_%d", i, s.field.MaxSelect, s.field.MaxSize), func(t *testing.T) { + result := s.field.CalculateMaxBodySize() + + if result != s.expected { + t.Fatalf("Expected %d, got %d", s.expected, result) + } + }) + } +} + +func TestFileFieldFindGetter(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "f1") + if err != nil { + t.Fatal(err) + } + f1.Name = "f1" + + f2, err := filesystem.NewFileFromBytes([]byte("test"), "f2") + if err != nil { + t.Fatal(err) + } + f2.Name = "f2" + + record, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy") + if err != nil { + t.Fatal(err) + } + record.Set("files+", []any{f1, f2}) + record.Set("files-", "test_FLurQTgrY8.txt") + + field, ok := record.Collection().Fields.GetByName("files").(*core.FileField) + if !ok { + t.Fatalf("Expected *core.FileField, got %T", record.Collection().Fields.GetByName("files")) + } + + scenarios := []struct { + name string + key string + hasGetter bool + expected string + }{ + { + "no match", + "example", + false, + "", + }, + { + "exact match", + field.GetName(), + true, + `["300_UhLKX91HVb.png",{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, + }, + { + "uploaded", + field.GetName() + ":uploaded", + true, + `[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + getter := field.FindGetter(s.key) + + hasGetter := getter != nil + if hasGetter != s.hasGetter { + t.Fatalf("Expected hasGetter %v, got %v", s.hasGetter, hasGetter) + } + + if !hasGetter { + return + } + + v := getter(record) + + raw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, rawStr) + } + }) + } +} + +func TestFileFieldFindSetter(t *testing.T) { + scenarios := []struct { + name string + key string + value any + field *core.FileField + hasSetter bool + expected string + }{ + { + "no match", + "example", + "b", + &core.FileField{Name: "test", MaxSelect: 1}, + false, + "", + }, + { + "exact match (single)", + "test", + "b", + &core.FileField{Name: "test", MaxSelect: 1}, + true, + `"b"`, + }, + { + "exact match (multiple)", + "test", + []string{"a", "b", "b"}, + &core.FileField{Name: "test", MaxSelect: 2}, + true, + `["a","b"]`, + }, + { + "append (single)", + "test+", + "b", + &core.FileField{Name: "test", MaxSelect: 1}, + true, + `"b"`, + }, + { + "append (multiple)", + "test+", + []string{"a"}, + &core.FileField{Name: "test", MaxSelect: 2}, + true, + `["c","d","a"]`, + }, + { + "prepend (single)", + "+test", + "b", + &core.FileField{Name: "test", MaxSelect: 1}, + true, + `"d"`, // the last of the existing values + }, + { + "prepend (multiple)", + "+test", + []string{"a"}, + &core.FileField{Name: "test", MaxSelect: 2}, + true, + `["a","c","d"]`, + }, + { + "subtract (single)", + "test-", + "d", + &core.FileField{Name: "test", MaxSelect: 1}, + true, + `"c"`, + }, + { + "subtract (multiple)", + "test-", + []string{"unknown", "c"}, + &core.FileField{Name: "test", MaxSelect: 2}, + true, + `["d"]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + setter := s.field.FindSetter(s.key) + + hasSetter := setter != nil + if hasSetter != s.hasSetter { + t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) + } + + if !hasSetter { + return + } + + record := core.NewRecord(collection) + record.SetRaw(s.field.GetName(), []string{"c", "d"}) + + setter(record, s.value) + + raw, err := json.Marshal(record.Get(s.field.GetName())) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, rawStr) + } + }) + } +} + +func TestFileFieldIntercept(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + demo1, err := testApp.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + demo1.Fields.GetByName("text").(*core.TextField).Required = true // trigger validation error + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "new1.txt") + if err != nil { + t.Fatal(err) + } + + f2, err := filesystem.NewFileFromBytes([]byte("test"), "new2.txt") + if err != nil { + t.Fatal(err) + } + + f3, err := filesystem.NewFileFromBytes([]byte("test"), "new3.txt") + if err != nil { + t.Fatal(err) + } + + f4, err := filesystem.NewFileFromBytes([]byte("test"), "new4.txt") + if err != nil { + t.Fatal(err) + } + + record := core.NewRecord(demo1) + + ok := t.Run("1. create - with validation error", func(t *testing.T) { + record.Set("file_many", []any{f1, f2}) + + err := testApp.Save(record) + + tests.TestValidationErrors(t, err, []string{"text"}) + + value, _ := record.GetRaw("file_many").([]any) + if len(value) != 2 { + t.Fatalf("Expected the file field value to be unchanged, got %v", value) + } + }) + if !ok { + return + } + + ok = t.Run("2. create - fixing the validation error", func(t *testing.T) { + record.Set("text", "abc") + + err := testApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + expectedKeys := []string{f1.Name, f2.Name} + + raw := record.GetRaw("file_many") + + // ensure that the value was replaced with the file names + value := list.ToUniqueStringSlice(raw) + if len(value) != len(expectedKeys) { + t.Fatalf("Expected the file field to be updated with the %d file names, got\n%v", len(expectedKeys), raw) + } + for _, name := range expectedKeys { + if !slices.Contains(value, name) { + t.Fatalf("Missing file %q in %v", name, value) + } + } + + checkRecordFiles(t, testApp, record, expectedKeys) + }) + if !ok { + return + } + + ok = t.Run("3. update - validation error", func(t *testing.T) { + record.Set("text", "") + record.Set("file_many+", f3) + record.Set("file_many-", f2.Name) + + err := testApp.Save(record) + + tests.TestValidationErrors(t, err, []string{"text"}) + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f1.Name, f3}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, testApp, record, []string{f1.Name, f2.Name}) + }) + if !ok { + return + } + + ok = t.Run("4. update - fixing the validation error", func(t *testing.T) { + record.Set("text", "abc2") + + err := testApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f1.Name, f3.Name}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, testApp, record, []string{f1.Name, f3.Name}) + }) + if !ok { + return + } + + t.Run("5. update - second time update", func(t *testing.T) { + record.Set("file_many-", f1.Name) + record.Set("file_many+", f4) + + err := testApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f3.Name, f4.Name}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, testApp, record, []string{f3.Name, f4.Name}) + }) +} + +func TestFileFieldInterceptTx(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + demo1, err := testApp.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + demo1.Fields.GetByName("text").(*core.TextField).Required = true // trigger validation error + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "new1.txt") + if err != nil { + t.Fatal(err) + } + + f2, err := filesystem.NewFileFromBytes([]byte("test"), "new2.txt") + if err != nil { + t.Fatal(err) + } + + f3, err := filesystem.NewFileFromBytes([]byte("test"), "new3.txt") + if err != nil { + t.Fatal(err) + } + + f4, err := filesystem.NewFileFromBytes([]byte("test"), "new4.txt") + if err != nil { + t.Fatal(err) + } + + var record *core.Record + + tx := func(succeed bool) func(txApp core.App) error { + var txErr error + if !succeed { + txErr = errors.New("tx error") + } + + return func(txApp core.App) error { + record = core.NewRecord(demo1) + ok := t.Run(fmt.Sprintf("[tx_%v] create with validation error", succeed), func(t *testing.T) { + record.Set("text", "") + record.Set("file_many", []any{f1, f2}) + + err := txApp.Save(record) + tests.TestValidationErrors(t, err, []string{"text"}) + + checkRecordFiles(t, txApp, record, []string{}) // no uploaded files + }) + if !ok { + return txErr + } + + // --- + + ok = t.Run(fmt.Sprintf("[tx_%v] create with fixed validation error", succeed), func(t *testing.T) { + record.Set("text", "abc") + + err = txApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + checkRecordFiles(t, txApp, record, []string{f1.Name, f2.Name}) + }) + if !ok { + return txErr + } + + // --- + + ok = t.Run(fmt.Sprintf("[tx_%v] update with validation error", succeed), func(t *testing.T) { + record.Set("text", "") + record.Set("file_many+", f3) + record.Set("file_many-", f2.Name) + + err = txApp.Save(record) + tests.TestValidationErrors(t, err, []string{"text"}) + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f1.Name, f3}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, txApp, record, []string{f1.Name, f2.Name}) // no file changes + }) + if !ok { + return txErr + } + + // --- + + ok = t.Run(fmt.Sprintf("[tx_%v] update with fixed validation error", succeed), func(t *testing.T) { + record.Set("text", "abc2") + + err = txApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f1.Name, f3.Name}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, txApp, record, []string{f1.Name, f3.Name, f2.Name}) // f2 shouldn't be deleted yet + }) + if !ok { + return txErr + } + + // --- + + ok = t.Run(fmt.Sprintf("[tx_%v] second time update", succeed), func(t *testing.T) { + record.Set("file_many-", f1.Name) + record.Set("file_many+", f4) + + err := txApp.Save(record) + if err != nil { + t.Fatalf("Expected save to succeed, got %v", err) + } + + raw, _ := json.Marshal(record.GetRaw("file_many")) + expectedRaw, _ := json.Marshal([]any{f3.Name, f4.Name}) + if !bytes.Equal(expectedRaw, raw) { + t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) + } + + checkRecordFiles(t, txApp, record, []string{f3.Name, f4.Name, f1.Name, f2.Name}) // f1 and f2 shouldn't be deleted yet + }) + if !ok { + return txErr + } + + // --- + + return txErr + } + } + + // failed transaction + txErr := testApp.RunInTransaction(tx(false)) + if txErr == nil { + t.Fatal("Expected transaction error") + } + // there shouldn't be any fails associated with the record id + checkRecordFiles(t, testApp, record, []string{}) + + txErr = testApp.RunInTransaction(tx(true)) + if txErr != nil { + t.Fatalf("Expected transaction to succeed, got %v", txErr) + } + // only the last updated files should remain + checkRecordFiles(t, testApp, record, []string{f3.Name, f4.Name}) +} + +// ------------------------------------------------------------------- + +func checkRecordFiles(t *testing.T, testApp core.App, record *core.Record, expectedKeys []string) { + fsys, err := testApp.NewFilesystem() + if err != nil { + t.Fatal(err) + } + defer fsys.Close() + + objects, err := fsys.List(record.BaseFilesPath() + "/") + if err != nil { + t.Fatal(err) + } + objectKeys := make([]string, 0, len(objects)) + for _, obj := range objects { + // exclude thumbs + if !strings.Contains(obj.Key, "/thumbs_") { + objectKeys = append(objectKeys, obj.Key) + } + } + + if len(objectKeys) != len(expectedKeys) { + t.Fatalf("Expected files:\n%v\ngot\n%v", expectedKeys, objectKeys) + } + for _, key := range expectedKeys { + fullKey := record.BaseFilesPath() + "/" + key + if !slices.Contains(objectKeys, fullKey) { + t.Fatalf("Missing expected file key\n%q\nin\n%v", fullKey, objectKeys) + } + } +} diff --git a/core/field_json.go b/core/field_json.go new file mode 100644 index 00000000..45adc473 --- /dev/null +++ b/core/field_json.go @@ -0,0 +1,182 @@ +package core + +import ( + "context" + "fmt" + "slices" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + Fields[FieldTypeJSON] = func() Field { + return &JSONField{} + } +} + +const FieldTypeJSON = "json" + +const DefaultJSONFieldMaxSize int64 = 5 << 20 + +var ( + _ Field = (*JSONField)(nil) + _ MaxBodySizeCalculator = (*JSONField)(nil) +) + +// JSONField defines "json" type field for storing any serialized JSON value. +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"` + + // --- + + // MaxSize specifies the maximum size of the allowed field value (in bytes). + // + // If zero, a default limit of 5MB is applied. + MaxSize int64 `form:"maxSize" json:"maxSize"` + + // Required will require the field value to be non-empty JSON value + // (aka. not "null", `""`, "[]", "{}"). + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *JSONField) Type() string { + return FieldTypeJSON +} + +// GetId implements [Field.GetId] interface method. +func (f *JSONField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *JSONField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *JSONField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *JSONField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *JSONField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *JSONField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *JSONField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *JSONField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *JSONField) ColumnType(app App) string { + return "JSON DEFAULT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *JSONField) PrepareValue(record *Record, raw any) (any, error) { + if str, ok := raw.(string); ok { + // in order to support seamlessly both json and multipart/form-data requests, + // the following normalization rules are applied for plain string values: + // - "true" is converted to the json `true` + // - "false" is converted to the json `false` + // - "null" is converted to the json `null` + // - "[1,2,3]" is converted to the json `[1,2,3]` + // - "{\"a\":1,\"b\":2}" is converted to the json `{"a":1,"b":2}` + // - numeric strings are converted to json number + // - double quoted strings are left as they are (aka. without normalizations) + // - any other string (empty string too) is double quoted + if str == "" { + raw = strconv.Quote(str) + } else if str == "null" || str == "true" || str == "false" { + raw = str + } else if ((str[0] >= '0' && str[0] <= '9') || + str[0] == '-' || + str[0] == '"' || + str[0] == '[' || + str[0] == '{') && + is.JSON.Validate(str) == nil { + raw = str + } else { + raw = strconv.Quote(str) + } + } + + return types.ParseJSONRaw(raw) +} + +var emptyJSONValues = []string{ + "null", `""`, "[]", "{}", "", +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *JSONField) ValidateValue(ctx context.Context, app App, record *Record) error { + raw, ok := record.GetRaw(f.Name).(types.JSONRaw) + if !ok { + return validators.ErrUnsupportedValueType + } + + maxSize := f.CalculateMaxBodySize() + + if int64(len(raw)) > maxSize { + return validation.NewError( + "validation_json_size_limit", + fmt.Sprintf("The maximum allowed JSON size is %v bytes", maxSize), + ).SetParams(map[string]any{"maxSize": maxSize}) + } + + if is.JSON.Validate(raw) != nil { + return validation.NewError("validation_invalid_json", "Must be a valid json value") + } + + rawStr := strings.TrimSpace(raw.String()) + + if f.Required && slices.Contains(emptyJSONValues, rawStr) { + return validation.ErrRequired + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *JSONField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.MaxSize, validation.Min(0)), + ) +} + +// CalculateMaxBodySize implements the [MaxBodySizeCalculator] interface. +func (f *JSONField) CalculateMaxBodySize() int64 { + if f.MaxSize <= 0 { + return DefaultJSONFieldMaxSize + } + + return f.MaxSize +} diff --git a/core/field_json_test.go b/core/field_json_test.go new file mode 100644 index 00000000..1904e59c --- /dev/null +++ b/core/field_json_test.go @@ -0,0 +1,266 @@ +package core_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestJSONFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeJSON) +} + +func TestJSONFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.JSONField{} + + expected := "JSON DEFAULT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestJSONFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.JSONField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"null", `null`}, + {"", `""`}, + {"true", `true`}, + {"false", `false`}, + {"test", `"test"`}, + {"123", `123`}, + {"-456", `-456`}, + {"[1,2,3]", `[1,2,3]`}, + {"[1,2,3", `"[1,2,3"`}, + {`{"a":1,"b":2}`, `{"a":1,"b":2}`}, + {`{"a":1,"b":2`, `"{\"a\":1,\"b\":2"`}, + {[]int{1, 2, 3}, `[1,2,3]`}, + {map[string]int{"a": 1, "b": 2}, `{"a":1,"b":2}`}, + {nil, `null`}, + {false, `false`}, + {true, `true`}, + {-78, `-78`}, + {123.456, `123.456`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + raw, ok := v.(types.JSONRaw) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + rawStr := raw.String() + + if rawStr != s.expected { + t.Fatalf("Expected\n%#v\ngot\n%#v", s.expected, rawStr) + } + }) + } +} + +func TestJSONFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.JSONField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.JSONField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.JSONField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw{}) + return record + }, + false, + }, + { + "zero field value (required)", + &core.JSONField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw{}) + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.JSONField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw("[1,2,3]")) + return record + }, + false, + }, + { + "non-zero field value (required)", + &core.JSONField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw(`"aaa"`)) + return record + }, + false, + }, + { + "> default MaxSize", + &core.JSONField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw(`"`+strings.Repeat("a", (5<<20))+`"`)) + return record + }, + true, + }, + { + "> MaxSize", + &core.JSONField{Name: "test", MaxSize: 5}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw(`"aaaa"`)) + return record + }, + true, + }, + { + "<= MaxSize", + &core.JSONField{Name: "test", MaxSize: 5}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", types.JSONRaw(`"aaa"`)) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestJSONFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeJSON) + testDefaultFieldNameValidation(t, core.FieldTypeJSON) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.JSONField + expectErrors []string + }{ + { + "< 0 MaxSize", + func() *core.JSONField { + return &core.JSONField{ + Id: "test", + Name: "test", + MaxSize: -1, + } + }, + []string{"maxSize"}, + }, + { + "= 0 MaxSize", + func() *core.JSONField { + return &core.JSONField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "> 0 MaxSize", + func() *core.JSONField { + return &core.JSONField{ + Id: "test", + Name: "test", + MaxSize: 1, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestJSONFieldCalculateMaxBodySize(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + field *core.JSONField + expected int64 + }{ + {&core.JSONField{}, core.DefaultJSONFieldMaxSize}, + {&core.JSONField{MaxSize: 10}, 10}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.field.MaxSize), func(t *testing.T) { + result := s.field.CalculateMaxBodySize() + + if result != s.expected { + t.Fatalf("Expected %d, got %d", s.expected, result) + } + }) + } +} diff --git a/core/field_number.go b/core/field_number.go new file mode 100644 index 00000000..0704d40f --- /dev/null +++ b/core/field_number.go @@ -0,0 +1,203 @@ +package core + +import ( + "context" + "fmt" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeNumber] = func() Field { + return &NumberField{} + } +} + +const FieldTypeNumber = "number" + +var ( + _ Field = (*NumberField)(nil) + _ SetterFinder = (*NumberField)(nil) +) + +// NumberField defines "number" type field for storing numeric (float64) value. +// +// The following additional setter keys are available: +// +// - "fieldName+" - appends to the existing record value. For example: +// record.Set("total+", 5) +// - "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"` + + // --- + + // Min specifies the min allowed field value. + // + // Leave it nil to skip the validator. + Min *float64 `form:"min" json:"min"` + + // Max specifies the max allowed field value. + // + // Leave it nil to skip the validator. + Max *float64 `form:"max" json:"max"` + + // OnlyInt will require the field value to be integer. + OnlyInt bool `form:"onlyInt" json:"onlyInt"` + + // Required will require the field value to be non-zero. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *NumberField) Type() string { + return FieldTypeNumber +} + +// GetId implements [Field.GetId] interface method. +func (f *NumberField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *NumberField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *NumberField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *NumberField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *NumberField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *NumberField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *NumberField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *NumberField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *NumberField) ColumnType(app App) string { + return "NUMERIC DEFAULT 0 NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *NumberField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToFloat64(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *NumberField) ValidateValue(ctx context.Context, app App, record *Record) error { + val, ok := record.GetRaw(f.Name).(float64) + if !ok { + return validators.ErrUnsupportedValueType + } + + if val == 0 { + if f.Required { + if err := validation.Required.Validate(val); err != nil { + return err + } + } + return nil + } + + if f.OnlyInt && val != float64(int64(val)) { + return validation.NewError("validation_only_int_constraint", "Decimal numbers are not allowed") + } + + if f.Min != nil && val < *f.Min { + return validation.NewError("validation_min_number_constraint", fmt.Sprintf("Must be larger than %f", *f.Min)) + } + + if f.Max != nil && val > *f.Max { + return validation.NewError("validation_max_number_constraint", fmt.Sprintf("Must be less than %f", *f.Max)) + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *NumberField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + maxRules := []validation.Rule{ + validation.By(f.checkOnlyInt), + } + if f.Min != nil && f.Max != nil { + maxRules = append(maxRules, validation.Min(*f.Min)) + } + + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.Min, validation.By(f.checkOnlyInt)), + validation.Field(&f.Max, maxRules...), + ) +} + +func (f *NumberField) checkOnlyInt(value any) error { + v, _ := value.(*float64) + if v == nil || !f.OnlyInt { + return nil // nothing to check + } + + if *v != float64(int64(*v)) { + return validation.NewError("validation_only_int_constraint", "Decimal numbers are not allowed.") + } + + return nil +} + +// FindSetter implements the [SetterFinder] interface. +func (f *NumberField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return f.setValue + case f.Name + "+": + return f.addValue + case f.Name + "-": + return f.subtractValue + default: + return nil + } +} + +func (f *NumberField) setValue(record *Record, raw any) { + record.SetRaw(f.Name, cast.ToFloat64(raw)) +} + +func (f *NumberField) addValue(record *Record, raw any) { + val := cast.ToFloat64(record.GetRaw(f.Name)) + + record.SetRaw(f.Name, val+cast.ToFloat64(raw)) +} + +func (f *NumberField) subtractValue(record *Record, raw any) { + val := cast.ToFloat64(record.GetRaw(f.Name)) + + record.SetRaw(f.Name, val-cast.ToFloat64(raw)) +} diff --git a/core/field_number_test.go b/core/field_number_test.go new file mode 100644 index 00000000..d1e06567 --- /dev/null +++ b/core/field_number_test.go @@ -0,0 +1,383 @@ +package core_test + +import ( + "context" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNumberFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeNumber) +} + +func TestNumberFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.NumberField{} + + expected := "NUMERIC DEFAULT 0 NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestNumberFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.NumberField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected float64 + }{ + {"", 0}, + {"test", 0}, + {false, 0}, + {true, 1}, + {-2, -2}, + {123.456, 123.456}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + vRaw, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + v, ok := vRaw.(float64) + if !ok { + t.Fatalf("Expected float64 instance, got %T", v) + } + + if v != s.expected { + t.Fatalf("Expected %f, got %f", s.expected, v) + } + }) + } +} + +func TestNumberFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.NumberField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.NumberField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "123") + return record + }, + true, + }, + { + "zero field value (not required)", + &core.NumberField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 0.0) + return record + }, + false, + }, + { + "zero field value (required)", + &core.NumberField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 0.0) + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.NumberField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123.0) + return record + }, + false, + }, + { + "decimal with onlyInt", + &core.NumberField{Name: "test", OnlyInt: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123.456) + return record + }, + true, + }, + { + "int with onlyInt", + &core.NumberField{Name: "test", OnlyInt: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123.0) + return record + }, + false, + }, + { + "< min", + &core.NumberField{Name: "test", Min: types.Pointer(2.0)}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 1.0) + return record + }, + true, + }, + { + ">= min", + &core.NumberField{Name: "test", Min: types.Pointer(2.0)}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 2.0) + return record + }, + false, + }, + { + "> max", + &core.NumberField{Name: "test", Max: types.Pointer(2.0)}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 3.0) + return record + }, + true, + }, + { + "<= max", + &core.NumberField{Name: "test", Max: types.Pointer(2.0)}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 2.0) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestNumberFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeNumber) + testDefaultFieldNameValidation(t, core.FieldTypeNumber) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.NumberField + expectErrors []string + }{ + { + "zero", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "decumal min", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + Min: types.Pointer(1.2), + } + }, + []string{}, + }, + { + "decumal min (onlyInt)", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + OnlyInt: true, + Min: types.Pointer(1.2), + } + }, + []string{"min"}, + }, + { + "int min (onlyInt)", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + OnlyInt: true, + Min: types.Pointer(1.0), + } + }, + []string{}, + }, + { + "decumal max", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + Max: types.Pointer(1.2), + } + }, + []string{}, + }, + { + "decumal max (onlyInt)", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + OnlyInt: true, + Max: types.Pointer(1.2), + } + }, + []string{"max"}, + }, + { + "int max (onlyInt)", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + OnlyInt: true, + Max: types.Pointer(1.0), + } + }, + []string{}, + }, + { + "min > max", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + Min: types.Pointer(2.0), + Max: types.Pointer(1.0), + } + }, + []string{"max"}, + }, + { + "min <= max", + func() *core.NumberField { + return &core.NumberField{ + Id: "test", + Name: "test", + Min: types.Pointer(2.0), + Max: types.Pointer(2.0), + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestNumberFieldFindSetter(t *testing.T) { + field := &core.NumberField{Name: "test"} + + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(field) + + t.Run("no match", func(t *testing.T) { + f := field.FindSetter("abc") + if f != nil { + t.Fatal("Expected nil setter") + } + }) + + t.Run("direct name match", func(t *testing.T) { + f := field.FindSetter("test") + if f == nil { + t.Fatal("Expected non-nil setter") + } + + record := core.NewRecord(collection) + record.SetRaw("test", 2.0) + + f(record, "123.456") // should be casted + + if v := record.Get("test"); v != 123.456 { + t.Fatalf("Expected %f, got %f", 123.456, v) + } + }) + + t.Run("name+ match", func(t *testing.T) { + f := field.FindSetter("test+") + if f == nil { + t.Fatal("Expected non-nil setter") + } + + record := core.NewRecord(collection) + record.SetRaw("test", 2.0) + + f(record, "1.5") // should be casted and appended to the existing value + + if v := record.Get("test"); v != 3.5 { + t.Fatalf("Expected %f, got %f", 3.5, v) + } + }) + + t.Run("name- match", func(t *testing.T) { + f := field.FindSetter("test-") + if f == nil { + t.Fatal("Expected non-nil setter") + } + + record := core.NewRecord(collection) + record.SetRaw("test", 2.0) + + f(record, "1.5") // should be casted and subtracted from the existing value + + if v := record.Get("test"); v != 0.5 { + t.Fatalf("Expected %f, got %f", 0.5, v) + } + }) +} diff --git a/core/field_password.go b/core/field_password.go new file mode 100644 index 00000000..147e279c --- /dev/null +++ b/core/field_password.go @@ -0,0 +1,306 @@ +package core + +import ( + "context" + "database/sql/driver" + "fmt" + "regexp" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" + "golang.org/x/crypto/bcrypt" +) + +func init() { + Fields[FieldTypePassword] = func() Field { + return &PasswordField{} + } +} + +const FieldTypePassword = "password" + +var ( + _ Field = (*PasswordField)(nil) + _ GetterFinder = (*PasswordField)(nil) + _ SetterFinder = (*PasswordField)(nil) + _ DriverValuer = (*PasswordField)(nil) + _ RecordInterceptor = (*PasswordField)(nil) +) + +// PasswordField defines "password" type field for storing bcrypt hashed strings +// (usually used only internally for the "password" auth collection system field). +// +// If you want to set a direct bcrypt hash as record field value you can use the SetRaw method, for example: +// +// // generates a bcrypt hash of "123456" and set it as field value +// // (record.GetString("password") returns the plain password until persisted, otherwise empty string) +// record.Set("password", "123456") +// +// // set directly a bcrypt hash of "123456" as field value +// // (record.GetString("password") returns empty string) +// record.SetRaw("password", "$2a$10$.5Elh8fgxypNUWhpUUr/xOa2sZm0VIaE0qWuGGl9otUfobb46T1Pq") +// +// The following additional getter keys are available: +// +// - "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"` + + // --- + + // Pattern specifies an optional regex pattern to match against the field value. + // + // Leave it empty to skip the pattern check. + Pattern string `form:"pattern" json:"pattern"` + + // Min specifies an optional required field string length. + Min int `form:"min" json:"min"` + + // Max specifies an optional required field string length. + // + // If zero, fallback to max 71 bytes. + Max int `form:"max" json:"max"` + + // Cost specifies the cost/weight/iteration/etc. bcrypt factor. + // + // If zero, fallback to [bcrypt.DefaultCost]. + // + // If explicitly set, must be between [bcrypt.MinCost] and [bcrypt.MaxCost]. + Cost int `form:"cost" json:"cost"` + + // Required will require the field value to be non-empty string. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *PasswordField) Type() string { + return FieldTypePassword +} + +// GetId implements [Field.GetId] interface method. +func (f *PasswordField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *PasswordField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *PasswordField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *PasswordField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *PasswordField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *PasswordField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *PasswordField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *PasswordField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *PasswordField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" +} + +// DriverValue implements the [DriverValuer] interface. +func (f *PasswordField) DriverValue(record *Record) (driver.Value, error) { + fp := f.getPasswordValue(record) + return fp.Hash, fp.LastError +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *PasswordField) PrepareValue(record *Record, raw any) (any, error) { + return &PasswordFieldValue{ + Hash: cast.ToString(raw), + }, nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *PasswordField) ValidateValue(ctx context.Context, app App, record *Record) error { + fp, ok := record.GetRaw(f.Name).(*PasswordFieldValue) + if !ok { + return validators.ErrUnsupportedValueType + } + + if fp.LastError != nil { + return fp.LastError + } + + if f.Required { + if err := validation.Required.Validate(fp.Hash); err != nil { + return err + } + } + + if fp.Plain == "" { + return nil // nothing to check + } + + // note: casted to []rune to count multi-byte chars as one for the + // sake of more intuitive UX and clearer user error messages + // + // note2: technically multi-byte strings could produce bigger length than the bcrypt limit + // but it should be fine as it will be just truncated (even if it cuts a byte sequence in the middle) + length := len([]rune(fp.Plain)) + + if length < f.Min { + return validation.NewError("validation_min_text_constraint", fmt.Sprintf("Must be at least %d character(s)", f.Min)) + } + + maxLength := f.Max + if maxLength <= 0 { + maxLength = 71 + } + if length > maxLength { + return validation.NewError("validation_max_text_constraint", fmt.Sprintf("Must be less than %d character(s)", maxLength)) + } + + if f.Pattern != "" { + match, _ := regexp.MatchString(f.Pattern, fp.Plain) + if !match { + return validation.NewError("validation_invalid_format", "Invalid value format") + } + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *PasswordField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.Min, validation.Min(1), validation.Max(71)), + validation.Field(&f.Max, validation.Min(f.Min), validation.Max(71)), + validation.Field(&f.Cost, validation.Min(bcrypt.MinCost), validation.Max(bcrypt.MaxCost)), + validation.Field(&f.Pattern, validation.By(validators.IsRegex)), + ) +} + +func (f *PasswordField) getPasswordValue(record *Record) *PasswordFieldValue { + raw := record.GetRaw(f.Name) + + switch v := raw.(type) { + case *PasswordFieldValue: + return v + case string: + // we assume that any raw string starting with $2 is bcrypt hash + if strings.HasPrefix(v, "$2") { + return &PasswordFieldValue{Hash: v} + } + } + + return &PasswordFieldValue{} +} + +// Intercept implements the [RecordInterceptor] interface. +func (f *PasswordField) Intercept( + ctx context.Context, + app App, + record *Record, + actionName string, + actionFunc func() error, +) error { + switch actionName { + case InterceptorActionAfterCreate, InterceptorActionAfterUpdate: + // unset the plain field value after successful create/update + fp := f.getPasswordValue(record) + fp.Plain = "" + } + + return actionFunc() +} + +// FindGetter implements the [GetterFinder] interface. +func (f *PasswordField) FindGetter(key string) GetterFunc { + switch key { + case f.Name: + return func(record *Record) any { + return f.getPasswordValue(record).Plain + } + case f.Name + ":hash": + return func(record *Record) any { + return f.getPasswordValue(record).Hash + } + default: + return nil + } +} + +// FindSetter implements the [SetterFinder] interface. +func (f *PasswordField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return f.setValue + default: + return nil + } +} + +func (f *PasswordField) setValue(record *Record, raw any) { + fv := &PasswordFieldValue{ + Plain: cast.ToString(raw), + } + + // hash the password + if fv.Plain != "" { + cost := f.Cost + if cost <= 0 { + cost = bcrypt.DefaultCost + } + + hash, err := bcrypt.GenerateFromPassword([]byte(fv.Plain), cost) + if err != nil { + fv.LastError = err + } + + fv.Hash = string(hash) + } + + record.SetRaw(f.Name, fv) +} + +// ------------------------------------------------------------------- + +type PasswordFieldValue struct { + LastError error + Hash string + Plain string +} + +func (pv PasswordFieldValue) Validate(pass string) bool { + if pv.Hash == "" || pv.LastError != nil { + return false + } + + err := bcrypt.CompareHashAndPassword([]byte(pv.Hash), []byte(pass)) + + return err == nil +} diff --git a/core/field_password_test.go b/core/field_password_test.go new file mode 100644 index 00000000..31a1113d --- /dev/null +++ b/core/field_password_test.go @@ -0,0 +1,568 @@ +package core_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "golang.org/x/crypto/bcrypt" +) + +func TestPasswordFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypePassword) +} + +func TestPasswordFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.PasswordField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestPasswordFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.PasswordField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + {123.456, "123.456"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + pv, ok := v.(*core.PasswordFieldValue) + if !ok { + t.Fatalf("Expected PasswordFieldValue instance, got %T", v) + } + + if pv.Hash != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, v) + } + }) + } +} + +func TestPasswordFieldDriverValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.PasswordField{Name: "test"} + + err := errors.New("example_err") + + scenarios := []struct { + raw any + expected *core.PasswordFieldValue + }{ + {123, &core.PasswordFieldValue{}}, + {"abc", &core.PasswordFieldValue{}}, + {"$2abc", &core.PasswordFieldValue{Hash: "$2abc"}}, + {&core.PasswordFieldValue{Hash: "test", LastError: err}, &core.PasswordFieldValue{Hash: "test", LastError: err}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%v", i, s.raw), func(t *testing.T) { + record := core.NewRecord(core.NewBaseCollection("test")) + record.SetRaw(f.GetName(), s.raw) + + v, err := f.DriverValue(record) + + vStr, ok := v.(string) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + + var errStr string + if err != nil { + errStr = err.Error() + } + + var expectedErrStr string + if s.expected.LastError != nil { + expectedErrStr = s.expected.LastError.Error() + } + + if errStr != expectedErrStr { + t.Fatalf("Expected error %q, got %q", expectedErrStr, errStr) + } + + if vStr != s.expected.Hash { + t.Fatalf("Expected hash %q, got %q", s.expected.Hash, vStr) + } + }) + } +} + +func TestPasswordFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.PasswordField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.PasswordField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "123") + return record + }, + true, + }, + { + "zero field value (not required)", + &core.PasswordField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{}) + return record + }, + false, + }, + { + "zero field value (required)", + &core.PasswordField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{}) + return record + }, + true, + }, + { + "empty hash but non-empty plain password (required)", + &core.PasswordField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "test"}) + return record + }, + true, + }, + { + "non-empty hash (required)", + &core.PasswordField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Hash: "test"}) + return record + }, + false, + }, + { + "with LastError", + &core.PasswordField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{LastError: errors.New("test")}) + return record + }, + true, + }, + { + "< Min", + &core.PasswordField{Name: "test", Min: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "аб"}) // multi-byte chars test + return record + }, + true, + }, + { + ">= Min", + &core.PasswordField{Name: "test", Min: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "абв"}) // multi-byte chars test + return record + }, + false, + }, + { + "> default Max", + &core.PasswordField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: strings.Repeat("a", 72)}) + return record + }, + true, + }, + { + "<= default Max", + &core.PasswordField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: strings.Repeat("a", 71)}) + return record + }, + false, + }, + { + "> Max", + &core.PasswordField{Name: "test", Max: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "абв"}) // multi-byte chars test + return record + }, + true, + }, + { + "<= Max", + &core.PasswordField{Name: "test", Max: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "аб"}) // multi-byte chars test + return record + }, + false, + }, + { + "non-matching pattern", + &core.PasswordField{Name: "test", Pattern: `\d+`}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "abc"}) + return record + }, + true, + }, + { + "matching pattern", + &core.PasswordField{Name: "test", Pattern: `\d+`}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", &core.PasswordFieldValue{Plain: "123"}) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestPasswordFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypePassword) + testDefaultFieldNameValidation(t, core.FieldTypePassword) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field func(col *core.Collection) *core.PasswordField + expectErrors []string + }{ + { + "zero minimal", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "invalid pattern", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Pattern: "(invalid", + } + }, + []string{"pattern"}, + }, + { + "valid pattern", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Pattern: `\d+`, + } + }, + []string{}, + }, + { + "Min < 0", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Min: -1, + } + }, + []string{"min"}, + }, + { + "Min > 71", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Min: 72, + } + }, + []string{"min"}, + }, + { + "valid Min", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Min: 5, + } + }, + []string{}, + }, + { + "Max < Min", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Min: 2, + Max: 1, + } + }, + []string{"max"}, + }, + { + "Min > Min", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Min: 2, + Max: 3, + } + }, + []string{}, + }, + { + "Max > 71", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Max: 72, + } + }, + []string{"max"}, + }, + { + "cost < bcrypt.MinCost", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Cost: bcrypt.MinCost - 1, + } + }, + []string{"cost"}, + }, + { + "cost > bcrypt.MaxCost", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Cost: bcrypt.MaxCost + 1, + } + }, + []string{"cost"}, + }, + { + "valid cost", + func(col *core.Collection) *core.PasswordField { + return &core.PasswordField{ + Id: "test", + Name: "test", + Cost: 12, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.GetByName("id").SetId("test") // set a dummy known id so that it can be replaced + + field := s.field(collection) + + collection.Fields.Add(field) + + errs := field.ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestPasswordFieldFindSetter(t *testing.T) { + scenarios := []struct { + name string + key string + value any + field *core.PasswordField + hasSetter bool + expected string + }{ + { + "no match", + "example", + "abc", + &core.PasswordField{Name: "test"}, + false, + "", + }, + { + "exact match", + "test", + "abc", + &core.PasswordField{Name: "test"}, + true, + `"abc"`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + setter := s.field.FindSetter(s.key) + + hasSetter := setter != nil + if hasSetter != s.hasSetter { + t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) + } + + if !hasSetter { + return + } + + record := core.NewRecord(collection) + record.SetRaw(s.field.GetName(), []string{"c", "d"}) + + setter(record, s.value) + + raw, err := json.Marshal(record.Get(s.field.GetName())) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, rawStr) + } + }) + } +} + +func TestPasswordFieldFindGetter(t *testing.T) { + scenarios := []struct { + name string + key string + field *core.PasswordField + hasGetter bool + expected string + }{ + { + "no match", + "example", + &core.PasswordField{Name: "test"}, + false, + "", + }, + { + "field name match", + "test", + &core.PasswordField{Name: "test"}, + true, + "test_plain", + }, + { + "field name hash modifier", + "test:hash", + &core.PasswordField{Name: "test"}, + true, + "test_hash", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + getter := s.field.FindGetter(s.key) + + hasGetter := getter != nil + if hasGetter != s.hasGetter { + t.Fatalf("Expected hasGetter %v, got %v", s.hasGetter, hasGetter) + } + + if !hasGetter { + return + } + + record := core.NewRecord(collection) + record.SetRaw(s.field.GetName(), &core.PasswordFieldValue{Hash: "test_hash", Plain: "test_plain"}) + + result := getter(record) + + if result != s.expected { + t.Fatalf("Expected %q, got %#v", s.expected, result) + } + }) + } +} diff --git a/core/field_relation.go b/core/field_relation.go new file mode 100644 index 00000000..e41a440b --- /dev/null +++ b/core/field_relation.go @@ -0,0 +1,337 @@ +package core + +import ( + "context" + "database/sql/driver" + "fmt" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + Fields[FieldTypeRelation] = func() Field { + return &RelationField{} + } +} + +const FieldTypeRelation = "relation" + +var ( + _ Field = (*RelationField)(nil) + _ MultiValuer = (*RelationField)(nil) + _ DriverValuer = (*RelationField)(nil) + _ SetterFinder = (*RelationField)(nil) +) + +// RelationField defines "relation" type field for storing single or +// multiple collection record references. +// +// Requires the CollectionId option to be set. +// +// If MaxSelect is not set or <= 1, then the field value is expected to be a single record id. +// +// If MaxSelect is > 1, then the field value is expected to be a slice of record ids. +// +// --- +// +// The following additional setter keys are available: +// +// - "fieldName+" - append one or more values to the existing record one. For example: +// +// record.Set("categories+", []string{"new1", "new2"}) // []string{"old1", "old2", "new1", "new2"} +// +// - "+fieldName" - prepend one or more values to the existing record one. For example: +// +// record.Set("+categories", []string{"new1", "new2"}) // []string{"new1", "new2", "old1", "old2"} +// +// - "fieldName-" - subtract one or more values from the existing record one. For example: +// +// 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"` + + // --- + + // CollectionId is the id of the related collection. + CollectionId string `form:"collectionId" json:"collectionId"` + + // CascadeDelete indicates whether the root model should be deleted + // in case of delete of all linked relations. + CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"` + + // MinSelect indicates the min number of allowed relation records + // that could be linked to the main model. + // + // No min limit is applied if it is zero or negative value. + MinSelect int `form:"minSelect" json:"minSelect"` + + // MaxSelect indicates the max number of allowed relation records + // that could be linked to the main model. + // + // For multiple select the value must be > 1, otherwise fallbacks to single (default). + // + // If MinSelect is set, MaxSelect must be at least >= MinSelect. + MaxSelect int `form:"maxSelect" json:"maxSelect"` + + // Required will require the field value to be non-empty. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *RelationField) Type() string { + return FieldTypeRelation +} + +// GetId implements [Field.GetId] interface method. +func (f *RelationField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *RelationField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *RelationField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *RelationField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *RelationField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *RelationField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *RelationField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *RelationField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// IsMultiple implements [MultiValuer] interface and checks whether the +// current field options support multiple values. +func (f *RelationField) IsMultiple() bool { + return f.MaxSelect > 1 +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *RelationField) ColumnType(app App) string { + if f.IsMultiple() { + return "JSON DEFAULT '[]' NOT NULL" + } + + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *RelationField) PrepareValue(record *Record, raw any) (any, error) { + return f.normalizeValue(raw), nil +} + +func (f *RelationField) normalizeValue(raw any) any { + val := list.ToUniqueStringSlice(raw) + + if !f.IsMultiple() { + if len(val) > 0 { + return val[len(val)-1] // the last selected + } + return "" + } + + return val +} + +// DriverValue implements the [DriverValuer] interface. +func (f *RelationField) DriverValue(record *Record) (driver.Value, error) { + val := list.ToUniqueStringSlice(record.GetRaw(f.Name)) + + if !f.IsMultiple() { + if len(val) > 0 { + return val[len(val)-1], nil // the last selected + } + return "", nil + } + + // serialize as json string array + return append(types.JSONArray[string]{}, val...), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *RelationField) ValidateValue(ctx context.Context, app App, record *Record) error { + ids := list.ToUniqueStringSlice(record.GetRaw(f.Name)) + if len(ids) == 0 { + if f.Required { + return validation.ErrRequired + } + return nil // nothing to check + } + + if f.MinSelect > 0 && len(ids) < f.MinSelect { + return validation.NewError("validation_not_enough_values", fmt.Sprintf("Select at least %d", f.MinSelect)). + SetParams(map[string]any{"minSelect": f.MinSelect}) + } + + maxSelect := max(f.MaxSelect, 1) + if len(ids) > maxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", maxSelect)). + SetParams(map[string]any{"maxSelect": maxSelect}) + } + + // check if the related records exist + // --- + relCollection, err := app.FindCachedCollectionByNameOrId(f.CollectionId) + if err != nil { + return validation.NewError("validation_missing_rel_collection", "Relation connection is missing or cannot be accessed") + } + + var total int + _ = app.DB(). + Select("count(*)"). + From(relCollection.Name). + AndWhere(dbx.In("id", list.ToInterfaceSlice(ids)...)). + Row(&total) + if total != len(ids) { + return validation.NewError("validation_missing_rel_records", "Failed to find all relation records with the provided ids") + } + // --- + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *RelationField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.CollectionId, validation.Required, validation.By(f.checkCollectionId(app, collection))), + validation.Field(&f.MinSelect, validation.Min(0)), + validation.Field(&f.MaxSelect, validation.When(f.MinSelect > 0, validation.Required), validation.Min(f.MinSelect)), + ) +} + +func (f *RelationField) checkCollectionId(app App, collection *Collection) validation.RuleFunc { + return func(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + var oldCollection *Collection + + if !collection.IsNew() { + var err error + oldCollection, err = app.FindCachedCollectionByNameOrId(collection.Id) + if err != nil { + return err + } + } + + // prevent collectionId change + if oldCollection != nil { + oldField, _ := oldCollection.Fields.GetById(f.Id).(*RelationField) + if oldField != nil && oldField.CollectionId != v { + return validation.NewError( + "validation_field_relation_change", + "The relation collection cannot be changed.", + ) + } + } + + relCollection, _ := app.FindCachedCollectionByNameOrId(v) + + // validate collectionId + if relCollection == nil || relCollection.Id != v { + return validation.NewError( + "validation_field_relation_missing_collection", + "The relation collection doesn't exist.", + ) + } + + // allow only views to have relations to other views + // (see https://github.com/pocketbase/pocketbase/issues/3000) + if !collection.IsView() && relCollection.IsView() { + return validation.NewError( + "validation_relation_field_non_view_base_collection", + "Only view collections are allowed to have relations to other views.", + ) + } + + return nil + } +} + +// --- + +// FindSetter implements [SetterFinder] interface method. +func (f *RelationField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return f.setValue + case "+" + f.Name: + return f.prependValue + case f.Name + "+": + return f.appendValue + case f.Name + "-": + return f.subtractValue + default: + return nil + } +} + +func (f *RelationField) setValue(record *Record, raw any) { + record.SetRaw(f.Name, f.normalizeValue(raw)) +} + +func (f *RelationField) appendValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = append( + list.ToUniqueStringSlice(val), + list.ToUniqueStringSlice(modifierValue)..., + ) + + f.setValue(record, val) +} + +func (f *RelationField) prependValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = append( + list.ToUniqueStringSlice(modifierValue), + list.ToUniqueStringSlice(val)..., + ) + + f.setValue(record, val) +} + +func (f *RelationField) subtractValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = list.SubtractSlice( + list.ToUniqueStringSlice(val), + list.ToUniqueStringSlice(modifierValue), + ) + + f.setValue(record, val) +} diff --git a/core/field_relation_test.go b/core/field_relation_test.go new file mode 100644 index 00000000..598e361b --- /dev/null +++ b/core/field_relation_test.go @@ -0,0 +1,603 @@ +package core_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestRelationFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeRelation) +} + +func TestRelationFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field *core.RelationField + expected string + }{ + { + "single (zero)", + &core.RelationField{}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "single", + &core.RelationField{MaxSelect: 1}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "multiple", + &core.RelationField{MaxSelect: 2}, + "JSON DEFAULT '[]' NOT NULL", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.ColumnType(app); v != s.expected { + t.Fatalf("Expected\n%q\ngot\n%q", s.expected, v) + } + }) + } +} + +func TestRelationFieldIsMultiple(t *testing.T) { + scenarios := []struct { + name string + field *core.RelationField + expected bool + }{ + { + "zero", + &core.RelationField{}, + false, + }, + { + "single", + &core.RelationField{MaxSelect: 1}, + false, + }, + { + "multiple", + &core.RelationField{MaxSelect: 2}, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.IsMultiple(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestRelationFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + field *core.RelationField + expected string + }{ + // single + {nil, &core.RelationField{MaxSelect: 1}, `""`}, + {"", &core.RelationField{MaxSelect: 1}, `""`}, + {123, &core.RelationField{MaxSelect: 1}, `"123"`}, + {"a", &core.RelationField{MaxSelect: 1}, `"a"`}, + {`["a"]`, &core.RelationField{MaxSelect: 1}, `"a"`}, + {[]string{}, &core.RelationField{MaxSelect: 1}, `""`}, + {[]string{"a", "b"}, &core.RelationField{MaxSelect: 1}, `"b"`}, + + // multiple + {nil, &core.RelationField{MaxSelect: 2}, `[]`}, + {"", &core.RelationField{MaxSelect: 2}, `[]`}, + {123, &core.RelationField{MaxSelect: 2}, `["123"]`}, + {"a", &core.RelationField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.RelationField{MaxSelect: 2}, `["a"]`}, + {[]string{}, &core.RelationField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.RelationField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + v, err := s.field.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestRelationFieldDriverValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + raw any + field *core.RelationField + expected string + }{ + // single + {nil, &core.RelationField{MaxSelect: 1}, `""`}, + {"", &core.RelationField{MaxSelect: 1}, `""`}, + {123, &core.RelationField{MaxSelect: 1}, `"123"`}, + {"a", &core.RelationField{MaxSelect: 1}, `"a"`}, + {`["a"]`, &core.RelationField{MaxSelect: 1}, `"a"`}, + {[]string{}, &core.RelationField{MaxSelect: 1}, `""`}, + {[]string{"a", "b"}, &core.RelationField{MaxSelect: 1}, `"b"`}, + + // multiple + {nil, &core.RelationField{MaxSelect: 2}, `[]`}, + {"", &core.RelationField{MaxSelect: 2}, `[]`}, + {123, &core.RelationField{MaxSelect: 2}, `["123"]`}, + {"a", &core.RelationField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.RelationField{MaxSelect: 2}, `["a"]`}, + {[]string{}, &core.RelationField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.RelationField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + record := core.NewRecord(core.NewBaseCollection("test")) + record.SetRaw(s.field.GetName(), s.raw) + + v, err := s.field.DriverValue(record) + if err != nil { + t.Fatal(err) + } + + if s.field.IsMultiple() { + _, ok := v.(types.JSONArray[string]) + if !ok { + t.Fatalf("Expected types.JSONArray value, got %T", v) + } + } else { + _, ok := v.(string) + if !ok { + t.Fatalf("Expected string value, got %T", v) + } + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestRelationFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + field *core.RelationField + record func() *core.Record + expectError bool + }{ + // single + { + "[single] zero field value (not required)", + &core.RelationField{Name: "test", MaxSelect: 1, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "[single] zero field value (required)", + &core.RelationField{Name: "test", MaxSelect: 1, CollectionId: demo1.Id, Required: true}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "[single] id from other collection", + &core.RelationField{Name: "test", MaxSelect: 1, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", "achvryl401bhse3") + return record + }, + true, + }, + { + "[single] valid id", + &core.RelationField{Name: "test", MaxSelect: 1, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", "84nmscqy84lsi1t") + return record + }, + false, + }, + { + "[single] > MaxSelect", + &core.RelationField{Name: "test", MaxSelect: 1, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}) + return record + }, + true, + }, + + // multiple + { + "[multiple] zero field value (not required)", + &core.RelationField{Name: "test", MaxSelect: 2, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{}) + return record + }, + false, + }, + { + "[multiple] zero field value (required)", + &core.RelationField{Name: "test", MaxSelect: 2, CollectionId: demo1.Id, Required: true}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{}) + return record + }, + true, + }, + { + "[multiple] id from other collection", + &core.RelationField{Name: "test", MaxSelect: 2, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t", "achvryl401bhse3"}) + return record + }, + true, + }, + { + "[multiple] valid id", + &core.RelationField{Name: "test", MaxSelect: 2, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}) + return record + }, + false, + }, + { + "[multiple] > MaxSelect", + &core.RelationField{Name: "test", MaxSelect: 2, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t", "al1h9ijdeojtsjy", "imy661ixudk5izi"}) + return record + }, + true, + }, + { + "[multiple] < MinSelect", + &core.RelationField{Name: "test", MinSelect: 2, MaxSelect: 99, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t"}) + return record + }, + true, + }, + { + "[multiple] >= MinSelect", + &core.RelationField{Name: "test", MinSelect: 2, MaxSelect: 99, CollectionId: demo1.Id}, + func() *core.Record { + record := core.NewRecord(core.NewBaseCollection("test_collection")) + record.SetRaw("test", []string{"84nmscqy84lsi1t", "al1h9ijdeojtsjy", "imy661ixudk5izi"}) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestRelationFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeRelation) + testDefaultFieldNameValidation(t, core.FieldTypeRelation) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + field func(col *core.Collection) *core.RelationField + expectErrors []string + }{ + { + "zero minimal", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + } + }, + []string{"collectionId"}, + }, + { + "invalid collectionId", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Name, + } + }, + []string{"collectionId"}, + }, + { + "valid collectionId", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Id, + } + }, + []string{}, + }, + { + "base->view", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: "v9gwnfh02gjq1q0", + } + }, + []string{"collectionId"}, + }, + { + "view->view", + func(col *core.Collection) *core.RelationField { + col.Type = core.CollectionTypeView + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: "v9gwnfh02gjq1q0", + } + }, + []string{}, + }, + { + "MinSelect < 0", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Id, + MinSelect: -1, + } + }, + []string{"minSelect"}, + }, + { + "MinSelect > 0", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Id, + MinSelect: 1, + } + }, + []string{"maxSelect"}, + }, + { + "MaxSelect < MinSelect", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Id, + MinSelect: 2, + MaxSelect: 1, + } + }, + []string{"maxSelect"}, + }, + { + "MaxSelect >= MinSelect", + func(col *core.Collection) *core.RelationField { + return &core.RelationField{ + Id: "test", + Name: "test", + CollectionId: demo1.Id, + MinSelect: 2, + MaxSelect: 2, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.GetByName("id").SetId("test") // set a dummy known id so that it can be replaced + + field := s.field(collection) + + collection.Fields.Add(field) + + errs := field.ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestRelationFieldFindSetter(t *testing.T) { + scenarios := []struct { + name string + key string + value any + field *core.RelationField + hasSetter bool + expected string + }{ + { + "no match", + "example", + "b", + &core.RelationField{Name: "test", MaxSelect: 1}, + false, + "", + }, + { + "exact match (single)", + "test", + "b", + &core.RelationField{Name: "test", MaxSelect: 1}, + true, + `"b"`, + }, + { + "exact match (multiple)", + "test", + []string{"a", "b"}, + &core.RelationField{Name: "test", MaxSelect: 2}, + true, + `["a","b"]`, + }, + { + "append (single)", + "test+", + "b", + &core.RelationField{Name: "test", MaxSelect: 1}, + true, + `"b"`, + }, + { + "append (multiple)", + "test+", + []string{"a"}, + &core.RelationField{Name: "test", MaxSelect: 2}, + true, + `["c","d","a"]`, + }, + { + "prepend (single)", + "+test", + "b", + &core.RelationField{Name: "test", MaxSelect: 1}, + true, + `"d"`, // the last of the existing values + }, + { + "prepend (multiple)", + "+test", + []string{"a"}, + &core.RelationField{Name: "test", MaxSelect: 2}, + true, + `["a","c","d"]`, + }, + { + "subtract (single)", + "test-", + "d", + &core.RelationField{Name: "test", MaxSelect: 1}, + true, + `"c"`, + }, + { + "subtract (multiple)", + "test-", + []string{"unknown", "c"}, + &core.RelationField{Name: "test", MaxSelect: 2}, + true, + `["d"]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + setter := s.field.FindSetter(s.key) + + hasSetter := setter != nil + if hasSetter != s.hasSetter { + t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) + } + + if !hasSetter { + return + } + + record := core.NewRecord(collection) + record.SetRaw(s.field.GetName(), []string{"c", "d"}) + + setter(record, s.value) + + raw, err := json.Marshal(record.Get(s.field.GetName())) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, rawStr) + } + }) + } +} diff --git a/core/field_select.go b/core/field_select.go new file mode 100644 index 00000000..18344095 --- /dev/null +++ b/core/field_select.go @@ -0,0 +1,262 @@ +package core + +import ( + "context" + "database/sql/driver" + "fmt" + "slices" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + Fields[FieldTypeSelect] = func() Field { + return &SelectField{} + } +} + +const FieldTypeSelect = "select" + +var ( + _ Field = (*SelectField)(nil) + _ MultiValuer = (*SelectField)(nil) + _ DriverValuer = (*SelectField)(nil) + _ SetterFinder = (*SelectField)(nil) +) + +// SelectField defines "select" type field for storing single or +// multiple string values from a predefined list. +// +// Requires the Values option to be set. +// +// If MaxSelect is not set or <= 1, then the field value is expected to be a single Values element. +// +// If MaxSelect is > 1, then the field value is expected to be a subset of Values slice. +// +// --- +// +// The following additional setter keys are available: +// +// - "fieldName+" - append one or more values to the existing record one. For example: +// +// record.Set("roles+", []string{"new1", "new2"}) // []string{"old1", "old2", "new1", "new2"} +// +// - "+fieldName" - prepend one or more values to the existing record one. For example: +// +// record.Set("+roles", []string{"new1", "new2"}) // []string{"new1", "new2", "old1", "old2"} +// +// - "fieldName-" - subtract one or more values from the existing record one. For example: +// +// 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"` + + // --- + + // Values specifies the list of accepted values. + Values []string `form:"values" json:"values"` + + // MaxSelect specifies the max allowed selected values. + // + // For multiple select the value must be > 1, otherwise fallbacks to single (default). + MaxSelect int `form:"maxSelect" json:"maxSelect"` + + // Required will require the field value to be non-empty. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *SelectField) Type() string { + return FieldTypeSelect +} + +// GetId implements [Field.GetId] interface method. +func (f *SelectField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *SelectField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *SelectField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *SelectField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *SelectField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *SelectField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *SelectField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *SelectField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// IsMultiple implements [MultiValuer] interface and checks whether the +// current field options support multiple values. +func (f *SelectField) IsMultiple() bool { + return f.MaxSelect > 1 +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *SelectField) ColumnType(app App) string { + if f.IsMultiple() { + return "JSON DEFAULT '[]' NOT NULL" + } + + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *SelectField) PrepareValue(record *Record, raw any) (any, error) { + return f.normalizeValue(raw), nil +} + +func (f *SelectField) normalizeValue(raw any) any { + val := list.ToUniqueStringSlice(raw) + + if !f.IsMultiple() { + if len(val) > 0 { + return val[len(val)-1] // the last selected + } + return "" + } + + return val +} + +// DriverValue implements the [DriverValuer] interface. +func (f *SelectField) DriverValue(record *Record) (driver.Value, error) { + val := list.ToUniqueStringSlice(record.GetRaw(f.Name)) + + if !f.IsMultiple() { + if len(val) > 0 { + return val[len(val)-1], nil // the last selected + } + return "", nil + } + + // serialize as json string array + return append(types.JSONArray[string]{}, val...), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *SelectField) ValidateValue(ctx context.Context, app App, record *Record) error { + normalizedVal := list.ToUniqueStringSlice(record.GetRaw(f.Name)) + if len(normalizedVal) == 0 { + if f.Required { + return validation.ErrRequired + } + return nil // nothing to check + } + + maxSelect := max(f.MaxSelect, 1) + + // check max selected items + if len(normalizedVal) > maxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", maxSelect)). + SetParams(map[string]any{"maxSelect": maxSelect}) + } + + // check against the allowed values + for _, val := range normalizedVal { + if !slices.Contains(f.Values, val) { + return validation.NewError("validation_invalid_value", "Invalid value "+val). + SetParams(map[string]any{"value": val}) + } + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *SelectField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + max := len(f.Values) + if max == 0 { + max = 1 + } + + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field(&f.Values, validation.Required), + validation.Field(&f.MaxSelect, validation.Min(0), validation.Max(max)), + ) +} + +// FindSetter implements the [SetterFinder] interface. +func (f *SelectField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return f.setValue + case "+" + f.Name: + return f.prependValue + case f.Name + "+": + return f.appendValue + case f.Name + "-": + return f.subtractValue + default: + return nil + } +} + +func (f *SelectField) setValue(record *Record, raw any) { + record.SetRaw(f.Name, f.normalizeValue(raw)) +} + +func (f *SelectField) appendValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = append( + list.ToUniqueStringSlice(val), + list.ToUniqueStringSlice(modifierValue)..., + ) + + f.setValue(record, val) +} + +func (f *SelectField) prependValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = append( + list.ToUniqueStringSlice(modifierValue), + list.ToUniqueStringSlice(val)..., + ) + + f.setValue(record, val) +} + +func (f *SelectField) subtractValue(record *Record, modifierValue any) { + val := record.GetRaw(f.Name) + + val = list.SubtractSlice( + list.ToUniqueStringSlice(val), + list.ToUniqueStringSlice(modifierValue), + ) + + f.setValue(record, val) +} diff --git a/core/field_select_test.go b/core/field_select_test.go new file mode 100644 index 00000000..808c1096 --- /dev/null +++ b/core/field_select_test.go @@ -0,0 +1,516 @@ +package core_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestSelectFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeSelect) +} + +func TestSelectFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field *core.SelectField + expected string + }{ + { + "single (zero)", + &core.SelectField{}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "single", + &core.SelectField{MaxSelect: 1}, + "TEXT DEFAULT '' NOT NULL", + }, + { + "multiple", + &core.SelectField{MaxSelect: 2}, + "JSON DEFAULT '[]' NOT NULL", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.ColumnType(app); v != s.expected { + t.Fatalf("Expected\n%q\ngot\n%q", s.expected, v) + } + }) + } +} + +func TestSelectFieldIsMultiple(t *testing.T) { + scenarios := []struct { + name string + field *core.SelectField + expected bool + }{ + { + "single (zero)", + &core.SelectField{}, + false, + }, + { + "single", + &core.SelectField{MaxSelect: 1}, + false, + }, + { + "multiple (>1)", + &core.SelectField{MaxSelect: 2}, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + if v := s.field.IsMultiple(); v != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, v) + } + }) + } +} + +func TestSelectFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + field *core.SelectField + expected string + }{ + // single + {nil, &core.SelectField{}, `""`}, + {"", &core.SelectField{}, `""`}, + {123, &core.SelectField{}, `"123"`}, + {"a", &core.SelectField{}, `"a"`}, + {`["a"]`, &core.SelectField{}, `"a"`}, + {[]string{}, &core.SelectField{}, `""`}, + {[]string{"a", "b"}, &core.SelectField{}, `"b"`}, + + // multiple + {nil, &core.SelectField{MaxSelect: 2}, `[]`}, + {"", &core.SelectField{MaxSelect: 2}, `[]`}, + {123, &core.SelectField{MaxSelect: 2}, `["123"]`}, + {"a", &core.SelectField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.SelectField{MaxSelect: 2}, `["a"]`}, + {[]string{}, &core.SelectField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.SelectField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + v, err := s.field.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestSelectFieldDriverValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + raw any + field *core.SelectField + expected string + }{ + // single + {nil, &core.SelectField{}, `""`}, + {"", &core.SelectField{}, `""`}, + {123, &core.SelectField{}, `"123"`}, + {"a", &core.SelectField{}, `"a"`}, + {`["a"]`, &core.SelectField{}, `"a"`}, + {[]string{}, &core.SelectField{}, `""`}, + {[]string{"a", "b"}, &core.SelectField{}, `"b"`}, + + // multiple + {nil, &core.SelectField{MaxSelect: 2}, `[]`}, + {"", &core.SelectField{MaxSelect: 2}, `[]`}, + {123, &core.SelectField{MaxSelect: 2}, `["123"]`}, + {"a", &core.SelectField{MaxSelect: 2}, `["a"]`}, + {`["a"]`, &core.SelectField{MaxSelect: 2}, `["a"]`}, + {[]string{}, &core.SelectField{MaxSelect: 2}, `[]`}, + {[]string{"a", "b", "c"}, &core.SelectField{MaxSelect: 2}, `["a","b","c"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { + record := core.NewRecord(core.NewBaseCollection("test")) + record.SetRaw(s.field.GetName(), s.raw) + + v, err := s.field.DriverValue(record) + if err != nil { + t.Fatal(err) + } + + if s.field.IsMultiple() { + _, ok := v.(types.JSONArray[string]) + if !ok { + t.Fatalf("Expected types.JSONArray value, got %T", v) + } + } else { + _, ok := v.(string) + if !ok { + t.Fatalf("Expected string value, got %T", v) + } + } + + vRaw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + if string(vRaw) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vRaw) + } + }) + } +} + +func TestSelectFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + values := []string{"a", "b", "c"} + + scenarios := []struct { + name string + field *core.SelectField + record func() *core.Record + expectError bool + }{ + // single + { + "[single] zero field value (not required)", + &core.SelectField{Name: "test", Values: values, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "[single] zero field value (required)", + &core.SelectField{Name: "test", Values: values, MaxSelect: 1, Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "[single] unknown value", + &core.SelectField{Name: "test", Values: values, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "unknown") + return record + }, + true, + }, + { + "[single] known value", + &core.SelectField{Name: "test", Values: values, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "a") + return record + }, + false, + }, + { + "[single] > MaxSelect", + &core.SelectField{Name: "test", Values: values, MaxSelect: 1}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{"a", "b"}) + return record + }, + true, + }, + + // multiple + { + "[multiple] zero field value (not required)", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{}) + return record + }, + false, + }, + { + "[multiple] zero field value (required)", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2, Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{}) + return record + }, + true, + }, + { + "[multiple] unknown value", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{"a", "unknown"}) + return record + }, + true, + }, + { + "[multiple] known value", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{"a", "b"}) + return record + }, + false, + }, + { + "[multiple] > MaxSelect", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{"a", "b", "c"}) + return record + }, + true, + }, + { + "[multiple] > MaxSelect (duplicated values)", + &core.SelectField{Name: "test", Values: values, MaxSelect: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", []string{"a", "b", "b", "a"}) + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestSelectFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeSelect) + testDefaultFieldNameValidation(t, core.FieldTypeSelect) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field func() *core.SelectField + expectErrors []string + }{ + { + "zero minimal", + func() *core.SelectField { + return &core.SelectField{ + Id: "test", + Name: "test", + } + }, + []string{"values"}, + }, + { + "MaxSelect > Values length", + func() *core.SelectField { + return &core.SelectField{ + Id: "test", + Name: "test", + Values: []string{"a", "b"}, + MaxSelect: 3, + } + }, + []string{"maxSelect"}, + }, + { + "MaxSelect <= Values length", + func() *core.SelectField { + return &core.SelectField{ + Id: "test", + Name: "test", + Values: []string{"a", "b"}, + MaxSelect: 2, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + field := s.field() + + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(field) + + errs := field.ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestSelectFieldFindSetter(t *testing.T) { + values := []string{"a", "b", "c", "d"} + + scenarios := []struct { + name string + key string + value any + field *core.SelectField + hasSetter bool + expected string + }{ + { + "no match", + "example", + "b", + &core.SelectField{Name: "test", MaxSelect: 1, Values: values}, + false, + "", + }, + { + "exact match (single)", + "test", + "b", + &core.SelectField{Name: "test", MaxSelect: 1, Values: values}, + true, + `"b"`, + }, + { + "exact match (multiple)", + "test", + []string{"a", "b"}, + &core.SelectField{Name: "test", MaxSelect: 2, Values: values}, + true, + `["a","b"]`, + }, + { + "append (single)", + "test+", + "b", + &core.SelectField{Name: "test", MaxSelect: 1, Values: values}, + true, + `"b"`, + }, + { + "append (multiple)", + "test+", + []string{"a"}, + &core.SelectField{Name: "test", MaxSelect: 2, Values: values}, + true, + `["c","d","a"]`, + }, + { + "prepend (single)", + "+test", + "b", + &core.SelectField{Name: "test", MaxSelect: 1, Values: values}, + true, + `"d"`, // the last of the existing values + }, + { + "prepend (multiple)", + "+test", + []string{"a"}, + &core.SelectField{Name: "test", MaxSelect: 2, Values: values}, + true, + `["a","c","d"]`, + }, + { + "subtract (single)", + "test-", + "d", + &core.SelectField{Name: "test", MaxSelect: 1, Values: values}, + true, + `"c"`, + }, + { + "subtract (multiple)", + "test-", + []string{"unknown", "c"}, + &core.SelectField{Name: "test", MaxSelect: 2, Values: values}, + true, + `["d"]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + setter := s.field.FindSetter(s.key) + + hasSetter := setter != nil + if hasSetter != s.hasSetter { + t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) + } + + if !hasSetter { + return + } + + record := core.NewRecord(collection) + record.SetRaw(s.field.GetName(), []string{"c", "d"}) + + setter(record, s.value) + + raw, err := json.Marshal(record.Get(s.field.GetName())) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, rawStr) + } + }) + } +} diff --git a/core/field_test.go b/core/field_test.go new file mode 100644 index 00000000..28bc46ec --- /dev/null +++ b/core/field_test.go @@ -0,0 +1,261 @@ +package core_test + +import ( + "context" + "strings" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func testFieldBaseMethods(t *testing.T, fieldType string) { + factory, ok := core.Fields[fieldType] + if !ok { + t.Fatalf("Missing %q field factory", fieldType) + } + + f := factory() + if f == nil { + t.Fatal("Expected non-nil Field instance") + } + + t.Run("type", func(t *testing.T) { + if v := f.Type(); v != fieldType { + t.Fatalf("Expected type %q, got %q", fieldType, v) + } + }) + + t.Run("id", func(t *testing.T) { + testValues := []string{"new_id", ""} + for _, expected := range testValues { + f.SetId(expected) + if v := f.GetId(); v != expected { + t.Fatalf("Expected id %q, got %q", expected, v) + } + } + }) + + t.Run("name", func(t *testing.T) { + testValues := []string{"new_name", ""} + for _, expected := range testValues { + f.SetName(expected) + if v := f.GetName(); v != expected { + t.Fatalf("Expected name %q, got %q", expected, v) + } + } + }) + + t.Run("system", func(t *testing.T) { + testValues := []bool{false, true} + for _, expected := range testValues { + f.SetSystem(expected) + if v := f.GetSystem(); v != expected { + t.Fatalf("Expected system %v, got %v", expected, v) + } + } + }) + + t.Run("hidden", func(t *testing.T) { + testValues := []bool{false, true} + for _, expected := range testValues { + f.SetHidden(expected) + if v := f.GetHidden(); v != expected { + t.Fatalf("Expected hidden %v, got %v", expected, v) + } + } + }) +} + +func testDefaultFieldIdValidation(t *testing.T, fieldType string) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() core.Field + expectError bool + }{ + { + "empty value", + func() core.Field { + f := core.Fields[fieldType]() + return f + }, + true, + }, + { + "invalid length", + func() core.Field { + f := core.Fields[fieldType]() + f.SetId(strings.Repeat("a", 256)) + return f + }, + true, + }, + { + "valid length", + func() core.Field { + f := core.Fields[fieldType]() + f.SetId(strings.Repeat("a", 255)) + return f + }, + false, + }, + } + + for _, s := range scenarios { + t.Run("[id] "+s.name, func(t *testing.T) { + errs, _ := s.field().ValidateSettings(context.Background(), app, collection).(validation.Errors) + + hasErr := errs["id"] != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } + }) + } +} + +func testDefaultFieldNameValidation(t *testing.T, fieldType string) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() core.Field + expectError bool + }{ + { + "empty value", + func() core.Field { + f := core.Fields[fieldType]() + return f + }, + true, + }, + { + "invalid length", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName(strings.Repeat("a", 256)) + return f + }, + true, + }, + { + "valid length", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName(strings.Repeat("a", 255)) + return f + }, + false, + }, + { + "invalid regex", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("test(") + return f + }, + true, + }, + { + "valid regex", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("test_123") + return f + }, + false, + }, + { + "_via_", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("a_via_b") + return f + }, + true, + }, + { + "system reserved - null", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("null") + return f + }, + true, + }, + { + "system reserved - false", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("false") + return f + }, + true, + }, + { + "system reserved - true", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("true") + return f + }, + true, + }, + { + "system reserved - _rowid_", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("_rowid_") + return f + }, + true, + }, + { + "system reserved - expand", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("expand") + return f + }, + true, + }, + { + "system reserved - collectionId", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("collectionId") + return f + }, + true, + }, + { + "system reserved - collectionName", + func() core.Field { + f := core.Fields[fieldType]() + f.SetName("collectionName") + return f + }, + true, + }, + } + + for _, s := range scenarios { + t.Run("[name] "+s.name, func(t *testing.T) { + errs, _ := s.field().ValidateSettings(context.Background(), app, collection).(validation.Errors) + + hasErr := errs["name"] != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } + }) + } +} diff --git a/core/field_text.go b/core/field_text.go new file mode 100644 index 00000000..c5aeb80c --- /dev/null +++ b/core/field_text.go @@ -0,0 +1,315 @@ +package core + +import ( + "context" + "fmt" + "regexp" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeText] = func() Field { + return &TextField{} + } +} + +const FieldTypeText = "text" + +const autogenerateModifier = ":autogenerate" + +var ( + _ Field = (*TextField)(nil) + _ SetterFinder = (*TextField)(nil) + _ RecordInterceptor = (*TextField)(nil) +) + +// TextField defines "text" type field for storing any string value. +// +// The following additional setter keys are available: +// +// - "fieldName:autogenerate" - autogenerate field value if AutogeneratePattern is set. For example: +// +// 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"` + + // --- + + // Min specifies the minimum required string characters. + // + // if zero value, no min limit is applied. + Min int `form:"min" json:"min"` + + // Max specifies the maximum allowed string characters. + // + // If zero, a default limit of 5000 is applied. + Max int `form:"max" json:"max"` + + // Pattern specifies an optional regex pattern to match against the field value. + // + // Leave it empty to skip the pattern check. + Pattern string `form:"pattern" json:"pattern"` + + // AutogeneratePattern specifies an optional regex pattern that could + // be used to generate random string from it and set it automatically + // on record create if no explicit value is set or when the `:autogenerate` modifier is used. + // + // Note: the generated value still needs to satisfy min, max, pattern (if set) + AutogeneratePattern string `form:"autogeneratePattern" json:"autogeneratePattern"` + + // Required will require the field value to be non-empty string. + Required bool `form:"required" json:"required"` + + // PrimaryKey will mark the field as primary key. + // + // A single collection can have only 1 field marked as primary key. + PrimaryKey bool `form:"primaryKey" json:"primaryKey"` +} + +// Type implements [Field.Type] interface method. +func (f *TextField) Type() string { + return FieldTypeText +} + +// GetId implements [Field.GetId] interface method. +func (f *TextField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *TextField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *TextField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *TextField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *TextField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *TextField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *TextField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *TextField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *TextField) ColumnType(app App) string { + if f.PrimaryKey { + // note: the default is just a last resort fallback to avoid empty + // string values in case the record was inserted with raw sql and + // it is not actually used when operating with the db abstraction + return "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL" + } + + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *TextField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToString(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *TextField) ValidateValue(ctx context.Context, app App, record *Record) error { + newVal, ok := record.GetRaw(f.Name).(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + // disallow PK change + if f.PrimaryKey && !record.IsNew() { + oldVal := record.LastSavedPK() + if oldVal != newVal { + return validation.NewError("validation_pk_change", "The record primary key cannot be changed.") + } + if oldVal != "" { + return nil // no need to further validate since the id can't be updated anyway + } + } + + return f.ValidatePlainValue(newVal) +} + +// ValidatePlainValue validates the provided string against the field options. +func (f *TextField) ValidatePlainValue(value string) error { + if f.Required || f.PrimaryKey { + if err := validation.Required.Validate(value); err != nil { + return err + } + } + + if value == "" { + return nil // nothing to check + } + + // note: casted to []rune to count multi-byte chars as one + length := len([]rune(value)) + + if f.Min > 0 && length < f.Min { + return validation.NewError("validation_min_text_constraint", fmt.Sprintf("Must be at least %d character(s)", f.Min)). + SetParams(map[string]any{"min": f.Min}) + } + + max := f.Max + if max == 0 { + max = 5000 + } + + if max > 0 && length > max { + return validation.NewError("validation_max_text_constraint", fmt.Sprintf("Must be less than %d character(s)", max)). + SetParams(map[string]any{"max": f.Max}) + } + + if f.Pattern != "" { + match, _ := regexp.MatchString(f.Pattern, value) + if !match { + return validation.NewError("validation_invalid_format", "Invalid value format") + } + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *TextField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, + validation.By(DefaultFieldNameValidationRule), + validation.When(f.PrimaryKey, validation.In(idColumn).Error(`The primary key must be named "id".`)), + ), + validation.Field(&f.PrimaryKey, validation.By(f.checkOtherFieldsForPK(collection))), + validation.Field(&f.Min, validation.Min(0)), + validation.Field(&f.Max, validation.Min(f.Min)), + validation.Field(&f.Pattern, validation.By(validators.IsRegex)), + validation.Field(&f.Hidden, validation.When(f.PrimaryKey, validation.Empty)), + validation.Field(&f.Required, validation.When(f.PrimaryKey, validation.Required)), + validation.Field(&f.AutogeneratePattern, validation.By(validators.IsRegex), validation.By(f.checkAutogeneratePattern)), + ) +} + +func (f *TextField) checkOtherFieldsForPK(collection *Collection) validation.RuleFunc { + return func(value any) error { + v, _ := value.(bool) + if !v { + return nil // not a pk + } + + totalPrimaryKeys := 0 + for _, field := range collection.Fields { + if text, ok := field.(*TextField); ok && text.PrimaryKey { + totalPrimaryKeys++ + } + + if totalPrimaryKeys > 1 { + return validation.NewError("validation_unsupported_composite_pk", "Composite PKs are not supported and the collection must have only 1 PK.") + } + } + + return nil + } +} + +func (f *TextField) checkAutogeneratePattern(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + // run 10 tests to check for conflicts with the other field validators + for i := 0; i < 10; i++ { + generated, err := security.RandomStringByRegex(v) + if err != nil { + return validation.NewError("validation_invalid_autogenerate_pattern", err.Error()) + } + + // (loosely) check whether the generated pattern satisfies the current field settings + if err := f.ValidatePlainValue(generated); err != nil { + return validation.NewError( + "validation_invalid_autogenerate_pattern_value", + fmt.Sprintf("The provided autogenerate pattern could produce invalid field values, ex.: %q", generated), + ) + } + } + + return nil +} + +// Intercept implements the [RecordInterceptor] interface. +func (f *TextField) Intercept( + ctx context.Context, + app App, + record *Record, + actionName string, + actionFunc func() error, +) error { + // set autogenerated value if missing for new records + switch actionName { + case InterceptorActionValidate, InterceptorActionCreate: + if f.AutogeneratePattern != "" && f.hasZeroValue(record) && record.IsNew() { + v, err := security.RandomStringByRegex(f.AutogeneratePattern) + if err != nil { + return fmt.Errorf("failed to autogenerate %q value: %w", f.Name, err) + } + record.SetRaw(f.Name, v) + } + } + + return actionFunc() +} + +func (f *TextField) hasZeroValue(record *Record) bool { + v, _ := record.GetRaw(f.Name).(string) + return v == "" +} + +// FindSetter implements the [SetterFinder] interface. +func (f *TextField) FindSetter(key string) SetterFunc { + switch key { + case f.Name: + return func(record *Record, raw any) { + record.SetRaw(f.Name, cast.ToString(raw)) + } + case f.Name + autogenerateModifier: + return func(record *Record, raw any) { + v := cast.ToString(raw) + + if f.AutogeneratePattern != "" { + generated, _ := security.RandomStringByRegex(f.AutogeneratePattern) + v += generated + } + + record.SetRaw(f.Name, v) + } + default: + return nil + } +} diff --git a/core/field_text_test.go b/core/field_text_test.go new file mode 100644 index 00000000..c05d26b2 --- /dev/null +++ b/core/field_text_test.go @@ -0,0 +1,536 @@ +package core_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestTextFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeText) +} + +func TestTextFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.TextField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestTextFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.TextField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + {123.456, "123.456"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vStr, ok := v.(string) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + + if vStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, v) + } + }) + } +} + +func TestTextFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.TextField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.TextField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.TextField{Name: "test", Pattern: `\d+`, Min: 10, Max: 100}, // other fields validators should be ignored + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "zero field value (required)", + &core.TextField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.TextField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abc") + return record + }, + false, + }, + { + "zero field value (primaryKey)", + &core.TextField{Name: "test", PrimaryKey: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "non-zero field value (primaryKey)", + &core.TextField{Name: "test", PrimaryKey: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abc") + return record + }, + false, + }, + { + "< min", + &core.TextField{Name: "test", Min: 4}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "абв") // multi-byte + return record + }, + true, + }, + { + ">= min", + &core.TextField{Name: "test", Min: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "абв") // multi-byte + return record + }, + false, + }, + { + "> default max", + &core.TextField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", strings.Repeat("a", 5001)) + return record + }, + true, + }, + { + "<= default max", + &core.TextField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", strings.Repeat("a", 500)) + return record + }, + false, + }, + { + "> max", + &core.TextField{Name: "test", Max: 2}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "абв") // multi-byte + return record + }, + true, + }, + { + "<= max", + &core.TextField{Name: "test", Min: 3}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "абв") // multi-byte + return record + }, + false, + }, + { + "mismatched pattern", + &core.TextField{Name: "test", Pattern: `\d+`}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "abc") + return record + }, + true, + }, + { + "matched pattern", + &core.TextField{Name: "test", Pattern: `\d+`}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "123") + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestTextFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeText) + testDefaultFieldNameValidation(t, core.FieldTypeText) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + field func() *core.TextField + expectErrors []string + }{ + { + "zero minimal", + func() *core.TextField { + return &core.TextField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "primaryKey without required", + func() *core.TextField { + return &core.TextField{ + Id: "test", + Name: "id", + PrimaryKey: true, + } + }, + []string{"required"}, + }, + { + "primaryKey with hidden", + func() *core.TextField { + return &core.TextField{ + Id: "test", + Name: "id", + Required: true, + PrimaryKey: true, + Hidden: true, + } + }, + []string{"hidden"}, + }, + { + "primaryKey with name != id", + func() *core.TextField { + return &core.TextField{ + Id: "test", + Name: "test", + PrimaryKey: true, + Required: true, + } + }, + []string{"name"}, + }, + { + "multiple primaryKey fields", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + PrimaryKey: true, + Required: true, + } + }, + []string{"primaryKey"}, + }, + { + "invalid pattern", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + Pattern: `(invalid`, + } + }, + []string{"pattern"}, + }, + { + "valid pattern", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + Pattern: `\d+`, + } + }, + []string{}, + }, + { + "invalid autogeneratePattern", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + AutogeneratePattern: `(invalid`, + } + }, + []string{"autogeneratePattern"}, + }, + { + "valid autogeneratePattern", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + AutogeneratePattern: `[a-z]+`, + } + }, + []string{}, + }, + { + "conflicting pattern and autogeneratePattern", + func() *core.TextField { + return &core.TextField{ + Id: "test2", + Name: "id", + Pattern: `\d+`, + AutogeneratePattern: `[a-z]+`, + } + }, + []string{"autogeneratePattern"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + field := s.field() + + collection := core.NewBaseCollection("test_collection") + collection.Fields.GetByName("id").SetId("test") // set a dummy known id so that it can be replaced + collection.Fields.Add(field) + + errs := field.ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} + +func TestTextFieldAutogenerate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + actionName string + field *core.TextField + record func() *core.Record + expected string + }{ + { + "non-matching action", + core.InterceptorActionUpdate, + &core.TextField{Name: "test", AutogeneratePattern: "abc"}, + func() *core.Record { + return core.NewRecord(collection) + }, + "", + }, + { + "matching action (create)", + core.InterceptorActionCreate, + &core.TextField{Name: "test", AutogeneratePattern: "abc"}, + func() *core.Record { + return core.NewRecord(collection) + }, + "abc", + }, + { + "matching action (validate)", + core.InterceptorActionValidate, + &core.TextField{Name: "test", AutogeneratePattern: "abc"}, + func() *core.Record { + return core.NewRecord(collection) + }, + "abc", + }, + { + "existing non-zero value", + core.InterceptorActionCreate, + &core.TextField{Name: "test", AutogeneratePattern: "abc"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "123") + return record + }, + "123", + }, + { + "non-new record", + core.InterceptorActionValidate, + &core.TextField{Name: "test", AutogeneratePattern: "abc"}, + func() *core.Record { + record := core.NewRecord(collection) + record.Id = "test" + record.PostScan() + return record + }, + "", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + actionCalls := 0 + record := s.record() + + err := s.field.Intercept(context.Background(), app, record, s.actionName, func() error { + actionCalls++ + return nil + }) + if err != nil { + t.Fatal(err) + } + + if actionCalls != 1 { + t.Fatalf("Expected actionCalls %d, got %d", 1, actionCalls) + } + + v := record.GetString(s.field.GetName()) + if v != s.expected { + t.Fatalf("Expected value %q, got %q", s.expected, v) + } + }) + } +} + +func TestTextFieldFindSetter(t *testing.T) { + scenarios := []struct { + name string + key string + value any + field *core.TextField + hasSetter bool + expected string + }{ + { + "no match", + "example", + "abc", + &core.TextField{Name: "test", AutogeneratePattern: "test"}, + false, + "", + }, + { + "exact match", + "test", + "abc", + &core.TextField{Name: "test", AutogeneratePattern: "test"}, + true, + "abc", + }, + { + "autogenerate modifier", + "test:autogenerate", + "abc", + &core.TextField{Name: "test", AutogeneratePattern: "test"}, + true, + "abctest", + }, + { + "autogenerate modifier without AutogeneratePattern option", + "test:autogenerate", + "abc", + &core.TextField{Name: "test"}, + true, + "abc", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(s.field) + + setter := s.field.FindSetter(s.key) + + hasSetter := setter != nil + if hasSetter != s.hasSetter { + t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) + } + + if !hasSetter { + return + } + + record := core.NewRecord(collection) + + setter(record, s.value) + + result := record.GetString(s.field.Name) + + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) + } +} diff --git a/core/field_url.go b/core/field_url.go new file mode 100644 index 00000000..7066f78e --- /dev/null +++ b/core/field_url.go @@ -0,0 +1,154 @@ +package core + +import ( + "context" + "net/url" + "slices" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/spf13/cast" +) + +func init() { + Fields[FieldTypeURL] = func() Field { + return &URLField{} + } +} + +const FieldTypeURL = "url" + +var _ Field = (*URLField)(nil) + +// URLField defines "url" type field for storing URL string value. +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"` + + // --- + + // ExceptDomains will require the URL domain to NOT be included in the listed ones. + // + // This validator can be set only if OnlyDomains is empty. + ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` + + // OnlyDomains will require the URL domain to be included in the listed ones. + // + // This validator can be set only if ExceptDomains is empty. + OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` + + // Required will require the field value to be non-empty URL string. + Required bool `form:"required" json:"required"` +} + +// Type implements [Field.Type] interface method. +func (f *URLField) Type() string { + return FieldTypeURL +} + +// GetId implements [Field.GetId] interface method. +func (f *URLField) GetId() string { + return f.Id +} + +// SetId implements [Field.SetId] interface method. +func (f *URLField) SetId(id string) { + f.Id = id +} + +// GetName implements [Field.GetName] interface method. +func (f *URLField) GetName() string { + return f.Name +} + +// SetName implements [Field.SetName] interface method. +func (f *URLField) SetName(name string) { + f.Name = name +} + +// GetSystem implements [Field.GetSystem] interface method. +func (f *URLField) GetSystem() bool { + return f.System +} + +// SetSystem implements [Field.SetSystem] interface method. +func (f *URLField) SetSystem(system bool) { + f.System = system +} + +// GetHidden implements [Field.GetHidden] interface method. +func (f *URLField) GetHidden() bool { + return f.Hidden +} + +// SetHidden implements [Field.SetHidden] interface method. +func (f *URLField) SetHidden(hidden bool) { + f.Hidden = hidden +} + +// ColumnType implements [Field.ColumnType] interface method. +func (f *URLField) ColumnType(app App) string { + return "TEXT DEFAULT '' NOT NULL" +} + +// PrepareValue implements [Field.PrepareValue] interface method. +func (f *URLField) PrepareValue(record *Record, raw any) (any, error) { + return cast.ToString(raw), nil +} + +// ValidateValue implements [Field.ValidateValue] interface method. +func (f *URLField) ValidateValue(ctx context.Context, app App, record *Record) error { + val, ok := record.GetRaw(f.Name).(string) + if !ok { + return validators.ErrUnsupportedValueType + } + + if f.Required { + if err := validation.Required.Validate(val); err != nil { + return err + } + } + + if val == "" { + return nil // nothing to check + } + + if is.URL.Validate(val) != nil { + return validation.NewError("validation_invalid_url", "Must be a valid url") + } + + // extract host/domain + u, _ := url.Parse(val) + + // only domains check + if len(f.OnlyDomains) > 0 && !slices.Contains(f.OnlyDomains, u.Host) { + return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") + } + + // except domains check + if len(f.ExceptDomains) > 0 && slices.Contains(f.ExceptDomains, u.Host) { + return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") + } + + return nil +} + +// ValidateSettings implements [Field.ValidateSettings] interface method. +func (f *URLField) ValidateSettings(ctx context.Context, app App, collection *Collection) error { + return validation.ValidateStruct(f, + validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)), + validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)), + validation.Field( + &f.ExceptDomains, + validation.When(len(f.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &f.OnlyDomains, + validation.When(len(f.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + ) +} diff --git a/core/field_url_test.go b/core/field_url_test.go new file mode 100644 index 00000000..0b2db39d --- /dev/null +++ b/core/field_url_test.go @@ -0,0 +1,271 @@ +package core_test + +import ( + "context" + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestURLFieldBaseMethods(t *testing.T) { + testFieldBaseMethods(t, core.FieldTypeURL) +} + +func TestURLFieldColumnType(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.URLField{} + + expected := "TEXT DEFAULT '' NOT NULL" + + if v := f.ColumnType(app); v != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, v) + } +} + +func TestURLFieldPrepareValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f := &core.URLField{} + record := core.NewRecord(core.NewBaseCollection("test")) + + scenarios := []struct { + raw any + expected string + }{ + {"", ""}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + {123.456, "123.456"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.raw), func(t *testing.T) { + v, err := f.PrepareValue(record, s.raw) + if err != nil { + t.Fatal(err) + } + + vStr, ok := v.(string) + if !ok { + t.Fatalf("Expected string instance, got %T", v) + } + + if vStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, v) + } + }) + } +} + +func TestURLFieldValidateValue(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field *core.URLField + record func() *core.Record + expectError bool + }{ + { + "invalid raw value", + &core.URLField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", 123) + return record + }, + true, + }, + { + "zero field value (not required)", + &core.URLField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + false, + }, + { + "zero field value (required)", + &core.URLField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "") + return record + }, + true, + }, + { + "non-zero field value (required)", + &core.URLField{Name: "test", Required: true}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "https://example.com") + return record + }, + false, + }, + { + "invalid url", + &core.URLField{Name: "test"}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "invalid") + return record + }, + true, + }, + { + "failed onlyDomains", + &core.URLField{Name: "test", OnlyDomains: []string{"example.org", "example.net"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "https://example.com") + return record + }, + true, + }, + { + "success onlyDomains", + &core.URLField{Name: "test", OnlyDomains: []string{"example.org", "example.com"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "https://example.com") + return record + }, + false, + }, + { + "failed exceptDomains", + &core.URLField{Name: "test", ExceptDomains: []string{"example.org", "example.com"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "https://example.com") + return record + }, + true, + }, + { + "success exceptDomains", + &core.URLField{Name: "test", ExceptDomains: []string{"example.org", "example.net"}}, + func() *core.Record { + record := core.NewRecord(collection) + record.SetRaw("test", "https://example.com") + return record + }, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := s.field.ValidateValue(context.Background(), app, s.record()) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestURLFieldValidateSettings(t *testing.T) { + testDefaultFieldIdValidation(t, core.FieldTypeURL) + testDefaultFieldNameValidation(t, core.FieldTypeURL) + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test_collection") + + scenarios := []struct { + name string + field func() *core.URLField + expectErrors []string + }{ + { + "zero minimal", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + } + }, + []string{}, + }, + { + "both onlyDomains and exceptDomains", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com"}, + ExceptDomains: []string{"example.org"}, + } + }, + []string{"onlyDomains", "exceptDomains"}, + }, + { + "invalid onlyDomains", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com", "invalid"}, + } + }, + []string{"onlyDomains"}, + }, + { + "valid onlyDomains", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + OnlyDomains: []string{"example.com", "example.org"}, + } + }, + []string{}, + }, + { + "invalid exceptDomains", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + ExceptDomains: []string{"example.com", "invalid"}, + } + }, + []string{"exceptDomains"}, + }, + { + "valid exceptDomains", + func() *core.URLField { + return &core.URLField{ + Id: "test", + Name: "test", + ExceptDomains: []string{"example.com", "example.org"}, + } + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := s.field().ValidateSettings(context.Background(), app, collection) + + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/fields_list.go b/core/fields_list.go new file mode 100644 index 00000000..e5f09b86 --- /dev/null +++ b/core/fields_list.go @@ -0,0 +1,261 @@ +package core + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + + "github.com/pocketbase/pocketbase/tools/security" +) + +// NewFieldsList creates a new FieldsList instance with the provided fields. +func NewFieldsList(fields ...Field) FieldsList { + s := make(FieldsList, 0, len(fields)) + + for _, f := range fields { + s.Add(f) + } + + return s +} + +// FieldsList defines a Collection slice of fields. +type FieldsList []Field + +// Clone creates a deep clone of the current list. +func (s FieldsList) Clone() (FieldsList, error) { + copyRaw, err := json.Marshal(s) + if err != nil { + return nil, err + } + + result := FieldsList{} + if err := json.Unmarshal(copyRaw, &result); err != nil { + return nil, err + } + + return result, nil +} + +// FieldNames returns a slice with the name of all list fields. +func (s FieldsList) FieldNames() []string { + result := make([]string, len(s)) + + for i, field := range s { + result[i] = field.GetName() + } + + return result +} + +// AsMap returns a map with all registered list field. +// The returned map is indexed with each field name. +func (s FieldsList) AsMap() map[string]Field { + result := make(map[string]Field, len(s)) + + for _, field := range s { + result[field.GetName()] = field + } + + return result +} + +// GetById returns a single field by its id. +func (s FieldsList) GetById(fieldId string) Field { + for _, field := range s { + if field.GetId() == fieldId { + return field + } + } + return nil +} + +// GetByName returns a single field by its name. +func (s FieldsList) GetByName(fieldName string) Field { + for _, field := range s { + if field.GetName() == fieldName { + return field + } + } + return nil +} + +// RemoveById removes a single field by its id. +// +// This method does nothing if field with the specified id doesn't exist. +func (s *FieldsList) RemoveById(fieldId string) { + fields := *s + for i, field := range fields { + if field.GetId() == fieldId { + *s = append(fields[:i], fields[i+1:]...) + return + } + } +} + +// RemoveByName removes a single field by its name. +// +// This method does nothing if field with the specified name doesn't exist. +func (s *FieldsList) RemoveByName(fieldName string) { + fields := *s + for i, field := range fields { + if field.GetName() == fieldName { + *s = append(fields[:i], fields[i+1:]...) + return + } + } +} + +// 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. +// +// If the list already has a field with the same id, +// then the existing field is replaced with the new one. +// +// Otherwise the new field is appended after the other list fields. +func (s *FieldsList) Add(fields ...Field) { + for _, f := range fields { + s.add(f) + } +} + +func (s *FieldsList) add(newField Field) { + newFieldId := newField.GetId() + + // set default id + if newFieldId == "" { + if newField.GetName() != "" { + newFieldId = newField.Type() + crc32Checksum(newField.GetName()) + } else { + newFieldId = newField.Type() + security.RandomString(5) + } + newField.SetId(newFieldId) + } + + fields := *s + + for i, field := range fields { + // replace existing + if newFieldId != "" && field.GetId() == newFieldId { + (*s)[i] = newField + return + } + } + + // add new field + *s = append(fields, newField) +} + +// String returns the string representation of the current list. +func (s FieldsList) String() string { + v, _ := json.Marshal(s) + return string(v) +} + +type onlyFieldType struct { + Type string `json:"type"` +} + +type fieldWithType struct { + Field + Type string `json:"type"` +} + +func (fwt *fieldWithType) UnmarshalJSON(data []byte) error { + // extract the field type to init a blank factory + t := &onlyFieldType{} + if err := json.Unmarshal(data, t); err != nil { + return fmt.Errorf("failed to unmarshal field type: %w", err) + } + + factory, ok := Fields[t.Type] + if !ok { + return fmt.Errorf("missing or unknown field type in %s", data) + } + + fwt.Type = t.Type + fwt.Field = factory() + + // unmarshal the rest of the data into the created field + if err := json.Unmarshal(data, fwt.Field); err != nil { + return fmt.Errorf("failed to unmarshal field: %w", err) + } + + return nil +} + +// UnmarshalJSON implements [json.Unmarshaler] and +// loads the provided json data into the current FieldsList. +func (s *FieldsList) UnmarshalJSON(data []byte) error { + fwts := []fieldWithType{} + + if err := json.Unmarshal(data, &fwts); err != nil { + return err + } + + *s = []Field{} // reset + + for _, fwt := range fwts { + s.Add(fwt.Field) + } + + return nil +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (s FieldsList) MarshalJSON() ([]byte, error) { + if s == nil { + s = []Field{} // always init to ensure that it is serialized as empty array + } + + wrapper := make([]map[string]any, 0, len(s)) + + for _, f := range s { + // precompute the json into a map so that we can append the type to a flatten object + raw, err := json.Marshal(f) + if err != nil { + return nil, err + } + + data := map[string]any{} + if err := json.Unmarshal(raw, &data); err != nil { + return nil, err + } + data["type"] = f.Type() + + wrapper = append(wrapper, data) + } + + return json.Marshal(wrapper) +} + +// Value implements the [driver.Valuer] interface. +func (s FieldsList) Value() (driver.Value, error) { + data, err := json.Marshal(s) + + return string(data), err +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current FieldsList instance. +func (s *FieldsList) Scan(value any) error { + var data []byte + switch v := value.(type) { + case nil: + // no cast needed + case []byte: + data = v + case string: + data = []byte(v) + default: + return fmt.Errorf("failed to unmarshal FieldsList value %q", value) + } + + if len(data) == 0 { + data = []byte("[]") + } + + return s.UnmarshalJSON(data) +} diff --git a/core/fields_list_test.go b/core/fields_list_test.go new file mode 100644 index 00000000..08defbdd --- /dev/null +++ b/core/fields_list_test.go @@ -0,0 +1,365 @@ +package core_test + +import ( + "slices" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" +) + +func TestNewFieldsList(t *testing.T) { + fields := core.NewFieldsList( + &core.TextField{Id: "id1", Name: "test1"}, + &core.TextField{Name: "test2"}, + &core.TextField{Id: "id1", Name: "test1_new"}, // should replace the original id1 field + ) + + if len(fields) != 2 { + t.Fatalf("Expected 2 fields, got %d (%v)", len(fields), fields) + } + + for _, f := range fields { + if f.GetId() == "" { + t.Fatalf("Expected field id to be set, found empty id for field %v", f) + } + } + + if fields[0].GetName() != "test1_new" { + t.Fatalf("Expected field with name test1_new, got %s", fields[0].GetName()) + } + + if fields[1].GetName() != "test2" { + t.Fatalf("Expected field with name test2, got %s", fields[1].GetName()) + } +} + +func TestFieldsListClone(t *testing.T) { + f1 := &core.TextField{Name: "test1"} + f2 := &core.EmailField{Name: "test2"} + s1 := core.NewFieldsList(f1, f2) + + s2, err := s1.Clone() + if err != nil { + t.Fatal(err) + } + + s1Str := s1.String() + s2Str := s2.String() + + if s1Str != s2Str { + t.Fatalf("Expected the cloned list to be equal, got \n%v\nVS\n%v", s1, s2) + } + + // change in one list shouldn't result to change in the other + // (aka. check if it is a deep clone) + s1[0].SetName("test1_update") + if s2[0].GetName() != "test1" { + t.Fatalf("Expected s2 field name to not change, got %q", s2[0].GetName()) + } +} + +func TestFieldsListFieldNames(t *testing.T) { + f1 := &core.TextField{Name: "test1"} + f2 := &core.EmailField{Name: "test2"} + testFieldsList := core.NewFieldsList(f1, f2) + + result := testFieldsList.FieldNames() + + expected := []string{f1.Name, f2.Name} + + if len(result) != len(expected) { + t.Fatalf("Expected %d slice elements, got %d\n%v", len(expected), len(result), result) + } + + for _, name := range expected { + if !slices.Contains(result, name) { + t.Fatalf("Missing name %q in %v", name, result) + } + } +} + +func TestFieldsListAsMap(t *testing.T) { + f1 := &core.TextField{Name: "test1"} + f2 := &core.EmailField{Name: "test2"} + testFieldsList := core.NewFieldsList(f1, f2) + + result := testFieldsList.AsMap() + + expectedIndexes := []string{f1.Name, f2.Name} + + if len(result) != len(expectedIndexes) { + t.Fatalf("Expected %d map elements, got %d\n%v", len(expectedIndexes), len(result), result) + } + + for _, index := range expectedIndexes { + if _, ok := result[index]; !ok { + t.Fatalf("Missing index %q", index) + } + } +} + +func TestFieldsListGetById(t *testing.T) { + f1 := &core.TextField{Id: "id1", Name: "test1"} + f2 := &core.EmailField{Id: "id2", Name: "test2"} + testFieldsList := core.NewFieldsList(f1, f2) + + // missing field id + result1 := testFieldsList.GetById("test1") + if result1 != nil { + t.Fatalf("Found unexpected field %v", result1) + } + + // existing field id + result2 := testFieldsList.GetById("id2") + if result2 == nil || result2.GetId() != "id2" { + t.Fatalf("Cannot find field with id %q, got %v ", "id2", result2) + } +} + +func TestFieldsListGetByName(t *testing.T) { + f1 := &core.TextField{Id: "id1", Name: "test1"} + f2 := &core.EmailField{Id: "id2", Name: "test2"} + testFieldsList := core.NewFieldsList(f1, f2) + + // missing field name + result1 := testFieldsList.GetByName("id1") + if result1 != nil { + t.Fatalf("Found unexpected field %v", result1) + } + + // existing field name + result2 := testFieldsList.GetByName("test2") + if result2 == nil || result2.GetName() != "test2" { + t.Fatalf("Cannot find field with name %q, got %v ", "test2", result2) + } +} + +func TestFieldsListRemove(t *testing.T) { + testFieldsList := core.NewFieldsList( + &core.TextField{Id: "id1", Name: "test1"}, + &core.TextField{Id: "id2", Name: "test2"}, + &core.TextField{Id: "id3", Name: "test3"}, + &core.TextField{Id: "id4", Name: "test4"}, + &core.TextField{Id: "id5", Name: "test5"}, + &core.TextField{Id: "id6", Name: "test6"}, + ) + + // remove by id + testFieldsList.RemoveById("id2") + testFieldsList.RemoveById("test3") // should do nothing + + // remove by name + testFieldsList.RemoveByName("test5") + testFieldsList.RemoveByName("id6") // should do nothing + + expected := []string{"test1", "test3", "test4", "test6"} + + if len(testFieldsList) != len(expected) { + t.Fatalf("Expected %d, got %d\n%v", len(expected), len(testFieldsList), testFieldsList) + } + + for _, name := range expected { + if f := testFieldsList.GetByName(name); f == nil { + t.Fatalf("Missing field %q", name) + } + } +} + +func TestFieldsListAdd(t *testing.T) { + f0 := &core.TextField{} + f1 := &core.TextField{Name: "test1"} + f2 := &core.TextField{Id: "f2Id", Name: "test2"} + f3 := &core.TextField{Id: "f3Id", Name: "test3"} + testFieldsList := core.NewFieldsList(f0, f1, f2, f3) + + f2New := &core.EmailField{Id: "f2Id", Name: "test2_new"} + f4 := &core.URLField{Name: "test4"} + + testFieldsList.Add(f2New) + testFieldsList.Add(f4) + + if len(testFieldsList) != 5 { + t.Fatalf("Expected %d, got %d\n%v", 5, len(testFieldsList), testFieldsList) + } + + // check if each field has id + for _, f := range testFieldsList { + if f.GetId() == "" { + t.Fatalf("Expected field id to be set, found empty id for field %v", f) + } + } + + // check if f2 field was replaced + if f := testFieldsList.GetById("f2Id"); f == nil || f.Type() != core.FieldTypeEmail { + t.Fatalf("Expected f2 field to be replaced, found %v", f) + } + + // check if f4 was added + if f := testFieldsList.GetByName("test4"); f == nil || f.GetName() != "test4" { + t.Fatalf("Expected f4 field to be added, found %v", f) + } +} + +func TestFieldsListStringAndValue(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + testFieldsList := core.NewFieldsList() + + str := testFieldsList.String() + if str != "[]" { + t.Fatalf("Expected empty slice, got\n%q", str) + } + + v, err := testFieldsList.Value() + if err != nil { + t.Fatal(err) + } + if v != str { + t.Fatalf("Expected String and Value to match") + } + }) + + t.Run("list with fields", func(t *testing.T) { + testFieldsList := core.NewFieldsList( + &core.TextField{Id: "f1id", Name: "test1"}, + &core.BoolField{Id: "f2id", Name: "test2"}, + &core.URLField{Id: "f3id", Name: "test3"}, + ) + + str := testFieldsList.String() + + v, err := testFieldsList.Value() + if err != nil { + t.Fatal(err) + } + if v != str { + t.Fatalf("Expected String and Value to match") + } + + expectedParts := []string{ + `"type":"bool"`, + `"type":"url"`, + `"type":"text"`, + `"id":"f1id"`, + `"id":"f2id"`, + `"id":"f3id"`, + `"name":"test1"`, + `"name":"test2"`, + `"name":"test3"`, + } + + for _, part := range expectedParts { + if !strings.Contains(str, part) { + t.Fatalf("Missing %q in\nn%v", part, str) + } + } + }) +} + +func TestFieldsListScan(t *testing.T) { + scenarios := []struct { + name string + data any + expectError bool + expectJSON string + }{ + {"nil", nil, false, "[]"}, + {"empty string", "", false, "[]"}, + {"empty byte", []byte{}, false, "[]"}, + {"empty string array", "[]", false, "[]"}, + {"invalid string", "invalid", true, "[]"}, + {"non-string", 123, true, "[]"}, + {"item with no field type", `[{}]`, true, "[]"}, + { + "unknown field type", + `[{"id":"123","name":"test1","type":"unknown"},{"id":"456","name":"test2","type":"bool"}]`, + true, + `[]`, + }, + { + "only the minimum field options", + `[{"id":"123","name":"test1","type":"text","required":true},{"id":"456","name":"test2","type":"bool"}]`, + false, + `[{"autogeneratePattern":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`, + }, + { + "all field options", + `[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`, + false, + `[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + testFieldsList := core.FieldsList{} + + err := testFieldsList.Scan(s.data) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + str := testFieldsList.String() + if str != s.expectJSON { + t.Fatalf("Expected\n%v\ngot\n%v", s.expectJSON, str) + } + }) + } +} + +func TestFieldsListJSON(t *testing.T) { + scenarios := []struct { + name string + data string + expectError bool + expectJSON string + }{ + {"empty string", "", true, "[]"}, + {"invalid string", "invalid", true, "[]"}, + {"empty string array", "[]", false, "[]"}, + {"item with no field type", `[{}]`, true, "[]"}, + { + "unknown field type", + `[{"id":"123","name":"test1","type":"unknown"},{"id":"456","name":"test2","type":"bool"}]`, + true, + `[]`, + }, + { + "only the minimum field options", + `[{"id":"123","name":"test1","type":"text","required":true},{"id":"456","name":"test2","type":"bool"}]`, + false, + `[{"autogeneratePattern":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`, + }, + { + "all field options", + `[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`, + false, + `[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + testFieldsList := core.FieldsList{} + + err := testFieldsList.UnmarshalJSON([]byte(s.data)) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + raw, err := testFieldsList.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + str := string(raw) + if str != s.expectJSON { + t.Fatalf("Expected\n%v\ngot\n%v", s.expectJSON, str) + } + }) + } +} diff --git a/core/log_model.go b/core/log_model.go new file mode 100644 index 00000000..c2c3e7aa --- /dev/null +++ b/core/log_model.go @@ -0,0 +1,22 @@ +package core + +import "github.com/pocketbase/pocketbase/tools/types" + +var ( + _ Model = (*Log)(nil) +) + +const LogsTableName = "_logs" + +type Log struct { + BaseModel + + Created types.DateTime `db:"created" json:"created"` + Data types.JSONMap[any] `db:"data" json:"data"` + Message string `db:"message" json:"message"` + Level int `db:"level" json:"level"` +} + +func (m *Log) TableName() string { + return LogsTableName +} diff --git a/core/log_printer_test.go b/core/log_printer_test.go new file mode 100644 index 00000000..26d26c2c --- /dev/null +++ b/core/log_printer_test.go @@ -0,0 +1,115 @@ +package core + +import ( + "context" + "log/slog" + "os" + "testing" + + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/logger" +) + +func TestBaseAppLoggerLevelDevPrint(t *testing.T) { + testLogLevel := 4 + + scenarios := []struct { + name string + isDev bool + levels []int + printedLevels []int + persistedLevels []int + }{ + { + "dev mode", + true, + []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, + []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, + []int{testLogLevel, testLogLevel + 1}, + }, + { + "nondev mode", + false, + []int{testLogLevel - 1, testLogLevel, testLogLevel + 1}, + []int{}, + []int{testLogLevel, testLogLevel + 1}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(BaseAppConfig{ + DataDir: testDataDir, + IsDev: s.isDev, + }) + defer app.ResetBootstrapState() + + if err := app.Bootstrap(); err != nil { + t.Fatal(err) + } + + app.Settings().Logs.MinLevel = testLogLevel + if err := app.Save(app.Settings()); err != nil { + t.Fatal(err) + } + + var printedLevels []int + var persistedLevels []int + + ctx := context.Background() + + // track printed logs + originalPrintLog := printLog + defer func() { + printLog = originalPrintLog + }() + printLog = func(log *logger.Log) { + printedLevels = append(printedLevels, int(log.Level)) + } + + // track persisted logs + app.OnModelAfterCreateSuccess("_logs").BindFunc(func(e *ModelEvent) error { + l, ok := e.Model.(*Log) + if ok { + persistedLevels = append(persistedLevels, l.Level) + } + return e.Next() + }) + + // write and persist logs + for _, l := range s.levels { + app.Logger().Log(ctx, slog.Level(l), "test") + } + handler, ok := app.Logger().Handler().(*logger.BatchHandler) + if !ok { + t.Fatalf("Expected BatchHandler, got %v", app.Logger().Handler()) + } + if err := handler.WriteAll(ctx); err != nil { + t.Fatalf("Failed to write all logs: %v", err) + } + + // check persisted log levels + if len(s.persistedLevels) != len(persistedLevels) { + t.Fatalf("Expected persisted levels \n%v\ngot\n%v", s.persistedLevels, persistedLevels) + } + for _, l := range persistedLevels { + if !list.ExistInSlice(l, s.persistedLevels) { + t.Fatalf("Missing expected persisted level %v in %v", l, persistedLevels) + } + } + + // check printed log levels + if len(s.printedLevels) != len(printedLevels) { + t.Fatalf("Expected printed levels \n%v\ngot\n%v", s.printedLevels, printedLevels) + } + for _, l := range printedLevels { + if !list.ExistInSlice(l, s.printedLevels) { + t.Fatalf("Missing expected printed level %v in %v", l, printedLevels) + } + } + }) + } +} diff --git a/daos/log.go b/core/log_query.go similarity index 61% rename from daos/log.go rename to core/log_query.go index eaa11a07..701bfad8 100644 --- a/daos/log.go +++ b/core/log_query.go @@ -1,23 +1,22 @@ -package daos +package core import ( "time" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/types" ) // LogQuery returns a new Log select query. -func (dao *Dao) LogQuery() *dbx.SelectQuery { - return dao.ModelQuery(&models.Log{}) +func (app *BaseApp) LogQuery() *dbx.SelectQuery { + return app.AuxModelQuery(&Log{}) } // FindLogById finds a single Log entry by its id. -func (dao *Dao) FindLogById(id string) (*models.Log, error) { - model := &models.Log{} +func (app *BaseApp) FindLogById(id string) (*Log, error) { + model := &Log{} - err := dao.LogQuery(). + err := app.LogQuery(). AndWhere(dbx.HashExp{"id": id}). Limit(1). One(model) @@ -29,16 +28,17 @@ func (dao *Dao) FindLogById(id string) (*models.Log, error) { return model, nil } +// LogsStatsItem defines the total number of logs for a specific time period. type LogsStatsItem struct { - Total int `db:"total" json:"total"` Date types.DateTime `db:"date" json:"date"` + Total int `db:"total" json:"total"` } // LogsStats returns hourly grouped requests logs statistics. -func (dao *Dao) LogsStats(expr dbx.Expression) ([]*LogsStatsItem, error) { +func (app *BaseApp) LogsStats(expr dbx.Expression) ([]*LogsStatsItem, error) { result := []*LogsStatsItem{} - query := dao.LogQuery(). + query := app.LogQuery(). Select("count(id) as total", "strftime('%Y-%m-%d %H:00:00', created) as date"). GroupBy("date") @@ -52,16 +52,11 @@ func (dao *Dao) LogsStats(expr dbx.Expression) ([]*LogsStatsItem, error) { } // DeleteOldLogs delete all requests that are created before createdBefore. -func (dao *Dao) DeleteOldLogs(createdBefore time.Time) error { +func (app *BaseApp) DeleteOldLogs(createdBefore time.Time) error { formattedDate := createdBefore.UTC().Format(types.DefaultDateLayout) expr := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": formattedDate}) - _, err := dao.NonconcurrentDB().Delete((&models.Log{}).TableName(), expr).Execute() + _, err := app.auxNonconcurrentDB.Delete((&Log{}).TableName(), expr).Execute() return err } - -// SaveLog upserts the provided Log model. -func (dao *Dao) SaveLog(log *models.Log) error { - return dao.Save(log) -} diff --git a/core/log_query_test.go b/core/log_query_test.go new file mode 100644 index 00000000..df361987 --- /dev/null +++ b/core/log_query_test.go @@ -0,0 +1,114 @@ +package core_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestFindLogById(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.StubLogsData(app) + + scenarios := []struct { + id string + expectError bool + }{ + {"", true}, + {"invalid", true}, + {"00000000-9f38-44fb-bf82-c8f53b310d91", true}, + {"873f2133-9f38-44fb-bf82-c8f53b310d91", false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.id), func(t *testing.T) { + log, err := app.FindLogById(s.id) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if log != nil && log.Id != s.id { + t.Fatalf("Expected log with id %q, got %q", s.id, log.Id) + } + }) + } +} + +func TestLogsStats(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.StubLogsData(app) + + expected := `[{"date":"2022-05-01 10:00:00.000Z","total":1},{"date":"2022-05-02 10:00:00.000Z","total":1}]` + + now := time.Now().UTC().Format(types.DefaultDateLayout) + exp := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": now}) + result, err := app.LogsStats(exp) + if err != nil { + t.Fatal(err) + } + + encoded, _ := json.Marshal(result) + if string(encoded) != expected { + t.Fatalf("Expected\n%q\ngot\n%q", expected, string(encoded)) + } +} + +func TestDeleteOldLogs(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.StubLogsData(app) + + scenarios := []struct { + date string + expectedTotal int + }{ + {"2022-01-01 10:00:00.000Z", 2}, // no logs to delete before that time + {"2022-05-01 11:00:00.000Z", 1}, // only 1 log should have left + {"2022-05-03 11:00:00.000Z", 0}, // no more logs should have left + {"2022-05-04 11:00:00.000Z", 0}, // no more logs should have left + } + + for _, s := range scenarios { + t.Run(s.date, func(t *testing.T) { + date, dateErr := time.Parse(types.DefaultDateLayout, s.date) + if dateErr != nil { + t.Fatalf("Date error %v", dateErr) + } + + deleteErr := app.DeleteOldLogs(date) + if deleteErr != nil { + t.Fatalf("Delete error %v", deleteErr) + } + + // check total remaining logs + var total int + countErr := app.AuxModelQuery(&core.Log{}).Select("count(*)").Row(&total) + if countErr != nil { + t.Errorf("Count error %v", countErr) + } + + if total != s.expectedTotal { + t.Errorf("Expected %d remaining logs, got %d", s.expectedTotal, total) + } + }) + } +} diff --git a/core/mfa_model.go b/core/mfa_model.go new file mode 100644 index 00000000..be9f36f5 --- /dev/null +++ b/core/mfa_model.go @@ -0,0 +1,157 @@ +package core + +import ( + "context" + "errors" + "time" + + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/types" +) + +const ( + MFAMethodPassword = "password" + MFAMethodOAuth2 = "oauth2" + MFAMethodOTP = "otp" +) + +const CollectionNameMFAs = "_mfas" + +var ( + _ Model = (*MFA)(nil) + _ PreValidator = (*MFA)(nil) + _ RecordProxy = (*MFA)(nil) +) + +// MFA defines a Record proxy for working with the mfas collection. +type MFA struct { + *Record +} + +// NewMFA instantiates and returns a new blank *MFA model. +// +// Example usage: +// +// mfa := core.NewMFA(app) +// mfa.SetRecordRef(user.Id) +// mfa.SetCollectionRef(user.Collection().Id) +// mfa.SetMethod(core.MFAMethodPassword) +// app.Save(mfa) +func NewMFA(app App) *MFA { + m := &MFA{} + + c, err := app.FindCachedCollectionByNameOrId(CollectionNameMFAs) + if err != nil { + // this is just to make tests easier since mfa is a system collection and it is expected to be always accessible + // (note: the loaded record is further checked on MFA.PreValidate()) + c = NewBaseCollection("@__invalid__") + } + + m.Record = NewRecord(c) + + return m +} + +// PreValidate implements the [PreValidator] interface and checks +// whether the proxy is properly loaded. +func (m *MFA) PreValidate(ctx context.Context, app App) error { + if m.Record == nil || m.Record.Collection().Name != CollectionNameMFAs { + return errors.New("missing or invalid mfa ProxyRecord") + } + + return nil +} + +// ProxyRecord returns the proxied Record model. +func (m *MFA) ProxyRecord() *Record { + return m.Record +} + +// SetProxyRecord loads the specified record model into the current proxy. +func (m *MFA) SetProxyRecord(record *Record) { + m.Record = record +} + +// CollectionRef returns the "collectionRef" field value. +func (m *MFA) CollectionRef() string { + return m.GetString("collectionRef") +} + +// SetCollectionRef updates the "collectionRef" record field value. +func (m *MFA) SetCollectionRef(collectionId string) { + m.Set("collectionRef", collectionId) +} + +// RecordRef returns the "recordRef" record field value. +func (m *MFA) RecordRef() string { + return m.GetString("recordRef") +} + +// SetRecordRef updates the "recordRef" record field value. +func (m *MFA) SetRecordRef(recordId string) { + m.Set("recordRef", recordId) +} + +// Method returns the "method" record field value. +func (m *MFA) Method() string { + return m.GetString("method") +} + +// SetMethod updates the "method" record field value. +func (m *MFA) SetMethod(method string) { + m.Set("method", method) +} + +// Created returns the "created" record field value. +func (m *MFA) Created() types.DateTime { + return m.GetDateTime("created") +} + +// Updated returns the "updated" record field value. +func (m *MFA) Updated() types.DateTime { + return m.GetDateTime("updated") +} + +// HasExpired checks if the mfa is expired, aka. whether it has been +// more than maxElapsed time since its creation. +func (m *MFA) HasExpired(maxElapsed time.Duration) bool { + return time.Since(m.Created().Time()) > maxElapsed +} + +func (app *BaseApp) registerMFAHooks() { + recordRefHooks[*MFA](app, CollectionNameMFAs, CollectionTypeAuth) + + // run on every hour to cleanup expired mfa sessions + app.Cron().Add("__mfasCleanup__", "0 * * * *", func() { + if err := app.DeleteExpiredMFAs(); err != nil { + app.Logger().Warn("Failed to delete expired MFA sessions", "error", err) + } + }) + + // delete existing mfas on password change + app.OnRecordUpdate().Bind(&hook.Handler[*RecordEvent]{ + Func: func(e *RecordEvent) error { + err := e.Next() + if err != nil || !e.Record.Collection().IsAuth() { + return err + } + + old := e.Record.Original().GetString(FieldNamePassword + ":hash") + new := e.Record.GetString(FieldNamePassword + ":hash") + if old != new { + err = e.App.DeleteAllMFAsByRecord(e.Record) + if err != nil { + e.App.Logger().Warn( + "Failed to delete all previous mfas", + "error", err, + "recordId", e.Record.Id, + "collectionId", e.Record.Collection().Id, + ) + } + } + + return nil + }, + Priority: 99, + }) +} diff --git a/core/mfa_model_test.go b/core/mfa_model_test.go new file mode 100644 index 00000000..97669749 --- /dev/null +++ b/core/mfa_model_test.go @@ -0,0 +1,302 @@ +package core_test + +import ( + "fmt" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewMFA(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + if mfa.Collection().Name != core.CollectionNameMFAs { + t.Fatalf("Expected record with %q collection, got %q", core.CollectionNameMFAs, mfa.Collection().Name) + } +} + +func TestMFAProxyRecord(t *testing.T) { + t.Parallel() + + record := core.NewRecord(core.NewBaseCollection("test")) + record.Id = "test_id" + + mfa := core.MFA{} + mfa.SetProxyRecord(record) + + if mfa.ProxyRecord() == nil || mfa.ProxyRecord().Id != record.Id { + t.Fatalf("Expected proxy record with id %q, got %v", record.Id, mfa.ProxyRecord()) + } +} + +func TestMFARecordRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + mfa.SetRecordRef(testValue) + + if v := mfa.RecordRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := mfa.GetString("recordRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestMFACollectionRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + mfa.SetCollectionRef(testValue) + + if v := mfa.CollectionRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := mfa.GetString("collectionRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestMFAMethod(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + mfa.SetMethod(testValue) + + if v := mfa.Method(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := mfa.GetString("method"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestMFACreated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + if v := mfa.Created().String(); v != "" { + t.Fatalf("Expected empty created, got %q", v) + } + + now := types.NowDateTime() + mfa.SetRaw("created", now) + + if v := mfa.Created().String(); v != now.String() { + t.Fatalf("Expected %q created, got %q", now.String(), v) + } +} + +func TestMFAUpdated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfa := core.NewMFA(app) + + if v := mfa.Updated().String(); v != "" { + t.Fatalf("Expected empty updated, got %q", v) + } + + now := types.NowDateTime() + mfa.SetRaw("updated", now) + + if v := mfa.Updated().String(); v != now.String() { + t.Fatalf("Expected %q updated, got %q", now.String(), v) + } +} + +func TestMFAHasExpired(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + now := types.NowDateTime() + + mfa := core.NewMFA(app) + mfa.SetRaw("created", now.Add(-5*time.Minute)) + + scenarios := []struct { + maxElapsed time.Duration + expected bool + }{ + {0 * time.Minute, true}, + {3 * time.Minute, true}, + {5 * time.Minute, true}, + {6 * time.Minute, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.maxElapsed.String()), func(t *testing.T) { + result := mfa.HasExpired(s.maxElapsed) + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestMFAPreValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + mfasCol, err := app.FindCollectionByNameOrId(core.CollectionNameMFAs) + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + t.Run("no proxy record", func(t *testing.T) { + mfa := &core.MFA{} + + if err := app.Validate(mfa); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("non-MFA collection", func(t *testing.T) { + mfa := &core.MFA{} + mfa.SetProxyRecord(core.NewRecord(core.NewBaseCollection("invalid"))) + mfa.SetRecordRef(user.Id) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetMethod("test123") + + if err := app.Validate(mfa); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("MFA collection", func(t *testing.T) { + mfa := &core.MFA{} + mfa.SetProxyRecord(core.NewRecord(mfasCol)) + mfa.SetRecordRef(user.Id) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetMethod("test123") + + if err := app.Validate(mfa); err != nil { + t.Fatalf("Expected nil validation error, got %v", err) + } + }) +} + +func TestMFAValidateHook(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + mfa func() *core.MFA + expectErrors []string + }{ + { + "empty", + func() *core.MFA { + return core.NewMFA(app) + }, + []string{"collectionRef", "recordRef", "method"}, + }, + { + "non-auth collection", + func() *core.MFA { + mfa := core.NewMFA(app) + mfa.SetCollectionRef(demo1.Collection().Id) + mfa.SetRecordRef(demo1.Id) + mfa.SetMethod("test123") + return mfa + }, + []string{"collectionRef"}, + }, + { + "missing record id", + func() *core.MFA { + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef("missing") + mfa.SetMethod("test123") + return mfa + }, + []string{"recordRef"}, + }, + { + "valid ref", + func() *core.MFA { + mfa := core.NewMFA(app) + mfa.SetCollectionRef(user.Collection().Id) + mfa.SetRecordRef(user.Id) + mfa.SetMethod("test123") + return mfa + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := app.Validate(s.mfa()) + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/mfa_query.go b/core/mfa_query.go new file mode 100644 index 00000000..1ccfd457 --- /dev/null +++ b/core/mfa_query.go @@ -0,0 +1,117 @@ +package core + +import ( + "errors" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/types" +) + +// FindAllMFAsByRecord returns all MFA models linked to the provided auth record. +func (app *BaseApp) FindAllMFAsByRecord(authRecord *Record) ([]*MFA, error) { + result := []*MFA{} + + err := app.RecordQuery(CollectionNameMFAs). + AndWhere(dbx.HashExp{ + "collectionRef": authRecord.Collection().Id, + "recordRef": authRecord.Id, + }). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindAllMFAsByCollection returns all MFA models linked to the provided collection. +func (app *BaseApp) FindAllMFAsByCollection(collection *Collection) ([]*MFA, error) { + result := []*MFA{} + + err := app.RecordQuery(CollectionNameMFAs). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindMFAById returns a single MFA model by its id. +func (app *BaseApp) FindMFAById(id string) (*MFA, error) { + result := &MFA{} + + err := app.RecordQuery(CollectionNameMFAs). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteAllMFAsByRecord deletes all MFA models associated with the provided record. +// +// Returns a combined error with the failed deletes. +func (app *BaseApp) DeleteAllMFAsByRecord(authRecord *Record) error { + models, err := app.FindAllMFAsByRecord(authRecord) + if err != nil { + return err + } + + var errs []error + for _, m := range models { + if err := app.Delete(m); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +// DeleteExpiredMFAs deletes the expired MFAs for all auth collections. +func (app *BaseApp) DeleteExpiredMFAs() error { + authCollections, err := app.FindAllCollections(CollectionTypeAuth) + if err != nil { + return err + } + + // note: perform even if MFA is disabled to ensure that there are no dangling old records + for _, collection := range authCollections { + minValidDate, err := types.ParseDateTime(time.Now().Add(-1 * collection.MFA.DurationTime())) + if err != nil { + return err + } + + items := []*Record{} + + err = app.RecordQuery(CollectionNameMFAs). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + AndWhere(dbx.NewExp("[[created]] < {:date}", dbx.Params{"date": minValidDate})). + All(&items) + if err != nil { + return err + } + + for _, item := range items { + err = app.Delete(item) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/core/mfa_query_test.go b/core/mfa_query_test.go new file mode 100644 index 00000000..e26e2b3c --- /dev/null +++ b/core/mfa_query_test.go @@ -0,0 +1,311 @@ +package core_test + +import ( + "fmt" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestFindAllMFAsByRecord(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + user1, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + expected []string + }{ + {demo1, nil}, + {superuser2, []string{"superuser2_0", "superuser2_3", "superuser2_2", "superuser2_1", "superuser2_4"}}, + {superuser4, nil}, + {user1, []string{"user1_0"}}, + } + + for _, s := range scenarios { + t.Run(s.record.Collection().Name+"_"+s.record.Id, func(t *testing.T) { + result, err := app.FindAllMFAsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total mfas %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindAllMFAsByCollection(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + + clients, err := app.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + users, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + collection *core.Collection + expected []string + }{ + {demo1, nil}, + {superusers, []string{ + "superuser2_0", + "superuser2_3", + "superuser3_0", + "superuser2_2", + "superuser3_1", + "superuser2_1", + "superuser2_4", + }}, + {clients, nil}, + {users, []string{"user1_0"}}, + } + + for _, s := range scenarios { + t.Run(s.collection.Name, func(t *testing.T) { + result, err := app.FindAllMFAsByCollection(s.collection) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total mfas %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindMFAById(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + + scenarios := []struct { + id string + expectError bool + }{ + {"", true}, + {"84nmscqy84lsi1t", true}, // non-mfa id + {"superuser2_0", false}, + {"superuser2_4", false}, // expired + {"user1_0", false}, + } + + for _, s := range scenarios { + t.Run(s.id, func(t *testing.T) { + result, err := app.FindMFAById(s.id) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if result.Id != s.id { + t.Fatalf("Expected record with id %q, got %q", s.id, result.Id) + } + }) + } +} + +func TestDeleteAllMFAsByRecord(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + deletedIds []string + }{ + {demo1, nil}, // non-auth record + {superuser2, []string{"superuser2_0", "superuser2_1", "superuser2_3", "superuser2_2", "superuser2_4"}}, + {superuser4, nil}, + {user1, []string{"user1_0"}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + + deletedIds := []string{} + app.OnRecordAfterDeleteSuccess().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + err := app.DeleteAllMFAsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(s.deletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds) + } + + for _, id := range s.deletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + }) + } +} + +func TestDeleteExpiredMFAs(t *testing.T) { + t.Parallel() + + checkDeletedIds := func(app core.App, t *testing.T, expectedDeletedIds []string) { + if err := tests.StubMFARecords(app); err != nil { + t.Fatal(err) + } + + deletedIds := []string{} + app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + if err := app.DeleteExpiredMFAs(); err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(expectedDeletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", expectedDeletedIds, deletedIds) + } + + for _, id := range expectedDeletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + } + + t.Run("default test collections", func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + checkDeletedIds(app, t, []string{ + "user1_0", + "superuser2_1", + "superuser2_4", + }) + }) + + t.Run("mfa collection duration mock", func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + superusers.MFA.Duration = 60 + if err := app.Save(superusers); err != nil { + t.Fatalf("Failed to mock superusers mfa duration: %v", err) + } + + checkDeletedIds(app, t, []string{ + "user1_0", + "superuser2_1", + "superuser2_2", + "superuser2_4", + "superuser3_1", + }) + }) +} diff --git a/tools/migrate/list.go b/core/migrations_list.go similarity index 72% rename from tools/migrate/list.go rename to core/migrations_list.go index 65d24992..87ef0b39 100644 --- a/tools/migrate/list.go +++ b/core/migrations_list.go @@ -1,17 +1,15 @@ -package migrate +package core import ( "path/filepath" "runtime" "sort" - - "github.com/pocketbase/dbx" ) type Migration struct { + Up func(txApp App) error + Down func(txApp App) error File string - Up func(db dbx.Builder) error - Down func(db dbx.Builder) error } // MigrationsList defines a list with migration definitions @@ -29,14 +27,21 @@ func (l *MigrationsList) Items() []*Migration { return l.list } +// Copy copies all provided list migrations into the current one. +func (l *MigrationsList) Copy(list MigrationsList) { + for _, item := range list.Items() { + l.Register(item.Up, item.Down, item.File) + } +} + // Register adds new migration definition to the list. // // If `optFilename` is not provided, it will try to get the name from its .go file. // // The list will be sorted automatically based on the migrations file name. func (l *MigrationsList) Register( - up func(db dbx.Builder) error, - down func(db dbx.Builder) error, + up func(txApp App) error, + down func(txApp App) error, optFilename ...string, ) { var file string @@ -53,7 +58,7 @@ func (l *MigrationsList) Register( Down: down, }) - sort.Slice(l.list, func(i int, j int) bool { + sort.SliceStable(l.list, func(i int, j int) bool { return l.list[i].File < l.list[j].File }) } diff --git a/core/migrations_list_test.go b/core/migrations_list_test.go new file mode 100644 index 00000000..13875a6d --- /dev/null +++ b/core/migrations_list_test.go @@ -0,0 +1,39 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" +) + +func TestMigrationsList(t *testing.T) { + l1 := core.MigrationsList{} + l1.Register(nil, nil, "3_test.go") + l1.Register(nil, nil, "1_test.go") + l1.Register(nil, nil, "2_test.go") + l1.Register(nil, nil /* auto detect file name */) + + l2 := core.MigrationsList{} + l2.Register(nil, nil, "4_test.go") + l2.Copy(l1) + + expected := []string{ + "1_test.go", + "2_test.go", + "3_test.go", + "4_test.go", + "migrations_list_test.go", + } + + items := l2.Items() + if len(items) != len(expected) { + t.Fatalf("Expected %d items, got %d: \n%#v", len(expected), len(items), items) + } + + for i, name := range expected { + item := l2.Item(i) + if item.File != name { + t.Fatalf("Expected name %s for index %d, got %s", name, i, item.File) + } + } +} diff --git a/core/migrations_runner.go b/core/migrations_runner.go new file mode 100644 index 00000000..43519850 --- /dev/null +++ b/core/migrations_runner.go @@ -0,0 +1,308 @@ +package core + +import ( + "fmt" + "strings" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + "github.com/pocketbase/dbx" + "github.com/spf13/cast" +) + +var AppMigrations MigrationsList + +var SystemMigrations MigrationsList + +const DefaultMigrationsTable = "_migrations" + +// MigrationsRunner defines a simple struct for managing the execution of db migrations. +type MigrationsRunner struct { + app App + tableName string + migrationsList MigrationsList + inited bool +} + +// NewMigrationsRunner creates and initializes a new db migrations MigrationsRunner instance. +func NewMigrationsRunner(app App, migrationsList MigrationsList) *MigrationsRunner { + return &MigrationsRunner{ + app: app, + migrationsList: migrationsList, + tableName: DefaultMigrationsTable, + } +} + +// Run interactively executes the current runner with the provided args. +// +// The following commands are supported: +// - up - applies all migrations +// - down [n] - reverts the last n (default 1) applied migrations +// - history-sync - syncs the migrations table with the runner's migrations list +func (r *MigrationsRunner) Run(args ...string) error { + if err := r.initMigrationsTable(); err != nil { + return err + } + + cmd := "up" + if len(args) > 0 { + cmd = args[0] + } + + switch cmd { + case "up": + applied, err := r.Up() + if err != nil { + return err + } + + if len(applied) == 0 { + color.Green("No new migrations to apply.") + } else { + for _, file := range applied { + color.Green("Applied %s", file) + } + } + + return nil + case "down": + toRevertCount := 1 + if len(args) > 1 { + toRevertCount = cast.ToInt(args[1]) + if toRevertCount < 0 { + // revert all applied migrations + toRevertCount = len(r.migrationsList.Items()) + } + } + + names, err := r.lastAppliedMigrations(toRevertCount) + if err != nil { + return err + } + + confirm := false + prompt := &survey.Confirm{ + Message: fmt.Sprintf( + "\n%v\nDo you really want to revert the last %d applied migration(s)?", + strings.Join(names, "\n"), + toRevertCount, + ), + } + survey.AskOne(prompt, &confirm) + if !confirm { + fmt.Println("The command has been cancelled") + return nil + } + + reverted, err := r.Down(toRevertCount) + if err != nil { + return err + } + + if len(reverted) == 0 { + color.Green("No migrations to revert.") + } else { + for _, file := range reverted { + color.Green("Reverted %s", file) + } + } + + return nil + case "history-sync": + if err := r.RemoveMissingAppliedMigrations(); err != nil { + return err + } + + color.Green("The %s table was synced with the available migrations.", r.tableName) + return nil + default: + return fmt.Errorf("Unsupported command: %q\n", cmd) + } +} + +// Up executes all unapplied migrations for the provided runner. +// +// On success returns list with the applied migrations file names. +func (r *MigrationsRunner) Up() ([]string, error) { + if err := r.initMigrationsTable(); err != nil { + return nil, err + } + + applied := []string{} + + err := r.app.AuxRunInTransaction(func(txApp App) error { + return txApp.RunInTransaction(func(txApp App) error { + for _, m := range r.migrationsList.Items() { + // skip applied + if r.isMigrationApplied(txApp, m.File) { + continue + } + + // ignore empty Up action + if m.Up != nil { + if err := m.Up(txApp); err != nil { + return fmt.Errorf("Failed to apply migration %s: %w", m.File, err) + } + } + + if err := r.saveAppliedMigration(txApp, m.File); err != nil { + return fmt.Errorf("Failed to save applied migration info for %s: %w", m.File, err) + } + + applied = append(applied, m.File) + } + + return nil + }) + }) + + if err != nil { + return nil, err + } + return applied, nil +} + +// Down reverts the last `toRevertCount` applied migrations +// (in the order they were applied). +// +// On success returns list with the reverted migrations file names. +func (r *MigrationsRunner) Down(toRevertCount int) ([]string, error) { + if err := r.initMigrationsTable(); err != nil { + return nil, err + } + + reverted := make([]string, 0, toRevertCount) + + names, appliedErr := r.lastAppliedMigrations(toRevertCount) + if appliedErr != nil { + return nil, appliedErr + } + + err := r.app.AuxRunInTransaction(func(txApp App) error { + return txApp.RunInTransaction(func(txApp App) error { + for _, name := range names { + for _, m := range r.migrationsList.Items() { + if m.File != name { + continue + } + + // revert limit reached + if toRevertCount-len(reverted) <= 0 { + return nil + } + + // ignore empty Down action + if m.Down != nil { + if err := m.Down(txApp); err != nil { + return fmt.Errorf("Failed to revert migration %s: %w", m.File, err) + } + } + + if err := r.saveRevertedMigration(txApp, m.File); err != nil { + return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err) + } + + reverted = append(reverted, m.File) + } + } + return nil + }) + }) + + if err != nil { + return nil, err + } + + return reverted, nil +} + +// RemoveMissingAppliedMigrations removes the db entries of all applied migrations +// that are not listed in the runner's migrations list. +func (r *MigrationsRunner) RemoveMissingAppliedMigrations() error { + loadedMigrations := r.migrationsList.Items() + + names := make([]any, len(loadedMigrations)) + for i, migration := range loadedMigrations { + names[i] = migration.File + } + + _, err := r.app.DB().Delete(r.tableName, dbx.Not(dbx.HashExp{ + "file": names, + })).Execute() + + return err +} + +func (r *MigrationsRunner) initMigrationsTable() error { + if r.inited { + return nil // already inited + } + + rawQuery := fmt.Sprintf( + "CREATE TABLE IF NOT EXISTS {{%s}} (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)", + r.tableName, + ) + + _, err := r.app.DB().NewQuery(rawQuery).Execute() + + if err == nil { + r.inited = true + } + + return err +} + +func (r *MigrationsRunner) isMigrationApplied(txApp App, file string) bool { + var exists bool + + err := txApp.DB().Select("count(*)"). + From(r.tableName). + Where(dbx.HashExp{"file": file}). + Limit(1). + Row(&exists) + + return err == nil && exists +} + +func (r *MigrationsRunner) saveAppliedMigration(txApp App, file string) error { + _, err := txApp.DB().Insert(r.tableName, dbx.Params{ + "file": file, + "applied": time.Now().UnixMicro(), + }).Execute() + + return err +} + +func (r *MigrationsRunner) saveRevertedMigration(txApp App, file string) error { + _, err := txApp.DB().Delete(r.tableName, dbx.HashExp{"file": file}).Execute() + + return err +} + +func (r *MigrationsRunner) lastAppliedMigrations(limit int) ([]string, error) { + var files = make([]string, 0, limit) + + loadedMigrations := r.migrationsList.Items() + + names := make([]any, len(loadedMigrations)) + for i, migration := range loadedMigrations { + names[i] = migration.File + } + + err := r.app.DB().Select("file"). + From(r.tableName). + Where(dbx.Not(dbx.HashExp{"applied": nil})). + AndWhere(dbx.HashExp{"file": names}). + // unify microseconds and seconds applied time for backward compatibility + OrderBy("substr(applied||'0000000000000000', 0, 17) DESC"). + AndOrderBy("file DESC"). + Limit(int64(limit)). + Column(&files) + + if err != nil { + return nil, err + } + + return files, nil +} diff --git a/core/migrations_runner_test.go b/core/migrations_runner_test.go new file mode 100644 index 00000000..247324c9 --- /dev/null +++ b/core/migrations_runner_test.go @@ -0,0 +1,197 @@ +package core_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestMigrationsRunnerUpAndDown(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + callsOrder := []string{} + + l := core.MigrationsList{} + l.Register(func(app core.App) error { + callsOrder = append(callsOrder, "up2") + return nil + }, func(app core.App) error { + callsOrder = append(callsOrder, "down2") + return nil + }, "2_test") + l.Register(func(app core.App) error { + callsOrder = append(callsOrder, "up3") + return nil + }, func(app core.App) error { + callsOrder = append(callsOrder, "down3") + return nil + }, "3_test") + l.Register(func(app core.App) error { + callsOrder = append(callsOrder, "up1") + return nil + }, func(app core.App) error { + callsOrder = append(callsOrder, "down1") + return nil + }, "1_test") + + runner := core.NewMigrationsRunner(app, l) + + // simulate partially out-of-order run migration + _, err := app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ + "file": "2_test", + "applied": time.Now().UnixMicro(), + }).Execute() + if err != nil { + t.Fatalf("Failed to insert 2_test migration: %v", err) + } + + // --------------------------------------------------------------- + // Up() + // --------------------------------------------------------------- + + if _, err := runner.Up(); err != nil { + t.Fatal(err) + } + + expectedUpCallsOrder := `["up1","up3"]` // skip up2 since it was applied previously + + upCallsOrder, err := json.Marshal(callsOrder) + if err != nil { + t.Fatal(err) + } + + if v := string(upCallsOrder); v != expectedUpCallsOrder { + t.Fatalf("Expected Up() calls order %s, got %s", expectedUpCallsOrder, upCallsOrder) + } + + // --------------------------------------------------------------- + + // reset callsOrder + callsOrder = []string{} + + // simulate unrun migration + l.Register(nil, func(app core.App) error { + callsOrder = append(callsOrder, "down4") + return nil + }, "4_test") + + // simulate applied migrations from different migrations list + _, err = app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ + "file": "from_different_list", + "applied": time.Now().UnixMicro(), + }).Execute() + if err != nil { + t.Fatalf("Failed to insert from_different_list migration: %v", err) + } + + // --------------------------------------------------------------- + + // --------------------------------------------------------------- + // Down() + // --------------------------------------------------------------- + + if _, err := runner.Down(2); err != nil { + t.Fatal(err) + } + + expectedDownCallsOrder := `["down3","down1"]` // revert in the applied order + + downCallsOrder, err := json.Marshal(callsOrder) + if err != nil { + t.Fatal(err) + } + + if v := string(downCallsOrder); v != expectedDownCallsOrder { + t.Fatalf("Expected Down() calls order %s, got %s", expectedDownCallsOrder, downCallsOrder) + } +} + +func TestMigrationsRunnerRemoveMissingAppliedMigrations(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // mock migrations history + for i := 1; i <= 3; i++ { + _, err := app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ + "file": fmt.Sprintf("%d_test", i), + "applied": time.Now().UnixMicro(), + }).Execute() + if err != nil { + t.Fatal(err) + } + } + + if !isMigrationApplied(app, "2_test") { + t.Fatalf("Expected 2_test migration to be applied") + } + + // create a runner without 2_test to mock deleted migration + l := core.MigrationsList{} + l.Register(func(app core.App) error { + return nil + }, func(app core.App) error { + return nil + }, "1_test") + l.Register(func(app core.App) error { + return nil + }, func(app core.App) error { + return nil + }, "3_test") + + r := core.NewMigrationsRunner(app, l) + + if err := r.RemoveMissingAppliedMigrations(); err != nil { + t.Fatalf("Failed to remove missing applied migrations: %v", err) + } + + if isMigrationApplied(app, "2_test") { + t.Fatalf("Expected 2_test migration to NOT be applied") + } +} + +func isMigrationApplied(app core.App, file string) bool { + var exists bool + + err := app.DB().Select("count(*)"). + From(core.DefaultMigrationsTable). + Where(dbx.HashExp{"file": file}). + Limit(1). + Row(&exists) + + return err == nil && exists +} + +// // ------------------------------------------------------------------- + +// type testDB struct { +// *dbx.DB +// CalledQueries []string +// } + +// // NB! Don't forget to call `db.Close()` at the end of the test. +// func createTestDB() (*testDB, error) { +// sqlDB, err := sql.Open("sqlite", ":memory:") +// if err != nil { +// return nil, err +// } + +// db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")} +// db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { +// db.CalledQueries = append(db.CalledQueries, sql) +// } +// db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { +// db.CalledQueries = append(db.CalledQueries, sql) +// } + +// return &db, nil +// } diff --git a/core/otp_model.go b/core/otp_model.go new file mode 100644 index 00000000..dd4fc52e --- /dev/null +++ b/core/otp_model.go @@ -0,0 +1,113 @@ +package core + +import ( + "context" + "errors" + "time" + + "github.com/pocketbase/pocketbase/tools/types" +) + +const CollectionNameOTPs = "_otps" + +var ( + _ Model = (*OTP)(nil) + _ PreValidator = (*OTP)(nil) + _ RecordProxy = (*OTP)(nil) +) + +// OTP defines a Record proxy for working with the otps collection. +type OTP struct { + *Record +} + +// NewOTP instantiates and returns a new blank *OTP model. +// +// Example usage: +// +// otp := core.NewOTP(app) +// otp.SetRecordRef(user.Id) +// otp.SetCollectionRef(user.Collection().Id) +// otp.SetPassword(security.RandomStringWithAlphabet(6, "1234567890")) +// app.Save(otp) +func NewOTP(app App) *OTP { + m := &OTP{} + + c, err := app.FindCachedCollectionByNameOrId(CollectionNameOTPs) + if err != nil { + // this is just to make tests easier since otp is a system collection and it is expected to be always accessible + // (note: the loaded record is further checked on OTP.PreValidate()) + c = NewBaseCollection("__invalid__") + } + + m.Record = NewRecord(c) + + return m +} + +// PreValidate implements the [PreValidator] interface and checks +// whether the proxy is properly loaded. +func (m *OTP) PreValidate(ctx context.Context, app App) error { + if m.Record == nil || m.Record.Collection().Name != CollectionNameOTPs { + return errors.New("missing or invalid otp ProxyRecord") + } + + return nil +} + +// ProxyRecord returns the proxied Record model. +func (m *OTP) ProxyRecord() *Record { + return m.Record +} + +// SetProxyRecord loads the specified record model into the current proxy. +func (m *OTP) SetProxyRecord(record *Record) { + m.Record = record +} + +// CollectionRef returns the "collectionRef" field value. +func (m *OTP) CollectionRef() string { + return m.GetString("collectionRef") +} + +// SetCollectionRef updates the "collectionRef" record field value. +func (m *OTP) SetCollectionRef(collectionId string) { + m.Set("collectionRef", collectionId) +} + +// RecordRef returns the "recordRef" record field value. +func (m *OTP) RecordRef() string { + return m.GetString("recordRef") +} + +// SetRecordRef updates the "recordRef" record field value. +func (m *OTP) SetRecordRef(recordId string) { + m.Set("recordRef", recordId) +} + +// Created returns the "created" record field value. +func (m *OTP) Created() types.DateTime { + return m.GetDateTime("created") +} + +// Updated returns the "updated" record field value. +func (m *OTP) Updated() types.DateTime { + return m.GetDateTime("updated") +} + +// HasExpired checks if the otp is expired, aka. whether it has been +// more than maxElapsed time since its creation. +func (m *OTP) HasExpired(maxElapsed time.Duration) bool { + return time.Since(m.Created().Time()) > maxElapsed +} + +func (app *BaseApp) registerOTPHooks() { + recordRefHooks[*OTP](app, CollectionNameOTPs, CollectionTypeAuth) + + // run on every hour to cleanup expired otp sessions + app.Cron().Add("__otpsCleanup__", "0 * * * *", func() { + if err := app.DeleteExpiredOTPs(); err != nil { + app.Logger().Warn("Failed to delete expired OTP sessions", "error", err) + } + }) +} diff --git a/core/otp_model_test.go b/core/otp_model_test.go new file mode 100644 index 00000000..fd670c5e --- /dev/null +++ b/core/otp_model_test.go @@ -0,0 +1,278 @@ +package core_test + +import ( + "fmt" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewOTP(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otp := core.NewOTP(app) + + if otp.Collection().Name != core.CollectionNameOTPs { + t.Fatalf("Expected record with %q collection, got %q", core.CollectionNameOTPs, otp.Collection().Name) + } +} + +func TestOTPProxyRecord(t *testing.T) { + t.Parallel() + + record := core.NewRecord(core.NewBaseCollection("test")) + record.Id = "test_id" + + otp := core.OTP{} + otp.SetProxyRecord(record) + + if otp.ProxyRecord() == nil || otp.ProxyRecord().Id != record.Id { + t.Fatalf("Expected proxy record with id %q, got %v", record.Id, otp.ProxyRecord()) + } +} + +func TestOTPRecordRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otp := core.NewOTP(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + otp.SetRecordRef(testValue) + + if v := otp.RecordRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := otp.GetString("recordRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestOTPCollectionRef(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otp := core.NewOTP(app) + + testValues := []string{"test_1", "test2", ""} + for i, testValue := range testValues { + t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) { + otp.SetCollectionRef(testValue) + + if v := otp.CollectionRef(); v != testValue { + t.Fatalf("Expected getter %q, got %q", testValue, v) + } + + if v := otp.GetString("collectionRef"); v != testValue { + t.Fatalf("Expected field value %q, got %q", testValue, v) + } + }) + } +} + +func TestOTPCreated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otp := core.NewOTP(app) + + if v := otp.Created().String(); v != "" { + t.Fatalf("Expected empty created, got %q", v) + } + + now := types.NowDateTime() + otp.SetRaw("created", now) + + if v := otp.Created().String(); v != now.String() { + t.Fatalf("Expected %q created, got %q", now.String(), v) + } +} + +func TestOTPUpdated(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otp := core.NewOTP(app) + + if v := otp.Updated().String(); v != "" { + t.Fatalf("Expected empty updated, got %q", v) + } + + now := types.NowDateTime() + otp.SetRaw("updated", now) + + if v := otp.Updated().String(); v != now.String() { + t.Fatalf("Expected %q updated, got %q", now.String(), v) + } +} + +func TestOTPHasExpired(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + now := types.NowDateTime() + + otp := core.NewOTP(app) + otp.SetRaw("created", now.Add(-5*time.Minute)) + + scenarios := []struct { + maxElapsed time.Duration + expected bool + }{ + {0 * time.Minute, true}, + {3 * time.Minute, true}, + {5 * time.Minute, true}, + {6 * time.Minute, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.maxElapsed.String()), func(t *testing.T) { + result := otp.HasExpired(s.maxElapsed) + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestOTPPreValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + otpsCol, err := app.FindCollectionByNameOrId(core.CollectionNameOTPs) + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + t.Run("no proxy record", func(t *testing.T) { + otp := &core.OTP{} + + if err := app.Validate(otp); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("non-OTP collection", func(t *testing.T) { + otp := &core.OTP{} + otp.SetProxyRecord(core.NewRecord(core.NewBaseCollection("invalid"))) + otp.SetRecordRef(user.Id) + otp.SetCollectionRef(user.Collection().Id) + otp.SetPassword("test123") + + if err := app.Validate(otp); err == nil { + t.Fatal("Expected collection validation error") + } + }) + + t.Run("OTP collection", func(t *testing.T) { + otp := &core.OTP{} + otp.SetProxyRecord(core.NewRecord(otpsCol)) + otp.SetRecordRef(user.Id) + otp.SetCollectionRef(user.Collection().Id) + otp.SetPassword("test123") + + if err := app.Validate(otp); err != nil { + t.Fatalf("Expected nil validation error, got %v", err) + } + }) +} + +func TestOTPValidateHook(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + otp func() *core.OTP + expectErrors []string + }{ + { + "empty", + func() *core.OTP { + return core.NewOTP(app) + }, + []string{"collectionRef", "recordRef", "password"}, + }, + { + "non-auth collection", + func() *core.OTP { + otp := core.NewOTP(app) + otp.SetCollectionRef(demo1.Collection().Id) + otp.SetRecordRef(demo1.Id) + otp.SetPassword("test123") + return otp + }, + []string{"collectionRef"}, + }, + { + "missing record id", + func() *core.OTP { + otp := core.NewOTP(app) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef("missing") + otp.SetPassword("test123") + return otp + }, + []string{"recordRef"}, + }, + { + "valid ref", + func() *core.OTP { + otp := core.NewOTP(app) + otp.SetCollectionRef(user.Collection().Id) + otp.SetRecordRef(user.Id) + otp.SetPassword("test123") + return otp + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + errs := app.Validate(s.otp()) + tests.TestValidationErrors(t, errs, s.expectErrors) + }) + } +} diff --git a/core/otp_query.go b/core/otp_query.go new file mode 100644 index 00000000..8b2b4a94 --- /dev/null +++ b/core/otp_query.go @@ -0,0 +1,117 @@ +package core + +import ( + "errors" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/types" +) + +// FindAllOTPsByRecord returns all OTP models linked to the provided auth record. +func (app *BaseApp) FindAllOTPsByRecord(authRecord *Record) ([]*OTP, error) { + result := []*OTP{} + + err := app.RecordQuery(CollectionNameOTPs). + AndWhere(dbx.HashExp{ + "collectionRef": authRecord.Collection().Id, + "recordRef": authRecord.Id, + }). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindAllOTPsByCollection returns all OTP models linked to the provided collection. +func (app *BaseApp) FindAllOTPsByCollection(collection *Collection) ([]*OTP, error) { + result := []*OTP{} + + err := app.RecordQuery(CollectionNameOTPs). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + OrderBy("created DESC"). + All(&result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// FindOTPById returns a single OTP model by its id. +func (app *BaseApp) FindOTPById(id string) (*OTP, error) { + result := &OTP{} + + err := app.RecordQuery(CollectionNameOTPs). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(result) + + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteAllOTPsByRecord deletes all OTP models associated with the provided record. +// +// Returns a combined error with the failed deletes. +func (app *BaseApp) DeleteAllOTPsByRecord(authRecord *Record) error { + models, err := app.FindAllOTPsByRecord(authRecord) + if err != nil { + return err + } + + var errs []error + for _, m := range models { + if err := app.Delete(m); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +// DeleteExpiredOTPs deletes the expired OTPs for all auth collections. +func (app *BaseApp) DeleteExpiredOTPs() error { + authCollections, err := app.FindAllCollections(CollectionTypeAuth) + if err != nil { + return err + } + + // note: perform even if OTP is disabled to ensure that there are no dangling old records + for _, collection := range authCollections { + minValidDate, err := types.ParseDateTime(time.Now().Add(-1 * collection.OTP.DurationTime())) + if err != nil { + return err + } + + items := []*Record{} + + err = app.RecordQuery(CollectionNameOTPs). + AndWhere(dbx.HashExp{"collectionRef": collection.Id}). + AndWhere(dbx.NewExp("[[created]] < {:date}", dbx.Params{"date": minValidDate})). + All(&items) + if err != nil { + return err + } + + for _, item := range items { + err = app.Delete(item) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/core/otp_query_test.go b/core/otp_query_test.go new file mode 100644 index 00000000..440ffb96 --- /dev/null +++ b/core/otp_query_test.go @@ -0,0 +1,310 @@ +package core_test + +import ( + "fmt" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestFindAllOTPsByRecord(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + user1, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + expected []string + }{ + {demo1, nil}, + {superuser2, []string{"superuser2_0", "superuser2_1", "superuser2_3", "superuser2_2", "superuser2_4"}}, + {superuser4, nil}, + {user1, []string{"user1_0"}}, + } + + for _, s := range scenarios { + t.Run(s.record.Collection().Name+"_"+s.record.Id, func(t *testing.T) { + result, err := app.FindAllOTPsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total otps %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindAllOTPsByCollection(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + + demo1, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + + clients, err := app.FindCollectionByNameOrId("clients") + if err != nil { + t.Fatal(err) + } + + users, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + collection *core.Collection + expected []string + }{ + {demo1, nil}, + {superusers, []string{ + "superuser2_0", + "superuser2_1", + "superuser2_3", + "superuser3_0", + "superuser3_1", + "superuser2_2", + "superuser2_4", + }}, + {clients, nil}, + {users, []string{"user1_0"}}, + } + + for _, s := range scenarios { + t.Run(s.collection.Name, func(t *testing.T) { + result, err := app.FindAllOTPsByCollection(s.collection) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected total otps %d, got %d", len(s.expected), len(result)) + } + + for i, id := range s.expected { + if result[i].Id != id { + t.Errorf("[%d] Expected id %q, got %q", i, id, result[i].Id) + } + } + }) + } +} + +func TestFindOTPById(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + + scenarios := []struct { + id string + expectError bool + }{ + {"", true}, + {"84nmscqy84lsi1t", true}, // non-otp id + {"superuser2_0", false}, + {"superuser2_4", false}, // expired + {"user1_0", false}, + } + + for _, s := range scenarios { + t.Run(s.id, func(t *testing.T) { + result, err := app.FindOTPById(s.id) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if result.Id != s.id { + t.Fatalf("Expected record with id %q, got %q", s.id, result.Id) + } + }) + } +} + +func TestDeleteAllOTPsByRecord(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + superuser2, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + t.Fatal(err) + } + + superuser4, err := testApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test4@example.com") + if err != nil { + t.Fatal(err) + } + + user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + deletedIds []string + }{ + {demo1, nil}, // non-auth record + {superuser2, []string{"superuser2_0", "superuser2_1", "superuser2_3", "superuser2_2", "superuser2_4"}}, + {superuser4, nil}, + {user1, []string{"user1_0"}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + + deletedIds := []string{} + app.OnRecordAfterDeleteSuccess().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + err := app.DeleteAllOTPsByRecord(s.record) + if err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(s.deletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds) + } + + for _, id := range s.deletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + }) + } +} + +func TestDeleteExpiredOTPs(t *testing.T) { + t.Parallel() + + checkDeletedIds := func(app core.App, t *testing.T, expectedDeletedIds []string) { + if err := tests.StubOTPRecords(app); err != nil { + t.Fatal(err) + } + + deletedIds := []string{} + app.OnRecordAfterDeleteSuccess().BindFunc(func(e *core.RecordEvent) error { + deletedIds = append(deletedIds, e.Record.Id) + return e.Next() + }) + + if err := app.DeleteExpiredOTPs(); err != nil { + t.Fatal(err) + } + + if len(deletedIds) != len(expectedDeletedIds) { + t.Fatalf("Expected deleted ids\n%v\ngot\n%v", expectedDeletedIds, deletedIds) + } + + for _, id := range expectedDeletedIds { + if !slices.Contains(deletedIds, id) { + t.Errorf("Expected to find deleted id %q in %v", id, deletedIds) + } + } + } + + t.Run("default test collections", func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + checkDeletedIds(app, t, []string{ + "user1_0", + "superuser2_2", + "superuser2_4", + }) + }) + + t.Run("otp collection duration mock", func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + superusers, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + t.Fatal(err) + } + superusers.OTP.Duration = 60 + if err := app.Save(superusers); err != nil { + t.Fatalf("Failed to mock superusers otp duration: %v", err) + } + + checkDeletedIds(app, t, []string{ + "user1_0", + "superuser2_2", + "superuser2_4", + "superuser3_1", + }) + }) +} diff --git a/resolvers/record_field_resolver.go b/core/record_field_resolver.go similarity index 58% rename from resolvers/record_field_resolver.go rename to core/record_field_resolver.go index e8e5a037..9caf9dbd 100644 --- a/resolvers/record_field_resolver.go +++ b/core/record_field_resolver.go @@ -1,14 +1,13 @@ -package resolvers +package core import ( "encoding/json" + "errors" "fmt" "strconv" "strings" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/security" "github.com/spf13/cast" @@ -21,32 +20,9 @@ const ( lengthModifier string = "length" ) -// list of auth filter fields that don't require join with the auth -// collection or any other extra checks to be resolved. -var plainRequestAuthFields = []string{ - "@request.auth." + schema.FieldNameId, - "@request.auth." + schema.FieldNameCollectionId, - "@request.auth." + schema.FieldNameCollectionName, - "@request.auth." + schema.FieldNameUsername, - "@request.auth." + schema.FieldNameEmail, - "@request.auth." + schema.FieldNameEmailVisibility, - "@request.auth." + schema.FieldNameVerified, - "@request.auth." + schema.FieldNameCreated, - "@request.auth." + schema.FieldNameUpdated, -} - // ensure that `search.FieldResolver` interface is implemented var _ search.FieldResolver = (*RecordFieldResolver)(nil) -// CollectionsFinder defines a common interface for retrieving -// collections and other related models. -// -// The interface at the moment is primarily used to avoid circular -// dependency with the daos.Dao package. -type CollectionsFinder interface { - FindCollectionByNameOrId(collectionNameOrId string) (*models.Collection, error) -} - // RecordFieldResolver defines a custom search resolver struct for // managing Record model search fields. // @@ -54,7 +30,7 @@ type CollectionsFinder interface { // Example: // // resolver := resolvers.NewRecordFieldResolver( -// app.Dao(), +// app, // myCollection, // &models.RequestInfo{...}, // true, @@ -62,39 +38,36 @@ type CollectionsFinder interface { // provider := search.NewProvider(resolver) // ... type RecordFieldResolver struct { - dao CollectionsFinder - baseCollection *models.Collection - requestInfo *models.RequestInfo + app App + baseCollection *Collection + requestInfo *RequestInfo staticRequestInfo map[string]any allowedFields []string - loadedCollections []*models.Collection joins []*join allowHiddenFields bool } // NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`. func NewRecordFieldResolver( - dao CollectionsFinder, - baseCollection *models.Collection, - requestInfo *models.RequestInfo, - // @todo consider moving per filter basis + app App, + baseCollection *Collection, + requestInfo *RequestInfo, allowHiddenFields bool, ) *RecordFieldResolver { r := &RecordFieldResolver{ - dao: dao, + app: app, baseCollection: baseCollection, requestInfo: requestInfo, - allowHiddenFields: allowHiddenFields, + allowHiddenFields: allowHiddenFields, // note: it is not based only on the requestInfo.auth since it could be used by a non-request internal method joins: []*join{}, - loadedCollections: []*models.Collection{baseCollection}, allowedFields: []string{ `^\w+[\w\.\:]*$`, `^\@request\.context$`, `^\@request\.method$`, `^\@request\.auth\.[\w\.\:]*\w+$`, - `^\@request\.data\.[\w\.\:]*\w+$`, + `^\@request\.body\.[\w\.\:]*\w+$`, `^\@request\.query\.[\w\.\:]*\w+$`, - `^\@request\.headers\.\w+$`, + `^\@request\.headers\.[\w\.\:]*\w+$`, `^\@collection\.\w+(\:\w+)?\.[\w\.\:]*\w+$`, }, } @@ -105,13 +78,14 @@ func NewRecordFieldResolver( r.staticRequestInfo["method"] = r.requestInfo.Method r.staticRequestInfo["query"] = r.requestInfo.Query r.staticRequestInfo["headers"] = r.requestInfo.Headers - r.staticRequestInfo["data"] = r.requestInfo.Data + r.staticRequestInfo["body"] = r.requestInfo.Body r.staticRequestInfo["auth"] = nil - if r.requestInfo.AuthRecord != nil { - authData := r.requestInfo.AuthRecord.PublicExport() - // always add the record email no matter of the emailVisibility field - authData[schema.FieldNameEmail] = r.requestInfo.AuthRecord.Email() - r.staticRequestInfo["auth"] = authData + if r.requestInfo.Auth != nil { + authClone := r.requestInfo.Auth.Clone() + r.staticRequestInfo["auth"] = authClone. + Unhide(authClone.Collection().Fields.FieldNames()...). + IgnoreEmailVisibility(true). + PublicExport() } } @@ -150,10 +124,10 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { // @request.query.filter // @request.headers.x_token // @request.auth.someRelation.name -// @request.data.someRelation.name -// @request.data.someField -// @request.data.someSelect:each -// @request.data.someField:isset +// @request.body.someRelation.name +// @request.body.someField +// @request.body.someSelect:each +// @request.body.someField:isset // @collection.product.name func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, error) { return parseAndRun(fieldName, r) @@ -161,7 +135,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search.ResolverResult, error) { if len(path) == 0 { - return nil, fmt.Errorf("at least one path key should be provided") + return nil, errors.New("at least one path key should be provided") } lastProp, modifier, err := splitModifier(path[len(path)-1]) @@ -172,7 +146,10 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search path[len(path)-1] = lastProp // extract value - resultVal, err := extractNestedMapVal(r.staticRequestInfo, path...) + resultVal, err := extractNestedVal(r.staticRequestInfo, path...) + if err != nil { + r.app.Logger().Debug("resolveStaticRequestField graceful fallback", "error", err.Error()) + } if modifier == issetModifier { if err != nil { @@ -191,8 +168,8 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search // check if it is a number field and explicitly try to cast to // float in case of a numeric string value was used // (this usually the case when the data is from a multipart/form-data request) - field := r.baseCollection.Schema.GetFieldByName(path[len(path)-1]) - if field != nil && field.Type == schema.FieldTypeNumber { + field := r.baseCollection.Fields.GetByName(path[len(path)-1]) + if field != nil && field.Type() == FieldTypeNumber { if nv, err := strconv.ParseFloat(v, 64); err == nil { resultVal = nv } @@ -216,7 +193,7 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search resultVal = val } - placeholder := "f" + security.PseudorandomString(5) + placeholder := "f" + security.PseudorandomString(6) return &search.ResolverResult{ Identifier: "{:" + placeholder + "}", @@ -224,22 +201,12 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search }, nil } -func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) { - // return already loaded - for _, collection := range r.loadedCollections { - if collection.Id == collectionNameOrId || strings.EqualFold(collection.Name, collectionNameOrId) { - return collection, nil - } +func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*Collection, error) { + if collectionNameOrId == r.baseCollection.Name || collectionNameOrId == r.baseCollection.Id { + return r.baseCollection, nil } - // load collection - collection, err := r.dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, err - } - r.loadedCollections = append(r.loadedCollections, collection) - - return collection, nil + return getCollectionByModelOrIdentifier(r.app, collectionNameOrId) } func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) { @@ -261,11 +228,93 @@ func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, r.joins = append(r.joins, join) } -func extractNestedMapVal(m map[string]any, keys ...string) (any, error) { +type mapExtractor interface { + AsMap() map[string]any +} + +func extractNestedVal(rawData any, keys ...string) (any, error) { if len(keys) == 0 { - return nil, fmt.Errorf("at least one key should be provided") + return nil, errors.New("at least one key should be provided") } + switch m := rawData.(type) { + // maps + case map[string]any: + return mapVal(m, keys...) + case map[string]string: + return mapVal(m, keys...) + case map[string]bool: + return mapVal(m, keys...) + case map[string]float32: + return mapVal(m, keys...) + case map[string]float64: + return mapVal(m, keys...) + case map[string]int: + return mapVal(m, keys...) + case map[string]int8: + return mapVal(m, keys...) + case map[string]int16: + return mapVal(m, keys...) + case map[string]int32: + return mapVal(m, keys...) + case map[string]int64: + return mapVal(m, keys...) + case map[string]uint: + return mapVal(m, keys...) + case map[string]uint8: + return mapVal(m, keys...) + case map[string]uint16: + return mapVal(m, keys...) + case map[string]uint32: + return mapVal(m, keys...) + case map[string]uint64: + return mapVal(m, keys...) + case mapExtractor: + return mapVal(m.AsMap(), keys...) + + // slices + case []string: + return arrVal(m, keys...) + case []bool: + return arrVal(m, keys...) + case []float32: + return arrVal(m, keys...) + case []float64: + return arrVal(m, keys...) + case []int: + return arrVal(m, keys...) + case []int8: + return arrVal(m, keys...) + case []int16: + return arrVal(m, keys...) + case []int32: + return arrVal(m, keys...) + case []int64: + return arrVal(m, keys...) + case []uint: + return arrVal(m, keys...) + case []uint8: + return arrVal(m, keys...) + case []uint16: + return arrVal(m, keys...) + case []uint32: + return arrVal(m, keys...) + case []uint64: + return arrVal(m, keys...) + case []mapExtractor: + extracted := make([]any, len(m)) + for i, v := range m { + extracted[i] = v.AsMap() + } + return arrVal(extracted, keys...) + case []any: + return arrVal(m, keys...) + default: + return nil, fmt.Errorf("expected map or array, got %#v", rawData) + } +} + +func mapVal[T any](m map[string]T, keys ...string) (any, error) { result, ok := m[keys[0]] if !ok { return nil, fmt.Errorf("invalid key path - missing key %q", keys[0]) @@ -276,11 +325,23 @@ func extractNestedMapVal(m map[string]any, keys ...string) (any, error) { return result, nil } - if m, ok = result.(map[string]any); !ok { - return nil, fmt.Errorf("expected map, got %#v", result) + return extractNestedVal(result, keys[1:]...) +} + +func arrVal[T any](m []T, keys ...string) (any, error) { + idx, err := strconv.Atoi(keys[0]) + if err != nil || idx < 0 || idx >= len(m) { + return nil, fmt.Errorf("invalid key path - invalid or missing array index %q", keys[0]) } - return extractNestedMapVal(m, keys[1:]...) + result := m[idx] + + // end key reached + if len(keys) == 1 { + return result, nil + } + + return extractNestedVal(result, keys[1:]...) } func splitModifier(combined string) (string, string, error) { diff --git a/resolvers/multi_match_subquery.go b/core/record_field_resolver_multi_match.go similarity index 98% rename from resolvers/multi_match_subquery.go rename to core/record_field_resolver_multi_match.go index 5169903a..bb51efdf 100644 --- a/resolvers/multi_match_subquery.go +++ b/core/record_field_resolver_multi_match.go @@ -1,4 +1,4 @@ -package resolvers +package core import ( "fmt" diff --git a/resolvers/record_field_resolve_runner.go b/core/record_field_resolver_runner.go similarity index 63% rename from resolvers/record_field_resolve_runner.go rename to core/record_field_resolver_runner.go index b4a9b6ab..46c84f1d 100644 --- a/resolvers/record_field_resolve_runner.go +++ b/core/record_field_resolver_runner.go @@ -1,15 +1,15 @@ -package resolvers +package core import ( "encoding/json" + "errors" "fmt" + "reflect" "regexp" "strconv" "strings" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/inflector" "github.com/pocketbase/pocketbase/tools/list" @@ -20,6 +20,17 @@ import ( // maxNestedRels defines the max allowed nested relations depth. const maxNestedRels = 6 +// list of auth filter fields that don't require join with the auth +// collection or any other extra checks to be resolved. +var plainRequestAuthFields = map[string]struct{}{ + "@request.auth." + FieldNameId: {}, + "@request.auth." + FieldNameCollectionId: {}, + "@request.auth." + FieldNameCollectionName: {}, + "@request.auth." + FieldNameEmail: {}, + "@request.auth." + FieldNameEmailVisibility: {}, + "@request.auth." + FieldNameVerified: {}, +} + // parseAndRun starts a new one-off RecordFieldResolver.Resolve execution. func parseAndRun(fieldName string, resolver *RecordFieldResolver) (*search.ResolverResult, error) { r := &runner{ @@ -49,7 +60,7 @@ type runner struct { func (r *runner) run() (*search.ResolverResult, error) { if r.used { - return nil, fmt.Errorf("the runner was already used") + return nil, errors.New("the runner was already used") } if len(r.resolver.allowedFields) > 0 && !list.ExistInSliceWithRegex(r.fieldName, r.resolver.allowedFields) { @@ -77,32 +88,30 @@ func (r *runner) run() (*search.ResolverResult, error) { return r.processRequestAuthField() } - if strings.HasPrefix(r.fieldName, "@request.data.") && len(r.activeProps) > 2 { + if strings.HasPrefix(r.fieldName, "@request.body.") && len(r.activeProps) > 2 { name, modifier, err := splitModifier(r.activeProps[2]) if err != nil { return nil, err } - dataField := r.resolver.baseCollection.Schema.GetFieldByName(name) - if dataField == nil { + bodyField := r.resolver.baseCollection.Fields.GetByName(name) + if bodyField == nil { return r.resolver.resolveStaticRequestField(r.activeProps[1:]...) } - dataField.InitOptions() - - // check for data relation field - if dataField.Type == schema.FieldTypeRelation && len(r.activeProps) > 3 { - return r.processRequestInfoRelationField(dataField) + // check for body relation field + if bodyField.Type() == FieldTypeRelation && len(r.activeProps) > 3 { + return r.processRequestInfoRelationField(bodyField) } - // check for data arrayble fields ":each" modifier - if modifier == eachModifier && list.ExistInSlice(dataField.Type, schema.ArraybleFieldTypes()) && len(r.activeProps) == 3 { - return r.processRequestInfoEachModifier(dataField) + // check for body arrayble fields ":each" modifier + if modifier == eachModifier && len(r.activeProps) == 3 { + return r.processRequestInfoEachModifier(bodyField) } - // check for data arrayble fields ":length" modifier - if modifier == lengthModifier && list.ExistInSlice(dataField.Type, schema.ArraybleFieldTypes()) && len(r.activeProps) == 3 { - return r.processRequestInfoLengthModifier(dataField) + // check for body arrayble fields ":length" modifier + if modifier == lengthModifier && len(r.activeProps) == 3 { + return r.processRequestInfoLengthModifier(bodyField) } } @@ -184,18 +193,17 @@ func (r *runner) processCollectionField() (*search.ResolverResult, error) { func (r *runner) processRequestAuthField() (*search.ResolverResult, error) { // plain auth field // --- - if list.ExistInSlice(r.fieldName, plainRequestAuthFields) { + if _, ok := plainRequestAuthFields[r.fieldName]; ok { return r.resolver.resolveStaticRequestField(r.activeProps[1:]...) } // resolve the auth collection field // --- - if r.resolver.requestInfo == nil || r.resolver.requestInfo.AuthRecord == nil || r.resolver.requestInfo.AuthRecord.Collection() == nil { + if r.resolver.requestInfo == nil || r.resolver.requestInfo.Auth == nil || r.resolver.requestInfo.Auth.Collection() == nil { return &search.ResolverResult{Identifier: "NULL"}, nil } - collection := r.resolver.requestInfo.AuthRecord.Collection() - r.resolver.loadedCollections = append(r.resolver.loadedCollections, collection) + collection := r.resolver.requestInfo.Auth.Collection() r.activeCollectionName = collection.Name r.activeTableAlias = "__auth_" + inflector.Columnify(r.activeCollectionName) @@ -206,7 +214,7 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) { r.activeTableAlias, dbx.HashExp{ // aka. __auth_users.id = :userId - (r.activeTableAlias + ".id"): r.resolver.requestInfo.AuthRecord.Id, + (r.activeTableAlias + ".id"): r.resolver.requestInfo.Auth.Id, }, ) @@ -218,7 +226,7 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) { tableName: inflector.Columnify(r.activeCollectionName), tableAlias: r.multiMatchActiveTableAlias, on: dbx.HashExp{ - (r.multiMatchActiveTableAlias + ".id"): r.resolver.requestInfo.AuthRecord.Id, + (r.multiMatchActiveTableAlias + ".id"): r.resolver.requestInfo.Auth.Id, }, }, ) @@ -230,40 +238,68 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) { return r.processActiveProps() } -func (r *runner) processRequestInfoLengthModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) { - dataItems := list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name]) +// note: nil value is returned as empty slice +func toSlice(value any) []any { + if value == nil { + return []any{} + } + + rv := reflect.ValueOf(value) + + kind := rv.Kind() + if kind != reflect.Slice && kind != reflect.Array { + return []any{value} + } + + rvLen := rv.Len() + + result := make([]interface{}, rvLen) + + for i := 0; i < rvLen; i++ { + result[i] = rv.Index(i).Interface() + } + + return result +} + +func (r *runner) processRequestInfoLengthModifier(bodyField Field) (*search.ResolverResult, error) { + if _, ok := bodyField.(MultiValuer); !ok { + return nil, fmt.Errorf("field %q doesn't support multivalue operations", bodyField.GetName()) + } + + bodyItems := toSlice(r.resolver.requestInfo.Body[bodyField.GetName()]) result := &search.ResolverResult{ - Identifier: fmt.Sprintf("%d", len(dataItems)), + Identifier: strconv.Itoa(len(bodyItems)), } return result, nil } -func (r *runner) processRequestInfoEachModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) { - options, ok := dataField.Options.(schema.MultiValuer) +func (r *runner) processRequestInfoEachModifier(bodyField Field) (*search.ResolverResult, error) { + multiValuer, ok := bodyField.(MultiValuer) if !ok { - return nil, fmt.Errorf("field %q options are not initialized or doesn't support multivaluer operations", dataField.Name) + return nil, fmt.Errorf("field %q doesn't support multivalue operations", bodyField.GetName()) } - dataItems := list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name]) - rawJson, err := json.Marshal(dataItems) + bodyItems := toSlice(r.resolver.requestInfo.Body[bodyField.GetName()]) + bodyItemsRaw, err := json.Marshal(bodyItems) if err != nil { return nil, fmt.Errorf("cannot serialize the data for field %q", r.activeProps[2]) } - placeholder := "dataEach" + security.PseudorandomString(4) - cleanFieldName := inflector.Columnify(dataField.Name) + placeholder := "dataEach" + security.PseudorandomString(6) + cleanFieldName := inflector.Columnify(bodyField.GetName()) jeTable := fmt.Sprintf("json_each({:%s})", placeholder) jeAlias := "__dataEach_" + cleanFieldName + "_je" r.resolver.registerJoin(jeTable, jeAlias, nil) result := &search.ResolverResult{ Identifier: fmt.Sprintf("[[%s.value]]", jeAlias), - Params: dbx.Params{placeholder: rawJson}, + Params: dbx.Params{placeholder: bodyItemsRaw}, } - if options.IsMultiple() { + if multiValuer.IsMultiple() { r.withMultiMatch = true } @@ -276,7 +312,7 @@ func (r *runner) processRequestInfoEachModifier(dataField *schema.SchemaField) ( tableName: jeTable2, tableAlias: jeAlias2, }) - r.multiMatch.params[placeholder2] = rawJson + r.multiMatch.params[placeholder2] = bodyItemsRaw r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2) result.MultiMatchSubQuery = r.multiMatch @@ -285,27 +321,27 @@ func (r *runner) processRequestInfoEachModifier(dataField *schema.SchemaField) ( return result, nil } -func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField) (*search.ResolverResult, error) { - options, ok := dataField.Options.(*schema.RelationOptions) +func (r *runner) processRequestInfoRelationField(bodyField Field) (*search.ResolverResult, error) { + relField, ok := bodyField.(*RelationField) if !ok { - return nil, fmt.Errorf("failed to initialize data field %q options", dataField.Name) + return nil, fmt.Errorf("failed to initialize data relation field %q", bodyField.GetName()) } - dataRelCollection, err := r.resolver.loadCollection(options.CollectionId) + dataRelCollection, err := r.resolver.loadCollection(relField.CollectionId) if err != nil { - return nil, fmt.Errorf("failed to load collection %q from data field %q", options.CollectionId, dataField.Name) + return nil, fmt.Errorf("failed to load collection %q from data field %q", relField.CollectionId, relField.Name) } var dataRelIds []string - if r.resolver.requestInfo != nil && len(r.resolver.requestInfo.Data) != 0 { - dataRelIds = list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name]) + if r.resolver.requestInfo != nil && len(r.resolver.requestInfo.Body) != 0 { + dataRelIds = list.ToUniqueStringSlice(r.resolver.requestInfo.Body[relField.Name]) } if len(dataRelIds) == 0 { return &search.ResolverResult{Identifier: "NULL"}, nil } r.activeCollectionName = dataRelCollection.Name - r.activeTableAlias = inflector.Columnify("__data_" + dataRelCollection.Name + "_" + dataField.Name) + r.activeTableAlias = inflector.Columnify("__data_" + dataRelCollection.Name + "_" + relField.Name) // join the data rel collection to the main collection r.resolver.registerJoin( @@ -317,12 +353,12 @@ func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField) ), ) - if options.IsMultiple() { + if relField.IsMultiple() { r.withMultiMatch = true } // join the data rel collection to the multi-match subquery - r.multiMatchActiveTableAlias = inflector.Columnify("__data_mm_" + dataRelCollection.Name + "_" + dataField.Name) + r.multiMatchActiveTableAlias = inflector.Columnify("__data_mm_" + dataRelCollection.Name + "_" + relField.Name) r.multiMatch.joins = append( r.multiMatch.joins, &join{ @@ -336,7 +372,7 @@ func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField) ) // leave only the data relation fields - // aka. @request.data.someRel.fieldA.fieldB -> fieldA.fieldB + // aka. @request.body.someRel.fieldA.fieldB -> fieldA.fieldB r.activeProps = r.activeProps[3:] return r.processActiveProps() @@ -355,131 +391,13 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { // last prop if i == totalProps-1 { - // system field, aka. internal model prop - // (always available but not part of the collection schema) - // ------------------------------------------------------- - if list.ExistInSlice(prop, resolvableSystemFieldNames(collection)) { - result := &search.ResolverResult{ - Identifier: fmt.Sprintf("[[%s.%s]]", r.activeTableAlias, inflector.Columnify(prop)), - } - - // allow querying only auth records with emails marked as public - if prop == schema.FieldNameEmail && !r.allowHiddenFields { - result.AfterBuild = func(expr dbx.Expression) dbx.Expression { - return dbx.Enclose(dbx.And(expr, dbx.NewExp(fmt.Sprintf( - "[[%s.%s]] = TRUE", - r.activeTableAlias, - schema.FieldNameEmailVisibility, - )))) - } - } - - if r.withMultiMatch { - r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.%s]]", r.multiMatchActiveTableAlias, inflector.Columnify(prop)) - result.MultiMatchSubQuery = r.multiMatch - } - - return result, nil - } - - name, modifier, err := splitModifier(prop) - if err != nil { - return nil, err - } - - field := collection.Schema.GetFieldByName(name) - if field == nil { - if r.nullifyMisingField { - return &search.ResolverResult{Identifier: "NULL"}, nil - } - return nil, fmt.Errorf("unknown field %q", name) - } - - cleanFieldName := inflector.Columnify(field.Name) - - // arrayable fields with ":length" modifier - // ------------------------------------------------------- - if modifier == lengthModifier && list.ExistInSlice(field.Type, schema.ArraybleFieldTypes()) { - jePair := r.activeTableAlias + "." + cleanFieldName - - result := &search.ResolverResult{ - Identifier: dbutils.JsonArrayLength(jePair), - } - - if r.withMultiMatch { - jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName - r.multiMatch.valueIdentifier = dbutils.JsonArrayLength(jePair2) - result.MultiMatchSubQuery = r.multiMatch - } - - return result, nil - } - - // arrayable fields with ":each" modifier - // ------------------------------------------------------- - if modifier == eachModifier && list.ExistInSlice(field.Type, schema.ArraybleFieldTypes()) { - jePair := r.activeTableAlias + "." + cleanFieldName - jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je" - r.resolver.registerJoin(dbutils.JsonEach(jePair), jeAlias, nil) - - result := &search.ResolverResult{ - Identifier: fmt.Sprintf("[[%s.value]]", jeAlias), - } - - options, ok := field.Options.(schema.MultiValuer) - if !ok { - return nil, fmt.Errorf("field %q options are not initialized or doesn't multivaluer arrayable operations", prop) - } - - if options.IsMultiple() { - r.withMultiMatch = true - } - - if r.withMultiMatch { - jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName - jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je" - - r.multiMatch.joins = append(r.multiMatch.joins, &join{ - tableName: dbutils.JsonEach(jePair2), - tableAlias: jeAlias2, - }) - r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2) - - result.MultiMatchSubQuery = r.multiMatch - } - - return result, nil - } - - // default - // ------------------------------------------------------- - result := &search.ResolverResult{ - Identifier: fmt.Sprintf("[[%s.%s]]", r.activeTableAlias, cleanFieldName), - } - - if r.withMultiMatch { - r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.%s]]", r.multiMatchActiveTableAlias, cleanFieldName) - result.MultiMatchSubQuery = r.multiMatch - } - - // wrap in json_extract to ensure that top-level primitives - // stored as json work correctly when compared to their SQL equivalent - // (https://github.com/pocketbase/pocketbase/issues/4068) - if field.Type == schema.FieldTypeJson { - result.NoCoalesce = true - result.Identifier = dbutils.JsonExtract(r.activeTableAlias+"."+cleanFieldName, "") - if r.withMultiMatch { - r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "") - } - } - - return result, nil + return r.processLastProp(collection, prop) } - field := collection.Schema.GetFieldByName(prop) + field := collection.Fields.GetByName(prop) // json field -> treat the rest of the props as json path - if field != nil && field.Type == schema.FieldTypeJson { + if field != nil && field.Type() == FieldTypeJSON { var jsonPath strings.Builder for j, p := range r.activeProps[i+1:] { if _, err := strconv.Atoi(p); err == nil { @@ -497,11 +415,11 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { result := &search.ResolverResult{ NoCoalesce: true, - Identifier: dbutils.JsonExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr), + Identifier: dbutils.JSONExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr), } if r.withMultiMatch { - r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr) + r.multiMatch.valueIdentifier = dbutils.JSONExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr) result.MultiMatchSubQuery = r.multiMatch } @@ -531,37 +449,36 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { return nil, fmt.Errorf("failed to load back relation field %q collection", prop) } - backField := backCollection.Schema.GetFieldByName(parts[2]) + backField := backCollection.Fields.GetByName(parts[2]) if backField == nil { if r.nullifyMisingField { return &search.ResolverResult{Identifier: "NULL"}, nil } return nil, fmt.Errorf("missing back relation field %q", parts[2]) } - if backField.Type != schema.FieldTypeRelation { + if backField.Type() != FieldTypeRelation { return nil, fmt.Errorf("invalid back relation field %q", parts[2]) } - backField.InitOptions() - backFieldOptions, ok := backField.Options.(*schema.RelationOptions) + backRelField, ok := backField.(*RelationField) if !ok { - return nil, fmt.Errorf("failed to initialize back relation field %q options", backField.Name) + return nil, fmt.Errorf("failed to initialize back relation field %q", backField.GetName()) } - if backFieldOptions.CollectionId != collection.Id { - return nil, fmt.Errorf("invalid back relation field %q collection reference", backField.Name) + if backRelField.CollectionId != collection.Id { + return nil, fmt.Errorf("invalid back relation field %q collection reference", backField.GetName()) } // join the back relation to the main query // --- cleanProp := inflector.Columnify(prop) - cleanBackFieldName := inflector.Columnify(backField.Name) + cleanBackFieldName := inflector.Columnify(backRelField.Name) newTableAlias := r.activeTableAlias + "_" + cleanProp newCollectionName := inflector.Columnify(backCollection.Name) - isBackRelMultiple := backFieldOptions.IsMultiple() + isBackRelMultiple := backRelField.IsMultiple() if !isBackRelMultiple { // additionally check if the rel field has a single column unique index - isBackRelMultiple = !dbutils.HasSingleColumnUniqueIndex(backField.Name, backCollection.Indexes) + isBackRelMultiple = !dbutils.HasSingleColumnUniqueIndex(backRelField.Name, backCollection.Indexes) } if !isBackRelMultiple { @@ -579,7 +496,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { "[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})", r.activeTableAlias, jeAlias, - dbutils.JsonEach(newTableAlias+"."+cleanBackFieldName), + dbutils.JSONEach(newTableAlias+"."+cleanBackFieldName), jeAlias, )), ) @@ -617,7 +534,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { "[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})", r.multiMatchActiveTableAlias, jeAlias2, - dbutils.JsonEach(newTableAlias2+"."+cleanBackFieldName), + dbutils.JSONEach(newTableAlias2+"."+cleanBackFieldName), jeAlias2, )), }, @@ -632,29 +549,36 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { // ----------------------------------------------------------- // check for direct relation - if field.Type != schema.FieldTypeRelation { + if field.Type() != FieldTypeRelation { return nil, fmt.Errorf("field %q is not a valid relation", prop) } // join the relation to the main query // --- - field.InitOptions() - options, ok := field.Options.(*schema.RelationOptions) + relField, ok := field.(*RelationField) if !ok { - return nil, fmt.Errorf("failed to initialize field %q options", prop) + return nil, fmt.Errorf("failed to initialize relation field %q", prop) } - relCollection, relErr := r.resolver.loadCollection(options.CollectionId) + relCollection, relErr := r.resolver.loadCollection(relField.CollectionId) if relErr != nil { return nil, fmt.Errorf("failed to load field %q collection", prop) } - cleanFieldName := inflector.Columnify(field.Name) + // "id" lookups optimization for single relations to avoid unnecessary joins, + // aka. "user.id" and "user" should produce the same query identifier + if !relField.IsMultiple() && + // the penultimate prop is "id" + i == totalProps-2 && r.activeProps[i+1] == FieldNameId { + return r.processLastProp(collection, relField.Name) + } + + cleanFieldName := inflector.Columnify(relField.Name) prefixedFieldName := r.activeTableAlias + "." + cleanFieldName newTableAlias := r.activeTableAlias + "_" + cleanFieldName newCollectionName := relCollection.Name - if !options.IsMultiple() { + if !relField.IsMultiple() { r.resolver.registerJoin( inflector.Columnify(newCollectionName), newTableAlias, @@ -662,7 +586,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { ) } else { jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je" - r.resolver.registerJoin(dbutils.JsonEach(prefixedFieldName), jeAlias, nil) + r.resolver.registerJoin(dbutils.JSONEach(prefixedFieldName), jeAlias, nil) r.resolver.registerJoin( inflector.Columnify(newCollectionName), newTableAlias, @@ -676,14 +600,14 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { // join the relation to the multi-match subquery // --- - if options.IsMultiple() { + if relField.IsMultiple() { r.withMultiMatch = true // enable multimatch if not already } newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName prefixedFieldName2 := r.multiMatchActiveTableAlias + "." + cleanFieldName - if !options.IsMultiple() { + if !relField.IsMultiple() { r.multiMatch.joins = append( r.multiMatch.joins, &join{ @@ -697,7 +621,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { r.multiMatch.joins = append( r.multiMatch.joins, &join{ - tableName: dbutils.JsonEach(prefixedFieldName2), + tableName: dbutils.JSONEach(prefixedFieldName2), tableAlias: jeAlias2, }, &join{ @@ -715,18 +639,109 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { return nil, fmt.Errorf("failed to resolve field %q", r.fieldName) } -func resolvableSystemFieldNames(collection *models.Collection) []string { - result := schema.BaseModelFieldNames() - - if collection.IsAuth() { - result = append( - result, - schema.FieldNameUsername, - schema.FieldNameVerified, - schema.FieldNameEmailVisibility, - schema.FieldNameEmail, - ) +func (r *runner) processLastProp(collection *Collection, prop string) (*search.ResolverResult, error) { + name, modifier, err := splitModifier(prop) + if err != nil { + return nil, err } - return result + field := collection.Fields.GetByName(name) + if field == nil { + if r.nullifyMisingField { + return &search.ResolverResult{Identifier: "NULL"}, nil + } + return nil, fmt.Errorf("unknown field %q", name) + } + + if field.GetHidden() && !r.allowHiddenFields { + return nil, fmt.Errorf("non-filterable field %q", name) + } + + multvaluer, isMultivaluer := field.(MultiValuer) + + cleanFieldName := inflector.Columnify(field.GetName()) + + // arrayable fields with ":length" modifier + // ------------------------------------------------------- + if modifier == lengthModifier && isMultivaluer { + jePair := r.activeTableAlias + "." + cleanFieldName + + result := &search.ResolverResult{ + Identifier: dbutils.JSONArrayLength(jePair), + } + + if r.withMultiMatch { + jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName + r.multiMatch.valueIdentifier = dbutils.JSONArrayLength(jePair2) + result.MultiMatchSubQuery = r.multiMatch + } + + return result, nil + } + + // arrayable fields with ":each" modifier + // ------------------------------------------------------- + if modifier == eachModifier && isMultivaluer { + jePair := r.activeTableAlias + "." + cleanFieldName + jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je" + r.resolver.registerJoin(dbutils.JSONEach(jePair), jeAlias, nil) + + result := &search.ResolverResult{ + Identifier: fmt.Sprintf("[[%s.value]]", jeAlias), + } + + if multvaluer.IsMultiple() { + r.withMultiMatch = true + } + + if r.withMultiMatch { + jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName + jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je" + + r.multiMatch.joins = append(r.multiMatch.joins, &join{ + tableName: dbutils.JSONEach(jePair2), + tableAlias: jeAlias2, + }) + r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2) + + result.MultiMatchSubQuery = r.multiMatch + } + + return result, nil + } + + // default + // ------------------------------------------------------- + result := &search.ResolverResult{ + Identifier: fmt.Sprintf("[[%s.%s]]", r.activeTableAlias, cleanFieldName), + } + + if r.withMultiMatch { + r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.%s]]", r.multiMatchActiveTableAlias, cleanFieldName) + result.MultiMatchSubQuery = r.multiMatch + } + + // allow querying only auth records with emails marked as public + if field.GetName() == FieldNameEmail && !r.allowHiddenFields && collection.IsAuth() { + result.AfterBuild = func(expr dbx.Expression) dbx.Expression { + return dbx.Enclose(dbx.And(expr, dbx.NewExp(fmt.Sprintf( + "[[%s.%s]] = TRUE", + r.activeTableAlias, + FieldNameEmailVisibility, + )))) + } + } + + // wrap in json_extract to ensure that top-level primitives + // stored as json work correctly when compared to their SQL equivalent + // (https://github.com/pocketbase/pocketbase/issues/4068) + if field.Type() == FieldTypeJSON { + result.NoCoalesce = true + result.Identifier = dbutils.JSONExtract(r.activeTableAlias+"."+cleanFieldName, "") + if r.withMultiMatch { + r.multiMatch.valueIdentifier = dbutils.JSONExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "") + } + } + + return result, nil } diff --git a/resolvers/record_field_resolver_test.go b/core/record_field_resolver_test.go similarity index 90% rename from resolvers/record_field_resolver_test.go rename to core/record_field_resolver_test.go index bd7c1530..0a709f18 100644 --- a/resolvers/record_field_resolver_test.go +++ b/core/record_field_resolver_test.go @@ -1,4 +1,4 @@ -package resolvers_test +package core_test import ( "encoding/json" @@ -6,9 +6,7 @@ import ( "strings" "testing" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/search" @@ -18,23 +16,23 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") + authRecord, err := app.FindRecordById("users", "4q1xlclmfloku33") if err != nil { t.Fatal(err) } - requestInfo := &models.RequestInfo{ + requestInfo := &core.RequestInfo{ Context: "ctx", - Headers: map[string]any{ + Headers: map[string]string{ "a": "123", "b": "456", }, - Query: map[string]any{ - "a": nil, - "b": 123, + Query: map[string]string{ + "a": "", // to ensure that :isset returns true because the key exists + "b": "123", }, - Data: map[string]any{ - "a": nil, + Body: map[string]any{ + "a": nil, // to ensure that :isset returns true because the key exists "b": 123, "number": 10, "select_many": []string{"optionA", "optionC"}, @@ -48,7 +46,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { "rel_one_cascade": "test1", "rel_one_no_cascade": "test1", }, - AuthRecord: authRecord, + Auth: authRecord, } scenarios := []struct { @@ -73,21 +71,49 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { "SELECT `demo4`.* FROM `demo4` WHERE ([[demo4.title]] = 1 OR [[demo4.title]] IS NOT {:TEST} OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})", }, { - "incomplete rel", + "single direct rel", "demo4", "self_rel_one > true", false, "SELECT `demo4`.* FROM `demo4` WHERE [[demo4.self_rel_one]] > 1", }, { - "single rel (self rel)", + "single direct rel (with id)", + "demo4", + "self_rel_one.id > true", // shouldn't have join + false, + "SELECT `demo4`.* FROM `demo4` WHERE [[demo4.self_rel_one]] > 1", + }, + { + "single direct rel (with non-id field)", + "demo4", + "self_rel_one.created > true", // should have join + false, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE [[demo4_self_rel_one.created]] > 1", + }, + { + "multiple direct rel", + "demo4", + "self_rel_many ?> true", + false, + "SELECT `demo4`.* FROM `demo4` WHERE [[demo4.self_rel_many]] > 1", + }, + { + "multiple direct rel (with id)", + "demo4", + "self_rel_many.id ?> true", // should have join + false, + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.id]] > 1", + }, + { + "nested single rel (self rel)", "demo4", "self_rel_one.title > true", false, "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE [[demo4_self_rel_one.title]] > 1", }, { - "single rel (other collection)", + "nested single rel (other collection)", "demo4", "rel_one_cascade.title > true", false, @@ -215,9 +241,9 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { { "@request.auth fields", "demo4", - "@request.auth.id > true || @request.auth.username > true || @request.auth.rel.title > true || @request.data.demo < true || @request.auth.missingA.missingB > false", + "@request.auth.id > true || @request.auth.username > true || @request.auth.rel.title > true || @request.body.demo < true || @request.auth.missingA.missingB > false", false, - "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON `__auth_users`.`id`={:p0} LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users.rel]] WHERE ({:TEST} > 1 OR {:TEST} > 1 OR [[__auth_users_rel.title]] > 1 OR NULL < 1 OR NULL > 0)", + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON `__auth_users`.`id`={:p0} LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users.rel]] WHERE ({:TEST} > 1 OR [[__auth_users.username]] > 1 OR [[__auth_users_rel.title]] > 1 OR NULL < 1 OR NULL > 0)", }, { "@request.* static fields", @@ -250,41 +276,43 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { { "isset key", "demo1", - "@request.data.a:isset > true ||" + - "@request.data.b:isset > true ||" + - "@request.data.c:isset > true ||" + + "@request.body.a:isset > true ||" + + "@request.body.b:isset > true ||" + + "@request.body.c:isset > true ||" + "@request.query.a:isset > true ||" + "@request.query.b:isset > true ||" + - "@request.query.c:isset > true", + "@request.query.c:isset > true ||" + + "@request.headers.a:isset > true ||" + + "@request.headers.c:isset > true", false, - "SELECT `demo1`.* FROM `demo1` WHERE (TRUE > 1 OR TRUE > 1 OR FALSE > 1 OR TRUE > 1 OR TRUE > 1 OR FALSE > 1)", + "SELECT `demo1`.* FROM `demo1` WHERE (TRUE > 1 OR TRUE > 1 OR FALSE > 1 OR TRUE > 1 OR TRUE > 1 OR FALSE > 1 OR TRUE > 1 OR FALSE > 1)", }, { - "@request.data.rel.* fields", + "@request.body.rel.* fields", "demo4", - "@request.data.rel_one_cascade.title > true &&" + + "@request.body.rel_one_cascade.title > true &&" + // reference the same as rel_one_cascade collection but should use a different join alias - "@request.data.rel_one_no_cascade.title < true &&" + + "@request.body.rel_one_no_cascade.title < true &&" + // different collection - "@request.data.self_rel_many.title = true", + "@request.body.self_rel_many.title = true", false, "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo3` `__data_demo3_rel_one_cascade` ON [[__data_demo3_rel_one_cascade.id]]={:p0} LEFT JOIN `demo3` `__data_demo3_rel_one_no_cascade` ON [[__data_demo3_rel_one_no_cascade.id]]={:p1} LEFT JOIN `demo4` `__data_demo4_self_rel_many` ON [[__data_demo4_self_rel_many.id]]={:p2} WHERE ([[__data_demo3_rel_one_cascade.title]] > 1 AND [[__data_demo3_rel_one_no_cascade.title]] < 1 AND (([[__data_demo4_self_rel_many.title]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo4` `__data_mm_demo4_self_rel_many` ON [[__data_mm_demo4_self_rel_many.id]]={:p3} WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))", }, { - "@request.data.arrayble:each fields", + "@request.body.arrayble:each fields", "demo1", - "@request.data.select_one:each > true &&" + - "@request.data.select_one:each ?< true &&" + - "@request.data.select_many:each > true &&" + - "@request.data.select_many:each ?< true &&" + - "@request.data.file_one:each > true &&" + - "@request.data.file_one:each ?< true &&" + - "@request.data.file_many:each > true &&" + - "@request.data.file_many:each ?< true &&" + - "@request.data.rel_one:each > true &&" + - "@request.data.rel_one:each ?< true &&" + - "@request.data.rel_many:each > true &&" + - "@request.data.rel_many:each ?< true", + "@request.body.select_one:each > true &&" + + "@request.body.select_one:each ?< true &&" + + "@request.body.select_many:each > true &&" + + "@request.body.select_many:each ?< true &&" + + "@request.body.file_one:each > true &&" + + "@request.body.file_one:each ?< true &&" + + "@request.body.file_many:each > true &&" + + "@request.body.file_many:each ?< true &&" + + "@request.body.rel_one:each > true &&" + + "@request.body.rel_one:each ?< true &&" + + "@request.body.rel_many:each > true &&" + + "@request.body.rel_many:each ?< true", false, "SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_select_one_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_select_many_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_file_one_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_file_many_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_rel_one_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_rel_many_je` WHERE ([[__dataEach_select_one_je.value]] > 1 AND [[__dataEach_select_one_je.value]] < 1 AND (([[__dataEach_select_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataEach_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[__dataEach_select_many_je.value]] < 1 AND [[__dataEach_file_one_je.value]] > 1 AND [[__dataEach_file_one_je.value]] < 1 AND (([[__dataEach_file_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataEach_file_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_file_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[__dataEach_file_many_je.value]] < 1 AND [[__dataEach_rel_one_je.value]] > 1 AND [[__dataEach_rel_one_je.value]] < 1 AND (([[__dataEach_rel_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataEach_rel_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_rel_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[__dataEach_rel_many_je.value]] < 1)", }, @@ -312,7 +340,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { "select_one:each != select_many:each &&" + "select_many:each > select_one:each &&" + "select_many:each ?< select_one:each &&" + - "select_many:each = @request.data.select_many:each", + "select_many:each = @request.body.select_many:each", false, "SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_one]]) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_many]]) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_select_many_je` WHERE (((COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[demo1_select_many_je.value]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT (COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[__smTEST.multiMatchValue]], ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND (([[demo1_select_many_je.value]] > [[demo1_select_one_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > [[demo1_select_one_je.value]])) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND (([[demo1_select_many_je.value]] = [[__dataEach_select_many_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm__dataEach_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))))", }, @@ -324,36 +352,36 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { "rel_many.rel.title ~ rel_one.email &&" + "@collection.demo2.active = rel_many.rel.active &&" + "@collection.demo2.active ?= rel_many.rel.active &&" + - "rel_many.email > @request.data.rel_many.email", + "rel_many.email > @request.body.rel_many.email", false, "SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] LEFT JOIN `demo2` `demo1_rel_many_rel` ON [[demo1_rel_many_rel.id]] = [[demo1_rel_many.rel]] LEFT JOIN `demo1` `demo1_rel_one` ON [[demo1_rel_one.id]] = [[demo1.rel_one]] LEFT JOIN `demo2` `__collection_demo2` LEFT JOIN `users` `__data_users_rel_many` ON [[__data_users_rel_many.id]] IN ({:p0}, {:p1}) WHERE (((COALESCE([[demo1_rel_many_rel.active]], '') IS NOT COALESCE([[demo1_rel_many.name]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many.name]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE ((NOT (COALESCE([[__mlTEST.multiMatchValue]], '') IS NOT COALESCE([[__mrTEST.multiMatchValue]], ''))) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL))))) AND COALESCE([[demo1_rel_many_rel.active]], '') = COALESCE([[demo1_rel_many.name]], '') AND (([[demo1_rel_many_rel.title]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.title]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND ((COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo2.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `demo2` `__mm__collection_demo2` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))) AND COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '') AND (((([[demo1_rel_many.email]] > [[__data_users_rel_many.email]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__data_mm_users_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users_rel_many` ON [[__data_mm_users_rel_many.id]] IN ({:p2}, {:p3}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE ((NOT ([[__mlTEST.multiMatchValue]] > [[__mrTEST.multiMatchValue]])) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL)))))) AND ([[demo1_rel_many.emailVisibility]] = TRUE)))", }, { - "@request.data.arrayable:length fields", + "@request.body.arrayable:length fields", "demo1", - "@request.data.select_one:length > 1 &&" + - "@request.data.select_one:length ?> 2 &&" + - "@request.data.select_many:length < 3 &&" + - "@request.data.select_many:length ?> 4 &&" + - "@request.data.rel_one:length = 5 &&" + - "@request.data.rel_one:length ?= 6 &&" + - "@request.data.rel_many:length != 7 &&" + - "@request.data.rel_many:length ?!= 8 &&" + - "@request.data.file_one:length = 9 &&" + - "@request.data.file_one:length ?= 0 &&" + - "@request.data.file_many:length != 1 &&" + - "@request.data.file_many:length ?!= 2", + "@request.body.select_one:length > 1 &&" + + "@request.body.select_one:length ?> 2 &&" + + "@request.body.select_many:length < 3 &&" + + "@request.body.select_many:length ?> 4 &&" + + "@request.body.rel_one:length = 5 &&" + + "@request.body.rel_one:length ?= 6 &&" + + "@request.body.rel_many:length != 7 &&" + + "@request.body.rel_many:length ?!= 8 &&" + + "@request.body.file_one:length = 9 &&" + + "@request.body.file_one:length ?= 0 &&" + + "@request.body.file_many:length != 1 &&" + + "@request.body.file_many:length ?!= 2", false, "SELECT `demo1`.* FROM `demo1` WHERE (0 > {:TEST} AND 0 > {:TEST} AND 2 < {:TEST} AND 2 > {:TEST} AND 1 = {:TEST} AND 1 = {:TEST} AND 2 IS NOT {:TEST} AND 2 IS NOT {:TEST} AND 1 = {:TEST} AND 1 = {:TEST} AND 3 IS NOT {:TEST} AND 3 IS NOT {:TEST})", }, { "regular arrayable:length fields", "demo4", - "@request.data.self_rel_one.self_rel_many:length > 1 &&" + - "@request.data.self_rel_one.self_rel_many:length ?> 2 &&" + - "@request.data.rel_many_cascade.files:length ?< 3 &&" + - "@request.data.rel_many_cascade.files:length < 4 &&" + - "@request.data.rel_one_cascade.files:length < 4.1 &&" + // to ensure that the join to the same as above table will be aliased + "@request.body.self_rel_one.self_rel_many:length > 1 &&" + + "@request.body.self_rel_one.self_rel_many:length ?> 2 &&" + + "@request.body.rel_many_cascade.files:length ?< 3 &&" + + "@request.body.rel_many_cascade.files:length < 4 &&" + + "@request.body.rel_one_cascade.files:length < 4.1 &&" + // to ensure that the join to the same as above table will be aliased "self_rel_one.self_rel_many:length = 5 &&" + "self_rel_one.self_rel_many:length ?= 6 &&" + "self_rel_one.rel_many_cascade.files:length != 7 &&" + @@ -386,14 +414,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - collection, err := app.Dao().FindCollectionByNameOrId(s.collectionIdOrName) + collection, err := app.FindCollectionByNameOrId(s.collectionIdOrName) if err != nil { t.Fatalf("[%s] Failed to load collection %s: %v", s.name, s.collectionIdOrName, err) } - query := app.Dao().RecordQuery(collection) + query := app.RecordQuery(collection) - r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, s.allowHiddenFields) + r := core.NewRecordFieldResolver(app, collection, requestInfo, s.allowHiddenFields) expr, err := search.FilterData(s.rule).BuildExpr(r) if err != nil { @@ -420,25 +448,25 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { } } -func TestRecordFieldResolverResolveSchemaFields(t *testing.T) { +func TestRecordFieldResolverResolveCollectionFields(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - collection, err := app.Dao().FindCollectionByNameOrId("demo4") + collection, err := app.FindCollectionByNameOrId("demo4") if err != nil { t.Fatal(err) } - authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") + authRecord, err := app.FindRecordById("users", "4q1xlclmfloku33") if err != nil { t.Fatal(err) } - requestInfo := &models.RequestInfo{ - AuthRecord: authRecord, + requestInfo := &core.RequestInfo{ + Auth: authRecord, } - r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, true) + r := core.NewRecordFieldResolver(app, collection, requestInfo, true) scenarios := []struct { fieldName string @@ -529,35 +557,35 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - collection, err := app.Dao().FindCollectionByNameOrId("demo1") + collection, err := app.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") + authRecord, err := app.FindRecordById("users", "4q1xlclmfloku33") if err != nil { t.Fatal(err) } - requestInfo := &models.RequestInfo{ + requestInfo := &core.RequestInfo{ Context: "ctx", Method: "get", - Query: map[string]any{ - "a": 123, + Query: map[string]string{ + "a": "123", }, - Data: map[string]any{ + Body: map[string]any{ "number": "10", "number_unknown": "20", "b": 456, "c": map[string]int{"sub": 1}, }, - Headers: map[string]any{ + Headers: map[string]string{ "d": "789", }, - AuthRecord: authRecord, + Auth: authRecord, } - r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, true) + r := core.NewRecordFieldResolver(app, collection, requestInfo, true) scenarios := []struct { fieldName string @@ -571,34 +599,35 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { {"@request.context", false, `"ctx"`}, {"@request.method", false, `"get"`}, {"@request.query", true, ``}, - {"@request.query.a", false, `123`}, + {"@request.query.a", false, `"123"`}, {"@request.query.a.missing", false, ``}, {"@request.headers", true, ``}, {"@request.headers.missing", false, ``}, {"@request.headers.d", false, `"789"`}, - {"@request.headers.d.sub", true, ``}, - {"@request.data", true, ``}, - {"@request.data.b", false, `456`}, - {"@request.data.number", false, `10`}, // number field normalization - {"@request.data.number_unknown", false, `"20"`}, // no numeric normalizations for unknown fields - {"@request.data.b.missing", false, ``}, - {"@request.data.c", false, `"{\"sub\":1}"`}, + {"@request.headers.d.sub", false, ``}, + {"@request.body", true, ``}, + {"@request.body.b", false, `456`}, + {"@request.body.number", false, `10`}, // number field normalization + {"@request.body.number_unknown", false, `"20"`}, // no numeric normalizations for unknown fields + {"@request.body.b.missing", false, ``}, + {"@request.body.c", false, `"{\"sub\":1}"`}, {"@request.auth", true, ""}, {"@request.auth.id", false, `"4q1xlclmfloku33"`}, - {"@request.auth.username", false, `"users75657"`}, + {"@request.auth.collectionId", false, `"` + authRecord.Collection().Id + `"`}, + {"@request.auth.collectionName", false, `"` + authRecord.Collection().Name + `"`}, {"@request.auth.verified", false, `false`}, {"@request.auth.emailVisibility", false, `false`}, {"@request.auth.email", false, `"test@example.com"`}, // should always be returned no matter of the emailVisibility state {"@request.auth.missing", false, `NULL`}, } - for i, s := range scenarios { + for _, s := range scenarios { t.Run(s.fieldName, func(t *testing.T) { r, err := r.Resolve(s.fieldName) hasErr := err != nil if hasErr != s.expectError { - t.Fatalf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } if hasErr { @@ -609,7 +638,7 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { // --- if len(r.Params) == 0 { if r.Identifier != "NULL" { - t.Fatalf("(%d) Expected 0 placeholder parameters for %v, got %v", i, r.Identifier, r.Params) + t.Fatalf("Expected 0 placeholder parameters for %v, got %v", r.Identifier, r.Params) } return } @@ -617,7 +646,7 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { // existing key // --- if len(r.Params) != 1 { - t.Fatalf("(%d) Expected 1 placeholder parameter for %v, got %v", i, r.Identifier, r.Params) + t.Fatalf("Expected 1 placeholder parameter for %v, got %v", r.Identifier, r.Params) } var paramName string @@ -628,12 +657,12 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { } if r.Identifier != ("{:" + paramName + "}") { - t.Fatalf("(%d) Expected parameter r.Identifier %q, got %q", i, paramName, r.Identifier) + t.Fatalf("Expected parameter r.Identifier %q, got %q", paramName, r.Identifier) } encodedParamValue, _ := json.Marshal(paramValue) if string(encodedParamValue) != s.expectParamValue { - t.Fatalf("(%d) Expected r.Params %v for %v, got %v", i, s.expectParamValue, r.Identifier, string(encodedParamValue)) + t.Fatalf("Expected r.Params %#v for %s, got %#v", s.expectParamValue, r.Identifier, string(encodedParamValue)) } }) } @@ -642,7 +671,7 @@ func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) { if authRecord.EmailVisibility() { t.Fatal("Expected the original authRecord emailVisibility to remain unchanged") } - if v, ok := authRecord.PublicExport()[schema.FieldNameEmail]; ok { + if v, ok := authRecord.PublicExport()[core.FieldNameEmail]; ok { t.Fatalf("Expected the original authRecord email to not be exported, got %q", v) } } diff --git a/core/record_model.go b/core/record_model.go new file mode 100644 index 00000000..e06599bf --- /dev/null +++ b/core/record_model.go @@ -0,0 +1,1530 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "slices" + "sort" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/store" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +// used as a workaround by some fields for persisting local state between various events +// (for now is kept private and cannot be changed or cloned outside of the core package) +const internalCustomFieldKeyPrefix = "@pbInternal" + +var ( + _ Model = (*Record)(nil) + _ HookTagger = (*Record)(nil) + _ DBExporter = (*Record)(nil) + _ FilesManager = (*Record)(nil) +) + +type Record struct { + collection *Collection + originalData map[string]any + customVisibility *store.Store[bool] + data *store.Store[any] + expand *store.Store[any] + + BaseModel + + exportCustomData bool + ignoreEmailVisibility bool + ignoreUnchangedFields bool +} + +const systemHookIdRecord = "__pbRecordSystemHook__" + +func (app *BaseApp) registerRecordHooks() { + app.OnModelValidate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordValidate().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelCreate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordCreate().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelCreateExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordCreateExecute().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterCreateSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordAfterCreateSuccess().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterCreateError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelErrorEvent) error { + if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok { + return me.App.OnRecordAfterCreateError().Trigger(re, func(re *RecordErrorEvent) error { + syncModelErrorEventWithRecordErrorEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelUpdate().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordUpdate().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelUpdateExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordUpdateExecute().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordAfterUpdateSuccess().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterUpdateError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelErrorEvent) error { + if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok { + return me.App.OnRecordAfterUpdateError().Trigger(re, func(re *RecordErrorEvent) error { + syncModelErrorEventWithRecordErrorEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelDelete().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordDelete().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelDeleteExecute().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordDeleteExecute().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelEvent) error { + if re, ok := newRecordEventFromModelEvent(me); ok { + return me.App.OnRecordAfterDeleteSuccess().Trigger(re, func(re *RecordEvent) error { + syncModelEventWithRecordEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + app.OnModelAfterDeleteError().Bind(&hook.Handler[*ModelErrorEvent]{ + Id: systemHookIdRecord, + Func: func(me *ModelErrorEvent) error { + if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok { + return me.App.OnRecordAfterDeleteError().Trigger(re, func(re *RecordErrorEvent) error { + syncModelErrorEventWithRecordErrorEvent(me, re) + return me.Next() + }) + } + + return me.Next() + }, + Priority: -99, + }) + + // --------------------------------------------------------------- + + app.OnRecordValidate().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionValidate, + func() error { + return onRecordValidate(e) + }, + ) + }, + Priority: 99, + }) + + app.OnRecordCreate().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionCreate, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordCreateExecute().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionCreateExecute, + func() error { + return onRecordSaveExecute(e) + }, + ) + }, + Priority: 99, + }) + + app.OnRecordAfterCreateSuccess().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterCreate, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordAfterCreateError().Bind(&hook.Handler[*RecordErrorEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordErrorEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterCreateError, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordUpdate().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionUpdate, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionUpdateExecute, + func() error { + return onRecordSaveExecute(e) + }, + ) + }, + Priority: 99, + }) + + app.OnRecordAfterUpdateSuccess().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterUpdate, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordAfterUpdateError().Bind(&hook.Handler[*RecordErrorEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordErrorEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterUpdateError, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordDelete().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionDelete, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordDeleteExecute().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionDeleteExecute, + func() error { + return onRecordDeleteExecute(e) + }, + ) + }, + Priority: 99, + }) + + app.OnRecordAfterDeleteSuccess().Bind(&hook.Handler[*RecordEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterDelete, + e.Next, + ) + }, + Priority: -99, + }) + + app.OnRecordAfterDeleteError().Bind(&hook.Handler[*RecordErrorEvent]{ + Id: systemHookIdRecord, + Func: func(e *RecordErrorEvent) error { + return e.Record.callFieldInterceptors( + e.Context, + e.App, + InterceptorActionAfterDeleteError, + e.Next, + ) + }, + Priority: -99, + }) +} + +// ------------------------------------------------------------------- + +// newRecordFromNullStringMap initializes a single new Record model +// with data loaded from the provided NullStringMap. +// +// Note that this method is intended to load and Scan data from a database row result. +func newRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) (*Record, error) { + record := NewRecord(collection) + + var fieldName string + for _, field := range collection.Fields { + fieldName = field.GetName() + + nullString, ok := data[fieldName] + + var value any + var err error + + if ok && nullString.Valid { + value, err = field.PrepareValue(record, nullString.String) + } else { + value, err = field.PrepareValue(record, nil) + } + + if err != nil { + return nil, err + } + + // we load only the original data to avoid unnecessary copying the same data into the record.data store + // (it is also the reason why we don't invoke PostScan on the record itself) + record.originalData[fieldName] = value + + if fieldName == FieldNameId { + record.Id = cast.ToString(value) + } + } + + record.BaseModel.PostScan() + + return record, nil +} + +// newRecordsFromNullStringMaps initializes a new Record model for +// each row in the provided NullStringMap slice. +// +// Note that this method is intended to load and Scan data from a database rows result. +func newRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) ([]*Record, error) { + result := make([]*Record, len(rows)) + + var err error + for i, row := range rows { + result[i], err = newRecordFromNullStringMap(collection, row) + if err != nil { + return nil, err + } + } + + return result, nil +} + +// ------------------------------------------------------------------- + +// NewRecord initializes a new empty Record model. +func NewRecord(collection *Collection) *Record { + record := &Record{ + collection: collection, + data: store.New[any](nil), + customVisibility: store.New[bool](nil), + originalData: make(map[string]any, len(collection.Fields)), + } + + // initialize default field values + for _, field := range collection.Fields { + if field.GetName() == FieldNameId { + continue + } + value, _ := field.PrepareValue(record, nil) + record.originalData[field.GetName()] = value + } + + return record +} + +// Collection returns the Collection model associated with the current Record model. +// +// NB! The returned collection is only for read purposes and it shouldn't be modified +// because it could have unintended side-effects on other Record models from the same collection. +func (m *Record) Collection() *Collection { + return m.collection +} + +// TableName returns the table name associated with the current Record model. +func (m *Record) TableName() string { + return m.collection.Name +} + +// PostScan implements the [dbx.PostScanner] interface. +// +// It essentially refreshes/updates the current Record original state +// as if the model was fetched from the databases for the first time. +// +// Or in other words, it means that m.Original().FieldsData() will have +// the same values as m.Record().FieldsData(). +func (m *Record) PostScan() error { + if m.Id == "" { + return errors.New("missing record primary key") + } + + if err := m.BaseModel.PostScan(); err != nil { + return err + } + + m.originalData = m.FieldsData() + + return nil +} + +// HookTags returns the hook tags associated with the current record. +func (m *Record) HookTags() []string { + return []string{m.collection.Name, m.collection.Id} +} + +// BaseFilesPath returns the storage dir path used by the record. +func (m *Record) BaseFilesPath() string { + id := cast.ToString(m.LastSavedPK()) + if id == "" { + id = m.Id + } + + return m.collection.BaseFilesPath() + "/" + id +} + +// Original returns a shallow copy of the current record model populated +// with its ORIGINAL db data state (aka. right after PostScan()) +// and everything else reset to the defaults. +// +// If record was created using NewRecord() the original will be always +// a blank record (until PostScan() is invoked). +func (m *Record) Original() *Record { + newRecord := NewRecord(m.collection) + + newRecord.originalData = maps.Clone(m.originalData) + + if newRecord.originalData[FieldNameId] != nil { + newRecord.lastSavedPK = cast.ToString(newRecord.originalData[FieldNameId]) + newRecord.Id = newRecord.lastSavedPK + } + + return newRecord +} + +// Fresh returns a shallow copy of the current record model populated +// with its LATEST data state and everything else reset to the defaults +// (aka. no expand, no unknown fields and with default visibility flags). +func (m *Record) Fresh() *Record { + newRecord := m.Original() + + // note: this will also load the Id field though m.Get + newRecord.Load(m.FieldsData()) + + return newRecord +} + +// Clone returns a shallow copy of the current record model with all of +// its collection and unknown fields data, expand and flags copied. +// +// use [Record.Fresh()] instead if you want a copy with only the latest +// collection fields data and everything else reset to the defaults. +func (m *Record) Clone() *Record { + newRecord := m.Original() + + newRecord.Id = m.Id + newRecord.exportCustomData = m.exportCustomData + newRecord.ignoreEmailVisibility = m.ignoreEmailVisibility + newRecord.ignoreUnchangedFields = m.ignoreUnchangedFields + newRecord.customVisibility.Reset(m.customVisibility.GetAll()) + + newRecord.Load(m.data.GetAll()) + + if m.expand != nil { + newRecord.SetExpand(m.expand.GetAll()) + } + + return newRecord +} + +// Expand returns a shallow copy of the current Record model expand data (if any). +func (m *Record) Expand() map[string]any { + if m.expand == nil { + // return a dummy initialized map to avoid assignment to nil map errors + return map[string]any{} + } + + return m.expand.GetAll() +} + +// SetExpand replaces the current Record's expand with the provided expand arg data (shallow copied). +func (m *Record) SetExpand(expand map[string]any) { + if m.expand == nil { + m.expand = store.New[any](nil) + } + + m.expand.Reset(expand) +} + +// MergeExpand merges recursively the provided expand data into +// the current model's expand (if any). +// +// Note that if an expanded prop with the same key is a slice (old or new expand) +// then both old and new records will be merged into a new slice (aka. a :merge: [b,c] => [a,b,c]). +// Otherwise the "old" expanded record will be replace with the "new" one (aka. a :merge: aNew => aNew). +func (m *Record) MergeExpand(expand map[string]any) { + // nothing to merge + if len(expand) == 0 { + return + } + + // no old expand + if m.expand == nil { + m.expand = store.New(expand) + return + } + + oldExpand := m.expand.GetAll() + + for key, new := range expand { + old, ok := oldExpand[key] + if !ok { + oldExpand[key] = new + continue + } + + var wasOldSlice bool + var oldSlice []*Record + switch v := old.(type) { + case *Record: + oldSlice = []*Record{v} + case []*Record: + wasOldSlice = true + oldSlice = v + default: + // invalid old expand data -> assign directly the new + // (no matter whether new is valid or not) + oldExpand[key] = new + continue + } + + var wasNewSlice bool + var newSlice []*Record + switch v := new.(type) { + case *Record: + newSlice = []*Record{v} + case []*Record: + wasNewSlice = true + newSlice = v + default: + // invalid new expand data -> skip + continue + } + + oldIndexed := make(map[string]*Record, len(oldSlice)) + for _, oldRecord := range oldSlice { + oldIndexed[oldRecord.Id] = oldRecord + } + + for _, newRecord := range newSlice { + oldRecord := oldIndexed[newRecord.Id] + if oldRecord != nil { + // note: there is no need to update oldSlice since oldRecord is a reference + oldRecord.MergeExpand(newRecord.Expand()) + } else { + // missing new entry + oldSlice = append(oldSlice, newRecord) + } + } + + if wasOldSlice || wasNewSlice || len(oldSlice) == 0 { + oldExpand[key] = oldSlice + } else { + oldExpand[key] = oldSlice[0] + } + } + + m.expand.Reset(oldExpand) +} + +// FieldsData returns a shallow copy ONLY of the collection's fields record's data. +func (m *Record) FieldsData() map[string]any { + result := make(map[string]any, len(m.collection.Fields)) + + for _, field := range m.collection.Fields { + result[field.GetName()] = m.Get(field.GetName()) + } + + return result +} + +// CustomData returns a shallow copy ONLY of the custom record fields data, +// aka. fields that are neither defined by the collection, nor special system ones. +// +// Note that custom fields prefixed with "@pbInternal" are always skipped. +func (m *Record) CustomData() map[string]any { + if m.data == nil { + return nil + } + + fields := m.Collection().Fields + + knownFields := make(map[string]struct{}, len(fields)) + + for _, f := range fields { + knownFields[f.GetName()] = struct{}{} + } + + result := map[string]any{} + + rawData := m.data.GetAll() + for k, v := range rawData { + if _, ok := knownFields[k]; !ok { + // skip internal custom fields + if strings.HasPrefix(k, internalCustomFieldKeyPrefix) { + continue + } + + result[k] = v + } + } + + return result +} + +// WithCustomData toggles the export/serialization of custom data fields +// (false by default). +func (m *Record) WithCustomData(state bool) *Record { + m.exportCustomData = state + return m +} + +// IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check. +func (m *Record) IgnoreEmailVisibility(state bool) *Record { + m.ignoreEmailVisibility = state + return m +} + +// IgnoreUnchangedFields toggles the flag to ignore the unchanged fields +// from the DB export for the UPDATE SQL query. +// +// This could be used if you want to save only the record fields that you've changed +// without overwrite other untouched fields in case of concurrent update. +func (m *Record) IgnoreUnchangedFields(state bool) *Record { + m.ignoreUnchangedFields = state + return m +} + +// Set sets the provided key-value data pair into the current Record +// model directly as it is WITHOUT NORMALIZATIONS. +// +// See also [Record.Set]. +func (m *Record) SetRaw(key string, value any) { + if key == FieldNameId { + m.Id = cast.ToString(value) + } + + m.data.Set(key, value) +} + +// SetIfFieldExists sets the provided key-value data pair into the current Record model +// ONLY if key is existing Collection field name/modifier. +// +// This method does nothing if key is not a known Collection field name/modifier. +// +// On success returns the matched Field, otherwise - nil. +// +// To set any key-value, including custom/unknown fields, use the [Record.Set] method. +func (m *Record) SetIfFieldExists(key string, value any) Field { + for _, field := range m.Collection().Fields { + ff, ok := field.(SetterFinder) + if ok { + setter := ff.FindSetter(key) + if setter != nil { + setter(m, value) + return field + } + } + + // fallback to the default field PrepareValue method for direct match + if key == field.GetName() { + value, _ = field.PrepareValue(m, value) + m.SetRaw(key, value) + return field + } + } + + return nil +} + +// Set sets the provided key-value data pair into the current Record model. +// +// If the record collection has field with name matching the provided "key", +// the value will be further normalized according to the field setter(s). +func (m *Record) Set(key string, value any) { + switch key { + case FieldNameExpand: // for backward-compatibility with earlier versions + m.SetExpand(cast.ToStringMap(value)) + default: + field := m.SetIfFieldExists(key, value) + if field == nil { + // custom key - set it without any transformations + m.SetRaw(key, value) + } + } +} + +func (m *Record) GetRaw(key string) any { + if key == FieldNameId { + return m.Id + } + + if v, ok := m.data.GetOk(key); ok { + return v + } + + return m.originalData[key] +} + +// Get returns a normalized single record model data value for "key". +func (m *Record) Get(key string) any { + switch key { + case FieldNameExpand: // for backward-compatibility with earlier versions + return m.Expand() + default: + for _, field := range m.Collection().Fields { + gm, ok := field.(GetterFinder) + if !ok { + continue // no custom getters + } + + getter := gm.FindGetter(key) + if getter != nil { + return getter(m) + } + } + + return m.GetRaw(key) + } +} + +// Load bulk loads the provided data into the current Record model. +func (m *Record) Load(data map[string]any) { + for k, v := range data { + m.Set(k, v) + } +} + +// GetBool returns the data value for "key" as a bool. +func (m *Record) GetBool(key string) bool { + return cast.ToBool(m.Get(key)) +} + +// GetString returns the data value for "key" as a string. +func (m *Record) GetString(key string) string { + return cast.ToString(m.Get(key)) +} + +// GetInt returns the data value for "key" as an int. +func (m *Record) GetInt(key string) int { + return cast.ToInt(m.Get(key)) +} + +// GetFloat returns the data value for "key" as a float64. +func (m *Record) GetFloat(key string) float64 { + return cast.ToFloat64(m.Get(key)) +} + +// GetDateTime returns the data value for "key" as a DateTime instance. +func (m *Record) GetDateTime(key string) types.DateTime { + d, _ := types.ParseDateTime(m.Get(key)) + return d +} + +// GetStringSlice returns the data value for "key" as a slice of non-zero unique strings. +func (m *Record) GetStringSlice(key string) []string { + return list.ToUniqueStringSlice(m.Get(key)) +} + +// GetUploadedFiles returns the uploaded files for the provided "file" field key, +// (aka. the current [*filesytem.File] values) so that you can apply further +// validations or modifications (including changing the file name or content before persisting). +// +// Example: +// +// files := record.GetUploadedFiles("documents") +// for _, f := range files { +// f.Name = "doc_" + f.Name // add a prefix to each file name +// } +// app.Save(record) // the files are pointers so the applied changes will transparently reflect on the record value +func (m *Record) GetUploadedFiles(key string) []*filesystem.File { + if !strings.HasSuffix(key, ":uploaded") { + key += ":uploaded" + } + + values, _ := m.Get(key).([]*filesystem.File) + + return values +} + +// Retrieves the "key" json field value and unmarshals it into "result". +// +// Example +// +// result := struct { +// FirstName string `json:"first_name"` +// }{} +// err := m.UnmarshalJSONField("my_field_name", &result) +func (m *Record) UnmarshalJSONField(key string, result any) error { + return json.Unmarshal([]byte(m.GetString(key)), &result) +} + +// ExpandedOne retrieves a single relation Record from the already +// loaded expand data of the current model. +// +// If the requested expand relation is multiple, this method returns +// only first available Record from the expanded relation. +// +// Returns nil if there is no such expand relation loaded. +func (m *Record) ExpandedOne(relField string) *Record { + if m.expand == nil { + return nil + } + + rel := m.expand.Get(relField) + + switch v := rel.(type) { + case *Record: + return v + case []*Record: + if len(v) > 0 { + return v[0] + } + } + + return nil +} + +// ExpandedAll retrieves a slice of relation Records from the already +// loaded expand data of the current model. +// +// If the requested expand relation is single, this method normalizes +// the return result and will wrap the single model as a slice. +// +// Returns nil slice if there is no such expand relation loaded. +func (m *Record) ExpandedAll(relField string) []*Record { + if m.expand == nil { + return nil + } + + rel := m.expand.Get(relField) + + switch v := rel.(type) { + case *Record: + return []*Record{v} + case []*Record: + return v + } + + return nil +} + +// FindFileFieldByFile returns the first file type field for which +// any of the record's data contains the provided filename. +func (m *Record) FindFileFieldByFile(filename string) *FileField { + for _, field := range m.Collection().Fields { + if field.Type() != FieldTypeFile { + continue + } + + f, ok := field.(*FileField) + if !ok { + continue + } + + filenames := m.GetStringSlice(f.GetName()) + if slices.Contains(filenames, filename) { + return f + } + } + + return nil +} + +// DBExport implements the [DBExporter] interface and returns a key-value +// map with the data to be persisted when saving the Record in the database. +func (m *Record) DBExport(app App) (map[string]any, error) { + result, err := m.dbExport() + if err != nil { + return nil, err + } + + // remove exported fields that haven't changed + // (with exception of the id column) + if !m.IsNew() && m.ignoreUnchangedFields { + oldResult, err := m.Original().dbExport() + if err != nil { + return nil, err + } + + for oldK, oldV := range oldResult { + if oldK == idColumn { + continue + } + newV, ok := result[oldK] + if ok && areValuesEqual(newV, oldV) { + delete(result, oldK) + } + } + } + + return result, nil +} + +func (m *Record) dbExport() (map[string]any, error) { + fields := m.Collection().Fields + + result := make(map[string]any, len(fields)) + + for _, field := range fields { + if f, ok := field.(DriverValuer); ok { + v, err := f.DriverValue(m) + if err != nil { + return nil, err + } + result[field.GetName()] = v + } else { + result[field.GetName()] = m.GetRaw(field.GetName()) + } + } + + return result, nil +} + +func areValuesEqual(a any, b any) bool { + switch av := a.(type) { + case string: + bv, ok := b.(string) + return ok && bv == av + case bool: + bv, ok := b.(bool) + return ok && bv == av + case float32: + bv, ok := b.(float32) + return ok && bv == av + case float64: + bv, ok := b.(float64) + return ok && bv == av + case uint: + bv, ok := b.(uint) + return ok && bv == av + case uint8: + bv, ok := b.(uint8) + return ok && bv == av + case uint16: + bv, ok := b.(uint16) + return ok && bv == av + case uint32: + bv, ok := b.(uint32) + return ok && bv == av + case uint64: + bv, ok := b.(uint64) + return ok && bv == av + case int: + bv, ok := b.(int) + return ok && bv == av + case int8: + bv, ok := b.(int8) + return ok && bv == av + case int16: + bv, ok := b.(int16) + return ok && bv == av + case int32: + bv, ok := b.(int32) + return ok && bv == av + case int64: + bv, ok := b.(int64) + return ok && bv == av + case []byte: + bv, ok := b.([]byte) + return ok && bytes.Equal(av, bv) + case []string: + bv, ok := b.([]string) + return ok && slices.Equal(av, bv) + case []int: + bv, ok := b.([]int) + return ok && slices.Equal(av, bv) + case []int32: + bv, ok := b.([]int32) + return ok && slices.Equal(av, bv) + case []int64: + bv, ok := b.([]int64) + return ok && slices.Equal(av, bv) + case []float32: + bv, ok := b.([]float32) + return ok && slices.Equal(av, bv) + case []float64: + bv, ok := b.([]float64) + return ok && slices.Equal(av, bv) + case types.JSONArray[string]: + bv, ok := b.(types.JSONArray[string]) + return ok && slices.Equal(av, bv) + case types.JSONRaw: + bv, ok := b.(types.JSONRaw) + return ok && bytes.Equal(av, bv) + default: + aRaw, err := json.Marshal(a) + if err != nil { + return false + } + + bRaw, err := json.Marshal(b) + if err != nil { + return false + } + + return bytes.Equal(aRaw, bRaw) + } +} + +// Hide hides the specified fields from the public safe serialization of the record. +func (record *Record) Hide(fieldNames ...string) *Record { + for _, name := range fieldNames { + record.customVisibility.Set(name, false) + } + + return record +} + +// Unhide forces to unhide the specified fields from the public safe serialization +// of the record (even when the collection field itself is marked as hidden). +func (record *Record) Unhide(fieldNames ...string) *Record { + for _, name := range fieldNames { + record.customVisibility.Set(name, true) + } + + return record +} + +// PublicExport exports only the record fields that are safe to be public. +// +// To export unknown data fields you need to set record.WithCustomData(true). +// +// For auth records, to force the export of the email field you need to set +// record.IgnoreEmailVisibility(true). +func (record *Record) PublicExport() map[string]any { + export := make(map[string]any, len(record.collection.Fields)+3) + + var isVisible, hasCustomVisibility bool + + customVisibility := record.customVisibility.GetAll() + + // export schema fields + for _, f := range record.collection.Fields { + isVisible, hasCustomVisibility = customVisibility[f.GetName()] + if !hasCustomVisibility { + isVisible = !f.GetHidden() + } + + if !isVisible { + continue + } + + export[f.GetName()] = record.Get(f.GetName()) + } + + // export custom fields + if record.exportCustomData { + for k, v := range record.CustomData() { + isVisible, hasCustomVisibility = customVisibility[k] + if !hasCustomVisibility || isVisible { + export[k] = v + } + } + } + + if record.Collection().IsAuth() { + // always hide the password and tokenKey fields + delete(export, FieldNamePassword) + delete(export, FieldNameTokenKey) + + if !record.ignoreEmailVisibility && !record.GetBool(FieldNameEmailVisibility) { + delete(export, FieldNameEmail) + } + } + + // add helper collection reference fields + isVisible, hasCustomVisibility = customVisibility[FieldNameCollectionId] + if !hasCustomVisibility || isVisible { + export[FieldNameCollectionId] = record.collection.Id + } + isVisible, hasCustomVisibility = customVisibility[FieldNameCollectionName] + if !hasCustomVisibility || isVisible { + export[FieldNameCollectionName] = record.collection.Name + } + + // add expand (if non-nil) + isVisible, hasCustomVisibility = customVisibility[FieldNameExpand] + if (!hasCustomVisibility || isVisible) && record.expand != nil { + export[FieldNameExpand] = record.expand.GetAll() + } + + return export +} + +// MarshalJSON implements the [json.Marshaler] interface. +// +// Only the data exported by `PublicExport()` will be serialized. +func (m Record) MarshalJSON() ([]byte, error) { + return json.Marshal(m.PublicExport()) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (m *Record) UnmarshalJSON(data []byte) error { + result := map[string]any{} + + if err := json.Unmarshal(data, &result); err != nil { + return err + } + + m.Load(result) + + return nil +} + +// ReplaceModifiers returns a new map with applied modifier +// values based on the current record and the specified data. +// +// The resolved modifier keys will be removed. +// +// Multiple modifiers will be applied one after another, +// while reusing the previous base key value result (ex. 1; -5; +2 => -2). +// +// Note that because Go doesn't guaranteed the iteration order of maps, +// we would explicitly apply shorter keys first for a more consistent and reproducible behavior. +// +// Example usage: +// +// newData := record.ReplaceModifiers(data) +// // record: {"field": 10} +// // data: {"field+": 5} +// // result: {"field": 15} +func (m *Record) ReplaceModifiers(data map[string]any) map[string]any { + if len(data) == 0 { + return data + } + + dataCopy := maps.Clone(data) + + recordCopy := m.Fresh() + + // key orders is not guaranteed so + sortedDataKeys := make([]string, 0, len(data)) + for k := range data { + sortedDataKeys = append(sortedDataKeys, k) + } + sort.SliceStable(sortedDataKeys, func(i int, j int) bool { + return len(sortedDataKeys[i]) < len(sortedDataKeys[j]) + }) + + for _, k := range sortedDataKeys { + field := recordCopy.SetIfFieldExists(k, data[k]) + if field != nil { + // delete the original key in case it is with a modifer (ex. "items+") + delete(dataCopy, k) + + // store the transformed value under the field name + dataCopy[field.GetName()] = recordCopy.Get(field.GetName()) + } + } + + return dataCopy +} + +// ------------------------------------------------------------------- + +func (m *Record) callFieldInterceptors( + ctx context.Context, + app App, + actionName string, + actionFunc func() error, +) error { + // the firing order of the fields doesn't matter + for _, field := range m.Collection().Fields { + if f, ok := field.(RecordInterceptor); ok { + oldfn := actionFunc + actionFunc = func() error { + return f.Intercept(ctx, app, m, actionName, oldfn) + } + } + } + + return actionFunc() +} + +func onRecordValidate(e *RecordEvent) error { + errs := validation.Errors{} + + for _, f := range e.Record.Collection().Fields { + if err := f.ValidateValue(e.Context, e.App, e.Record); err != nil { + errs[f.GetName()] = err + } + } + + if len(errs) > 0 { + return errs + } + + return e.Next() +} + +func onRecordSaveExecute(e *RecordEvent) error { + if e.Record.Collection().IsAuth() { + // ensure that the token key is different on password change + old := e.Record.Original() + if !e.Record.IsNew() && + old.TokenKey() == e.Record.TokenKey() && + old.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) { + e.Record.RefreshTokenKey() + } + + // cross-check that the auth record id is unique across all auth collections. + authCollections, err := e.App.FindAllCollections(CollectionTypeAuth) + if err != nil { + return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err) + } + for _, collection := range authCollections { + if e.Record.Collection().Id == collection.Id { + continue // skip current collection (sqlite will do the check for us) + } + record, _ := e.App.FindRecordById(collection, e.Record.Id) + if record != nil { + return validation.Errors{ + FieldNameId: validation.NewError("validation_invalid_auth_id", "Invalid or duplicated auth record id."), + } + } + } + } + + err := e.Next() + if err == nil { + return nil + } + + return validators.NormalizeUniqueIndexError( + err, + e.Record.Collection().Name, + e.Record.Collection().Fields.FieldNames(), + ) +} + +func onRecordDeleteExecute(e *RecordEvent) error { + // fetch rel references (if any) + // + // note: the select is outside of the transaction to minimize + // SQLITE_BUSY errors when mixing read&write in a single transaction + refs, err := e.App.FindCollectionReferences(e.Record.Collection()) + if err != nil { + return err + } + + originalApp := e.App + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + // delete the record before the relation references to ensure that there + // will be no "A<->B" relations to prevent deadlock when calling DeleteRecord recursively + if err := e.Next(); err != nil { + return err + } + + return cascadeRecordDelete(txApp, e.Record, refs) + }) + e.App = originalApp + + return txErr +} + +// cascadeRecordDelete triggers cascade deletion for the provided references. +// +// NB! This method is expected to be called from inside of a transaction. +func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Field) error { + // Sort the refs keys to ensure that the cascade events firing order is always the same. + // This is not necessary for the operation to function correctly but it helps having deterministic output during testing. + sortedRefKeys := make([]*Collection, 0, len(refs)) + for k := range refs { + sortedRefKeys = append(sortedRefKeys, k) + } + sort.Slice(sortedRefKeys, func(i, j int) bool { + return sortedRefKeys[i].Name < sortedRefKeys[j].Name + }) + + for _, refCollection := range sortedRefKeys { + fields, ok := refs[refCollection] + + if refCollection.IsView() || !ok { + continue // skip missing or view collections + } + + for _, field := range fields { + recordTableName := inflector.Columnify(refCollection.Name) + prefixedFieldName := recordTableName + "." + inflector.Columnify(field.GetName()) + + query := app.RecordQuery(refCollection) + + if opt, ok := field.(MultiValuer); !ok || !opt.IsMultiple() { + query.AndWhere(dbx.HashExp{prefixedFieldName: mainRecord.Id}) + } else { + query.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf( + `SELECT 1 FROM json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) {{__je__}} WHERE [[__je__.value]]={:jevalue}`, + prefixedFieldName, prefixedFieldName, prefixedFieldName, + ), dbx.Params{ + "jevalue": mainRecord.Id, + }))) + } + + if refCollection.Id == mainRecord.Collection().Id { + query.AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id})) + } + + // trigger cascade for each batchSize rel items until there is none + batchSize := 4000 + rows := make([]*Record, 0, batchSize) + for { + if err := query.Limit(int64(batchSize)).All(&rows); err != nil { + return err + } + + total := len(rows) + if total == 0 { + break + } + + err := deleteRefRecords(app, mainRecord, rows, field) + if err != nil { + return err + } + + if total < batchSize { + break // no more items + } + + rows = rows[:0] // keep allocated memory + } + } + } + + return nil +} + +// deleteRefRecords checks if related records has to be deleted (if `CascadeDelete` is set) +// OR +// just unset the record id from any relation field values (if they are not required). +// +// NB! This method is expected to be called from inside of a transaction. +func deleteRefRecords(app App, mainRecord *Record, refRecords []*Record, field Field) error { + relField, _ := field.(*RelationField) + if relField == nil { + return errors.New("only RelationField is supported at the moment, got " + field.Type()) + } + + for _, refRecord := range refRecords { + ids := refRecord.GetStringSlice(relField.Name) + + // unset the record id + for i := len(ids) - 1; i >= 0; i-- { + if ids[i] == mainRecord.Id { + ids = append(ids[:i], ids[i+1:]...) + break + } + } + + // cascade delete the reference + // (only if there are no other active references in case of multiple select) + if relField.CascadeDelete && len(ids) == 0 { + if err := app.Delete(refRecord); err != nil { + return err + } + // no further actions are needed (the reference is deleted) + continue + } + + if relField.Required && len(ids) == 0 { + return fmt.Errorf("the record cannot be deleted because it is part of a required reference in record %s (%s collection)", refRecord.Id, refRecord.Collection().Name) + } + + // save the reference changes + // (without validation because it is possible that another relation field to have a reference to a previous deleted record) + refRecord.Set(relField.Name, ids) + if err := app.SaveNoValidate(refRecord); err != nil { + return err + } + } + + return nil +} diff --git a/core/record_model_auth.go b/core/record_model_auth.go new file mode 100644 index 00000000..00fae0f8 --- /dev/null +++ b/core/record_model_auth.go @@ -0,0 +1,64 @@ +package core + +// Email returns the "email" record field value (usually available with Auth collections). +func (m *Record) Email() string { + return m.GetString(FieldNameEmail) +} + +// SetEmail sets the "email" record field value (usually available with Auth collections). +func (m *Record) SetEmail(email string) { + m.Set(FieldNameEmail, email) +} + +// Verified returns the "emailVisibility" record field value (usually available with Auth collections). +func (m *Record) EmailVisibility() bool { + return m.GetBool(FieldNameEmailVisibility) +} + +// SetEmailVisibility sets the "emailVisibility" record field value (usually available with Auth collections). +func (m *Record) SetEmailVisibility(visible bool) { + m.Set(FieldNameEmailVisibility, visible) +} + +// Verified returns the "verified" record field value (usually available with Auth collections). +func (m *Record) Verified() bool { + return m.GetBool(FieldNameVerified) +} + +// SetVerified sets the "verified" record field value (usually available with Auth collections). +func (m *Record) SetVerified(verified bool) { + m.Set(FieldNameVerified, verified) +} + +// TokenKey returns the "tokenKey" record field value (usually available with Auth collections). +func (m *Record) TokenKey() string { + return m.GetString(FieldNameTokenKey) +} + +// SetTokenKey sets the "tokenKey" record field value (usually available with Auth collections). +func (m *Record) SetTokenKey(key string) { + m.Set(FieldNameTokenKey, key) +} + +// RefreshTokenKey generates and sets a new random auth record "tokenKey". +func (m *Record) RefreshTokenKey() { + m.Set(FieldNameTokenKey+autogenerateModifier, "") +} + +// SetPassword sets the "password" record field value (usually available with Auth collections). +func (m *Record) SetPassword(password string) { + // note: the tokenKey will be auto changed if necessary before db write + m.Set(FieldNamePassword, password) +} + +// ValidatePassword validates a plain password against the "password" record field. +// +// Returns false if the password is incorrect. +func (m *Record) ValidatePassword(password string) bool { + pv, ok := m.GetRaw(FieldNamePassword).(*PasswordFieldValue) + if !ok { + return false + } + + return pv.Validate(password) +} diff --git a/core/record_model_auth_test.go b/core/record_model_auth_test.go new file mode 100644 index 00000000..da748f00 --- /dev/null +++ b/core/record_model_auth_test.go @@ -0,0 +1,119 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestRecordEmail(t *testing.T) { + record := core.NewRecord(core.NewAuthCollection("test")) + + if record.Email() != "" { + t.Fatalf("Expected email %q, got %q", "", record.Email()) + } + + email := "test@example.com" + record.SetEmail(email) + + if record.Email() != email { + t.Fatalf("Expected email %q, got %q", email, record.Email()) + } +} + +func TestRecordEmailVisibility(t *testing.T) { + record := core.NewRecord(core.NewAuthCollection("test")) + + if record.EmailVisibility() != false { + t.Fatalf("Expected emailVisibility %v, got %v", false, record.EmailVisibility()) + } + + record.SetEmailVisibility(true) + + if record.EmailVisibility() != true { + t.Fatalf("Expected emailVisibility %v, got %v", true, record.EmailVisibility()) + } +} + +func TestRecordVerified(t *testing.T) { + record := core.NewRecord(core.NewAuthCollection("test")) + + if record.Verified() != false { + t.Fatalf("Expected verified %v, got %v", false, record.Verified()) + } + + record.SetVerified(true) + + if record.Verified() != true { + t.Fatalf("Expected verified %v, got %v", true, record.Verified()) + } +} + +func TestRecordTokenKey(t *testing.T) { + record := core.NewRecord(core.NewAuthCollection("test")) + + if record.TokenKey() != "" { + t.Fatalf("Expected tokenKey %q, got %q", "", record.TokenKey()) + } + + tokenKey := "example" + + record.SetTokenKey(tokenKey) + + if record.TokenKey() != tokenKey { + t.Fatalf("Expected tokenKey %q, got %q", tokenKey, record.TokenKey()) + } + + record.RefreshTokenKey() + + if record.TokenKey() == tokenKey { + t.Fatalf("Expected tokenKey to be random generated, got %q", tokenKey) + } + + if len(record.TokenKey()) != 50 { + t.Fatalf("Expected %d characters, got %d", 50, len(record.TokenKey())) + } +} + +func TestRecordPassword(t *testing.T) { + scenarios := []struct { + name string + password string + expected bool + }{ + { + "empty password", + "", + false, + }, + { + "non-empty password", + "123456", + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + record := core.NewRecord(core.NewAuthCollection("test")) + + if record.ValidatePassword(s.password) { + t.Fatal("[before set] Expected password to be invalid") + } + + record.SetPassword(s.password) + + result := record.ValidatePassword(s.password) + + if result != s.expected { + t.Fatalf("[after set] Expected ValidatePassword %v, got %v", result, s.expected) + } + + // try with a random string to ensure that not any string validates + if record.ValidatePassword(security.PseudorandomString(5)) { + t.Fatal("[random] Expected password to be invalid") + } + }) + } +} diff --git a/core/record_model_superusers.go b/core/record_model_superusers.go new file mode 100644 index 00000000..256d6aee --- /dev/null +++ b/core/record_model_superusers.go @@ -0,0 +1,80 @@ +package core + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" +) + +const CollectionNameSuperusers = "_superusers" + +func (app *BaseApp) registerSuperuserHooks() { + app.OnRecordDelete(CollectionNameSuperusers).Bind(&hook.Handler[*RecordEvent]{ + Id: "pbSuperusersRecordDelete", + Func: func(e *RecordEvent) error { + originalApp := e.App + txErr := e.App.RunInTransaction(func(txApp App) error { + e.App = txApp + + total, err := e.App.CountRecords(CollectionNameSuperusers) + if err != nil { + return fmt.Errorf("failed to fetch total superusers count: %w", err) + } + + if total == 1 { + return router.NewBadRequestError("You can't delete the only existing superuser", nil) + } + + return e.Next() + }) + e.App = originalApp + + return txErr + }, + Priority: -99, + }) + + recordSaveHandler := &hook.Handler[*RecordEvent]{ + Id: "pbSuperusersRecordSaveExec", + Func: func(e *RecordEvent) error { + e.Record.SetVerified(true) // always mark superusers as verified + return e.Next() + }, + Priority: -99, + } + app.OnRecordCreateExecute(CollectionNameSuperusers).Bind(recordSaveHandler) + app.OnRecordUpdateExecute(CollectionNameSuperusers).Bind(recordSaveHandler) + + collectionSaveHandler := &hook.Handler[*CollectionEvent]{ + Id: "pbSuperusersCollectionSaveExec", + Func: func(e *CollectionEvent) error { + // don't allow name change even if executed with SaveNoValidate + e.Collection.Name = CollectionNameSuperusers + + // for now don't allow superusers OAuth2 since we don't want + // to accidentally create a new superuser by just OAuth2 signin + e.Collection.OAuth2.Enabled = false + e.Collection.OAuth2.Providers = nil + + // force password auth + e.Collection.PasswordAuth.Enabled = true + + // for superusers we don't allow for now standalone OTP auth and always require to be combined with MFA + if e.Collection.OTP.Enabled { + e.Collection.MFA.Enabled = true + } + + return e.Next() + }, + Priority: 99, + } + app.OnCollectionCreateExecute(CollectionNameSuperusers).Bind(collectionSaveHandler) + app.OnCollectionUpdateExecute(CollectionNameSuperusers).Bind(collectionSaveHandler) +} + +// IsSuperuser returns whether the current record is a superuser, aka. +// whether the record is from the _superusers collection. +func (m *Record) IsSuperuser() bool { + return m.Collection().Name == CollectionNameSuperusers +} diff --git a/core/record_model_superusers_test.go b/core/record_model_superusers_test.go new file mode 100644 index 00000000..db8a70a4 --- /dev/null +++ b/core/record_model_superusers_test.go @@ -0,0 +1,48 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordIsSuperUser(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + record *core.Record + expected bool + }{ + {demo1, false}, + {user, false}, + {superuser, true}, + } + + for _, s := range scenarios { + t.Run(s.record.Collection().Name, func(t *testing.T) { + result := s.record.IsSuperuser() + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} diff --git a/core/record_model_test.go b/core/record_model_test.go new file mode 100644 index 00000000..d19c7a43 --- /dev/null +++ b/core/record_model_test.go @@ -0,0 +1,2080 @@ +package core_test + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "regexp" + "slices" + "strings" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +func TestNewRecord(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add(&core.BoolField{Name: "status"}) + + m := core.NewRecord(collection) + + rawData, err := json.Marshal(m.FieldsData()) // should be initialized with the defaults + if err != nil { + t.Fatal(err) + } + + expected := `{"id":"","status":false}` + + if str := string(rawData); str != expected { + t.Fatalf("Expected schema data\n%v\ngot\n%v", expected, str) + } +} + +func TestRecordCollection(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + m := core.NewRecord(collection) + + if m.Collection().Name != collection.Name { + t.Fatalf("Expected collection with name %q, got %q", collection.Name, m.Collection().Name) + } +} + +func TestRecordTableName(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + m := core.NewRecord(collection) + + if m.TableName() != collection.Name { + t.Fatalf("Expected table %q, got %q", collection.Name, m.TableName()) + } +} + +func TestRecordPostScan(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test_collection") + collection.Fields.Add(&core.TextField{Name: "test"}) + + m := core.NewRecord(collection) + + // calling PostScan without id + err := m.PostScan() + if err == nil { + t.Fatal("Expected PostScan id error, got nil") + } + + m.Id = "test_id" + m.Set("test", "abc") + + if v := m.IsNew(); v != true { + t.Fatalf("[before PostScan] Expected IsNew %v, got %v", true, v) + } + if v := m.Original().PK(); v != "" { + t.Fatalf("[before PostScan] Expected the original PK to be empty string, got %v", v) + } + if v := m.Original().Get("test"); v != "" { + t.Fatalf("[before PostScan] Expected the original 'test' field to be empty string, got %v", v) + } + + err = m.PostScan() + if err != nil { + t.Fatalf("Expected PostScan nil error, got %v", err) + } + + if v := m.IsNew(); v != false { + t.Fatalf("[after PostScan] Expected IsNew %v, got %v", false, v) + } + if v := m.Original().PK(); v != "test_id" { + t.Fatalf("[after PostScan] Expected the original PK to be %q, got %v", "test_id", v) + } + if v := m.Original().Get("test"); v != "abc" { + t.Fatalf("[after PostScan] Expected the original 'test' field to be %q, got %v", "abc", v) + } +} + +func TestRecordHookTags(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + m := core.NewRecord(collection) + + tags := m.HookTags() + + expectedTags := []string{collection.Id, collection.Name} + + if len(tags) != len(expectedTags) { + t.Fatalf("Expected tags\n%v\ngot\n%v", expectedTags, tags) + } + + for _, tag := range tags { + if !slices.Contains(expectedTags, tag) { + t.Errorf("Missing expected tag %q", tag) + } + } +} + +func TestRecordBaseFilesPath(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + m := core.NewRecord(collection) + m.Id = "abc" + + result := m.BaseFilesPath() + expected := collection.BaseFilesPath() + "/" + m.Id + if result != expected { + t.Fatalf("Expected %q, got %q", expected, result) + } +} + +func TestRecordOriginal(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + originalId := record.Id + originalName := record.GetString("name") + + extraFieldsCheck := []string{`"email":`, `"custom":`} + + // change the fields + record.Id = "changed" + record.Set("name", "name_new") + record.Set("custom", "test_custom") + record.SetExpand(map[string]any{"test": 123}) + record.IgnoreEmailVisibility(true) + record.IgnoreUnchangedFields(true) + record.WithCustomData(true) + record.Unhide(record.Collection().Fields.FieldNames()...) + + // ensure that the email visibility and the custom data toggles are active + raw, err := record.MarshalJSON() + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + for _, f := range extraFieldsCheck { + if !strings.Contains(rawStr, f) { + t.Fatalf("Expected %s in\n%s", f, rawStr) + } + } + + // check changes + if v := record.GetString("name"); v != "name_new" { + t.Fatalf("Expected name to be %q, got %q", "name_new", v) + } + if v := record.GetString("custom"); v != "test_custom" { + t.Fatalf("Expected custom to be %q, got %q", "test_custom", v) + } + + // check original + if v := record.Original().PK(); v != originalId { + t.Fatalf("Expected the original PK to be %q, got %q", originalId, v) + } + if v := record.Original().Id; v != originalId { + t.Fatalf("Expected the original id to be %q, got %q", originalId, v) + } + if v := record.Original().GetString("name"); v != originalName { + t.Fatalf("Expected the original name to be %q, got %q", originalName, v) + } + if v := record.Original().GetString("custom"); v != "" { + t.Fatalf("Expected the original custom to be %q, got %q", "", v) + } + if v := record.Original().Expand(); len(v) != 0 { + t.Fatalf("Expected empty original expand, got\n%v", v) + } + + // ensure that the email visibility and the custom flag toggles weren't copied + originalRaw, err := record.Original().MarshalJSON() + if err != nil { + t.Fatal(err) + } + originalRawStr := string(originalRaw) + for _, f := range extraFieldsCheck { + if strings.Contains(originalRawStr, f) { + t.Fatalf("Didn't expected %s in original\n%s", f, originalRawStr) + } + } + + // loading new data shouldn't affect the original state + record.Load(map[string]any{"name": "name_new2"}) + + if v := record.GetString("name"); v != "name_new2" { + t.Fatalf("Expected name to be %q, got %q", "name_new2", v) + } + + if v := record.Original().GetString("name"); v != originalName { + t.Fatalf("Expected the original name still to be %q, got %q", originalName, v) + } +} + +func TestRecordFresh(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + originalId := record.Id + + extraFieldsCheck := []string{`"email":`, `"custom":`} + + // change the fields + record.Id = "changed" + record.Set("name", "name_new") + record.Set("custom", "test_custom") + record.SetExpand(map[string]any{"test": 123}) + record.IgnoreEmailVisibility(true) + record.IgnoreUnchangedFields(true) + record.WithCustomData(true) + record.Unhide(record.Collection().Fields.FieldNames()...) + + // ensure that the email visibility and the custom data toggles are active + raw, err := record.MarshalJSON() + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + for _, f := range extraFieldsCheck { + if !strings.Contains(rawStr, f) { + t.Fatalf("Expected %s in\n%s", f, rawStr) + } + } + + // check changes + if v := record.GetString("name"); v != "name_new" { + t.Fatalf("Expected name to be %q, got %q", "name_new", v) + } + if v := record.GetString("custom"); v != "test_custom" { + t.Fatalf("Expected custom to be %q, got %q", "test_custom", v) + } + + // check fresh + if v := record.Fresh().LastSavedPK(); v != originalId { + t.Fatalf("Expected the fresh LastSavedPK to be %q, got %q", originalId, v) + } + if v := record.Fresh().PK(); v != record.Id { + t.Fatalf("Expected the fresh PK to be %q, got %q", record.Id, v) + } + if v := record.Fresh().Id; v != record.Id { + t.Fatalf("Expected the fresh id to be %q, got %q", record.Id, v) + } + if v := record.Fresh().GetString("name"); v != record.GetString("name") { + t.Fatalf("Expected the fresh name to be %q, got %q", record.GetString("name"), v) + } + if v := record.Fresh().GetString("custom"); v != "" { + t.Fatalf("Expected the fresh custom to be %q, got %q", "", v) + } + if v := record.Fresh().Expand(); len(v) != 0 { + t.Fatalf("Expected empty fresh expand, got\n%v", v) + } + + // ensure that the email visibility and the custom flag toggles weren't copied + freshRaw, err := record.Fresh().MarshalJSON() + if err != nil { + t.Fatal(err) + } + freshRawStr := string(freshRaw) + for _, f := range extraFieldsCheck { + if strings.Contains(freshRawStr, f) { + t.Fatalf("Didn't expected %s in fresh\n%s", f, freshRawStr) + } + } +} + +func TestRecordClone(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + originalId := record.Id + + extraFieldsCheck := []string{`"email":`, `"custom":`} + + // change the fields + record.Id = "changed" + record.Set("name", "name_new") + record.Set("custom", "test_custom") + record.SetExpand(map[string]any{"test": 123}) + record.IgnoreEmailVisibility(true) + record.WithCustomData(true) + record.Unhide(record.Collection().Fields.FieldNames()...) + + // ensure that the email visibility and the custom data toggles are active + raw, err := record.MarshalJSON() + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + for _, f := range extraFieldsCheck { + if !strings.Contains(rawStr, f) { + t.Fatalf("Expected %s in\n%s", f, rawStr) + } + } + + // check changes + if v := record.GetString("name"); v != "name_new" { + t.Fatalf("Expected name to be %q, got %q", "name_new", v) + } + if v := record.GetString("custom"); v != "test_custom" { + t.Fatalf("Expected custom to be %q, got %q", "test_custom", v) + } + + // check clone + if v := record.Clone().LastSavedPK(); v != originalId { + t.Fatalf("Expected the clone LastSavedPK to be %q, got %q", originalId, v) + } + if v := record.Clone().PK(); v != record.Id { + t.Fatalf("Expected the clone PK to be %q, got %q", record.Id, v) + } + if v := record.Clone().Id; v != record.Id { + t.Fatalf("Expected the clone id to be %q, got %q", record.Id, v) + } + if v := record.Clone().GetString("name"); v != record.GetString("name") { + t.Fatalf("Expected the clone name to be %q, got %q", record.GetString("name"), v) + } + if v := record.Clone().GetString("custom"); v != "test_custom" { + t.Fatalf("Expected the clone custom to be %q, got %q", "test_custom", v) + } + if _, ok := record.Clone().Expand()["test"]; !ok { + t.Fatalf("Expected non-empty clone expand") + } + + // ensure that the email visibility and the custom data toggles state were copied + cloneRaw, err := record.Clone().MarshalJSON() + if err != nil { + t.Fatal(err) + } + cloneRawStr := string(cloneRaw) + for _, f := range extraFieldsCheck { + if !strings.Contains(cloneRawStr, f) { + t.Fatalf("Expected %s in clone\n%s", f, cloneRawStr) + } + } +} + +func TestRecordExpand(t *testing.T) { + t.Parallel() + + record := core.NewRecord(core.NewBaseCollection("test")) + + expand := record.Expand() + if expand == nil || len(expand) != 0 { + t.Fatalf("Expected empty map expand, got %v", expand) + } + + data1 := map[string]any{"a": 123, "b": 456} + data2 := map[string]any{"c": 123} + record.SetExpand(data1) + record.SetExpand(data2) // should overwrite the previous call + + // modify the expand map to check for shallow copy + data2["d"] = 456 + + expand = record.Expand() + if len(expand) != 1 { + t.Fatalf("Expected empty map expand, got %v", expand) + } + if v := expand["c"]; v != 123 { + t.Fatalf("Expected to find expand.c %v, got %v", 123, v) + } +} + +func TestRecordMergeExpand(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + m := core.NewRecord(collection) + m.Id = "m" + + // a + a := core.NewRecord(collection) + a.Id = "a" + a1 := core.NewRecord(collection) + a1.Id = "a1" + a2 := core.NewRecord(collection) + a2.Id = "a2" + a3 := core.NewRecord(collection) + a3.Id = "a3" + a31 := core.NewRecord(collection) + a31.Id = "a31" + a32 := core.NewRecord(collection) + a32.Id = "a32" + a.SetExpand(map[string]any{ + "a1": a1, + "a23": []*core.Record{a2, a3}, + }) + a3.SetExpand(map[string]any{ + "a31": a31, + "a32": []*core.Record{a32}, + }) + + // b + b := core.NewRecord(collection) + b.Id = "b" + b1 := core.NewRecord(collection) + b1.Id = "b1" + b.SetExpand(map[string]any{ + "b1": b1, + }) + + // c + c := core.NewRecord(collection) + c.Id = "c" + + // load initial expand + m.SetExpand(map[string]any{ + "a": a, + "b": b, + "c": []*core.Record{c}, + }) + + // a (new) + aNew := core.NewRecord(collection) + aNew.Id = a.Id + a3New := core.NewRecord(collection) + a3New.Id = a3.Id + a32New := core.NewRecord(collection) + a32New.Id = "a32New" + a33New := core.NewRecord(collection) + a33New.Id = "a33New" + a3New.SetExpand(map[string]any{ + "a32": []*core.Record{a32New}, + "a33New": a33New, + }) + aNew.SetExpand(map[string]any{ + "a23": []*core.Record{a2, a3New}, + }) + + // b (new) + bNew := core.NewRecord(collection) + bNew.Id = "bNew" + dNew := core.NewRecord(collection) + dNew.Id = "dNew" + + // merge expands + m.MergeExpand(map[string]any{ + "a": aNew, + "b": []*core.Record{bNew}, + "dNew": dNew, + }) + + result := m.Expand() + + raw, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + 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"}}` + + if expected != rawStr { + t.Fatalf("Expected \n%v, \ngot \n%v", expected, rawStr) + } +} + +func TestRecordMergeExpandNilCheck(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + scenarios := []struct { + name string + expand map[string]any + expected string + }{ + { + "nil expand", + nil, + `{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}`, + }, + { + "empty expand", + map[string]any{}, + `{"collectionId":"_pbc_3632233996","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":""}`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + m := core.NewRecord(collection) + m.MergeExpand(s.expand) + + raw, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected \n%v, \ngot \n%v", s.expected, rawStr) + } + }) + } +} + +func TestRecordExpandedOne(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + main := core.NewRecord(collection) + + single := core.NewRecord(collection) + single.Id = "single" + + multiple1 := core.NewRecord(collection) + multiple1.Id = "multiple1" + + multiple2 := core.NewRecord(collection) + multiple2.Id = "multiple2" + + main.SetExpand(map[string]any{ + "single": single, + "multiple": []*core.Record{multiple1, multiple2}, + }) + + if v := main.ExpandedOne("missing"); v != nil { + t.Fatalf("Expected nil, got %v", v) + } + + if v := main.ExpandedOne("single"); v == nil || v.Id != "single" { + t.Fatalf("Expected record with id %q, got %v", "single", v) + } + + if v := main.ExpandedOne("multiple"); v == nil || v.Id != "multiple1" { + t.Fatalf("Expected record with id %q, got %v", "multiple1", v) + } +} + +func TestRecordExpandedAll(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + + main := core.NewRecord(collection) + + single := core.NewRecord(collection) + single.Id = "single" + + multiple1 := core.NewRecord(collection) + multiple1.Id = "multiple1" + + multiple2 := core.NewRecord(collection) + multiple2.Id = "multiple2" + + main.SetExpand(map[string]any{ + "single": single, + "multiple": []*core.Record{multiple1, multiple2}, + }) + + if v := main.ExpandedAll("missing"); v != nil { + t.Fatalf("Expected nil, got %v", v) + } + + if v := main.ExpandedAll("single"); len(v) != 1 || v[0].Id != "single" { + t.Fatalf("Expected [single] slice, got %v", v) + } + + if v := main.ExpandedAll("multiple"); len(v) != 2 || v[0].Id != "multiple1" || v[1].Id != "multiple2" { + t.Fatalf("Expected [multiple1, multiple2] slice, got %v", v) + } +} + +func TestRecordFieldsData(t *testing.T) { + t.Parallel() + + collection := core.NewAuthCollection("test") + collection.Fields.Add(&core.TextField{Name: "field1"}) + collection.Fields.Add(&core.TextField{Name: "field2"}) + + m := core.NewRecord(collection) + m.Id = "test_id" // direct id assignment + m.Set("email", "test@example.com") + m.Set("password", "123") // hidden fields should be also returned + m.Set("tokenKey", "789") + m.Set("field1", 123) + m.Set("field2", 456) + m.Set("unknown", 789) + + raw, err := json.Marshal(m.FieldsData()) + if err != nil { + t.Fatal(err) + } + + expected := `{"email":"test@example.com","emailVisibility":false,"field1":"123","field2":"456","id":"test_id","password":"123","tokenKey":"789","verified":false}` + + if v := string(raw); v != expected { + t.Fatalf("Expected\n%v\ngot\n%v", expected, v) + } +} + +func TestRecordCustomData(t *testing.T) { + t.Parallel() + + collection := core.NewAuthCollection("test") + collection.Fields.Add(&core.TextField{Name: "field1"}) + collection.Fields.Add(&core.TextField{Name: "field2"}) + + m := core.NewRecord(collection) + m.Id = "test_id" // direct id assignment + m.Set("email", "test@example.com") + m.Set("password", "123") // hidden fields should be also returned + m.Set("tokenKey", "789") + m.Set("field1", 123) + m.Set("field2", 456) + m.Set("unknown", 789) + + raw, err := json.Marshal(m.CustomData()) + if err != nil { + t.Fatal(err) + } + + expected := `{"unknown":789}` + + if v := string(raw); v != expected { + t.Fatalf("Expected\n%v\ngot\n%v", expected, v) + } +} + +func TestRecordSetGet(t *testing.T) { + t.Parallel() + + f1 := &mockField{} + f1.Name = "mock1" + + f2 := &mockField{} + f2.Name = "mock2" + + f3 := &mockField{} + f3.Name = "mock3" + + collection := core.NewBaseCollection("test") + collection.Fields.Add(&core.TextField{Name: "text1"}) + collection.Fields.Add(&core.TextField{Name: "text2"}) + collection.Fields.Add(f1) + collection.Fields.Add(f2) + collection.Fields.Add(f3) + + record := core.NewRecord(collection) + record.Set("text1", 123) // should be converted to string using the ScanValue fallback + record.SetRaw("text2", 456) + record.Set("mock1", 1) // should be converted to string using the setter + record.SetRaw("mock2", 1) + record.Set("mock3:test", "abc") + record.Set("unknown", 789) + + t.Run("GetRaw", func(t *testing.T) { + expected := map[string]any{ + "text1": "123", + "text2": 456, + "mock1": "1", + "mock2": 1, + "mock3": "modifier_set", + "mock3:test": nil, + "unknown": 789, + } + + for k, v := range expected { + raw := record.GetRaw(k) + if raw != v { + t.Errorf("Expected %q to be %v, got %v", k, v, raw) + } + } + }) + + t.Run("Get", func(t *testing.T) { + expected := map[string]any{ + "text1": "123", + "text2": 456, + "mock1": "1", + "mock2": 1, + "mock3": "modifier_set", + "mock3:test": "modifier_get", + "unknown": 789, + } + + for k, v := range expected { + get := record.Get(k) + if get != v { + t.Errorf("Expected %q to be %v, got %v", k, v, get) + } + } + }) +} + +func TestRecordLoad(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add(&core.TextField{Name: "text"}) + + record := core.NewRecord(collection) + record.Load(map[string]any{ + "text": 123, + "custom": 456, + }) + + expected := map[string]any{ + "text": "123", + "custom": 456, + } + + for k, v := range expected { + get := record.Get(k) + if get != v { + t.Errorf("Expected %q to be %#v, got %#v", k, v, get) + } + } +} + +func TestRecordGetBool(t *testing.T) { + t.Parallel() + + scenarios := []struct { + value any + expected bool + }{ + {nil, false}, + {"", false}, + {0, false}, + {1, true}, + {[]string{"true"}, false}, + {time.Now(), false}, + {"test", false}, + {"false", false}, + {"true", true}, + {false, false}, + {true, true}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetBool("test") + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRecordGetString(t *testing.T) { + t.Parallel() + + scenarios := []struct { + value any + expected string + }{ + {nil, ""}, + {"", ""}, + {0, "0"}, + {1.4, "1.4"}, + {[]string{"true"}, ""}, + {map[string]int{"test": 1}, ""}, + {[]byte("abc"), "abc"}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetString("test") + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) + } +} + +func TestRecordGetInt(t *testing.T) { + t.Parallel() + + scenarios := []struct { + value any + expected int + }{ + {nil, 0}, + {"", 0}, + {[]string{"true"}, 0}, + {map[string]int{"test": 1}, 0}, + {time.Now(), 0}, + {"test", 0}, + {123, 123}, + {2.4, 2}, + {"123", 123}, + {"123.5", 0}, + {false, 0}, + {true, 1}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetInt("test") + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRecordGetFloat(t *testing.T) { + t.Parallel() + + scenarios := []struct { + value any + expected float64 + }{ + {nil, 0}, + {"", 0}, + {[]string{"true"}, 0}, + {map[string]int{"test": 1}, 0}, + {time.Now(), 0}, + {"test", 0}, + {123, 123}, + {2.4, 2.4}, + {"123", 123}, + {"123.5", 123.5}, + {false, 0}, + {true, 1}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetFloat("test") + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRecordGetDateTime(t *testing.T) { + t.Parallel() + + nowTime := time.Now() + testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000Z") + + scenarios := []struct { + value any + expected time.Time + }{ + {nil, time.Time{}}, + {"", time.Time{}}, + {false, time.Time{}}, + {true, time.Time{}}, + {"test", time.Time{}}, + {[]string{"true"}, time.Time{}}, + {map[string]int{"test": 1}, time.Time{}}, + {1641024040, testTime}, + {"2022-01-01 08:00:40.000", testTime}, + {nowTime, nowTime}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetDateTime("test") + if !result.Time().Equal(s.expected) { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestRecordGetStringSlice(t *testing.T) { + t.Parallel() + + nowTime := time.Now() + + scenarios := []struct { + value any + expected []string + }{ + {nil, []string{}}, + {"", []string{}}, + {false, []string{"false"}}, + {true, []string{"true"}}, + {nowTime, []string{}}, + {123, []string{"123"}}, + {"test", []string{"test"}}, + {map[string]int{"test": 1}, []string{}}, + {`["test1", "test2"]`, []string{"test1", "test2"}}, + {[]int{123, 123, 456}, []string{"123", "456"}}, + {[]string{"test", "test", "123"}, []string{"test", "123"}}, + } + + collection := core.NewBaseCollection("test") + record := core.NewRecord(collection) + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("test", s.value) + + result := record.GetStringSlice("test") + + if len(result) != len(s.expected) { + t.Fatalf("Expected %d elements, got %d: %v", len(s.expected), len(result), result) + } + + for _, v := range result { + if !slices.Contains(s.expected, v) { + t.Fatalf("Cannot find %v in %v", v, s.expected) + } + } + }) + } +} + +func TestRecordGetUploadedFiles(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f1, err := filesystem.NewFileFromBytes([]byte("test"), "f1") + if err != nil { + t.Fatal(err) + } + f1.Name = "f1" + + f2, err := filesystem.NewFileFromBytes([]byte("test"), "f2") + if err != nil { + t.Fatal(err) + } + f2.Name = "f2" + + record, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy") + if err != nil { + t.Fatal(err) + } + record.Set("files+", []any{f1, f2}) + + scenarios := []struct { + key string + expected string + }{ + { + "", + "null", + }, + { + "title", + "null", + }, + { + "files", + `[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, + }, + { + "files:uploaded", + `[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.key), func(t *testing.T) { + v := record.GetUploadedFiles(s.key) + + raw, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected\n%s\ngot\n%s", s.expected, rawStr) + } + }) + } +} + +func TestRecordUnmarshalJSONField(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add(&core.JSONField{Name: "field"}) + + record := core.NewRecord(collection) + + var testPointer *string + var testStr string + var testInt int + var testBool bool + var testSlice []int + var testMap map[string]any + + scenarios := []struct { + value any + destination any + expectError bool + expectedJSON string + }{ + {nil, testPointer, false, `null`}, + {nil, testStr, false, `""`}, + {"", testStr, false, `""`}, + {1, testInt, false, `1`}, + {true, testBool, false, `true`}, + {[]int{1, 2, 3}, testSlice, false, `[1,2,3]`}, + {map[string]any{"test": 123}, testMap, false, `{"test":123}`}, + // json encoded values + {`null`, testPointer, false, `null`}, + {`true`, testBool, false, `true`}, + {`456`, testInt, false, `456`}, + {`"test"`, testStr, false, `"test"`}, + {`[4,5,6]`, testSlice, false, `[4,5,6]`}, + {`{"test":456}`, testMap, false, `{"test":456}`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + record.Set("field", s.value) + + err := record.UnmarshalJSONField("field", &s.destination) + hasErr := err != nil + + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } + + raw, _ := json.Marshal(s.destination) + if v := string(raw); v != s.expectedJSON { + t.Fatalf("Expected %q, got %q", s.expectedJSON, v) + } + }) + } +} + +func TestRecordFindFileFieldByFile(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add( + &core.TextField{Name: "field1"}, + &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1}, + &core.FileField{Name: "field3", MaxSelect: 2, MaxSize: 1}, + ) + + m := core.NewRecord(collection) + m.Set("field1", "test") + m.Set("field2", "test.png") + m.Set("field3", []string{"test1.png", "test2.png"}) + + scenarios := []struct { + filename string + expectField string + }{ + {"", ""}, + {"test", ""}, + {"test2", ""}, + {"test.png", "field2"}, + {"test2.png", "field3"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.filename), func(t *testing.T) { + result := m.FindFileFieldByFile(s.filename) + + var fieldName string + if result != nil { + fieldName = result.Name + } + + if s.expectField != fieldName { + t.Fatalf("Expected field %v, got %v", s.expectField, result) + } + }) + } +} + +func TestRecordDBExport(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + f1 := &core.TextField{Name: "field1"} + f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1} + f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}} + f4 := &core.RelationField{Name: "field4", MaxSelect: 2} + + colBase := core.NewBaseCollection("test_base") + colBase.Fields.Add(f1, f2, f3, f4) + + colAuth := core.NewAuthCollection("test_auth") + colAuth.Fields.Add(f1, f2, f3, f4) + + scenarios := []struct { + collection *core.Collection + expected string + }{ + { + colBase, + `{"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id"}`, + }, + { + colAuth, + `{"email":"test_email","emailVisibility":true,"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id","password":"_TEST_","tokenKey":"test_tokenKey","verified":false}`, + }, + } + + data := map[string]any{ + "id": "test_id", + "field1": "test", + "field2": "test.png", + "field3": []string{"test1", "test2"}, + "field4": []string{"test11", "test12", "test11"}, // strip duplicate, + "unknown": "test_unknown", + "password": "test_passwordHash", + "username": "test_username", + "emailVisibility": true, + "email": "test_email", + "verified": "invalid", // should be casted + "tokenKey": "test_tokenKey", + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.collection.Type, s.collection.Name), func(t *testing.T) { + record := core.NewRecord(s.collection) + + record.Load(data) + + result, err := record.DBExport(app) + if err != nil { + t.Fatal(err) + } + + raw, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + // replace _TEST_ placeholder with .+ regex pattern + pattern := regexp.MustCompile(strings.ReplaceAll( + "^"+regexp.QuoteMeta(s.expected)+"$", + "_TEST_", + `.+`, + )) + + if !pattern.MatchString(rawStr) { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, rawStr) + } + }) + } +} + +func TestRecordIgnoreUnchangedFields(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + col, err := app.FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + new := core.NewRecord(col) + + existing, err := app.FindRecordById(col, "mk5fmymtx4wsprk") + if err != nil { + t.Fatal(err) + } + existing.Set("title", "test_new") + existing.Set("files", existing.Get("files")) // no change + + scenarios := []struct { + ignoreUnchangedFields bool + record *core.Record + expected []string + }{ + { + false, + new, + []string{"id", "created", "updated", "title", "files"}, + }, + { + true, + new, + []string{"id", "created", "updated", "title", "files"}, + }, + { + false, + existing, + []string{"id", "created", "updated", "title", "files"}, + }, + { + true, + existing, + []string{"id", "title"}, + }, + } + + for i, s := range scenarios { + action := "create" + if !s.record.IsNew() { + action = "update" + } + + t.Run(fmt.Sprintf("%d_%s_%v", i, action, s.ignoreUnchangedFields), func(t *testing.T) { + s.record.IgnoreUnchangedFields(s.ignoreUnchangedFields) + + result, err := s.record.DBExport(app) + if err != nil { + t.Fatal(err) + } + + if len(result) != len(s.expected) { + t.Fatalf("Expected %d keys, got %d:\n%v", len(s.expected), len(result), result) + } + + for _, key := range s.expected { + if _, ok := result[key]; !ok { + t.Fatalf("Missing expected key %q in\n%v", key, result) + } + } + }) + } +} + +func TestRecordPublicExportAndMarshalJSON(t *testing.T) { + t.Parallel() + + f1 := &core.TextField{Name: "field1"} + f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1} + f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}} + f4 := &core.TextField{Name: "field4", Hidden: true} + f5 := &core.TextField{Name: "field5", Hidden: true} + + colBase := core.NewBaseCollection("test_base") + colBase.Fields.Add(f1, f2, f3, f4, f5) + + colAuth := core.NewAuthCollection("test_auth") + colAuth.Fields.Add(f1, f2, f3, f4, f5) + + scenarios := []struct { + name string + collection *core.Collection + ignoreEmailVisibility bool + withCustomData bool + hideFields []string + unhideFields []string + expectedJSON string + }{ + // base + { + "[base] no extra flags", + colBase, + false, + 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"}`, + }, + { + "[base] with email visibility", + colBase, + true, // should have no effect + 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"}`, + }, + { + "[base] with custom data", + colBase, + true, // should have no effect + 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}`, + }, + { + "[base] with explicit hide and unhide fields", + colBase, + false, + true, + []string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "tokenKey", "unknown"}, + []string{"field4", "@pbInternalAbc"}, + `{"emailVisibility":"test_invalid","field2":"field_2.png","field4":"field_4","id":"test_id","password":"test_passwordHash","verified":true}`, + }, + { + "[base] trying to unhide custom fields without explicit WithCustomData", + colBase, + false, + 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}`, + }, + + // auth + { + "[auth] no extra flags", + colAuth, + false, + 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}`, + }, + { + "[auth] with email visibility", + colAuth, + true, + 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}`, + }, + { + "[auth] with custom data", + colAuth, + false, + 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}`, + }, + { + "[auth] with explicit hide and unhide fields", + colAuth, + true, + true, + []string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "unknown"}, + []string{"field4", "@pbInternalAbc"}, + `{"emailVisibility":false,"field2":"field_2.png","field4":"field_4","id":"test_id","verified":true}`, + }, + { + "[auth] trying to unhide custom fields without explicit WithCustomData", + colAuth, + false, + 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}`, + }, + } + + data := map[string]any{ + "id": "test_id", + "field1": "field_1", + "field2": "field_2.png", + "field3": []string{"test1", "test2"}, + "field4": "field_4", + "field5": "field_5", + "expand": map[string]any{"test": 123}, + "collectionId": "m_id", // should be always ignored + "collectionName": "m_name", // should be always ignored + "unknown": "test_unknown", + "password": "test_passwordHash", + "emailVisibility": "test_invalid", // for auth collections should be casted to bool + "email": "test_email", + "verified": true, + "tokenKey": "test_tokenKey", + "@pbInternalAbc": "test_custom_inter", // always hidden + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + m := core.NewRecord(s.collection) + + m.Load(data) + m.IgnoreEmailVisibility(s.ignoreEmailVisibility) + m.WithCustomData(s.withCustomData) + m.Unhide(s.unhideFields...) + m.Hide(s.hideFields...) + + exportResult, err := json.Marshal(m.PublicExport()) + if err != nil { + t.Fatal(err) + } + exportResultStr := string(exportResult) + + // MarshalJSON and PublicExport should return the same + marshalResult, err := m.MarshalJSON() + if err != nil { + t.Fatal(err) + } + marshalResultStr := string(marshalResult) + + if exportResultStr != marshalResultStr { + t.Fatalf("Expected the PublicExport to be the same as MarshalJSON, but got \n%v \nvs \n%v", exportResultStr, marshalResultStr) + } + + if exportResultStr != s.expectedJSON { + t.Fatalf("Expected json \n%v \ngot \n%v", s.expectedJSON, exportResultStr) + } + }) + } +} + +func TestRecordUnmarshalJSON(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add(&core.TextField{Name: "text"}) + + record := core.NewRecord(collection) + + data := map[string]any{ + "text": 123, + "custom": 456.789, + } + rawData, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + err = record.UnmarshalJSON(rawData) + if err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + + expected := map[string]any{ + "text": "123", + "custom": 456.789, + } + + for k, v := range expected { + get := record.Get(k) + if get != v { + t.Errorf("Expected %q to be %#v, got %#v", k, v, get) + } + } +} + +func TestRecordReplaceModifiers(t *testing.T) { + t.Parallel() + + collection := core.NewBaseCollection("test") + collection.Fields.Add( + &mockField{core.TextField{Name: "mock"}}, + &core.NumberField{Name: "number"}, + ) + + originalData := map[string]any{ + "mock": "a", + "number": 2.1, + } + + record := core.NewRecord(collection) + for k, v := range originalData { + record.Set(k, v) + } + + result := record.ReplaceModifiers(map[string]any{ + "mock:test": "b", + "number+": 3, + }) + + expected := map[string]any{ + "mock": "modifier_set", + "number": 5.1, + } + + if len(result) != len(expected) { + t.Fatalf("Expected\n%v\ngot\n%v", expected, result) + } + + for k, v := range expected { + if result[k] != v { + t.Errorf("Expected %q %#v, got %#v", k, v, result[k]) + } + } + + // ensure that the original data hasn't changed + for k, v := range originalData { + rv := record.Get(k) + if rv != v { + t.Errorf("Expected original %q %#v, got %#v", k, v, rv) + } + } +} + +func TestRecordValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := core.NewBaseCollection("test") + + collection.Fields.Add( + // dummy fields to ensure that its validators are triggered + &core.TextField{Name: "f1", Min: 3}, + &core.NumberField{Name: "f2", Required: true}, + ) + + record := core.NewRecord(collection) + record.Id = "!invalid" + + t.Run("no data set", func(t *testing.T) { + tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f2"}) + }) + + t.Run("failing the text field min requirement", func(t *testing.T) { + record.Set("f1", "a") + tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f1", "f2"}) + }) + + t.Run("satisfying the fields validations", func(t *testing.T) { + record.Id = strings.Repeat("a", 15) + record.Set("f1", "abc") + record.Set("f2", 1) + tests.TestValidationErrors(t, app.Validate(record), nil) + }) +} + +func TestRecordSave(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + record func(app core.App) (*core.Record, error) + expectError bool + }{ + // trigger validators + { + name: "create - trigger validators", + record: func(app core.App) (*core.Record, error) { + c, _ := app.FindCollectionByNameOrId("demo2") + record := core.NewRecord(c) + return record, nil + }, + expectError: true, + }, + { + name: "update - trigger validators", + record: func(app core.App) (*core.Record, error) { + record, _ := app.FindFirstRecordByData("demo2", "title", "test1") + record.Set("title", "") + return record, nil + }, + expectError: true, + }, + + // create + { + name: "create base record", + record: func(app core.App) (*core.Record, error) { + c, _ := app.FindCollectionByNameOrId("demo2") + record := core.NewRecord(c) + record.Set("title", "new_test") + return record, nil + }, + expectError: false, + }, + { + name: "create auth record", + record: func(app core.App) (*core.Record, error) { + c, _ := app.FindCollectionByNameOrId("nologin") + record := core.NewRecord(c) + record.Set("email", "test_new@example.com") + record.Set("password", "1234567890") + return record, nil + }, + expectError: false, + }, + { + name: "create view record", + record: func(app core.App) (*core.Record, error) { + c, _ := app.FindCollectionByNameOrId("view2") + record := core.NewRecord(c) + record.Set("state", true) + return record, nil + }, + expectError: true, // view records are read-only + }, + + // update + { + name: "update base record", + record: func(app core.App) (*core.Record, error) { + record, _ := app.FindFirstRecordByData("demo2", "title", "test1") + record.Set("title", "test_new") + return record, nil + }, + expectError: false, + }, + { + name: "update auth record", + record: func(app core.App) (*core.Record, error) { + record, _ := app.FindAuthRecordByEmail("nologin", "test@example.com") + record.Set("name", "test_new") + record.Set("email", "test_new@example.com") + return record, nil + }, + expectError: false, + }, + { + name: "update view record", + record: func(app core.App) (*core.Record, error) { + record, _ := app.FindFirstRecordByData("view2", "state", true) + record.Set("state", false) + return record, nil + }, + expectError: true, // view records are read-only + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := s.record(app) + if err != nil { + t.Fatalf("Failed to retrieve test record: %v", err) + } + + saveErr := app.Save(record) + + hasErr := saveErr != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", hasErr, s.expectError, saveErr) + } + + if hasErr { + return + } + + // the record should always have an id after successful Save + if record.Id == "" { + t.Fatal("Expected record id to be set") + } + + if record.IsNew() { + t.Fatal("Expected the record to be marked as not new") + } + + // refetch and compare the serialization + refreshed, err := app.FindRecordById(record.Collection(), record.Id) + if err != nil { + t.Fatal(err) + } + + rawRefreshed, err := refreshed.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + raw, err := record.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(raw, rawRefreshed) { + t.Fatalf("Expected the refreshed record to be the same as the saved one, got\n%s\nVS\n%s", raw, rawRefreshed) + } + }) + } +} + +func TestRecordSaveIdFromOtherCollection(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + baseCollection, _ := app.FindCollectionByNameOrId("demo2") + authCollection, _ := app.FindCollectionByNameOrId("nologin") + + // base collection test + r1 := core.NewRecord(baseCollection) + r1.Set("title", "test_new") + r1.Set("id", "mk5fmymtx4wsprk") // existing id of demo3 record + if err := app.Save(r1); err != nil { + t.Fatalf("Expected nil, got error %v", err) + } + + // auth collection test + r2 := core.NewRecord(authCollection) + r2.SetEmail("test_new@example.com") + r2.SetPassword("1234567890") + r2.Set("id", "gk390qegs4y47wn") // existing id of "clients" record + if err := app.Save(r2); err == nil { + t.Fatal("Expected error, got nil") + } + + // try again with unique id + r2.Set("id", strings.Repeat("a", 15)) + if err := app.Save(r2); err != nil { + t.Fatalf("Expected nil, got error %v", err) + } +} + +func TestRecordSaveIdUpdateNoValidation(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + rec, err := app.FindRecordById("demo3", "7nwo8tuiatetxdm") + if err != nil { + t.Fatal(err) + } + + rec.Id = strings.Repeat("a", 15) + + err = app.SaveNoValidate(rec) + if err == nil { + t.Fatal("Expected save to fail, got nil") + } + + // no changes + rec.Load(rec.Original().FieldsData()) + err = app.SaveNoValidate(rec) + if err != nil { + t.Fatalf("Expected save to succeed, got error %v", err) + } +} + +func TestRecordSaveWithChangedPassword(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := app.FindAuthRecordByEmail("nologin", "test@example.com") + if err != nil { + t.Fatal(err) + } + + originalTokenKey := record.TokenKey() + + t.Run("no password change shouldn't change the tokenKey", func(t *testing.T) { + record.Set("name", "example") + + if err := app.Save(record); err != nil { + t.Fatal(err) + } + + tokenKey := record.TokenKey() + if tokenKey == "" || originalTokenKey != tokenKey { + t.Fatalf("Expected tokenKey to not change, got %q VS %q", originalTokenKey, tokenKey) + } + }) + + t.Run("password change should change the tokenKey", func(t *testing.T) { + record.Set("password", "1234567890") + + if err := app.Save(record); err != nil { + t.Fatal(err) + } + + tokenKey := record.TokenKey() + if tokenKey == "" || originalTokenKey == tokenKey { + t.Fatalf("Expected tokenKey to change, got %q VS %q", originalTokenKey, tokenKey) + } + }) +} + +func TestRecordDelete(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demoCollection, _ := app.FindCollectionByNameOrId("demo2") + + // delete unsaved record + // --- + rec0 := core.NewRecord(demoCollection) + if err := app.Delete(rec0); err == nil { + t.Fatal("(rec0) Didn't expect to succeed deleting unsaved record") + } + + // delete existing record + external auths + // --- + rec1, _ := app.FindRecordById("users", "4q1xlclmfloku33") + if err := app.Delete(rec1); err != nil { + t.Fatalf("(rec1) Expected nil, got error %v", err) + } + // check if it was really deleted + if refreshed, _ := app.FindRecordById(rec1.Collection().Id, rec1.Id); refreshed != nil { + t.Fatalf("(rec1) Expected record to be deleted, got %v", refreshed) + } + // check if the external auths were deleted + if auths, _ := app.FindAllExternalAuthsByRecord(rec1); len(auths) > 0 { + t.Fatalf("(rec1) Expected external auths to be deleted, got %v", auths) + } + + // delete existing record while being part of a non-cascade required relation + // --- + rec2, _ := app.FindRecordById("demo3", "7nwo8tuiatetxdm") + if err := app.Delete(rec2); err == nil { + t.Fatalf("(rec2) Expected error, got nil") + } + + // delete existing record + cascade + // --- + calledQueries := []string{} + app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + calledQueries = append(calledQueries, sql) + } + app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + calledQueries = append(calledQueries, sql) + } + app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + calledQueries = append(calledQueries, sql) + } + app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + calledQueries = append(calledQueries, sql) + } + rec3, _ := app.FindRecordById("users", "oap640cot4yru2s") + // delete + if err := app.Delete(rec3); err != nil { + t.Fatalf("(rec3) Expected nil, got error %v", err) + } + // check if it was really deleted + rec3, _ = app.FindRecordById(rec3.Collection().Id, rec3.Id) + if rec3 != nil { + t.Fatalf("(rec3) Expected record to be deleted, got %v", rec3) + } + // check if the operation cascaded + rel, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if rel != nil { + t.Fatalf("(rec3) Expected the delete to cascade, found relation %v", rel) + } + // ensure that the json rel fields were prefixed + joinedQueries := strings.Join(calledQueries, " ") + expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='" + if !strings.Contains(joinedQueries, expectedRelManyPart) { + t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries) + } + expectedRelOnePart := "SELECT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`='" + if !strings.Contains(joinedQueries, expectedRelOnePart) { + t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries) + } +} + +func TestRecordDeleteBatchProcessing(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + if err := createMockBatchProcessingData(app); err != nil { + t.Fatal(err) + } + + // find and delete the first c1 record to trigger cascade + mainRecord, _ := app.FindRecordById("c1", "a") + if err := app.Delete(mainRecord); err != nil { + t.Fatal(err) + } + + // check if the main record was deleted + _, err := app.FindRecordById(mainRecord.Collection().Id, mainRecord.Id) + if err == nil { + t.Fatal("The main record wasn't deleted") + } + + // check if the c1 b rel field were updated + c1RecordB, err := app.FindRecordById("c1", "b") + if err != nil || c1RecordB.GetString("rel") != "" { + t.Fatalf("Expected c1RecordB.rel to be nil, got %v", c1RecordB.GetString("rel")) + } + + // check if the c2 rel fields were updated + c2Records, err := app.FindAllRecords("c2", nil) + if err != nil || len(c2Records) == 0 { + t.Fatalf("Failed to fetch c2 records: %v", err) + } + for _, r := range c2Records { + ids := r.GetStringSlice("rel") + if len(ids) != 1 || ids[0] != "b" { + t.Fatalf("Expected only 'b' rel id, got %v", ids) + } + } + + // check if all c3 relations were deleted + c3Records, err := app.FindAllRecords("c3", nil) + if err != nil { + t.Fatalf("Failed to fetch c3 records: %v", err) + } + if total := len(c3Records); total != 0 { + t.Fatalf("Expected c3 records to be deleted, found %d", total) + } +} + +func createMockBatchProcessingData(app core.App) error { + // create mock collection without relation + c1 := core.NewBaseCollection("c1") + c1.Id = "c1" + c1.Fields.Add( + &core.TextField{Name: "text"}, + &core.RelationField{ + Name: "rel", + MaxSelect: 1, + CollectionId: "c1", + CascadeDelete: false, // should unset all rel fields + }, + ) + if err := app.SaveNoValidate(c1); err != nil { + return err + } + + // create mock collection with a multi-rel field + c2 := core.NewBaseCollection("c2") + c2.Id = "c2" + c2.Fields.Add( + &core.TextField{Name: "text"}, + &core.RelationField{ + Name: "rel", + MaxSelect: 10, + CollectionId: "c1", + CascadeDelete: false, // should unset all rel fields + }, + ) + if err := app.SaveNoValidate(c2); err != nil { + return err + } + + // create mock collection with a single-rel field + c3 := core.NewBaseCollection("c3") + c3.Id = "c3" + c3.Fields.Add( + &core.RelationField{ + Name: "rel", + MaxSelect: 1, + CollectionId: "c1", + CascadeDelete: true, // should delete all c3 records + }, + ) + if err := app.SaveNoValidate(c3); err != nil { + return err + } + + // insert mock records + c1RecordA := core.NewRecord(c1) + c1RecordA.Id = "a" + c1RecordA.Set("rel", c1RecordA.Id) // self reference + if err := app.SaveNoValidate(c1RecordA); err != nil { + return err + } + c1RecordB := core.NewRecord(c1) + c1RecordB.Id = "b" + c1RecordB.Set("rel", c1RecordA.Id) // rel to another record from the same collection + if err := app.SaveNoValidate(c1RecordB); err != nil { + return err + } + for i := 0; i < 4500; i++ { + c2Record := core.NewRecord(c2) + c2Record.Set("rel", []string{c1RecordA.Id, c1RecordB.Id}) + if err := app.SaveNoValidate(c2Record); err != nil { + return err + } + + c3Record := core.NewRecord(c3) + c3Record.Set("rel", c1RecordA.Id) + if err := app.SaveNoValidate(c3Record); err != nil { + return err + } + } + + // set the same id as the relation for at least 1 record + // to check whether the correct condition will be added + c3Record := core.NewRecord(c3) + c3Record.Set("rel", c1RecordA.Id) + c3Record.Id = c1RecordA.Id + if err := app.SaveNoValidate(c3Record); err != nil { + return err + } + + return nil +} + +// ------------------------------------------------------------------- + +type mockField struct { + core.TextField +} + +func (f *mockField) FindGetter(key string) core.GetterFunc { + switch key { + case f.Name + ":test": + return func(record *core.Record) any { + return "modifier_get" + } + default: + return nil + } +} + +func (f *mockField) FindSetter(key string) core.SetterFunc { + switch key { + case f.Name: + return func(record *core.Record, raw any) { + record.SetRaw(f.Name, cast.ToString(raw)) + } + case f.Name + ":test": + return func(record *core.Record, raw any) { + record.SetRaw(f.Name, "modifier_set") + } + default: + return nil + } +} diff --git a/core/record_proxy.go b/core/record_proxy.go new file mode 100644 index 00000000..644ede7d --- /dev/null +++ b/core/record_proxy.go @@ -0,0 +1,32 @@ +package core + +// RecordProxy defines an interface for a Record proxy/project model, +// aka. custom model struct that acts on behalve the proxied Record to +// allow for example typed getter/setters for the Record fields. +// +// To implement the interface it is usually enough to embed the [BaseRecordProxy] struct. +type RecordProxy interface { + // ProxyRecord returns the proxied Record model. + ProxyRecord() *Record + + // SetProxyRecord loads the specified record model into the current proxy. + SetProxyRecord(record *Record) +} + +var _ RecordProxy = (*BaseRecordProxy)(nil) + +// BaseRecordProxy implements the [RecordProxy] interface and it is intended +// to be used as embed to custom user provided Record proxy structs. +type BaseRecordProxy struct { + *Record +} + +// ProxyRecord returns the proxied Record model. +func (m *BaseRecordProxy) ProxyRecord() *Record { + return m.Record +} + +// SetProxyRecord loads the specified record model into the current proxy. +func (m *BaseRecordProxy) SetProxyRecord(record *Record) { + m.Record = record +} diff --git a/core/record_proxy_test.go b/core/record_proxy_test.go new file mode 100644 index 00000000..bc8f004b --- /dev/null +++ b/core/record_proxy_test.go @@ -0,0 +1,20 @@ +package core_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/core" +) + +func TestBaseRecordProxy(t *testing.T) { + p := core.BaseRecordProxy{} + + record := core.NewRecord(core.NewBaseCollection("test")) + record.Id = "test" + + p.SetProxyRecord(record) + + if p.ProxyRecord() == nil || p.ProxyRecord().Id != p.Id || p.Id != "test" { + t.Fatalf("Expected proxy record to be set") + } +} diff --git a/core/record_query.go b/core/record_query.go new file mode 100644 index 00000000..fac7313d --- /dev/null +++ b/core/record_query.go @@ -0,0 +1,607 @@ +package core + +import ( + "context" + "database/sql" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/security" +) + +var recordProxyType = reflect.TypeOf((*RecordProxy)(nil)).Elem() + +// RecordQuery returns a new Record select query from a collection model, id or name. +// +// In case a collection id or name is provided and that collection doesn't +// actually exists, the generated query will be created with a cancelled context +// and will fail once an executor (Row(), One(), All(), etc.) is called. +func (app *BaseApp) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuery { + var tableName string + + collection, collectionErr := getCollectionByModelOrIdentifier(app, collectionModelOrIdentifier) + if collection != nil { + tableName = collection.Name + } + if tableName == "" { + // update with some fake table name for easier debugging + tableName = "@@__invalidCollectionModelOrIdentifier" + } + + query := app.DB().Select(app.DB().QuoteSimpleColumnName(tableName) + ".*").From(tableName) + + // in case of an error attach a new context and cancel it immediately with the error + if collectionErr != nil { + ctx, cancelFunc := context.WithCancelCause(context.Background()) + query.WithContext(ctx) + cancelFunc(collectionErr) + } + + return query.WithBuildHook(func(q *dbx.Query) { + q.WithExecHook(execLockRetry(app.config.QueryTimeout, defaultMaxLockRetries)). + WithOneHook(func(q *dbx.Query, a any, op func(b any) error) error { + if a == nil { + return op(a) + } + + switch v := a.(type) { + case *Record: + record, err := resolveRecordOneHook(collection, op) + if err != nil { + return err + } + + *v = *record + + return nil + case RecordProxy: + record, err := resolveRecordOneHook(collection, op) + if err != nil { + return err + } + + v.SetProxyRecord(record) + return nil + default: + return op(a) + } + }). + WithAllHook(func(q *dbx.Query, sliceA any, op func(sliceB any) error) error { + if sliceA == nil { + return op(sliceA) + } + + switch v := sliceA.(type) { + case *[]*Record: + records, err := resolveRecordAllHook(collection, op) + if err != nil { + return err + } + + *v = records + + return nil + case *[]Record: + records, err := resolveRecordAllHook(collection, op) + if err != nil { + return err + } + + nonPointers := make([]Record, len(records)) + for i, r := range records { + nonPointers[i] = *r + } + + *v = nonPointers + + return nil + default: // expects []RecordProxy slice + records, err := resolveRecordAllHook(collection, op) + if err != nil { + return err + } + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return errors.New("must be a pointer") + } + + rv = dereference(rv) + + if rv.Kind() != reflect.Slice { + return errors.New("must be a slice of RecordSetters") + } + + // create an empty slice + if rv.IsNil() { + rv.Set(reflect.MakeSlice(rv.Type(), 0, len(records))) + } + + et := rv.Type().Elem() + + var isSliceOfPointers bool + if et.Kind() == reflect.Ptr { + isSliceOfPointers = true + et = et.Elem() + } + + if !reflect.PointerTo(et).Implements(recordProxyType) { + return op(sliceA) + } + + for _, record := range records { + ev := reflect.New(et) + + if !ev.CanInterface() { + continue + } + + ps, ok := ev.Interface().(RecordProxy) + if !ok { + continue + } + + ps.SetProxyRecord(record) + + ev = ev.Elem() + if isSliceOfPointers { + ev = ev.Addr() + } + + rv.Set(reflect.Append(rv, ev)) + } + + return nil + } + }) + }) +} + +func resolveRecordOneHook(collection *Collection, op func(dst any) error) (*Record, error) { + data := dbx.NullStringMap{} + if err := op(&data); err != nil { + return nil, err + } + return newRecordFromNullStringMap(collection, data) +} + +func resolveRecordAllHook(collection *Collection, op func(dst any) error) ([]*Record, error) { + data := []dbx.NullStringMap{} + if err := op(&data); err != nil { + return nil, err + } + return newRecordsFromNullStringMaps(collection, data) +} + +// dereference returns the underlying value v points to. +func dereference(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + // initialize with a new value and continue searching + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + return v +} + +func getCollectionByModelOrIdentifier(app App, collectionModelOrIdentifier any) (*Collection, error) { + switch c := collectionModelOrIdentifier.(type) { + case *Collection: + return c, nil + case Collection: + return &c, nil + case string: + return app.FindCachedCollectionByNameOrId(c) + default: + return nil, errors.New("unknown collection identifier - must be collection model, id or name") + } +} + +// ------------------------------------------------------------------- + +// FindRecordById finds the Record model by its id. +func (app *BaseApp) FindRecordById( + collectionModelOrIdentifier any, + recordId string, + optFilters ...func(q *dbx.SelectQuery) error, +) (*Record, error) { + collection, err := getCollectionByModelOrIdentifier(app, collectionModelOrIdentifier) + if err != nil { + return nil, err + } + + record := &Record{} + + query := app.RecordQuery(collection). + AndWhere(dbx.HashExp{collection.Name + ".id": recordId}) + + // apply filter funcs (if any) + for _, filter := range optFilters { + if filter == nil { + continue + } + if err = filter(query); err != nil { + return nil, err + } + } + + err = query.Limit(1).One(record) + if err != nil { + return nil, err + } + + return record, nil +} + +// FindRecordsByIds finds all records by the specified ids. +// If no records are found, returns an empty slice. +func (app *BaseApp) FindRecordsByIds( + collectionModelOrIdentifier any, + recordIds []string, + optFilters ...func(q *dbx.SelectQuery) error, +) ([]*Record, error) { + collection, err := getCollectionByModelOrIdentifier(app, collectionModelOrIdentifier) + if err != nil { + return nil, err + } + + query := app.RecordQuery(collection). + AndWhere(dbx.In( + collection.Name+".id", + list.ToInterfaceSlice(recordIds)..., + )) + + for _, filter := range optFilters { + if filter == nil { + continue + } + if err = filter(query); err != nil { + return nil, err + } + } + + records := make([]*Record, 0, len(recordIds)) + + err = query.All(&records) + if err != nil { + return nil, err + } + + return records, nil +} + +// FindAllRecords finds all records matching specified db expressions. +// +// Returns all collection records if no expression is provided. +// +// Returns an empty slice if no records are found. +// +// Example: +// +// // no extra expressions +// app.FindAllRecords("example") +// +// // with extra expressions +// expr1 := dbx.HashExp{"email": "test@example.com"} +// expr2 := dbx.NewExp("LOWER(username) = {:username}", dbx.Params{"username": "test"}) +// app.FindAllRecords("example", expr1, expr2) +func (app *BaseApp) FindAllRecords(collectionModelOrIdentifier any, exprs ...dbx.Expression) ([]*Record, error) { + query := app.RecordQuery(collectionModelOrIdentifier) + + for _, expr := range exprs { + if expr != nil { // add only the non-nil expressions + query.AndWhere(expr) + } + } + + var records []*Record + + if err := query.All(&records); err != nil { + return nil, err + } + + return records, nil +} + +// FindFirstRecordByData returns the first found record matching +// the provided key-value pair. +func (app *BaseApp) FindFirstRecordByData(collectionModelOrIdentifier any, key string, value any) (*Record, error) { + record := &Record{} + + err := app.RecordQuery(collectionModelOrIdentifier). + AndWhere(dbx.HashExp{inflector.Columnify(key): value}). + Limit(1). + One(record) + if err != nil { + return nil, err + } + + return record, nil +} + +// FindRecordsByFilter returns limit number of records matching the +// provided string filter. +// +// NB! Use the last "params" argument to bind untrusted user variables! +// +// The filter argument is optional and can be empty string to target +// all available records. +// +// The sort argument is optional and can be empty string OR the same format +// used in the web APIs, ex. "-created,title". +// +// If the limit argument is <= 0, no limit is applied to the query and +// all matching records are returned. +// +// Returns an empty slice if no records are found. +// +// Example: +// +// app.FindRecordsByFilter( +// "posts", +// "title ~ {:title} && visible = {:visible}", +// "-created", +// 10, +// 0, +// dbx.Params{"title": "lorem ipsum", "visible": true} +// ) +func (app *BaseApp) FindRecordsByFilter( + collectionModelOrIdentifier any, + filter string, + sort string, + limit int, + offset int, + params ...dbx.Params, +) ([]*Record, error) { + collection, err := getCollectionByModelOrIdentifier(app, collectionModelOrIdentifier) + if err != nil { + return nil, err + } + + q := app.RecordQuery(collection) + + // build a fields resolver and attach the generated conditions to the query + // --- + resolver := NewRecordFieldResolver( + app, + collection, // the base collection + nil, // no request data + true, // allow searching hidden/protected fields like "email" + ) + + if filter != "" { + expr, err := search.FilterData(filter).BuildExpr(resolver, params...) + if err != nil { + return nil, fmt.Errorf("invalid filter expression: %w", err) + } + q.AndWhere(expr) + } + + if sort != "" { + for _, sortField := range search.ParseSortFromString(sort) { + expr, err := sortField.BuildExpr(resolver) + if err != nil { + return nil, err + } + if expr != "" { + q.AndOrderBy(expr) + } + } + } + + resolver.UpdateQuery(q) // attaches any adhoc joins and aliases + // --- + + if offset > 0 { + q.Offset(int64(offset)) + } + + if limit > 0 { + q.Limit(int64(limit)) + } + + records := []*Record{} + + if err := q.All(&records); err != nil { + return nil, err + } + + return records, nil +} + +// FindFirstRecordByFilter returns the first available record matching the provided filter (if any). +// +// NB! Use the last params argument to bind untrusted user variables! +// +// Returns sql.ErrNoRows if no record is found. +// +// Example: +// +// app.FindFirstRecordByFilter("posts", "") +// app.FindFirstRecordByFilter("posts", "slug={:slug} && status='public'", dbx.Params{"slug": "test"}) +func (app *BaseApp) FindFirstRecordByFilter( + collectionModelOrIdentifier any, + filter string, + params ...dbx.Params, +) (*Record, error) { + result, err := app.FindRecordsByFilter(collectionModelOrIdentifier, filter, "", 1, 0, params...) + if err != nil { + return nil, err + } + + if len(result) == 0 { + return nil, sql.ErrNoRows + } + + return result[0], nil +} + +// CountRecords returns the total number of records in a collection. +func (app *BaseApp) CountRecords(collectionModelOrIdentifier any, exprs ...dbx.Expression) (int64, error) { + var total int64 + + q := app.RecordQuery(collectionModelOrIdentifier).Select("count(*)") + + for _, expr := range exprs { + if expr != nil { // add only the non-nil expressions + q.AndWhere(expr) + } + } + + err := q.Row(&total) + + return total, err +} + +// FindAuthRecordByToken finds the auth record associated with the provided JWT +// (auth, file, verifyEmail, changeEmail, passwordReset types). +// +// Optionally specify a list of validTypes to check tokens only from those types. +// +// Returns an error if the JWT is invalid, expired or not associated to an auth collection record. +func (app *BaseApp) FindAuthRecordByToken(token string, validTypes ...string) (*Record, error) { + if token == "" { + return nil, errors.New("missing token") + } + + unverifiedClaims, err := security.ParseUnverifiedJWT(token) + if err != nil { + return nil, err + } + + // check required claims + id, _ := unverifiedClaims[TokenClaimId].(string) + collectionId, _ := unverifiedClaims[TokenClaimCollectionId].(string) + tokenType, _ := unverifiedClaims[TokenClaimType].(string) + if id == "" || collectionId == "" || tokenType == "" { + return nil, errors.New("missing or invalid token claims") + } + + // check types (if explicitly set) + if len(validTypes) > 0 && !list.ExistInSlice(tokenType, validTypes) { + return nil, fmt.Errorf("invalid token type %q, expects %q", tokenType, strings.Join(validTypes, ",")) + } + + record, err := app.FindRecordById(collectionId, id) + if err != nil { + return nil, err + } + + if !record.Collection().IsAuth() { + return nil, errors.New("the token is not associated to an auth collection record") + } + + var baseTokenKey string + switch tokenType { + case TokenTypeAuth: + baseTokenKey = record.Collection().AuthToken.Secret + case TokenTypeFile: + baseTokenKey = record.Collection().FileToken.Secret + case TokenTypeVerification: + baseTokenKey = record.Collection().VerificationToken.Secret + case TokenTypePasswordReset: + baseTokenKey = record.Collection().PasswordResetToken.Secret + case TokenTypeEmailChange: + baseTokenKey = record.Collection().EmailChangeToken.Secret + default: + return nil, errors.New("unknown token type " + tokenType) + } + + secret := record.TokenKey() + baseTokenKey + + // verify token signature + _, err = security.ParseJWT(token, secret) + if err != nil { + return nil, err + } + + return record, nil +} + +// FindAuthRecordByEmail finds the auth record associated with the provided email. +// +// Returns an error if it is not an auth collection or the record is not found. +func (app *BaseApp) FindAuthRecordByEmail(collectionModelOrIdentifier any, email string) (*Record, error) { + collection, err := getCollectionByModelOrIdentifier(app, collectionModelOrIdentifier) + if err != nil { + return nil, fmt.Errorf("failed to fetch auth collection: %w", err) + } + if !collection.IsAuth() { + return nil, fmt.Errorf("%q is not an auth collection", collection.Name) + } + + record := &Record{} + + err = app.RecordQuery(collection). + AndWhere(dbx.HashExp{FieldNameEmail: email}). + Limit(1). + One(record) + if err != nil { + return nil, err + } + + return record, nil +} + +// 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. +// +// The returned error indicate that something unexpected happened during +// the check (eg. invalid rule or db query error). +// +// The method always return false on invalid rule or db query error. +// +// Example: +// +// requestInfo, _ := e.RequestInfo() +// record, _ := app.FindRecordById("example", "RECORD_ID") +// rule := types.Pointer("@request.auth.id != '' || status = 'public'") +// // ... or use one of the record collection's rule, eg. record.Collection().ViewRule +// +// if ok, _ := app.CanAccessRecord(record, requestInfo, rule); ok { ... } +func (app *BaseApp) CanAccessRecord(record *Record, requestInfo *RequestInfo, accessRule *string) (bool, error) { + // superusers can access everything + if requestInfo.HasSuperuserAuth() { + return true, nil + } + + // only superusers can access this record + if accessRule == nil { + return false, nil + } + + // empty public rule, aka. everyone can access + if *accessRule == "" { + return true, nil + } + + var exists bool + + query := app.RecordQuery(record.Collection()). + Select("(1)"). + AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id}) + + // parse and apply the access rule filter + resolver := NewRecordFieldResolver(app, record.Collection(), requestInfo, true) + expr, err := search.FilterData(*accessRule).BuildExpr(resolver) + if err != nil { + return false, err + } + resolver.UpdateQuery(query) + + err = query.AndWhere(expr).Limit(1).Row(&exists) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return false, err + } + + return exists, nil +} diff --git a/daos/record_expand.go b/core/record_query_expand.go similarity index 59% rename from daos/record_expand.go rename to core/record_query_expand.go index 857b5eec..79df2969 100644 --- a/daos/record_expand.go +++ b/core/record_query_expand.go @@ -1,4 +1,4 @@ -package daos +package core import ( "errors" @@ -8,21 +8,12 @@ import ( "strings" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" ) -// MaxExpandDepth specifies the max allowed nested expand depth path. -// -// @todo Consider eventually reusing resolvers.maxNestedRels -const MaxExpandDepth = 6 - // ExpandFetchFunc defines the function that is used to fetch the expanded relation records. -type ExpandFetchFunc func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) +type ExpandFetchFunc func(relCollection *Collection, relIds []string) ([]*Record, error) // ExpandRecord expands the relations of a single Record model. // @@ -30,8 +21,8 @@ type ExpandFetchFunc func(relCollection *models.Collection, relIds []string) ([] // that returns all relation records. // // Returns a map with the failed expand parameters and their errors. -func (dao *Dao) ExpandRecord(record *models.Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error { - return dao.ExpandRecords([]*models.Record{record}, expands, optFetchFunc) +func (app *BaseApp) ExpandRecord(record *Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error { + return app.ExpandRecords([]*Record{record}, expands, optFetchFunc) } // ExpandRecords expands the relations of the provided Record models list. @@ -40,13 +31,13 @@ func (dao *Dao) ExpandRecord(record *models.Record, expands []string, optFetchFu // that returns all relation records. // // Returns a map with the failed expand parameters and their errors. -func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error { +func (app *BaseApp) ExpandRecords(records []*Record, expands []string, optFetchFunc ExpandFetchFunc) map[string]error { normalized := normalizeExpands(expands) failed := map[string]error{} for _, expand := range normalized { - if err := dao.expandRecords(records, expand, optFetchFunc, 1); err != nil { + if err := app.expandRecords(records, expand, optFetchFunc, 1); err != nil { failed[expand] = err } } @@ -62,24 +53,23 @@ var indirectExpandRegex = regexp.MustCompile(`^(\w+)_via_(\w+)$`) // notes: // - if fetchFunc is nil, dao.FindRecordsByIds will be used // - all records are expected to be from the same collection -// - if MaxExpandDepth is reached, the function returns nil ignoring the remaining expand path -func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error { +// - if maxNestedRels(6) is reached, the function returns nil ignoring the remaining expand path +func (app *BaseApp) expandRecords(records []*Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error { if fetchFunc == nil { // load a default fetchFunc - fetchFunc = func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) { - return dao.FindRecordsByIds(relCollection.Id, relIds) + fetchFunc = func(relCollection *Collection, relIds []string) ([]*Record, error) { + return app.FindRecordsByIds(relCollection.Id, relIds) } } - if expandPath == "" || recursionLevel > MaxExpandDepth || len(records) == 0 { + if expandPath == "" || recursionLevel > maxNestedRels || len(records) == 0 { return nil } mainCollection := records[0].Collection() - var relField *schema.SchemaField - var relFieldOptions *schema.RelationOptions - var relCollection *models.Collection + var relField *RelationField + var relCollection *Collection parts := strings.SplitN(expandPath, ".", 2) var matches []string @@ -100,33 +90,27 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch } if len(matches) == 3 { - indirectRel, _ := dao.FindCollectionByNameOrId(matches[1]) + indirectRel, _ := getCollectionByModelOrIdentifier(app, matches[1]) if indirectRel == nil { return fmt.Errorf("couldn't find back-related collection %q", matches[1]) } - indirectRelField := indirectRel.Schema.GetFieldByName(matches[2]) - if indirectRelField == nil || indirectRelField.Type != schema.FieldTypeRelation { + indirectRelField, _ := indirectRel.Fields.GetByName(matches[2]).(*RelationField) + if indirectRelField == nil || indirectRelField.CollectionId != mainCollection.Id { return fmt.Errorf("couldn't find back-relation field %q in collection %q", matches[2], indirectRel.Name) } - indirectRelField.InitOptions() - indirectRelFieldOptions, _ := indirectRelField.Options.(*schema.RelationOptions) - if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id { - return fmt.Errorf("invalid back-relation field path %q", parts[0]) - } - // add the related id(s) as a dynamic relation field value to // allow further expand checks at later stage in a more unified manner prepErr := func() error { - q := dao.DB().Select("id"). + q := app.DB().Select("id"). From(indirectRel.Name). Limit(1000) // the limit is arbitrary chosen and may change in the future - if indirectRelFieldOptions.IsMultiple() { + if indirectRelField.IsMultiple() { q.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf( "SELECT 1 FROM %s je WHERE je.value = {:id}", - dbutils.JsonEach(indirectRelField.Name), + dbutils.JSONEach(indirectRelField.Name), )))) } else { q.AndWhere(dbx.NewExp("[[" + indirectRelField.Name + "]] = {:id}")) @@ -153,36 +137,26 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch return prepErr } - relFieldOptions = &schema.RelationOptions{ - MaxSelect: nil, + // indirect/back relation + relField = &RelationField{ + Name: parts[0], + MaxSelect: 2147483647, CollectionId: indirectRel.Id, } - if dbutils.HasSingleColumnUniqueIndex(indirectRelField.Name, indirectRel.Indexes) { - relFieldOptions.MaxSelect = types.Pointer(1) - } - // indirect/back relation - relField = &schema.SchemaField{ - Id: "_" + parts[0] + security.PseudorandomString(3), - Type: schema.FieldTypeRelation, - Name: parts[0], - Options: relFieldOptions, + if dbutils.HasSingleColumnUniqueIndex(indirectRelField.GetName(), indirectRel.Indexes) { + relField.MaxSelect = 1 } relCollection = indirectRel } else { // direct relation - relField = mainCollection.Schema.GetFieldByName(parts[0]) - if relField == nil || relField.Type != schema.FieldTypeRelation { - return fmt.Errorf("Couldn't find relation field %q in collection %q.", parts[0], mainCollection.Name) - } - relField.InitOptions() - relFieldOptions, _ = relField.Options.(*schema.RelationOptions) - if relFieldOptions == nil { - return fmt.Errorf("Couldn't initialize the options of relation field %q.", parts[0]) + relField, _ = mainCollection.Fields.GetByName(parts[0]).(*RelationField) + if relField == nil { + return fmt.Errorf("couldn't find relation field %q in collection %q", parts[0], mainCollection.Name) } - relCollection, _ = dao.FindCollectionByNameOrId(relFieldOptions.CollectionId) + relCollection, _ = getCollectionByModelOrIdentifier(app, relField.CollectionId) if relCollection == nil { - return fmt.Errorf("Couldn't find related collection %q.", relFieldOptions.CollectionId) + return fmt.Errorf("couldn't find related collection %q", relField.CollectionId) } } @@ -202,22 +176,28 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch // expand nested fields if len(parts) > 1 { - err := dao.expandRecords(rels, parts[1], fetchFunc, recursionLevel+1) + err := app.expandRecords(rels, parts[1], fetchFunc, recursionLevel+1) if err != nil { return err } } // reindex with the rel id - indexedRels := make(map[string]*models.Record, len(rels)) + indexedRels := make(map[string]*Record, len(rels)) for _, rel := range rels { - indexedRels[rel.GetId()] = rel + indexedRels[rel.Id] = rel } for _, model := range records { + // init expand if not already + // (this is done to ensure that the "expand" key will be returned in the response even if empty) + if model.expand == nil { + model.SetExpand(nil) + } + relIds := model.GetStringSlice(relField.Name) - validRels := make([]*models.Record, 0, len(relIds)) + validRels := make([]*Record, 0, len(relIds)) for _, id := range relIds { if rel, ok := indexedRels[id]; ok { validRels = append(validRels, rel) @@ -231,13 +211,13 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch expandData := model.Expand() // normalize access to the previously expanded rel records (if any) - var oldExpandedRels []*models.Record + var oldExpandedRels []*Record switch v := expandData[relField.Name].(type) { case nil: // no old expands - case *models.Record: - oldExpandedRels = []*models.Record{v} - case []*models.Record: + case *Record: + oldExpandedRels = []*Record{v} + case []*Record: oldExpandedRels = v } @@ -254,10 +234,10 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch } // update the expanded data - if relFieldOptions.MaxSelect != nil && *relFieldOptions.MaxSelect <= 1 { - expandData[relField.Name] = validRels[0] - } else { + if relField.IsMultiple() { expandData[relField.Name] = validRels + } else { + expandData[relField.Name] = validRels[0] } model.SetExpand(expandData) @@ -300,14 +280,3 @@ func normalizeExpands(paths []string) []string { return list.ToUniqueStringSlice(result) } - -func isRelFieldUnique(collection *models.Collection, fieldName string) bool { - for _, idx := range collection.Indexes { - parsed := dbutils.ParseIndex(idx) - if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, fieldName) { - return true - } - } - - return false -} diff --git a/daos/record_expand_test.go b/core/record_query_expand_test.go similarity index 53% rename from daos/record_expand_test.go rename to core/record_query_expand_test.go index dcb0da1f..04b5cbf1 100644 --- a/daos/record_expand_test.go +++ b/core/record_query_expand_test.go @@ -1,4 +1,4 @@ -package daos_test +package core_test import ( "encoding/json" @@ -6,9 +6,7 @@ import ( "strings" "testing" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" ) @@ -20,21 +18,21 @@ func TestExpandRecords(t *testing.T) { defer app.Cleanup() scenarios := []struct { - testName string - collectionIdOrName string - recordIds []string - expands []string - fetchFunc daos.ExpandFetchFunc - expectExpandProps int - expectExpandFailures int + testName string + collectionIdOrName string + recordIds []string + expands []string + fetchFunc core.ExpandFetchFunc + expectNonemptyExpandProps int + expectExpandFailures int }{ { "empty records", "", []string{}, []string{"self_rel_one", "self_rel_many.self_rel_one"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 0, @@ -44,8 +42,8 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"i9naidtvr6qsgb4", "qzaqccwrmva4o1n"}, []string{}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 0, @@ -55,7 +53,7 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"i9naidtvr6qsgb4", "qzaqccwrmva4o1n"}, []string{"self_rel_one", "self_rel_many.self_rel_one"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { + func(c *core.Collection, ids []string) ([]*core.Record, error) { return nil, errors.New("test error") }, 0, @@ -66,8 +64,8 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"i9naidtvr6qsgb4", "qzaqccwrmva4o1n"}, []string{"missing"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -77,8 +75,8 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"i9naidtvr6qsgb4", "qzaqccwrmva4o1n"}, []string{"title"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -88,8 +86,8 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"i9naidtvr6qsgb4", "qzaqccwrmva4o1n"}, []string{"rel_one_no_cascade.title"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -117,8 +115,8 @@ func TestExpandRecords(t *testing.T) { "self_rel_many", "self_rel_many.", " self_rel_many ", "", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 9, 0, @@ -132,8 +130,8 @@ func TestExpandRecords(t *testing.T) { "oap640cot4yru2s", // no rels }, []string{"rel"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 2, 0, @@ -156,8 +154,8 @@ func TestExpandRecords(t *testing.T) { "demo4", []string{"qzaqccwrmva4o1n"}, []string{"self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 6, 0, @@ -167,8 +165,8 @@ func TestExpandRecords(t *testing.T) { "demo3", []string{"lcl9d87w22ml6jy"}, []string{"demo4(rel_one_no_cascade_required)"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 1, 0, @@ -178,8 +176,8 @@ func TestExpandRecords(t *testing.T) { "demo3", []string{"lcl9d87w22ml6jy"}, []string{"demo4_via_rel_one_no_cascade_required"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 1, 0, @@ -191,8 +189,8 @@ func TestExpandRecords(t *testing.T) { []string{ "demo4_via_rel_one_no_cascade_required.self_rel_many.self_rel_many.self_rel_one", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 5, 0, @@ -204,8 +202,8 @@ func TestExpandRecords(t *testing.T) { []string{ "demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 7, 0, @@ -220,8 +218,8 @@ func TestExpandRecords(t *testing.T) { "self_rel_many.self_rel_one.rel_many_cascade", "self_rel_many.self_rel_one.rel_many_no_cascade_required", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 5, 0, @@ -229,21 +227,25 @@ func TestExpandRecords(t *testing.T) { } for _, s := range scenarios { - ids := list.ToUniqueStringSlice(s.recordIds) - records, _ := app.Dao().FindRecordsByIds(s.collectionIdOrName, ids) - failed := app.Dao().ExpandRecords(records, s.expands, s.fetchFunc) + t.Run(s.testName, func(t *testing.T) { + ids := list.ToUniqueStringSlice(s.recordIds) + records, _ := app.FindRecordsByIds(s.collectionIdOrName, ids) + failed := app.ExpandRecords(records, s.expands, s.fetchFunc) - if len(failed) != s.expectExpandFailures { - t.Errorf("[%s] Expected %d failures, got %d: \n%v", s.testName, s.expectExpandFailures, len(failed), failed) - } + if len(failed) != s.expectExpandFailures { + t.Errorf("Expected %d failures, got %d\n%v", s.expectExpandFailures, len(failed), failed) + } - encoded, _ := json.Marshal(records) - encodedStr := string(encoded) - totalExpandProps := strings.Count(encodedStr, schema.FieldNameExpand) + encoded, _ := json.Marshal(records) + encodedStr := string(encoded) + totalExpandProps := strings.Count(encodedStr, `"`+core.FieldNameExpand+`":`) + totalEmptyExpands := strings.Count(encodedStr, `"`+core.FieldNameExpand+`":{}`) + totalNonemptyExpands := totalExpandProps - totalEmptyExpands - if s.expectExpandProps != totalExpandProps { - t.Errorf("[%s] Expected %d expand props, got %d: \n%v", s.testName, s.expectExpandProps, totalExpandProps, encodedStr) - } + if s.expectNonemptyExpandProps != totalNonemptyExpands { + t.Errorf("Expected %d expand props, got %d\n%v", s.expectNonemptyExpandProps, totalNonemptyExpands, encodedStr) + } + }) } } @@ -254,21 +256,21 @@ func TestExpandRecord(t *testing.T) { defer app.Cleanup() scenarios := []struct { - testName string - collectionIdOrName string - recordId string - expands []string - fetchFunc daos.ExpandFetchFunc - expectExpandProps int - expectExpandFailures int + testName string + collectionIdOrName string + recordId string + expands []string + fetchFunc core.ExpandFetchFunc + expectNonemptyExpandProps int + expectExpandFailures int }{ { "empty expand", "demo4", "i9naidtvr6qsgb4", []string{}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 0, @@ -278,7 +280,7 @@ func TestExpandRecord(t *testing.T) { "demo4", "i9naidtvr6qsgb4", []string{"self_rel_one", "self_rel_many.self_rel_one"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { + func(c *core.Collection, ids []string) ([]*core.Record, error) { return nil, errors.New("test error") }, 0, @@ -289,8 +291,8 @@ func TestExpandRecord(t *testing.T) { "demo4", "i9naidtvr6qsgb4", []string{"missing"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -300,8 +302,8 @@ func TestExpandRecord(t *testing.T) { "demo4", "i9naidtvr6qsgb4", []string{"title"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -311,8 +313,8 @@ func TestExpandRecord(t *testing.T) { "demo4", "qzaqccwrmva4o1n", []string{"rel_one_no_cascade.title"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 1, @@ -327,8 +329,8 @@ func TestExpandRecord(t *testing.T) { "self_rel_many", "self_rel_many.", " self_rel_many ", "", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 8, 0, @@ -338,8 +340,8 @@ func TestExpandRecord(t *testing.T) { "users", "oap640cot4yru2s", []string{"rel"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 0, 0, @@ -349,8 +351,8 @@ func TestExpandRecord(t *testing.T) { "demo4", "qzaqccwrmva4o1n", []string{"self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 6, 0, @@ -360,8 +362,8 @@ func TestExpandRecord(t *testing.T) { "demo3", "lcl9d87w22ml6jy", []string{"demo4(rel_one_no_cascade_required)"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 1, 0, @@ -371,8 +373,8 @@ func TestExpandRecord(t *testing.T) { "demo3", "lcl9d87w22ml6jy", []string{"demo4_via_rel_one_no_cascade_required"}, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 1, 0, @@ -384,8 +386,8 @@ func TestExpandRecord(t *testing.T) { []string{ "demo4(rel_one_no_cascade_required).self_rel_many.self_rel_many.self_rel_one", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 5, 0, @@ -397,8 +399,8 @@ func TestExpandRecord(t *testing.T) { []string{ "demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required", }, - func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }, 7, 0, @@ -406,44 +408,48 @@ func TestExpandRecord(t *testing.T) { } for _, s := range scenarios { - record, _ := app.Dao().FindRecordById(s.collectionIdOrName, s.recordId) - failed := app.Dao().ExpandRecord(record, s.expands, s.fetchFunc) + t.Run(s.testName, func(t *testing.T) { + record, _ := app.FindRecordById(s.collectionIdOrName, s.recordId) + failed := app.ExpandRecord(record, s.expands, s.fetchFunc) - if len(failed) != s.expectExpandFailures { - t.Errorf("[%s] Expected %d failures, got %d: \n%v", s.testName, s.expectExpandFailures, len(failed), failed) - } + if len(failed) != s.expectExpandFailures { + t.Errorf("Expected %d failures, got %d\n%v", s.expectExpandFailures, len(failed), failed) + } - encoded, _ := json.Marshal(record) - encodedStr := string(encoded) - totalExpandProps := strings.Count(encodedStr, schema.FieldNameExpand) + encoded, _ := json.Marshal(record) + encodedStr := string(encoded) + totalExpandProps := strings.Count(encodedStr, `"`+core.FieldNameExpand+`":`) + totalEmptyExpands := strings.Count(encodedStr, `"`+core.FieldNameExpand+`":{}`) + totalNonemptyExpands := totalExpandProps - totalEmptyExpands - if s.expectExpandProps != totalExpandProps { - t.Errorf("[%s] Expected %d expand props, got %d: \n%v", s.testName, s.expectExpandProps, totalExpandProps, encodedStr) - } + if s.expectNonemptyExpandProps != totalNonemptyExpands { + t.Errorf("Expected %d expand props, got %d\n%v", s.expectNonemptyExpandProps, totalNonemptyExpands, encodedStr) + } + }) } } -func TestIndirectExpandSingeVsArrayResult(t *testing.T) { +func TestBackRelationExpandSingeVsArrayResult(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() - record, err := app.Dao().FindRecordById("demo3", "7nwo8tuiatetxdm") + record, err := app.FindRecordById("demo3", "7nwo8tuiatetxdm") if err != nil { t.Fatal(err) } // non-unique indirect expand { - errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + errs := app.ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }) if len(errs) > 0 { t.Fatal(errs) } - result, ok := record.Expand()["demo4_via_rel_one_cascade"].([]*models.Record) + result, ok := record.Expand()["demo4_via_rel_one_cascade"].([]*core.Record) if !ok { t.Fatalf("Expected the expanded result to be a slice, got %v", result) } @@ -453,26 +459,26 @@ func TestIndirectExpandSingeVsArrayResult(t *testing.T) { { // mock a unique constraint for the rel_one_cascade field // --- - demo4, err := app.Dao().FindCollectionByNameOrId("demo4") + demo4, err := app.FindCollectionByNameOrId("demo4") if err != nil { t.Fatal(err) } demo4.Indexes = append(demo4.Indexes, "create unique index idx_unique_expand on demo4 (rel_one_cascade)") - if err := app.Dao().SaveCollection(demo4); err != nil { + if err := app.Save(demo4); err != nil { t.Fatalf("Failed to mock unique constraint: %v", err) } // --- - errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) { - return app.Dao().FindRecordsByIds(c.Id, ids, nil) + errs := app.ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *core.Collection, ids []string) ([]*core.Record, error) { + return app.FindRecordsByIds(c.Id, ids, nil) }) if len(errs) > 0 { t.Fatal(errs) } - result, ok := record.Expand()["demo4_via_rel_one_cascade"].(*models.Record) + result, ok := record.Expand()["demo4_via_rel_one_cascade"].(*core.Record) if !ok { t.Fatalf("Expected the expanded result to be a single model, got %v", result) } diff --git a/core/record_query_test.go b/core/record_query_test.go new file mode 100644 index 00000000..85a8824d --- /dev/null +++ b/core/record_query_test.go @@ -0,0 +1,1143 @@ +package core_test + +import ( + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestRecordQueryWithDifferentCollectionValues(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + collection any + expectedTotal int + expectError bool + }{ + {"with nil value", nil, 0, true}, + {"with invalid or missing collection id/name", "missing", 0, true}, + {"with pointer model", collection, 3, false}, + {"with value model", *collection, 3, false}, + {"with name", "demo1", 3, false}, + {"with id", "wsmn24bux7wo113", 3, false}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + var records []*core.Record + err := app.RecordQuery(s.collection).All(&records) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasError %v, got %v", s.expectError, hasErr) + } + + if total := len(records); total != s.expectedTotal { + t.Fatalf("Expected %d records, got %d", s.expectedTotal, total) + } + }) + } +} + +func TestRecordQueryOne(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + collection string + recordId string + model core.Model + }{ + { + "record model", + "demo1", + "84nmscqy84lsi1t", + &core.Record{}, + }, + { + "record proxy", + "demo1", + "84nmscqy84lsi1t", + &struct { + core.BaseRecordProxy + }{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection, err := app.FindCollectionByNameOrId(s.collection) + if err != nil { + t.Fatal(err) + } + + q := app.RecordQuery(collection). + Where(dbx.HashExp{"id": s.recordId}) + + if err := q.One(s.model); err != nil { + t.Fatal(err) + } + + if s.model.PK() != s.recordId { + t.Fatalf("Expected record with id %q, got %q", s.recordId, s.model.PK()) + } + }) + } +} + +func TestRecordQueryAll(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + type mockRecordProxy struct { + core.BaseRecordProxy + } + + scenarios := []struct { + name string + collection string + recordIds []any + result any + }{ + { + "slice of Record models", + "demo1", + []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, + &[]core.Record{}, + }, + { + "slice of pointer Record models", + "demo1", + []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, + &[]*core.Record{}, + }, + { + "slice of Record proxies", + "demo1", + []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, + &[]mockRecordProxy{}, + }, + { + "slice of pointer Record proxies", + "demo1", + []any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"}, + &[]mockRecordProxy{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + collection, err := app.FindCollectionByNameOrId(s.collection) + if err != nil { + t.Fatal(err) + } + + q := app.RecordQuery(collection). + Where(dbx.HashExp{"id": s.recordIds}) + + if err := q.All(s.result); err != nil { + t.Fatal(err) + } + + raw, err := json.Marshal(s.result) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + sliceOfMaps := []any{} + if err := json.Unmarshal(raw, &sliceOfMaps); err != nil { + t.Fatal(err) + } + + if len(sliceOfMaps) != len(s.recordIds) { + t.Fatalf("Expected %d items, got %d", len(s.recordIds), len(sliceOfMaps)) + } + + for _, id := range s.recordIds { + if !strings.Contains(rawStr, fmt.Sprintf(`"id":%q`, id)) { + t.Fatalf("Missing id %q in\n%s", id, rawStr) + } + } + }) + } +} + +func TestFindRecordById(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionIdOrName string + id string + filters []func(q *dbx.SelectQuery) error + expectError bool + }{ + {"demo2", "missing", nil, true}, + {"missing", "0yxhwia2amd8gec", nil, true}, + {"demo2", "0yxhwia2amd8gec", nil, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{}, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{nil, nil}, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + nil, + func(q *dbx.SelectQuery) error { return nil }, + }, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "missing"}) + return nil + }, + }, true}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + return errors.New("test error") + }, + }, true}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "test3"}) + return nil + }, + }, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "test3"}) + return nil + }, + nil, + }, false}, + {"demo2", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "test3"}) + return nil + }, + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"active": false}) + return nil + }, + }, true}, + {"sz5l5z67tg7gku0", "0yxhwia2amd8gec", []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "test3"}) + return nil + }, + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"active": true}) + return nil + }, + }, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s_%d", i, s.collectionIdOrName, s.id, len(s.filters)), func(t *testing.T) { + record, err := app.FindRecordById( + s.collectionIdOrName, + s.id, + s.filters..., + ) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if record != nil && record.Id != s.id { + t.Fatalf("Expected record with id %s, got %s", s.id, record.Id) + } + }) + } +} + +func TestFindRecordsByIds(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionIdOrName string + ids []string + filters []func(q *dbx.SelectQuery) error + expectTotal int + expectError bool + }{ + {"demo2", []string{}, nil, 0, false}, + {"demo2", []string{""}, nil, 0, false}, + {"demo2", []string{"missing"}, nil, 0, false}, + {"missing", []string{"0yxhwia2amd8gec"}, nil, 0, true}, + {"demo2", []string{"0yxhwia2amd8gec"}, nil, 1, false}, + {"sz5l5z67tg7gku0", []string{"0yxhwia2amd8gec"}, nil, 1, false}, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + nil, + 2, + false, + }, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{}, + 2, + false, + }, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{nil, nil}, + 2, + false, + }, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + return nil // empty filter + }, + }, + 2, + false, + }, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + return nil // empty filter + }, + func(q *dbx.SelectQuery) error { + return errors.New("test error") + }, + }, + 0, + true, + }, + { + "demo2", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"active": true}) + return nil + }, + nil, + }, + 1, + false, + }, + { + "sz5l5z67tg7gku0", + []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, + []func(q *dbx.SelectQuery) error{ + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"active": true}) + return nil + }, + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.Not(dbx.HashExp{"title": ""})) + return nil + }, + }, + 1, + false, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%v_%d", i, s.collectionIdOrName, s.ids, len(s.filters)), func(t *testing.T) { + records, err := app.FindRecordsByIds( + s.collectionIdOrName, + s.ids, + s.filters..., + ) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if len(records) != s.expectTotal { + t.Fatalf("Expected %d records, got %d", s.expectTotal, len(records)) + } + + for _, r := range records { + if !slices.Contains(s.ids, r.Id) { + t.Fatalf("Couldn't find id %s in %v", r.Id, s.ids) + } + } + }) + } +} + +func TestFindAllRecords(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionIdOrName string + expressions []dbx.Expression + expectIds []string + expectError bool + }{ + { + "missing", + nil, + []string{}, + true, + }, + { + "demo2", + nil, + []string{ + "achvryl401bhse3", + "llvuca81nly1qls", + "0yxhwia2amd8gec", + }, + false, + }, + { + "demo2", + []dbx.Expression{ + nil, + dbx.HashExp{"id": "123"}, + }, + []string{}, + false, + }, + { + "sz5l5z67tg7gku0", + []dbx.Expression{ + dbx.Like("title", "test").Match(true, true), + dbx.HashExp{"active": true}, + }, + []string{ + "achvryl401bhse3", + "0yxhwia2amd8gec", + }, + false, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.collectionIdOrName), func(t *testing.T) { + records, err := app.FindAllRecords(s.collectionIdOrName, s.expressions...) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if len(records) != len(s.expectIds) { + t.Fatalf("Expected %d records, got %d", len(s.expectIds), len(records)) + } + + for _, r := range records { + if !slices.Contains(s.expectIds, r.Id) { + t.Fatalf("Couldn't find id %s in %v", r.Id, s.expectIds) + } + } + }) + } +} + +func TestFindFirstRecordByData(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionIdOrName string + key string + value any + expectId string + expectError bool + }{ + { + "missing", + "id", + "llvuca81nly1qls", + "llvuca81nly1qls", + true, + }, + { + "demo2", + "", + "llvuca81nly1qls", + "", + true, + }, + { + "demo2", + "id", + "invalid", + "", + true, + }, + { + "demo2", + "id", + "llvuca81nly1qls", + "llvuca81nly1qls", + false, + }, + { + "sz5l5z67tg7gku0", + "title", + "test3", + "0yxhwia2amd8gec", + false, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s_%v", i, s.collectionIdOrName, s.key, s.value), func(t *testing.T) { + record, err := app.FindFirstRecordByData(s.collectionIdOrName, s.key, s.value) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if !s.expectError && record.Id != s.expectId { + t.Fatalf("Expected record with id %s, got %v", s.expectId, record.Id) + } + }) + } +} + +func TestFindRecordsByFilter(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + collectionIdOrName string + filter string + sort string + limit int + offset int + params []dbx.Params + expectError bool + expectRecordIds []string + }{ + { + "missing collection", + "missing", + "id != ''", + "", + 0, + 0, + nil, + true, + nil, + }, + { + "invalid filter", + "demo2", + "someMissingField > 1", + "", + 0, + 0, + nil, + true, + nil, + }, + { + "empty filter", + "demo2", + "", + "", + 0, + 0, + nil, + false, + []string{ + "llvuca81nly1qls", + "achvryl401bhse3", + "0yxhwia2amd8gec", + }, + }, + { + "simple filter", + "demo2", + "id != ''", + "", + 0, + 0, + nil, + false, + []string{ + "llvuca81nly1qls", + "achvryl401bhse3", + "0yxhwia2amd8gec", + }, + }, + { + "multi-condition filter with sort", + "demo2", + "id != '' && active=true", + "-created,title", + -1, // should behave the same as 0 + 0, + nil, + false, + []string{ + "0yxhwia2amd8gec", + "achvryl401bhse3", + }, + }, + { + "with limit and offset", + "sz5l5z67tg7gku0", + "id != ''", + "title", + 2, + 1, + nil, + false, + []string{ + "achvryl401bhse3", + "0yxhwia2amd8gec", + }, + }, + { + "with placeholder params", + "demo2", + "active = {:active}", + "", + 10, + 0, + []dbx.Params{{"active": false}}, + false, + []string{ + "llvuca81nly1qls", + }, + }, + { + "with json filter and sort", + "demo4", + "json_object != null && json_object.a.b = 'test'", + "-json_object.a", + 10, + 0, + []dbx.Params{{"active": false}}, + false, + []string{ + "i9naidtvr6qsgb4", + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + records, err := app.FindRecordsByFilter( + s.collectionIdOrName, + s.filter, + s.sort, + s.limit, + s.offset, + s.params..., + ) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if len(records) != len(s.expectRecordIds) { + t.Fatalf("Expected %d records, got %d", len(s.expectRecordIds), len(records)) + } + + for i, id := range s.expectRecordIds { + if id != records[i].Id { + t.Fatalf("Expected record with id %q, got %q at index %d", id, records[i].Id, i) + } + } + }) + } +} + +func TestFindFirstRecordByFilter(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + collectionIdOrName string + filter string + params []dbx.Params + expectError bool + expectRecordId string + }{ + { + "missing collection", + "missing", + "id != ''", + nil, + true, + "", + }, + { + "invalid filter", + "demo2", + "someMissingField > 1", + nil, + true, + "", + }, + { + "empty filter", + "demo2", + "", + nil, + false, + "llvuca81nly1qls", + }, + { + "valid filter but no matches", + "demo2", + "id = 'test'", + nil, + true, + "", + }, + { + "valid filter and multiple matches", + "sz5l5z67tg7gku0", + "id != ''", + nil, + false, + "llvuca81nly1qls", + }, + { + "with placeholder params", + "demo2", + "active = {:active}", + []dbx.Params{{"active": false}}, + false, + "llvuca81nly1qls", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + record, err := app.FindFirstRecordByFilter(s.collectionIdOrName, s.filter, s.params...) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if record.Id != s.expectRecordId { + t.Fatalf("Expected record with id %q, got %q", s.expectRecordId, record.Id) + } + }) + } +} + +func TestCountRecords(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + collectionIdOrName string + expressions []dbx.Expression + expectTotal int64 + expectError bool + }{ + { + "missing collection", + "missing", + nil, + 0, + true, + }, + { + "valid collection name", + "demo2", + nil, + 3, + false, + }, + { + "valid collection id", + "sz5l5z67tg7gku0", + nil, + 3, + false, + }, + { + "nil expression", + "demo2", + []dbx.Expression{nil}, + 3, + false, + }, + { + "no matches", + "demo2", + []dbx.Expression{ + nil, + dbx.Like("title", "missing"), + dbx.HashExp{"active": true}, + }, + 0, + false, + }, + { + "with matches", + "demo2", + []dbx.Expression{ + nil, + dbx.Like("title", "test"), + dbx.HashExp{"active": true}, + }, + 2, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + total, err := app.CountRecords(s.collectionIdOrName, s.expressions...) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if total != s.expectTotal { + t.Fatalf("Expected total %d, got %d", s.expectTotal, total) + } + }) + } +} + +func TestFindAuthRecordByToken(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + token string + types []string + expectedId string + }{ + { + "empty token", + "", + nil, + "", + }, + { + "invalid token", + "invalid", + nil, + "", + }, + { + "expired token", + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.2D3tmqPn3vc5LoqqCz8V-iCDVXo9soYiH0d32G7FQT4", + nil, + "", + }, + { + "valid auth token", + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + nil, + "4q1xlclmfloku33", + }, + { + "valid verification token", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6InZlcmlmaWNhdGlvbiIsImNvbGxlY3Rpb25JZCI6ImtwdjcwOXNrMmxxYnFrOCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.5GmuZr4vmwk3Cb_3ZZWNxwbE75KZC-j71xxIPR9AsVw", + nil, + "dc49k6jgejn40h3", + }, + { + "auth token with file type only check", + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + []string{core.TokenTypeFile}, + "", + }, + { + "auth token with file and auth type check", + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo", + []string{core.TokenTypeFile, core.TokenTypeAuth}, + "4q1xlclmfloku33", + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + record, err := app.FindAuthRecordByToken(s.token, s.types...) + + hasErr := err != nil + expectErr := s.expectedId == "" + if hasErr != expectErr { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", expectErr, hasErr, err) + } + + if hasErr { + return + } + + if record.Id != s.expectedId { + t.Fatalf("Expected record with id %q, got %q", s.expectedId, record.Id) + } + }) + } +} + +func TestFindAuthRecordByEmail(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + collectionIdOrName string + email string + expectError bool + }{ + {"missing", "test@example.com", true}, + {"demo2", "test@example.com", true}, + {"users", "missing@example.com", true}, + {"users", "test@example.com", false}, + {"clients", "test2@example.com", false}, + } + + for _, s := range scenarios { + t.Run(fmt.Sprintf("%s_%s", s.collectionIdOrName, s.email), func(t *testing.T) { + record, err := app.FindAuthRecordByEmail(s.collectionIdOrName, s.email) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if record.Email() != s.email { + t.Fatalf("Expected record with email %s, got %s", s.email, record.Email()) + } + }) + } +} + +func TestCanAccessRecord(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + record, err := app.FindRecordById("demo1", "imy661ixudk5izi") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + name string + record *core.Record + requestInfo *core.RequestInfo + rule *string + expected bool + expectError bool + }{ + { + "as superuser with nil rule", + record, + &core.RequestInfo{ + Auth: superuser, + }, + nil, + true, + false, + }, + { + "as superuser with non-empty rule", + record, + &core.RequestInfo{ + Auth: superuser, + }, + types.Pointer("id = ''"), // the filter rule should be ignored + true, + false, + }, + { + "as superuser with invalid rule", + record, + &core.RequestInfo{ + Auth: superuser, + }, + types.Pointer("id ?!@ 1"), // the filter rule should be ignored + true, + false, + }, + { + "as guest with nil rule", + record, + &core.RequestInfo{}, + nil, + false, + false, + }, + { + "as guest with empty rule", + record, + &core.RequestInfo{}, + types.Pointer(""), + true, + false, + }, + { + "as guest with invalid rule", + record, + &core.RequestInfo{}, + types.Pointer("id ?!@ 1"), + false, + true, + }, + { + "as guest with mismatched rule", + record, + &core.RequestInfo{}, + types.Pointer("@request.auth.id != ''"), + false, + false, + }, + { + "as guest with matched rule", + record, + &core.RequestInfo{ + Body: map[string]any{"test": 1}, + }, + types.Pointer("@request.auth.id != '' || @request.body.test = 1"), + true, + false, + }, + { + "as auth record with nil rule", + record, + &core.RequestInfo{ + Auth: user, + }, + nil, + false, + false, + }, + { + "as auth record with empty rule", + record, + &core.RequestInfo{ + Auth: user, + }, + types.Pointer(""), + true, + false, + }, + { + "as auth record with invalid rule", + record, + &core.RequestInfo{ + Auth: user, + }, + types.Pointer("id ?!@ 1"), + false, + true, + }, + { + "as auth record with mismatched rule", + record, + &core.RequestInfo{ + Auth: user, + Body: map[string]any{"test": 1}, + }, + types.Pointer("@request.auth.id != '' && @request.body.test > 1"), + false, + false, + }, + { + "as auth record with matched rule", + record, + &core.RequestInfo{ + Auth: user, + Body: map[string]any{"test": 2}, + }, + types.Pointer("@request.auth.id != '' && @request.body.test > 1"), + true, + false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result, err := app.CanAccessRecord(s.record, s.requestInfo, s.rule) + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} diff --git a/core/record_tokens.go b/core/record_tokens.go new file mode 100644 index 00000000..e1bf7a95 --- /dev/null +++ b/core/record_tokens.go @@ -0,0 +1,165 @@ +package core + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/pocketbase/pocketbase/tools/security" +) + +// Supported record token types +const ( + TokenTypeAuth = "auth" + TokenTypeFile = "file" + TokenTypeVerification = "verification" + TokenTypePasswordReset = "passwordReset" + TokenTypeEmailChange = "emailChange" +) + +// List with commonly used record token claims +const ( + TokenClaimId = "id" + TokenClaimType = "type" + TokenClaimCollectionId = "collectionId" + TokenClaimEmail = "email" + TokenClaimNewEmail = "newEmail" + TokenClaimRefreshable = "refreshable" +) + +// Common token related errors +var ( + ErrNotAuthRecord = errors.New("not an auth collection record") + ErrMissingSigningKey = errors.New("missing or invalid signing key") +) + +// NewStaticAuthToken generates and returns a new static record authentication token. +// +// Static auth tokens are similar to the regular auth tokens, but are +// non-refreshable and support custom duration. +// +// Zero or negative duration will fallback to the duration from the auth collection settings. +func (m *Record) NewStaticAuthToken(duration time.Duration) (string, error) { + return m.newAuthToken(duration, false) +} + +// NewAuthToken generates and returns a new record authentication token. +func (m *Record) NewAuthToken() (string, error) { + return m.newAuthToken(0, true) +} + +func (m *Record) newAuthToken(duration time.Duration, refreshable bool) (string, error) { + if !m.Collection().IsAuth() { + return "", ErrNotAuthRecord + } + + key := (m.TokenKey() + m.Collection().AuthToken.Secret) + if key == "" { + return "", ErrMissingSigningKey + } + + claims := jwt.MapClaims{ + TokenClaimType: TokenTypeAuth, + TokenClaimId: m.Id, + TokenClaimCollectionId: m.Collection().Id, + TokenClaimRefreshable: refreshable, + } + + if duration <= 0 { + duration = m.Collection().AuthToken.DurationTime() + } + + return security.NewJWT(claims, key, duration) +} + +// NewVerificationToken generates and returns a new record verification token. +func (m *Record) NewVerificationToken() (string, error) { + if !m.Collection().IsAuth() { + return "", ErrNotAuthRecord + } + + key := (m.TokenKey() + m.Collection().VerificationToken.Secret) + if key == "" { + return "", ErrMissingSigningKey + } + + return security.NewJWT( + jwt.MapClaims{ + TokenClaimType: TokenTypeVerification, + TokenClaimId: m.Id, + TokenClaimCollectionId: m.Collection().Id, + TokenClaimEmail: m.Email(), + }, + key, + m.Collection().VerificationToken.DurationTime(), + ) +} + +// NewPasswordResetToken generates and returns a new auth record password reset request token. +func (m *Record) NewPasswordResetToken() (string, error) { + if !m.Collection().IsAuth() { + return "", ErrNotAuthRecord + } + + key := (m.TokenKey() + m.Collection().PasswordResetToken.Secret) + if key == "" { + return "", ErrMissingSigningKey + } + + return security.NewJWT( + jwt.MapClaims{ + TokenClaimType: TokenTypePasswordReset, + TokenClaimId: m.Id, + TokenClaimCollectionId: m.Collection().Id, + TokenClaimEmail: m.Email(), + }, + key, + m.Collection().PasswordResetToken.DurationTime(), + ) +} + +// NewEmailChangeToken generates and returns a new auth record change email request token. +func (m *Record) NewEmailChangeToken(newEmail string) (string, error) { + if !m.Collection().IsAuth() { + return "", ErrNotAuthRecord + } + + key := (m.TokenKey() + m.Collection().EmailChangeToken.Secret) + if key == "" { + return "", ErrMissingSigningKey + } + + return security.NewJWT( + jwt.MapClaims{ + TokenClaimType: TokenTypeEmailChange, + TokenClaimId: m.Id, + TokenClaimCollectionId: m.Collection().Id, + TokenClaimEmail: m.Email(), + TokenClaimNewEmail: newEmail, + }, + key, + m.Collection().EmailChangeToken.DurationTime(), + ) +} + +// NewFileToken generates and returns a new record private file access token. +func (m *Record) NewFileToken() (string, error) { + if !m.Collection().IsAuth() { + return "", ErrNotAuthRecord + } + + key := (m.TokenKey() + m.Collection().FileToken.Secret) + if key == "" { + return "", ErrMissingSigningKey + } + + return security.NewJWT( + jwt.MapClaims{ + TokenClaimType: TokenTypeFile, + TokenClaimId: m.Id, + TokenClaimCollectionId: m.Collection().Id, + }, + key, + m.Collection().FileToken.DurationTime(), + ) +} diff --git a/core/record_tokens_test.go b/core/record_tokens_test.go new file mode 100644 index 00000000..34ad909a --- /dev/null +++ b/core/record_tokens_test.go @@ -0,0 +1,176 @@ +package core_test + +import ( + "fmt" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +func TestNewStaticAuthToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypeAuth, func(record *core.Record) (string, error) { + return record.NewStaticAuthToken(0) + }, map[string]any{ + core.TokenClaimRefreshable: false, + }) +} + +func TestNewStaticAuthTokenWithCustomDuration(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + var tolerance int64 = 1 // in sec + + durations := []int64{-100, 0, 100} + + for i, d := range durations { + t.Run(fmt.Sprintf("%d_%d", i, d), func(t *testing.T) { + now := time.Now() + + duration := time.Duration(d) * time.Second + + token, err := user.NewStaticAuthToken(duration) + if err != nil { + t.Fatal(err) + } + + claims, err := security.ParseUnverifiedJWT(token) + if err != nil { + t.Fatal(err) + } + + exp := cast.ToInt64(claims["exp"]) + + expectedDuration := duration + // should fallback to the collection setting + if expectedDuration <= 0 { + expectedDuration = user.Collection().AuthToken.DurationTime() + } + expectedMinExp := now.Add(expectedDuration).Unix() - tolerance + expectedMaxExp := now.Add(expectedDuration).Unix() + tolerance + + if exp < expectedMinExp { + t.Fatalf("Expected token exp to be greater than %d, got %d", expectedMinExp, exp) + } + + if exp > expectedMaxExp { + t.Fatalf("Expected token exp to be less than %d, got %d", expectedMaxExp, exp) + } + }) + } +} + +func TestNewAuthToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypeAuth, func(record *core.Record) (string, error) { + return record.NewAuthToken() + }, map[string]any{ + core.TokenClaimRefreshable: true, + }) +} + +func TestNewVerificationToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypeVerification, func(record *core.Record) (string, error) { + return record.NewVerificationToken() + }, nil) +} + +func TestNewPasswordResetToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypePasswordReset, func(record *core.Record) (string, error) { + return record.NewPasswordResetToken() + }, nil) +} + +func TestNewEmailChangeToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypeEmailChange, func(record *core.Record) (string, error) { + return record.NewEmailChangeToken("new@example.com") + }, nil) +} + +func TestNewFileToken(t *testing.T) { + t.Parallel() + + testRecordToken(t, core.TokenTypeFile, func(record *core.Record) (string, error) { + return record.NewFileToken() + }, nil) +} + +func testRecordToken( + t *testing.T, + tokenType string, + tokenFunc func(record *core.Record) (string, error), + expectedClaims map[string]any, +) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t") + if err != nil { + t.Fatal(err) + } + + user, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + t.Fatal(err) + } + + t.Run("non-auth record", func(t *testing.T) { + _, err = tokenFunc(demo1) + if err == nil { + t.Fatal("Expected error for non-auth records") + } + }) + + t.Run("auth record", func(t *testing.T) { + token, err := tokenFunc(user) + if err != nil { + t.Fatal(err) + } + + tokenRecord, _ := app.FindAuthRecordByToken(token, tokenType) + if tokenRecord == nil || tokenRecord.Id != user.Id { + t.Fatalf("Expected auth record\n%v\ngot\n%v", user, tokenRecord) + } + + if len(expectedClaims) > 0 { + claims, _ := security.ParseUnverifiedJWT(token) + for k, v := range expectedClaims { + if claims[k] != v { + t.Errorf("Expected claim %q with value %#v, got %#v", k, v, claims[k]) + } + } + } + }) + + t.Run("empty signing key", func(t *testing.T) { + user.SetTokenKey("") + collection := user.Collection() + *collection = core.Collection{} + collection.Type = core.CollectionTypeAuth + + _, err := tokenFunc(user) + if err == nil { + t.Fatal("Expected empty signing key error") + } + }) +} diff --git a/core/settings_model.go b/core/settings_model.go new file mode 100644 index 00000000..c918371a --- /dev/null +++ b/core/settings_model.go @@ -0,0 +1,675 @@ +package core + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/cron" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" +) + +const ( + paramsTable = "_params" + + paramsKeySettings = "settings" + + systemHookIdSettings = "__pbSettingsSystemHook__" +) + +func (app *BaseApp) registerSettingsHooks() { + saveFunc := func(me *ModelEvent) error { + if err := me.Next(); err != nil { + return err + } + + if me.Model.PK() == paramsKeySettings { + // auto reload the app settings because we don't know whether + // the Settings model is the app one or a different one + return errors.Join( + me.App.Settings().PostScan(), + me.App.ReloadSettings(), + ) + } + + return nil + } + + app.OnModelAfterCreateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdSettings, + Func: saveFunc, + Priority: -999, + }) + + app.OnModelAfterUpdateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdSettings, + Func: saveFunc, + Priority: -999, + }) + + app.OnModelDelete(paramsTable).Bind(&hook.Handler[*ModelEvent]{ + Id: systemHookIdSettings, + Func: func(me *ModelEvent) error { + if me.Model.PK() == paramsKeySettings { + return errors.New("the app params settings cannot be deleted") + } + + return me.Next() + }, + Priority: -999, + }) + + app.OnCollectionUpdate().Bind(&hook.Handler[*CollectionEvent]{ + Id: systemHookIdSettings, + Func: func(e *CollectionEvent) error { + oldCollection, err := e.App.FindCachedCollectionByNameOrId(e.Collection.Id) + if err != nil { + return fmt.Errorf("failed to retrieve old cached collection: %w", err) + } + + err = e.Next() + if err != nil { + return err + } + + // update existing rate limit rules on collection rename + if oldCollection.Name != e.Collection.Name { + var hasChange bool + + rules := e.App.Settings().RateLimits.Rules + for i := 0; i < len(rules); i++ { + if strings.HasPrefix(rules[i].Label, oldCollection.Name+":") { + rules[i].Label = strings.Replace(rules[i].Label, oldCollection.Name+":", e.Collection.Name+":", 1) + hasChange = true + } + } + + if hasChange { + e.App.Settings().RateLimits.Rules = rules + err = e.App.Save(e.App.Settings()) + if err != nil { + return err + } + } + } + + return nil + }, + Priority: 99, + }) +} + +var ( + _ Model = (*Settings)(nil) + _ PostValidator = (*Settings)(nil) + _ DBExporter = (*Settings)(nil) +) + +type settings struct { + SMTP SMTPConfig `form:"smtp" json:"smtp"` + Backups BackupsConfig `form:"backups" json:"backups"` + S3 S3Config `form:"s3" json:"s3"` + Meta MetaConfig `form:"meta" json:"meta"` + Logs LogsConfig `form:"logs" json:"logs"` + Batch BatchConfig `form:"batch" json:"batch"` + RateLimits RateLimitsConfig `form:"rateLimits" json:"rateLimits"` + TrustedProxy TrustedProxyConfig `form:"trustedProxy" json:"trustedProxy"` +} + +// Settings defines the PocketBase app settings. +type Settings struct { + settings + + mu sync.RWMutex + isNew bool +} + +func newDefaultSettings() *Settings { + return &Settings{ + isNew: true, + settings: settings{ + Meta: MetaConfig{ + AppName: "Acme", + AppURL: "http://localhost:8090", + HideControls: false, + SenderName: "Support", + SenderAddress: "support@example.com", + }, + Logs: LogsConfig{ + MaxDays: 5, + LogIP: true, + }, + SMTP: SMTPConfig{ + Enabled: false, + Host: "smtp.example.com", + Port: 587, + Username: "", + Password: "", + TLS: false, + }, + Backups: BackupsConfig{ + CronMaxKeep: 3, + }, + Batch: BatchConfig{ + Enabled: false, + MaxRequests: 50, + Timeout: 3, + }, + RateLimits: RateLimitsConfig{ + Enabled: false, // @todo once tested enough enable by default for new installations + Rules: []RateLimitRule{ + {Label: "*:auth", MaxRequests: 2, Duration: 3}, + {Label: "*:create", MaxRequests: 20, Duration: 5}, + {Label: "/api/batch", MaxRequests: 3, Duration: 1}, + {Label: "/api/", MaxRequests: 300, Duration: 10}, + }, + }, + }, + } +} + +// TableName implements [Model.TableName] interface method. +func (s *Settings) TableName() string { + return paramsTable +} + +// PK implements [Model.LastSavedPK] interface method. +func (s *Settings) LastSavedPK() any { + return paramsKeySettings +} + +// PK implements [Model.PK] interface method. +func (s *Settings) PK() any { + return paramsKeySettings +} + +// IsNew implements [Model.IsNew] interface method. +func (s *Settings) IsNew() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.isNew +} + +// MarkAsNew implements [Model.MarkAsNew] interface method. +func (s *Settings) MarkAsNew() { + s.mu.Lock() + defer s.mu.Unlock() + + s.isNew = true +} + +// MarkAsNew implements [Model.MarkAsNotNew] interface method. +func (s *Settings) MarkAsNotNew() { + s.mu.Lock() + defer s.mu.Unlock() + + s.isNew = false +} + +// PostScan implements [Model.PostScan] interface method. +func (s *Settings) PostScan() error { + s.MarkAsNotNew() + return nil +} + +// String returns a serialized string representation of the current settings. +func (s *Settings) String() string { + s.mu.RLock() + defer s.mu.RUnlock() + + raw, _ := json.Marshal(s) + return string(raw) +} + +// DBExport prepares and exports the current settings for db persistence. +func (s *Settings) DBExport(app App) (map[string]any, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + now := types.NowDateTime() + + result := map[string]any{ + "id": s.PK(), + } + + if s.IsNew() { + result["created"] = now + } + result["updated"] = now + + encoded, err := json.Marshal(s.settings) + if err != nil { + return nil, err + } + + encryptionKey := os.Getenv(app.EncryptionEnv()) + if encryptionKey != "" { + encryptVal, encryptErr := security.Encrypt(encoded, encryptionKey) + if encryptErr != nil { + return nil, encryptErr + } + + result["value"] = encryptVal + } else { + result["value"] = encoded + } + + return result, nil +} + +// PostValidate implements the [PostValidator] interface and defines +// the Settings model validations. +func (s *Settings) PostValidate(ctx context.Context, app App) error { + s.mu.RLock() + defer s.mu.RUnlock() + + return validation.ValidateStructWithContext(ctx, s, + validation.Field(&s.Meta), + validation.Field(&s.Logs), + validation.Field(&s.SMTP), + validation.Field(&s.S3), + validation.Field(&s.Backups), + validation.Field(&s.Batch), + validation.Field(&s.RateLimits), + validation.Field(&s.TrustedProxy), + ) +} + +// Merge merges the "other" settings into the current one. +func (s *Settings) Merge(other *Settings) error { + other.mu.RLock() + defer other.mu.RUnlock() + + raw, err := json.Marshal(other.settings) + if err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + return json.Unmarshal(raw, &s) +} + +// Clone creates a new deep copy of the current settings. +func (s *Settings) Clone() (*Settings, error) { + clone := &Settings{ + isNew: s.isNew, + } + + if err := clone.Merge(s); err != nil { + return nil, err + } + + return clone, nil +} + +// MarshalJSON implements the [json.Marshaler] interface. +// +// Note that sensitive fields (S3 secret, SMTP password, etc.) are excluded. +func (s *Settings) MarshalJSON() ([]byte, error) { + s.mu.RLock() + copy := s.settings + s.mu.RUnlock() + + sensitiveFields := []*string{ + ©.SMTP.Password, + ©.S3.Secret, + ©.Backups.S3.Secret, + } + + // mask all sensitive fields + for _, v := range sensitiveFields { + if v != nil && *v != "" { + *v = "" + } + } + + return json.Marshal(copy) +} + +// ------------------------------------------------------------------- + +type SMTPConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + Port int `form:"port" json:"port"` + Host string `form:"host" json:"host"` + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password,omitempty"` + + // SMTP AUTH - PLAIN (default) or LOGIN + AuthMethod string `form:"authMethod" json:"authMethod"` + + // Whether to enforce TLS encryption for the mail server connection. + // + // When set to false StartTLS command is send, leaving the server + // to decide whether to upgrade the connection or not. + TLS bool `form:"tls" json:"tls"` + + // LocalName is optional domain name or IP address used for the + // EHLO/HELO exchange (if not explicitly set, defaults to "localhost"). + // + // This is required only by some SMTP servers, such as Gmail SMTP-relay. + LocalName string `form:"localName" json:"localName"` +} + +// Validate makes SMTPConfig validatable by implementing [validation.Validatable] interface. +func (c SMTPConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field( + &c.Host, + validation.When(c.Enabled, validation.Required), + is.Host, + ), + validation.Field( + &c.Port, + validation.When(c.Enabled, validation.Required), + validation.Min(0), + ), + validation.Field( + &c.AuthMethod, + // don't require it for backward compatibility + // (fallback internally to PLAIN) + // validation.When(c.Enabled, validation.Required), + validation.In(mailer.SMTPAuthLogin, mailer.SMTPAuthPlain), + ), + validation.Field(&c.LocalName, is.Host), + ) +} + +// ------------------------------------------------------------------- + +type S3Config struct { + Enabled bool `form:"enabled" json:"enabled"` + Bucket string `form:"bucket" json:"bucket"` + Region string `form:"region" json:"region"` + Endpoint string `form:"endpoint" json:"endpoint"` + AccessKey string `form:"accessKey" json:"accessKey"` + Secret string `form:"secret" json:"secret,omitempty"` + ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"` +} + +// Validate makes S3Config validatable by implementing [validation.Validatable] interface. +func (c S3Config) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)), + ) +} + +// ------------------------------------------------------------------- + +type BatchConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + + // MaxRequests is the maximum allowed batch request to execute. + MaxRequests int `form:"maxRequests" json:"maxRequests"` + + // Timeout is the the max duration in seconds to wait before cancelling the batch transaction. + Timeout int64 `form:"timeout" json:"timeout"` + + // MaxBodySize is the maximum allowed batch request body size in bytes. + // + // If not set, fallbacks to max ~128MB. + MaxBodySize int64 `form:"maxBodySize" json:"maxBodySize"` +} + +// Validate makes BatchConfig validatable by implementing [validation.Validatable] interface. +func (c BatchConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.MaxRequests, validation.When(c.Enabled, validation.Required), validation.Min(0)), + validation.Field(&c.Timeout, validation.When(c.Enabled, validation.Required), validation.Min(0)), + validation.Field(&c.MaxBodySize, validation.Min(0)), + ) +} + +// ------------------------------------------------------------------- + +type BackupsConfig struct { + // Cron is a cron expression to schedule auto backups, eg. "* * * * *". + // + // Leave it empty to disable the auto backups functionality. + Cron string `form:"cron" json:"cron"` + + // CronMaxKeep is the the max number of cron generated backups to + // keep before removing older entries. + // + // This field works only when the cron config has valid cron expression. + CronMaxKeep int `form:"cronMaxKeep" json:"cronMaxKeep"` + + // S3 is an optional S3 storage config specifying where to store the app backups. + S3 S3Config `form:"s3" json:"s3"` +} + +// Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface. +func (c BackupsConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.S3), + validation.Field(&c.Cron, validation.By(checkCronExpression)), + validation.Field( + &c.CronMaxKeep, + validation.When(c.Cron != "", validation.Required), + validation.Min(1), + ), + ) +} + +func checkCronExpression(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + _, err := cron.NewSchedule(v) + if err != nil { + return validation.NewError("validation_invalid_cron", err.Error()) + } + + return nil +} + +// ------------------------------------------------------------------- + +type MetaConfig struct { + AppName string `form:"appName" json:"appName"` + AppURL string `form:"appURL" json:"appURL"` + SenderName string `form:"senderName" json:"senderName"` + SenderAddress string `form:"senderAddress" json:"senderAddress"` + HideControls bool `form:"hideControls" json:"hideControls"` +} + +// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. +func (c MetaConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)), + validation.Field(&c.AppURL, validation.Required, is.URL), + validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)), + validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required), + ) +} + +// ------------------------------------------------------------------- + +type LogsConfig struct { + MaxDays int `form:"maxDays" json:"maxDays"` + MinLevel int `form:"minLevel" json:"minLevel"` + LogIP bool `form:"logIP" json:"logIP"` + LogAuthId bool `form:"logAuthId" json:"logAuthId"` +} + +// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface. +func (c LogsConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.MaxDays, validation.Min(0)), + ) +} + +// ------------------------------------------------------------------- + +type TrustedProxyConfig struct { + // Headers is a list of explicit trusted header(s) to check. + Headers []string `form:"headers" json:"headers"` + + // UseLeftmostIP specifies to use the left-mostish IP from the trusted headers. + // + // Note that this could be insecure when used with X-Forward-For header + // because some proxies like AWS ELB allow users to prepend their own header value + // before appending the trusted ones. + UseLeftmostIP bool `form:"useLeftmostIP" json:"useLeftmostIP"` +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (c TrustedProxyConfig) MarshalJSON() ([]byte, error) { + type alias TrustedProxyConfig + + // serialize as empty array + if c.Headers == nil { + c.Headers = []string{} + } + + return json.Marshal(alias(c)) +} + +// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface. +func (c TrustedProxyConfig) Validate() error { + return nil +} + +// ------------------------------------------------------------------- + +type RateLimitsConfig struct { + Rules []RateLimitRule `form:"rules" json:"rules"` + Enabled bool `form:"enabled" json:"enabled"` +} + +// FindRateLimitRule returns the first matching rule based on the provided labels. +func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string) (RateLimitRule, bool) { + var prefixRules []int + + for i, label := range searchLabels { + // check for direct match + for j := range c.Rules { + if label == c.Rules[j].Label { + return c.Rules[j], true + } + + if i == 0 && strings.HasSuffix(c.Rules[j].Label, "/") { + prefixRules = append(prefixRules, j) + } + } + + // check for prefix match + if len(prefixRules) > 0 { + for j := range prefixRules { + if strings.HasPrefix(label+"/", c.Rules[prefixRules[j]].Label) { + return c.Rules[prefixRules[j]], true + } + } + } + } + + return RateLimitRule{}, false +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (c RateLimitsConfig) MarshalJSON() ([]byte, error) { + type alias RateLimitsConfig + + // serialize as empty array + if c.Rules == nil { + c.Rules = []RateLimitRule{} + } + + return json.Marshal(alias(c)) +} + +// Validate makes RateLimitsConfig validatable by implementing [validation.Validatable] interface. +func (c RateLimitsConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field( + &c.Rules, + validation.When(c.Enabled, validation.Required), + validation.By(checkUniqueRuleLabel), + ), + ) +} + +func checkUniqueRuleLabel(value any) error { + rules, ok := value.([]RateLimitRule) + if !ok { + return validators.ErrUnsupportedValueType + } + + labels := make(map[string]struct{}, len(rules)) + + for i, rule := range rules { + _, ok := labels[rule.Label] + if ok { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "label": validation.NewError("validation_duplicated_rate_limit_tag", "Rate limit tag with label "+rule.Label+" already exists."). + SetParams(map[string]any{"label": rule.Label}), + }, + } + } else { + labels[rule.Label] = struct{}{} + } + } + + return nil +} + +var rateLimitRuleLabelRegex = regexp.MustCompile(`^(\w+\ \/[\w\/-]*|\/[\w\/-]*|^\w+\:\w+|\*\:\w+|\w+)$`) + +type RateLimitRule struct { + // Label is the identifier of the current rule. + // + // It could be a tag, complete path or path prerefix (when ends with `/`). + // + // Example supported labels: + // - test_a (plain text "tag") + // - users:create + // - *:create + // - / + // - /api + // - POST /api/collections/ + Label string `form:"label" json:"label"` + + // MaxRequests is the max allowed number of requests per Duration. + MaxRequests int `form:"maxRequests" json:"maxRequests"` + + // Duration specifies the interval (in seconds) per which to reset + // the counted/accumulated rate limiter tokens. + Duration int64 `form:"duration" json:"duration"` +} + +// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface. +func (c RateLimitRule) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Label, validation.Required, validation.Match(rateLimitRuleLabelRegex)), + validation.Field(&c.MaxRequests, validation.Required, validation.Min(1)), + validation.Field(&c.Duration, validation.Required, validation.Min(1)), + ) +} + +// DurationTime returns the tag's Duration as [time.Duration]. +func (c RateLimitRule) DurationTime() time.Duration { + return time.Duration(c.Duration) * time.Second +} diff --git a/core/settings_model_test.go b/core/settings_model_test.go new file mode 100644 index 00000000..81dca9c5 --- /dev/null +++ b/core/settings_model_test.go @@ -0,0 +1,691 @@ +package core_test + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/mailer" +) + +func TestSettingsDelete(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + err := app.Delete(app.Settings()) + if err == nil { + t.Fatal("Exected settings delete to fail") + } +} + +func TestSettingsMerge(t *testing.T) { + s1 := &core.Settings{} + s1.Meta.AppURL = "app_url" // should be unset + + s2 := &core.Settings{} + s2.Meta.AppName = "test" + s2.Logs.MaxDays = 123 + s2.SMTP.Host = "test" + s2.SMTP.Enabled = true + s2.S3.Enabled = true + s2.S3.Endpoint = "test" + s2.Backups.Cron = "* * * * *" + s2.Batch.Timeout = 15 + + if err := s1.Merge(s2); err != nil { + t.Fatal(err) + } + + s1Encoded, err := json.Marshal(s1) + if err != nil { + t.Fatal(err) + } + + s2Encoded, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + + if string(s1Encoded) != string(s2Encoded) { + t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", string(s1Encoded), string(s2Encoded)) + } +} + +func TestSettingsClone(t *testing.T) { + s1 := &core.Settings{} + s1.Meta.AppName = "test_name" + + s2, err := s1.Clone() + if err != nil { + t.Fatal(err) + } + + s1Bytes, err := json.Marshal(s1) + if err != nil { + t.Fatal(err) + } + + s2Bytes, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + + if string(s1Bytes) != string(s2Bytes) { + t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes)) + } + + // verify that it is a deep copy + s2.Meta.AppName = "new_test_name" + if s1.Meta.AppName == s2.Meta.AppName { + t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName) + } +} + +func TestSettingsMarshalJSON(t *testing.T) { + settings := &core.Settings{} + + // control fields + settings.Meta.AppName = "test123" + settings.SMTP.Username = "abc" + + // secrets + testSecret := "test_secret" + settings.SMTP.Password = testSecret + settings.S3.Secret = testSecret + settings.Backups.S3.Secret = testSecret + + raw, err := json.Marshal(settings) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + expected := `{"smtp":{"enabled":false,"port":0,"host":"","username":"abc","authMethod":"","tls":false,"localName":""},"backups":{"cron":"","cronMaxKeep":0,"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false},"meta":{"appName":"test123","appURL":"","senderName":"","senderAddress":"","hideControls":false},"logs":{"maxDays":0,"minLevel":0,"logIP":false,"logAuthId":false},"batch":{"enabled":false,"maxRequests":0,"timeout":0,"maxBodySize":0},"rateLimits":{"rules":[],"enabled":false},"trustedProxy":{"headers":[],"useLeftmostIP":false}}` + + if rawStr != expected { + t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr) + } +} + +func TestSettingsValidate(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + s := app.Settings() + + // set invalid settings data + s.Meta.AppName = "" + s.Logs.MaxDays = -10 + s.SMTP.Enabled = true + s.SMTP.Host = "" + s.S3.Enabled = true + s.S3.Endpoint = "invalid" + s.Backups.Cron = "invalid" + s.Backups.CronMaxKeep = -10 + s.Batch.Enabled = true + s.Batch.MaxRequests = -1 + s.Batch.Timeout = -1 + s.RateLimits.Enabled = true + s.RateLimits.Rules = nil + + // check if Validate() is triggering the members validate methods. + err := app.Validate(s) + if err == nil { + t.Fatalf("Expected error, got nil") + } + + expectations := []string{ + `"meta":{`, + `"logs":{`, + `"smtp":{`, + `"s3":{`, + `"backups":{`, + `"batch":{`, + `"rateLimits":{`, + } + + errBytes, _ := json.Marshal(err) + jsonErr := string(errBytes) + for _, expected := range expectations { + if !strings.Contains(jsonErr, expected) { + t.Errorf("Expected error key %s in %v", expected, jsonErr) + } + } +} + +func TestMetaConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.MetaConfig + expectedErrors []string + }{ + { + "zero values", + core.MetaConfig{}, + []string{ + "appName", + "appURL", + "senderName", + "senderAddress", + }, + }, + { + "invalid data", + core.MetaConfig{ + AppName: strings.Repeat("a", 300), + AppURL: "test", + SenderName: strings.Repeat("a", 300), + SenderAddress: "invalid_email", + }, + []string{ + "appName", + "appURL", + "senderName", + "senderAddress", + }, + }, + { + "valid data", + core.MetaConfig{ + AppName: "test", + AppURL: "https://example.com", + SenderName: "test", + SenderAddress: "test@example.com", + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestLogsConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.LogsConfig + expectedErrors []string + }{ + { + "zero values", + core.LogsConfig{}, + []string{}, + }, + { + "invalid data", + core.LogsConfig{MaxDays: -1}, + []string{"maxDays"}, + }, + { + "valid data", + core.LogsConfig{MaxDays: 2}, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestSMTPConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.SMTPConfig + expectedErrors []string + }{ + { + "zero values (disabled)", + core.SMTPConfig{}, + []string{}, + }, + { + "zero values (enabled)", + core.SMTPConfig{Enabled: true}, + []string{"host", "port"}, + }, + { + "invalid data", + core.SMTPConfig{ + Enabled: true, + Host: "test:test:test", + Port: -10, + LocalName: "invalid!", + AuthMethod: "invalid", + }, + []string{"host", "port", "authMethod", "localName"}, + }, + { + "valid data (no explicit auth method and localName)", + core.SMTPConfig{ + Enabled: true, + Host: "example.com", + Port: 100, + TLS: true, + }, + []string{}, + }, + { + "valid data (explicit auth method and localName)", + core.SMTPConfig{ + Enabled: true, + Host: "example.com", + Port: 100, + AuthMethod: mailer.SMTPAuthLogin, + LocalName: "example.com", + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestS3ConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.S3Config + expectedErrors []string + }{ + { + "zero values (disabled)", + core.S3Config{}, + []string{}, + }, + { + "zero values (enabled)", + core.S3Config{Enabled: true}, + []string{ + "bucket", + "region", + "endpoint", + "accessKey", + "secret", + }, + }, + { + "invalid data", + core.S3Config{ + Enabled: true, + Endpoint: "test:test:test", + }, + []string{ + "bucket", + "region", + "endpoint", + "accessKey", + "secret", + }, + }, + { + "valid data (url endpoint)", + core.S3Config{ + Enabled: true, + Endpoint: "https://localhost:8090", + Bucket: "test", + Region: "test", + AccessKey: "test", + Secret: "test", + }, + []string{}, + }, + { + "valid data (hostname endpoint)", + core.S3Config{ + Enabled: true, + Endpoint: "example.com", + Bucket: "test", + Region: "test", + AccessKey: "test", + Secret: "test", + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestBackupsConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.BackupsConfig + expectedErrors []string + }{ + { + "zero value", + core.BackupsConfig{}, + []string{}, + }, + { + "invalid cron", + core.BackupsConfig{ + Cron: "invalid", + CronMaxKeep: 0, + }, + []string{"cron", "cronMaxKeep"}, + }, + { + "invalid enabled S3", + core.BackupsConfig{ + S3: core.S3Config{ + Enabled: true, + }, + }, + []string{"s3"}, + }, + { + "valid data", + core.BackupsConfig{ + S3: core.S3Config{ + Enabled: true, + Endpoint: "example.com", + Bucket: "test", + Region: "test", + AccessKey: "test", + Secret: "test", + }, + Cron: "*/10 * * * *", + CronMaxKeep: 1, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestBatchConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.BatchConfig + expectedErrors []string + }{ + { + "zero value", + core.BatchConfig{}, + []string{}, + }, + { + "zero value (enabled)", + core.BatchConfig{Enabled: true}, + []string{"maxRequests", "timeout"}, + }, + { + "invalid data (negative values)", + core.BatchConfig{ + MaxRequests: -1, + Timeout: -1, + MaxBodySize: -1, + }, + []string{"maxRequests", "timeout", "maxBodySize"}, + }, + { + "min fields valid data", + core.BatchConfig{ + Enabled: true, + MaxRequests: 1, + Timeout: 1, + }, + []string{}, + }, + { + "all fields valid data", + core.BatchConfig{ + Enabled: true, + MaxRequests: 10, + Timeout: 1, + MaxBodySize: 1, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestRateLimitsConfigValidate(t *testing.T) { + scenarios := []struct { + name string + config core.RateLimitsConfig + expectedErrors []string + }{ + { + "zero value (disabled)", + core.RateLimitsConfig{}, + []string{}, + }, + { + "zero value (enabled)", + core.RateLimitsConfig{Enabled: true}, + []string{"rules"}, + }, + { + "invalid data", + core.RateLimitsConfig{ + Enabled: true, + Rules: []core.RateLimitRule{ + { + Label: "/123abc/", + Duration: 1, + MaxRequests: 2, + }, + { + Label: "!abc", + Duration: -1, + MaxRequests: -1, + }, + }, + }, + []string{"rules"}, + }, + { + "valid data", + core.RateLimitsConfig{ + Enabled: true, + Rules: []core.RateLimitRule{ + { + Label: "123_abc", + Duration: 1, + MaxRequests: 2, + }, + { + Label: "/456-abc", + Duration: 1, + MaxRequests: 2, + }, + }, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestRateLimitsFindRateLimitRule(t *testing.T) { + limits := core.RateLimitsConfig{ + Rules: []core.RateLimitRule{ + {Label: "abc"}, + {Label: "POST /test/a/"}, + {Label: "/test/a/"}, + {Label: "POST /test/a"}, + {Label: "/test/a"}, + }, + } + + scenarios := []struct { + labels []string + expected string + }{ + {[]string{}, ""}, + {[]string{"missing"}, ""}, + {[]string{"abc"}, "abc"}, + {[]string{"/test"}, ""}, + {[]string{"/test/a"}, "/test/a"}, + {[]string{"GET /test/a"}, ""}, + {[]string{"POST /test/a"}, "POST /test/a"}, + {[]string{"/test/a/b/c"}, "/test/a/"}, + {[]string{"GET /test/a/b/c"}, ""}, + {[]string{"POST /test/a/b/c"}, "POST /test/a/"}, + {[]string{"/test/a", "abc"}, "/test/a"}, // priority checks + } + + for _, s := range scenarios { + t.Run(strings.Join(s.labels, ""), func(t *testing.T) { + rule, ok := limits.FindRateLimitRule(s.labels) + + hasLabel := rule.Label != "" + if hasLabel != ok { + t.Fatalf("Expected hasLabel %v, got %v", hasLabel, ok) + } + + if rule.Label != s.expected { + t.Fatalf("Expected rule with label %q, got %q", s.expected, rule.Label) + } + }) + } +} + +func TestRateLimitRuleValidate(t *testing.T) { + scenarios := []struct { + name string + config core.RateLimitRule + expectedErrors []string + }{ + { + "zero value", + core.RateLimitRule{}, + []string{"label", "duration", "maxRequests"}, + }, + { + "invalid data", + core.RateLimitRule{ + Label: "@abc", + Duration: -1, + MaxRequests: -1, + }, + []string{"label", "duration", "maxRequests"}, + }, + { + "valid data (name)", + core.RateLimitRule{ + Label: "abc:123", + Duration: 1, + MaxRequests: 1, + }, + []string{}, + }, + { + "valid data (name:action)", + core.RateLimitRule{ + Label: "abc:123", + Duration: 1, + MaxRequests: 1, + }, + []string{}, + }, + { + "valid data (*:action)", + core.RateLimitRule{ + Label: "*:123", + Duration: 1, + MaxRequests: 1, + }, + []string{}, + }, + { + "valid data (path /a/b)", + core.RateLimitRule{ + Label: "/a/b", + Duration: 1, + MaxRequests: 1, + }, + []string{}, + }, + { + "valid data (path POST /a/b)", + core.RateLimitRule{ + Label: "POST /a/b/", + Duration: 1, + MaxRequests: 1, + }, + []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.config.Validate() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) + } +} + +func TestRateLimitRuleDurationTime(t *testing.T) { + scenarios := []struct { + config core.RateLimitRule + expected time.Duration + }{ + {core.RateLimitRule{}, 0 * time.Second}, + {core.RateLimitRule{Duration: 1234}, 1234 * time.Second}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) { + result := s.config.DurationTime() + + if result != s.expected { + t.Fatalf("Expected duration %d, got %d", s.expected, result) + } + }) + } +} diff --git a/core/settings_query.go b/core/settings_query.go new file mode 100644 index 00000000..c1883c61 --- /dev/null +++ b/core/settings_query.go @@ -0,0 +1,88 @@ +package core + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" +) + +type Param struct { + BaseModel + + Created types.DateTime `db:"created" json:"created"` + Updated types.DateTime `db:"Updated" json:"Updated"` + Value types.JSONRaw `db:"value" json:"value"` +} + +func (m *Param) TableName() string { + return paramsTable +} + +// ReloadSettings initializes and reloads the stored application settings. +// +// If no settings were stored it will persist the current app ones. +func (app *BaseApp) ReloadSettings() error { + param := &Param{} + err := app.ModelQuery(param).Model(paramsKeySettings, param) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + // no settings were previously stored -> save + // (ReloadSettings() will be invoked again by a system hook after successful save) + if param.Id == "" { + // force insert in case the param entry was deleted manually after application start + app.Settings().MarkAsNew() + return app.Save(app.Settings()) + } + + event := new(SettingsReloadEvent) + event.App = app + + return app.OnSettingsReload().Trigger(event, func(e *SettingsReloadEvent) error { + return e.App.Settings().loadParam(e.App, param) + }) +} + +// loadParam loads the settings from the stored param into the app ones. +// +// @todo note that the encryption may get removed in the future since it doesn't +// really accomplish much and it might be better to find a way to encrypt the backups +// or implement support for resolving env variables. +func (s *Settings) loadParam(app App, param *Param) error { + // try first without decryption + s.mu.Lock() + plainDecodeErr := json.Unmarshal(param.Value, s) + s.mu.Unlock() + + // failed, try to decrypt + if plainDecodeErr != nil { + encryptionKey := os.Getenv(app.EncryptionEnv()) + + // load without decryption has failed and there is no encryption key to use for decrypt + if encryptionKey == "" { + return fmt.Errorf("invalid settings db data or missing encryption key %q", app.EncryptionEnv()) + } + + // decrypt + decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey) + if decryptErr != nil { + return decryptErr + } + + // decode again + s.mu.Lock() + decryptedDecodeErr := json.Unmarshal(decrypted, s) + s.mu.Unlock() + if decryptedDecodeErr != nil { + return decryptedDecodeErr + } + } + + return s.PostScan() +} diff --git a/core/settings_query_test.go b/core/settings_query_test.go new file mode 100644 index 00000000..a7542889 --- /dev/null +++ b/core/settings_query_test.go @@ -0,0 +1,159 @@ +package core_test + +import ( + "os" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestReloadSettings(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // cleanup all stored settings + // --- + if _, err := app.DB().NewQuery("DELETE from _params;").Execute(); err != nil { + t.Fatalf("Failed to delete all test settings: %v", err) + } + + // check if the new settings are saved in the db + // --- + app.Settings().Meta.AppName = "test_name_after_delete" + + app.ResetEventCalls() + if err := app.ReloadSettings(); err != nil { + t.Fatalf("Failed to reload the settings after delete: %v", err) + } + testEventCalls(t, app, map[string]int{ + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnSettingsReload": 1, + }) + + param := &core.Param{} + err := app.ModelQuery(param).Model("settings", param) + if err != nil { + t.Fatalf("Expected new settings to be persisted, got %v", err) + } + + if !strings.Contains(param.Value.String(), "test_name_after_delete") { + t.Fatalf("Expected to find AppName test_name_after_delete in\n%s", param.Value.String()) + } + + // change the db entry and reload the app settings (ensure that there was no db update) + // --- + param.Value = types.JSONRaw([]byte(`{"meta": {"appName":"test_name_after_update"}}`)) + if err := app.Save(param); err != nil { + t.Fatalf("Failed to update the test settings: %v", err) + } + + app.ResetEventCalls() + if err := app.ReloadSettings(); err != nil { + t.Fatalf("Failed to reload app settings: %v", err) + } + testEventCalls(t, app, map[string]int{ + "OnSettingsReload": 1, + }) + + // try to reload again without doing any changes + // --- + app.ResetEventCalls() + if err := app.ReloadSettings(); err != nil { + t.Fatalf("Failed to reload app settings without change: %v", err) + } + testEventCalls(t, app, map[string]int{ + "OnSettingsReload": 1, + }) + + if app.Settings().Meta.AppName != "test_name_after_update" { + t.Fatalf("Expected AppName %q, got %q", "test_name_after_update", app.Settings().Meta.AppName) + } +} + +func TestReloadSettingsWithEncryption(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + os.Setenv("pb_test_env", strings.Repeat("a", 32)) + + // cleanup all stored settings + // --- + if _, err := app.DB().NewQuery("DELETE from _params;").Execute(); err != nil { + t.Fatalf("Failed to delete all test settings: %v", err) + } + + // check if the new settings are saved in the db + // --- + app.Settings().Meta.AppName = "test_name_after_delete" + + app.ResetEventCalls() + if err := app.ReloadSettings(); err != nil { + t.Fatalf("Failed to reload the settings after delete: %v", err) + } + testEventCalls(t, app, map[string]int{ + "OnModelCreate": 1, + "OnModelCreateExecute": 1, + "OnModelAfterCreateSuccess": 1, + "OnModelValidate": 1, + "OnSettingsReload": 1, + }) + + param := &core.Param{} + err := app.ModelQuery(param).Model("settings", param) + if err != nil { + t.Fatalf("Expected new settings to be persisted, got %v", err) + } + rawValue := param.Value.String() + if rawValue == "" || strings.Contains(rawValue, "test_name") { + t.Fatalf("Expected inserted settings to be encrypted, found\n%s", rawValue) + } + + // change and reload the app settings (ensure that there was no db update) + // --- + app.Settings().Meta.AppName = "test_name_after_update" + if err := app.Save(app.Settings()); err != nil { + t.Fatalf("Failed to update app settings: %v", err) + } + + // try to reload again without doing any changes + // --- + app.ResetEventCalls() + if err := app.ReloadSettings(); err != nil { + t.Fatalf("Failed to reload app settings: %v", err) + } + testEventCalls(t, app, map[string]int{ + "OnSettingsReload": 1, + }) + + // refetch the settings param to ensure that the new value was stored encrypted + err = app.ModelQuery(param).Model("settings", param) + if err != nil { + t.Fatalf("Expected new settings to be persisted, got %v", err) + } + rawValue = param.Value.String() + if rawValue == "" || strings.Contains(rawValue, "test_name") { + t.Fatalf("Expected updated settings to be encrypted, found\n%s", rawValue) + } + + if app.Settings().Meta.AppName != "test_name_after_update" { + t.Fatalf("Expected AppName %q, got %q", "test_name_after_update", app.Settings().Meta.AppName) + } +} + +func testEventCalls(t *testing.T, app *tests.TestApp, events map[string]int) { + if len(events) != len(app.EventCalls) { + t.Fatalf("Expected events doesn't match:\n%v\ngot\n%v", events, app.EventCalls) + } + + for name, total := range events { + if v, ok := app.EventCalls[name]; !ok || v != total { + t.Fatalf("Expected events doesn't exist or match:\n%v\ngot\n%v", events, app.EventCalls) + } + } +} diff --git a/core/validators/db.go b/core/validators/db.go new file mode 100644 index 00000000..bf437b1d --- /dev/null +++ b/core/validators/db.go @@ -0,0 +1,78 @@ +package validators + +import ( + "database/sql" + "errors" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" +) + +// UniqueId checks whether a field string id already exists in the specified table. +// +// Example: +// +// validation.Field(&form.RelId, validation.By(validators.UniqueId(form.app.DB(), "tbl_example")) +func UniqueId(db dbx.Builder, tableName string) validation.RuleFunc { + return func(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + var foundId string + + err := db. + Select("id"). + From(tableName). + Where(dbx.HashExp{"id": v}). + Limit(1). + Row(&foundId) + + if (err != nil && !errors.Is(err, sql.ErrNoRows)) || foundId != "" { + return validation.NewError("validation_invalid_or_existing_id", "The model id is invalid or already exists.") + } + + return nil + } +} + +// NormalizeUniqueIndexError attempts to convert a +// "unique constraint failed" error into a validation.Errors. +// +// The provided err is returned as it is without changes if: +// - err is nil +// - err is already validation.Errors +// - err is not "unique constraint failed" error +func NormalizeUniqueIndexError(err error, tableOrAlias string, fieldNames []string) error { + if err == nil { + return err + } + + // + if _, ok := err.(validation.Errors); ok { + return err + } + + msg := strings.ToLower(err.Error()) + + // check for unique constraint failure + if strings.Contains(msg, "unique constraint failed") { + normalizedErrs := validation.Errors{} + msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ") + + for _, name := range fieldNames { + // blank space to unify multi-columns lookup + if strings.Contains(msg+" ", strings.ToLower(tableOrAlias+"."+name)) { + normalizedErrs[name] = validation.NewError("validation_not_unique", "Value must be unique") + } + } + + if len(normalizedErrs) > 0 { + return normalizedErrs + } + } + + return err +} diff --git a/core/validators/db_test.go b/core/validators/db_test.go new file mode 100644 index 00000000..f5ba1f26 --- /dev/null +++ b/core/validators/db_test.go @@ -0,0 +1,111 @@ +package validators_test + +import ( + "errors" + "fmt" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUniqueId(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + tableName string + expectError bool + }{ + {"", "", false}, + {"test", "", true}, + {"wsmn24bux7wo113", "_collections", true}, + {"test_unique_id", "unknown_table", true}, + {"test_unique_id", "_collections", false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.id, s.tableName), func(t *testing.T) { + err := validators.UniqueId(app.DB(), s.tableName)(s.id) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestNormalizeUniqueIndexError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + err error + table string + names []string + expectedKeys []string + }{ + { + "nil error (no changes)", + nil, + "test", + []string{"a", "b"}, + nil, + }, + { + "non-unique index error (no changes)", + errors.New("abc"), + "test", + []string{"a", "b"}, + nil, + }, + { + "validation error (no changes)", + validation.Errors{"c": errors.New("abc")}, + "test", + []string{"a", "b"}, + []string{"c"}, + }, + { + "unique index error but mismatched table name", + errors.New("UNIQUE constraint failed for fields test.a,test.b"), + "example", + []string{"a", "b"}, + nil, + }, + { + "unique index error but mismatched fields", + errors.New("UNIQUE constraint failed for fields test.a,test.b"), + "test", + []string{"c", "d"}, + nil, + }, + { + "unique index error with matching table name and fields", + errors.New("UNIQUE constraint failed for fields test.a,test.b"), + "test", + []string{"a", "b", "c"}, + []string{"a", "b"}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := validators.NormalizeUniqueIndexError(s.err, s.table, s.names) + + if len(s.expectedKeys) == 0 { + if result != s.err { + t.Fatalf("Expected no error change, got %v", result) + } + return + } + + tests.TestValidationErrors(t, result, s.expectedKeys) + }) + } +} diff --git a/core/validators/equal.go b/core/validators/equal.go new file mode 100644 index 00000000..9ee673cb --- /dev/null +++ b/core/validators/equal.go @@ -0,0 +1,85 @@ +package validators + +import ( + "reflect" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +// Equal checks whether the validated value matches another one from the same type. +// +// It expects the compared values to be from the same type and works +// with booleans, numbers, strings and their pointer variants. +// +// If one of the value is pointer, the comparison is based on its +// underlying value (when possible to determine). +// +// Note that empty/zero values are also compared (this differ from other validation.RuleFunc). +// +// Example: +// +// validation.Field(&form.PasswordConfirm, validation.By(validators.Equal(form.Password))) +func Equal[T comparable](valueToCompare T) validation.RuleFunc { + return func(value any) error { + if compareValues(value, valueToCompare) { + return nil + } + + return validation.NewError("validation_values_mismatch", "Values don't match.") + } +} + +func compareValues(a, b any) bool { + if a == b { + return true + } + + if checkIsNil(a) && checkIsNil(b) { + return true + } + + var result bool + + defer func() { + if err := recover(); err != nil { + result = false + } + }() + + reflectA := reflect.ValueOf(a) + reflectB := reflect.ValueOf(b) + + dereferencedA := dereference(reflectA) + dereferencedB := dereference(reflectB) + if dereferencedA.CanInterface() && dereferencedB.CanInterface() { + result = dereferencedA.Interface() == dereferencedB.Interface() + } + + return result +} + +// note https://github.com/golang/go/issues/51649 +func checkIsNil(value any) bool { + if value == nil { + return true + } + + var result bool + + defer func() { + if err := recover(); err != nil { + result = false + } + }() + + result = reflect.ValueOf(value).IsNil() + + return result +} + +func dereference(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Pointer { + v = v.Elem() + } + return v +} diff --git a/core/validators/equal_test.go b/core/validators/equal_test.go new file mode 100644 index 00000000..dedb3763 --- /dev/null +++ b/core/validators/equal_test.go @@ -0,0 +1,62 @@ +package validators_test + +import ( + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core/validators" +) + +func Equal(t *testing.T) { + t.Parallel() + + strA := "abc" + strB := "abc" + strC := "123" + var strNilPtr *string + var strNilPtr2 *string + + scenarios := []struct { + valA any + valB any + expectError bool + }{ + {nil, nil, false}, + {"", "", false}, + {"", "456", true}, + {"123", "", true}, + {"123", "456", true}, + {"123", "123", false}, + {true, false, true}, + {false, true, true}, + {false, false, false}, + {true, true, false}, + {0, 0, false}, + {0, 1, true}, + {1, 2, true}, + {1, 1, false}, + {&strA, &strA, false}, + {&strA, &strB, false}, + {&strA, &strC, true}, + {"abc", &strA, false}, + {&strA, "abc", false}, + {"abc", &strC, true}, + {"test", 123, true}, + {nil, 123, true}, + {nil, strA, true}, + {nil, &strA, true}, + {nil, strNilPtr, false}, + {strNilPtr, strNilPtr2, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%v_%v", i, s.valA, s.valB), func(t *testing.T) { + err := validators.Equal(s.valA)(s.valB) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} diff --git a/forms/validators/file.go b/core/validators/file.go similarity index 77% rename from forms/validators/file.go rename to core/validators/file.go index 2177078e..9e008c11 100644 --- a/forms/validators/file.go +++ b/core/validators/file.go @@ -9,31 +9,38 @@ import ( "github.com/pocketbase/pocketbase/tools/filesystem" ) -// UploadedFileSize checks whether the validated `rest.UploadedFile` +// UploadedFileSize checks whether the validated [*filesystem.File] // size is no more than the provided maxBytes. // // Example: // // validation.Field(&form.File, validation.By(validators.UploadedFileSize(1000))) -func UploadedFileSize(maxBytes int) validation.RuleFunc { +func UploadedFileSize(maxBytes int64) validation.RuleFunc { return func(value any) error { - v, _ := value.(*filesystem.File) + v, ok := value.(*filesystem.File) + if !ok { + return ErrUnsupportedValueType + } + if v == nil { return nil // nothing to validate } - if int(v.Size) > maxBytes { + if v.Size > maxBytes { return validation.NewError( "validation_file_size_limit", fmt.Sprintf("Failed to upload %q - the maximum allowed file size is %v bytes.", v.OriginalName, maxBytes), - ) + ).SetParams(map[string]any{ + "file": v.OriginalName, + "maxSize": maxBytes, + }) } return nil } } -// UploadedFileMimeType checks whether the validated `rest.UploadedFile` +// UploadedFileMimeType checks whether the validated [*filesystem.File] // mimetype is within the provided allowed mime types. // // Example: @@ -42,7 +49,11 @@ func UploadedFileSize(maxBytes int) validation.RuleFunc { // validation.Field(&form.File, validation.By(validators.UploadedFileMimeType(validMimeTypes))) func UploadedFileMimeType(validTypes []string) validation.RuleFunc { return func(value any) error { - v, _ := value.(*filesystem.File) + v, ok := value.(*filesystem.File) + if !ok { + return ErrUnsupportedValueType + } + if v == nil { return nil // nothing to validate } diff --git a/core/validators/file_test.go b/core/validators/file_test.go new file mode 100644 index 00000000..3af6fe53 --- /dev/null +++ b/core/validators/file_test.go @@ -0,0 +1,75 @@ +package validators_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core/validators" + "github.com/pocketbase/pocketbase/tools/filesystem" +) + +func TestUploadedFileSize(t *testing.T) { + t.Parallel() + + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + maxBytes int64 + file *filesystem.File + expectError bool + }{ + {0, nil, false}, + {4, nil, false}, + {3, file, true}, // all test files have "test" as content + {4, file, false}, + {5, file, false}, + } + + for _, s := range scenarios { + t.Run(fmt.Sprintf("%d", s.maxBytes), func(t *testing.T) { + err := validators.UploadedFileSize(s.maxBytes)(s.file) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} + +func TestUploadedFileMimeType(t *testing.T) { + t.Parallel() + + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.png") // the extension shouldn't matter + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + types []string + file *filesystem.File + expectError bool + }{ + {nil, nil, false}, + {[]string{"image/jpeg"}, nil, false}, + {[]string{}, file, true}, + {[]string{"image/jpeg"}, file, true}, + // test files are detected as "text/plain; charset=utf-8" content type + {[]string{"image/jpeg", "text/plain; charset=utf-8"}, file, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, strings.Join(s.types, ";")), func(t *testing.T) { + err := validators.UploadedFileMimeType(s.types)(s.file) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} diff --git a/core/validators/string.go b/core/validators/string.go new file mode 100644 index 00000000..c0d885b5 --- /dev/null +++ b/core/validators/string.go @@ -0,0 +1,29 @@ +package validators + +import ( + "regexp" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +// IsRegex checks whether the validated value is a valid regular expression pattern. +// +// Example: +// +// validation.Field(&form.Pattern, validation.By(validators.IsRegex)) +func IsRegex(value any) error { + v, ok := value.(string) + if !ok { + return ErrUnsupportedValueType + } + + if v == "" { + return nil // nothing to check + } + + if _, err := regexp.Compile(v); err != nil { + return validation.NewError("validation_invalid_regex", err.Error()) + } + + return nil +} diff --git a/core/validators/string_test.go b/core/validators/string_test.go new file mode 100644 index 00000000..dead6df7 --- /dev/null +++ b/core/validators/string_test.go @@ -0,0 +1,33 @@ +package validators_test + +import ( + "fmt" + "testing" + + "github.com/pocketbase/pocketbase/core/validators" +) + +func TestIsRegex(t *testing.T) { + t.Parallel() + + scenarios := []struct { + val string + expectError bool + }{ + {"", false}, + {`abc`, false}, + {`\w+`, false}, + {`\w*((abc+`, true}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + err := validators.IsRegex(s.val) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } + }) + } +} diff --git a/core/validators/validators.go b/core/validators/validators.go new file mode 100644 index 00000000..a4ce3a6f --- /dev/null +++ b/core/validators/validators.go @@ -0,0 +1,40 @@ +// Package validators implements some common custom PocketBase validators. +package validators + +import ( + "errors" + "maps" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +var ErrUnsupportedValueType = validation.NewError("validation_unsupported_value_type", "Invalid or unsupported value type.") + +// JoinValidationErrors attempts to join the provided [validation.Errors] arguments. +// +// If only one of the arguments is [validation.Errors], it returns the first non-empty [validation.Errors]. +// +// If both arguments are not [validation.Errors] then it returns a combined [errors.Join] error. +func JoinValidationErrors(errA, errB error) error { + vErrA, okA := errA.(validation.Errors) + vErrB, okB := errB.(validation.Errors) + + // merge + if okA && okB { + result := maps.Clone(vErrA) + maps.Copy(result, vErrB) + if len(result) > 0 { + return result + } + } + + if okA && len(vErrA) > 0 { + return vErrA + } + + if okB && len(vErrB) > 0 { + return vErrB + } + + return errors.Join(errA, errB) +} diff --git a/core/validators/validators_test.go b/core/validators/validators_test.go new file mode 100644 index 00000000..0a54fa8e --- /dev/null +++ b/core/validators/validators_test.go @@ -0,0 +1,39 @@ +package validators_test + +import ( + "errors" + "fmt" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core/validators" +) + +func TestJoinValidationErrors(t *testing.T) { + scenarios := []struct { + errA error + errB error + expected string + }{ + {nil, nil, ""}, + {errors.New("abc"), nil, "abc"}, + {nil, errors.New("abc"), "abc"}, + {errors.New("abc"), errors.New("456"), "abc\n456"}, + {validation.Errors{"test1": errors.New("test1_err")}, nil, "test1: test1_err."}, + {nil, validation.Errors{"test2": errors.New("test2_err")}, "test2: test2_err."}, + {validation.Errors{}, errors.New("456"), "\n456"}, + {errors.New("456"), validation.Errors{}, "456\n"}, + {validation.Errors{"test1": errors.New("test1_err")}, errors.New("456"), "test1: test1_err."}, + {errors.New("456"), validation.Errors{"test2": errors.New("test2_err")}, "test2: test2_err."}, + {validation.Errors{"test1": errors.New("test1_err")}, validation.Errors{"test2": errors.New("test2_err")}, "test1: test1_err; test2: test2_err."}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#T_%T", i, s.errA, s.errB), func(t *testing.T) { + result := fmt.Sprintf("%v", validators.JoinValidationErrors(s.errA, s.errB)) + if result != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, result) + } + }) + } +} diff --git a/daos/view.go b/core/view.go similarity index 66% rename from daos/view.go rename to core/view.go index a7cc7053..da30d8b5 100644 --- a/daos/view.go +++ b/core/view.go @@ -1,4 +1,4 @@ -package daos +package core import ( "errors" @@ -8,23 +8,20 @@ import ( "strings" "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/inflector" - "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/security" "github.com/pocketbase/pocketbase/tools/tokenizer" - "github.com/pocketbase/pocketbase/tools/types" ) // DeleteView drops the specified view name. // // This method is a no-op if a view with the provided name doesn't exist. // -// Be aware that this method is vulnerable to SQL injection and the +// NB! Be aware that this method is vulnerable to SQL injection and the // "name" argument must come only from trusted input! -func (dao *Dao) DeleteView(name string) error { - _, err := dao.DB().NewQuery(fmt.Sprintf( +func (app *BaseApp) DeleteView(name string) error { + _, err := app.DB().NewQuery(fmt.Sprintf( "DROP VIEW IF EXISTS {{%s}}", name, )).Execute() @@ -34,18 +31,18 @@ func (dao *Dao) DeleteView(name string) error { // SaveView creates (or updates already existing) persistent SQL view. // -// Be aware that this method is vulnerable to SQL injection and the +// NB! Be aware that this method is vulnerable to SQL injection and the // "selectQuery" argument must come only from trusted input! -func (dao *Dao) SaveView(name string, selectQuery string) error { - return dao.RunInTransaction(func(txDao *Dao) error { +func (app *BaseApp) SaveView(name string, selectQuery string) error { + return app.RunInTransaction(func(txApp App) error { // delete old view (if exists) - if err := txDao.DeleteView(name); err != nil { + if err := txApp.DeleteView(name); err != nil { return err } selectQuery = strings.Trim(strings.TrimSpace(selectQuery), ";") - // try to eagerly detect multiple inline statements + // try to loosely detect multiple inline statements tk := tokenizer.NewFromString(selectQuery) tk.Separators(';') if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 { @@ -55,18 +52,18 @@ func (dao *Dao) SaveView(name string, selectQuery string) error { // (re)create the view // // note: the query is wrapped in a secondary SELECT as a rudimentary - // measure to discourage multiple inline sql statements execution. + // measure to discourage multiple inline sql statements execution viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", name, selectQuery) - if _, err := txDao.DB().NewQuery(viewQuery).Execute(); err != nil { + if _, err := txApp.DB().NewQuery(viewQuery).Execute(); err != nil { return err } // fetch the view table info to ensure that the view was created // because missing tables or columns won't return an error - if _, err := txDao.TableInfo(name); err != nil { + if _, err := txApp.TableInfo(name); err != nil { // manually cleanup previously created view in case the func // is called in a nested transaction and the error is discarded - txDao.DeleteView(name) + txApp.DeleteView(name) return err } @@ -75,31 +72,23 @@ func (dao *Dao) SaveView(name string, selectQuery string) error { }) } -// CreateViewSchema creates a new view schema from the provided select query. +// CreateViewFields creates a new FieldsList from the provided select query. // // There are some caveats: // - The select query must have an "id" column. // - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data. -func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) { - result := schema.NewSchema() +func (app *BaseApp) CreateViewFields(selectQuery string) (FieldsList, error) { + result := NewFieldsList() - suggestedFields, err := dao.parseQueryToFields(selectQuery) + suggestedFields, err := parseQueryToFields(app, selectQuery) if err != nil { return result, err } // note wrap in a transaction in case the selectQuery contains // multiple statements allowing us to rollback on any error - txErr := dao.RunInTransaction(func(txDao *Dao) error { - tempView := "_temp_" + security.PseudorandomString(5) - // create a temp view with the provided query - if err := txDao.SaveView(tempView, selectQuery); err != nil { - return err - } - defer txDao.DeleteView(tempView) - - // extract the generated view table info - info, err := txDao.TableInfo(tempView) + txErr := app.RunInTransaction(func(txApp App) error { + info, err := getQueryTableInfo(txApp, selectQuery) if err != nil { return err } @@ -107,15 +96,11 @@ func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) { var hasId bool for _, row := range info { - if row.Name == schema.FieldNameId { + if row.Name == FieldNameId { hasId = true } - if list.ExistInSlice(row.Name, schema.BaseModelFieldNames()) { - continue // skip base model fields since they are not part of the schema - } - - var field *schema.SchemaField + var field Field if f, ok := suggestedFields[row.Name]; ok { field = f.field @@ -123,7 +108,7 @@ func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) { field = defaultViewField(row.Name) } - result.AddField(field) + result.Add(field) } if !hasId { @@ -136,14 +121,9 @@ func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) { return result, txErr } -// FindRecordByViewFile returns the original models.Record of the -// provided view collection file. -func (dao *Dao) FindRecordByViewFile( - viewCollectionNameOrId string, - fileFieldName string, - filename string, -) (*models.Record, error) { - view, err := dao.FindCollectionByNameOrId(viewCollectionNameOrId) +// FindRecordByViewFile returns the original Record of the provided view collection file. +func (app *BaseApp) FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error) { + view, err := getCollectionByModelOrIdentifier(app, viewCollectionModelOrIdentifier) if err != nil { return nil, err } @@ -160,7 +140,7 @@ func (dao *Dao) FindRecordByViewFile( return nil, errors.New("reached the max recursion level of view collection file field queries") } - queryFields, err := dao.parseQueryToFields(view.ViewOptions().Query) + queryFields, err := parseQueryToFields(app, view.ViewQuery) if err != nil { return nil, err } @@ -168,13 +148,13 @@ func (dao *Dao) FindRecordByViewFile( for _, item := range queryFields { if item.collection == nil || item.original == nil || - item.field.Name != fileFieldName { + item.field.GetName() != fileFieldName { continue } if item.collection.IsView() { view = item.collection - fileFieldName = item.original.Name + fileFieldName = item.original.GetName() return findFirstNonViewQueryFileField(level + 1) } @@ -189,19 +169,19 @@ func (dao *Dao) FindRecordByViewFile( return nil, err } - cleanFieldName := inflector.Columnify(qf.original.Name) + cleanFieldName := inflector.Columnify(qf.original.GetName()) - record := &models.Record{} + record := &Record{} - query := dao.RecordQuery(qf.collection).Limit(1) + query := app.RecordQuery(qf.collection).Limit(1) - if opt, ok := qf.original.Options.(schema.MultiValuer); !ok || !opt.IsMultiple() { + if opt, ok := qf.original.(MultiValuer); !ok || !opt.IsMultiple() { query.AndWhere(dbx.HashExp{cleanFieldName: filename}) } else { - query.InnerJoin(fmt.Sprintf( - `json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{_je_file}}`, - cleanFieldName, cleanFieldName, cleanFieldName, - ), dbx.HashExp{"_je_file.value": filename}) + query.InnerJoin( + fmt.Sprintf(`%s as {{_je_file}}`, dbutils.JSONEach(cleanFieldName)), + dbx.HashExp{"_je_file.value": filename}, + ) } if err := query.One(record); err != nil { @@ -217,36 +197,33 @@ func (dao *Dao) FindRecordByViewFile( type queryField struct { // field is the final resolved field. - field *schema.SchemaField + field Field // collection refers to the original field's collection model. - // It could be nil if the found query field is not from a collection schema. - collection *models.Collection + // It could be nil if the found query field is not from a collection + collection *Collection // original is the original found collection field. - // It could be nil if the found query field is not from a collection schema. - original *schema.SchemaField + // It could be nil if the found query field is not from a collection + original Field } -func defaultViewField(name string) *schema.SchemaField { - return &schema.SchemaField{ - Name: name, - Type: schema.FieldTypeJson, - Options: &schema.JsonOptions{ - MaxSize: 1, // the size doesn't matter in this case - }, +func defaultViewField(name string) Field { + return &JSONField{ + Name: name, + MaxSize: 1, // unused for views } } var castRegex = regexp.MustCompile(`(?i)^cast\s*\(.*\s+as\s+(\w+)\s*\)$`) -func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, error) { +func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, error) { p := new(identifiersParser) if err := p.parse(selectQuery); err != nil { return nil, err } - collections, err := dao.findCollectionsByIdentifiers(p.tables) + collections, err := findCollectionsByIdentifiers(app, p.tables) if err != nil { return nil, err } @@ -262,12 +239,24 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, for _, col := range p.columns { colLower := strings.ToLower(col.original) + // pk (always assume text field for now) + if col.alias == FieldNameId { + result[col.alias] = &queryField{ + field: &TextField{ + Name: col.alias, + System: true, + Required: true, + PrimaryKey: true, + }, + } + continue + } + // numeric aggregations if strings.HasPrefix(colLower, "count(") || strings.HasPrefix(colLower, "total(") { result[col.alias] = &queryField{ - field: &schema.SchemaField{ + field: &NumberField{ Name: col.alias, - Type: schema.FieldTypeNumber, }, } continue @@ -280,25 +269,22 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, switch castMatch[1] { case "real", "integer", "int", "decimal", "numeric": result[col.alias] = &queryField{ - field: &schema.SchemaField{ + field: &NumberField{ Name: col.alias, - Type: schema.FieldTypeNumber, }, } continue case "text": result[col.alias] = &queryField{ - field: &schema.SchemaField{ + field: &TextField{ Name: col.alias, - Type: schema.FieldTypeText, }, } continue case "boolean", "bool": result[col.alias] = &queryField{ - field: &schema.SchemaField{ + field: &BoolField{ Name: col.alias, - Type: schema.FieldTypeBool, }, } continue @@ -308,7 +294,7 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, parts := strings.Split(col.original, ".") var fieldName string - var collection *models.Collection + var collection *Collection if len(parts) == 2 { fieldName = parts[1] @@ -318,7 +304,7 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, collection = collections[mainTable.alias] } - // fallback to the default field if the found column is not from a collection schema + // fallback to the default field if collection == nil { result[col.alias] = &queryField{ field: defaultViewField(col.alias), @@ -331,83 +317,62 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, } // find the first field by name (case insensitive) - var field *schema.SchemaField - for _, f := range collection.Schema.Fields() { - if strings.EqualFold(f.Name, fieldName) { + var field Field + for _, f := range collection.Fields { + if strings.EqualFold(f.GetName(), fieldName) { field = f break } } - if field != nil { - clone := *field - clone.Id = "" // unset to prevent duplications if the same field is aliased multiple times - clone.Name = col.alias - result[col.alias] = &queryField{ - field: &clone, - collection: collection, - original: field, - } - continue - } - - if fieldName == schema.FieldNameId { - // convert to relation since it is a direct id reference - result[col.alias] = &queryField{ - field: &schema.SchemaField{ - Name: col.alias, - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(1), - CollectionId: collection.Id, - }, - }, - collection: collection, - } - } else if fieldName == schema.FieldNameCreated || fieldName == schema.FieldNameUpdated { - result[col.alias] = &queryField{ - field: &schema.SchemaField{ - Name: col.alias, - Type: schema.FieldTypeDate, - }, - collection: collection, - } - } else if fieldName == schema.FieldNameUsername && collection.IsAuth() { - result[col.alias] = &queryField{ - field: &schema.SchemaField{ - Name: col.alias, - Type: schema.FieldTypeText, - }, - collection: collection, - } - } else if fieldName == schema.FieldNameEmail && collection.IsAuth() { - result[col.alias] = &queryField{ - field: &schema.SchemaField{ - Name: col.alias, - Type: schema.FieldTypeEmail, - }, - collection: collection, - } - } else if (fieldName == schema.FieldNameVerified || fieldName == schema.FieldNameEmailVisibility) && collection.IsAuth() { - result[col.alias] = &queryField{ - field: &schema.SchemaField{ - Name: col.alias, - Type: schema.FieldTypeBool, - }, - collection: collection, - } - } else { + // fallback to the default field + if field == nil { result[col.alias] = &queryField{ field: defaultViewField(col.alias), collection: collection, } + continue + } + + // convert to relation since it is an id reference + if strings.EqualFold(fieldName, FieldNameId) { + result[col.alias] = &queryField{ + field: &RelationField{ + Name: col.alias, + MaxSelect: 1, + CollectionId: collection.Id, + }, + collection: collection, + } + continue + } + + // we fetch a brand new collection object to avoid using reflection + // or having a dedicated Clone method for each field type + tempCollection, err := app.FindCollectionByNameOrId(collection.Id) + if err != nil { + return nil, err + } + + clone := tempCollection.Fields.GetById(field.GetId()) + if clone == nil { + return nil, fmt.Errorf("missing expected field %q (%q) in collection %q", field.GetName(), field.GetId(), tempCollection.Name) + } + // set new random id to prevent duplications if the same field is aliased multiple times + clone.SetId("_clone_" + security.PseudorandomString(4)) + clone.SetName(col.alias) + + result[col.alias] = &queryField{ + original: field, + field: clone, + collection: collection, } } return result, nil } -func (dao *Dao) findCollectionsByIdentifiers(tables []identifier) (map[string]*models.Collection, error) { +func findCollectionsByIdentifiers(app App, tables []identifier) (map[string]*Collection, error) { names := make([]any, 0, len(tables)) for _, table := range tables { @@ -421,10 +386,10 @@ func (dao *Dao) findCollectionsByIdentifiers(tables []identifier) (map[string]*m return nil, nil } - result := make(map[string]*models.Collection, len(names)) - collections := make([]*models.Collection, 0, len(names)) + result := make(map[string]*Collection, len(names)) + collections := make([]*Collection, 0, len(names)) - err := dao.CollectionQuery(). + err := app.CollectionQuery(). AndWhere(dbx.In("name", names...)). All(&collections) if err != nil { @@ -442,12 +407,37 @@ func (dao *Dao) findCollectionsByIdentifiers(tables []identifier) (map[string]*m return result, nil } +func getQueryTableInfo(app App, selectQuery string) ([]*TableInfoRow, error) { + tempView := "_temp_" + security.PseudorandomString(6) + + var info []*TableInfoRow + + txErr := app.RunInTransaction(func(txApp App) error { + // create a temp view with the provided query + err := txApp.SaveView(tempView, selectQuery) + if err != nil { + return err + } + + // extract the generated view table info + info, err = txApp.TableInfo(tempView) + + return errors.Join(err, txApp.DeleteView(tempView)) + }) + + if txErr != nil { + return nil, txErr + } + + return info, nil +} + // ------------------------------------------------------------------- // Raw query identifiers parser // ------------------------------------------------------------------- -var joinReplaceRegex = regexp.MustCompile(`(?im)\s+(inner join|outer join|left join|right join|join)\s+?`) -var discardReplaceRegex = regexp.MustCompile(`(?im)\s+(where|group by|having|order|limit|with)\s+?`) +var joinReplaceRegex = regexp.MustCompile(`(?im)\s+(full\s+outer\s+join|left\s+outer\s+join|right\s+outer\s+join|full\s+join|cross\s+join|inner\s+join|outer\s+join|left\s+join|right\s+join|join)\s+?`) +var discardReplaceRegex = regexp.MustCompile(`(?im)\s+(where|group\s+by|having|order|limit|with)\s+?`) var commentsReplaceRegex = regexp.MustCompile(`(?m)(\/\*[\s\S]+\*\/)|(--.+$)`) type identifier struct { diff --git a/daos/view_test.go b/core/view_test.go similarity index 60% rename from daos/view_test.go rename to core/view_test.go index a84a00a9..bd4ec3d6 100644 --- a/daos/view_test.go +++ b/core/view_test.go @@ -1,23 +1,20 @@ -package daos_test +package core_test import ( "encoding/json" "fmt" + "slices" "testing" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/list" ) func ensureNoTempViews(app core.App, t *testing.T) { var total int - err := app.Dao().DB().Select("count(*)"). + err := app.DB().Select("count(*)"). From("sqlite_schema"). AndWhere(dbx.HashExp{"type": "view"}). AndWhere(dbx.NewExp(`[[name]] LIKE '%\_temp\_%' ESCAPE '\'`)). @@ -50,7 +47,7 @@ func TestDeleteView(t *testing.T) { } for i, s := range scenarios { - err := app.Dao().DeleteView(s.viewName) + err := app.DeleteView(s.viewName) hasErr := err != nil if hasErr != s.expectError { @@ -84,7 +81,7 @@ func TestSaveView(t *testing.T) { { "empty name", "", - "select * from _admins", + "select * from " + core.CollectionNameSuperusers, true, nil, }, @@ -112,39 +109,40 @@ func TestSaveView(t *testing.T) { { "non select query", "123Test", - "drop table _admins", + "drop table " + core.CollectionNameSuperusers, true, nil, }, { "multiple select queries", "123Test", - "select *, count(id) as c from _admins; select * from demo1;", + "select *, count(id) as c from " + core.CollectionNameSuperusers + "; select * from demo1;", true, nil, }, { "try to break the parent parenthesis", "123Test", - "select *, count(id) as c from `_admins`)", + "select *, count(id) as c from `" + core.CollectionNameSuperusers + "`)", true, nil, }, { "simple select query (+ trimmed semicolon)", "123Test", - ";select *, count(id) as c from _admins;", + ";select *, count(id) as c from " + core.CollectionNameSuperusers + ";", false, []string{ "id", "created", "updated", - "passwordHash", "tokenKey", "email", - "lastResetSentAt", "avatar", "c", + "password", "tokenKey", "email", + "emailVisibility", "verified", + "c", }, }, { "update old view with new query", "123Test", - "select 1 as test from _admins", + "select 1 as test from " + core.CollectionNameSuperusers, false, []string{"test"}, }, @@ -152,7 +150,7 @@ func TestSaveView(t *testing.T) { for _, s := range scenarios { t.Run(s.scenarioName, func(t *testing.T) { - err := app.Dao().SaveView(s.viewName, s.query) + err := app.SaveView(s.viewName, s.query) hasErr := err != nil if hasErr != s.expectError { @@ -163,7 +161,7 @@ func TestSaveView(t *testing.T) { return } - infoRows, err := app.Dao().TableInfo(s.viewName) + infoRows, err := app.TableInfo(s.viewName) if err != nil { t.Fatalf("Failed to fetch table info for %s: %v", s.viewName, err) } @@ -173,7 +171,7 @@ func TestSaveView(t *testing.T) { } for _, row := range infoRows { - if !list.ExistInSlice(row.Name, s.expectColumns) { + if !slices.Contains(s.expectColumns, row.Name) { t.Fatalf("Missing %q column in %v", row.Name, s.expectColumns) } } @@ -183,14 +181,14 @@ func TestSaveView(t *testing.T) { ensureNoTempViews(app, t) } -func TestCreateViewSchemaWithDiscardedNestedTransaction(t *testing.T) { +func TestCreateViewFieldsWithDiscardedNestedTransaction(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() - app.Dao().RunInTransaction(func(txDao *daos.Dao) error { - _, err := txDao.CreateViewSchema("select id from missing") + app.RunInTransaction(func(txApp core.App) error { + _, err := txApp.CreateViewFields("select id from missing") if err == nil { t.Fatal("Expected error, got nil") } @@ -201,7 +199,7 @@ func TestCreateViewSchemaWithDiscardedNestedTransaction(t *testing.T) { ensureNoTempViews(app, t) } -func TestCreateViewSchema(t *testing.T) { +func TestCreateViewFields(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() @@ -256,8 +254,11 @@ func TestCreateViewSchema(t *testing.T) { `, false, map[string]string{ - "text": schema.FieldTypeText, - "url": schema.FieldTypeUrl, + "id": core.FieldTypeText, + "text": core.FieldTypeText, + "url": core.FieldTypeURL, + "created": core.FieldTypeAutodate, + "updated": core.FieldTypeAutodate, }, }, { @@ -285,20 +286,23 @@ func TestCreateViewSchema(t *testing.T) { `, false, map[string]string{ - "text": schema.FieldTypeText, - "bool": schema.FieldTypeBool, - "url": schema.FieldTypeUrl, - "select_one": schema.FieldTypeSelect, - "select_many": schema.FieldTypeSelect, - "file_one": schema.FieldTypeFile, - "file_many": schema.FieldTypeFile, - "number_alias": schema.FieldTypeNumber, - "email": schema.FieldTypeEmail, - "datetime": schema.FieldTypeDate, - "json": schema.FieldTypeJson, - "rel_one": schema.FieldTypeRelation, - "rel_many": schema.FieldTypeRelation, - "single_quoted_column": schema.FieldTypeJson, + "id": core.FieldTypeText, + "created": core.FieldTypeAutodate, + "updated": core.FieldTypeAutodate, + "text": core.FieldTypeText, + "bool": core.FieldTypeBool, + "url": core.FieldTypeURL, + "select_one": core.FieldTypeSelect, + "select_many": core.FieldTypeSelect, + "file_one": core.FieldTypeFile, + "file_many": core.FieldTypeFile, + "number_alias": core.FieldTypeNumber, + "email": core.FieldTypeEmail, + "datetime": core.FieldTypeDate, + "json": core.FieldTypeJSON, + "rel_one": core.FieldTypeRelation, + "rel_many": core.FieldTypeRelation, + "single_quoted_column": core.FieldTypeJSON, }, }, { @@ -306,7 +310,9 @@ func TestCreateViewSchema(t *testing.T) { "select a.id, b.id as bid, b.created from demo1 as a left join demo2 b", false, map[string]string{ - "bid": schema.FieldTypeRelation, + "id": core.FieldTypeText, + "bid": core.FieldTypeRelation, + "created": core.FieldTypeAutodate, }, }, { @@ -318,24 +324,25 @@ func TestCreateViewSchema(t *testing.T) { lj.id cid, ij.id as did, a.bool, - _admins.id as eid, - _admins.email + ` + core.CollectionNameSuperusers + `.id as eid, + ` + core.CollectionNameSuperusers + `.email from demo1 a, demo2 as b left join demo3 lj on lj.id = 123 inner join demo4 as ij on ij.id = 123 - join _admins + join ` + core.CollectionNameSuperusers + ` where 1=1 group by a.id limit 10 `, false, map[string]string{ - "bid": schema.FieldTypeRelation, - "cid": schema.FieldTypeRelation, - "did": schema.FieldTypeRelation, - "bool": schema.FieldTypeBool, - "eid": schema.FieldTypeJson, // not from collection - "email": schema.FieldTypeJson, // not from collection + "id": core.FieldTypeText, + "bid": core.FieldTypeRelation, + "cid": core.FieldTypeRelation, + "did": core.FieldTypeRelation, + "bool": core.FieldTypeBool, + "eid": core.FieldTypeRelation, + "email": core.FieldTypeEmail, }, }, { @@ -359,21 +366,22 @@ func TestCreateViewSchema(t *testing.T) { from demo1 a`, false, map[string]string{ - "count": schema.FieldTypeNumber, - "total": schema.FieldTypeNumber, - "cast_int": schema.FieldTypeNumber, - "cast_integer": schema.FieldTypeNumber, - "cast_real": schema.FieldTypeNumber, - "cast_decimal": schema.FieldTypeNumber, - "cast_numeric": schema.FieldTypeNumber, - "cast_text": schema.FieldTypeText, - "cast_bool": schema.FieldTypeBool, - "cast_boolean": schema.FieldTypeBool, + "id": core.FieldTypeText, + "count": core.FieldTypeNumber, + "total": core.FieldTypeNumber, + "cast_int": core.FieldTypeNumber, + "cast_integer": core.FieldTypeNumber, + "cast_real": core.FieldTypeNumber, + "cast_decimal": core.FieldTypeNumber, + "cast_numeric": core.FieldTypeNumber, + "cast_text": core.FieldTypeText, + "cast_bool": core.FieldTypeBool, + "cast_boolean": core.FieldTypeBool, // json because they are nullable - "sum": schema.FieldTypeJson, - "avg": schema.FieldTypeJson, - "min": schema.FieldTypeJson, - "max": schema.FieldTypeJson, + "sum": core.FieldTypeJSON, + "avg": core.FieldTypeJSON, + "min": core.FieldTypeJSON, + "max": core.FieldTypeJSON, }, }, { @@ -391,11 +399,12 @@ func TestCreateViewSchema(t *testing.T) { `, false, map[string]string{ - "username": schema.FieldTypeText, - "email": schema.FieldTypeEmail, - "emailVisibility": schema.FieldTypeBool, - "verified": schema.FieldTypeBool, - "relid": schema.FieldTypeRelation, + "id": core.FieldTypeText, + "username": core.FieldTypeText, + "email": core.FieldTypeEmail, + "emailVisibility": core.FieldTypeBool, + "verified": core.FieldTypeBool, + "relid": core.FieldTypeRelation, }, }, { @@ -413,14 +422,15 @@ func TestCreateViewSchema(t *testing.T) { from demo1`, false, map[string]string{ - "id2": schema.FieldTypeRelation, - "text_alias": schema.FieldTypeText, - "url_alias": schema.FieldTypeUrl, - "bool_alias": schema.FieldTypeBool, - "number_alias": schema.FieldTypeNumber, - "created_alias": schema.FieldTypeDate, - "updated_alias": schema.FieldTypeDate, - "custom": schema.FieldTypeJson, + "id": core.FieldTypeText, + "id2": core.FieldTypeRelation, + "text_alias": core.FieldTypeText, + "url_alias": core.FieldTypeURL, + "bool_alias": core.FieldTypeBool, + "number_alias": core.FieldTypeNumber, + "created_alias": core.FieldTypeAutodate, + "updated_alias": core.FieldTypeAutodate, + "custom": core.FieldTypeJSON, }, }, { @@ -432,8 +442,9 @@ func TestCreateViewSchema(t *testing.T) { from demo1`, false, map[string]string{ - "id2": schema.FieldTypeRelation, - "custom": schema.FieldTypeJson, + "id2": core.FieldTypeRelation, + "id": core.FieldTypeText, + "custom": core.FieldTypeJSON, }, }, { @@ -448,46 +459,45 @@ func TestCreateViewSchema(t *testing.T) { left join demo1 as b`, false, map[string]string{ - "alias1": schema.FieldTypeText, - "alias2": schema.FieldTypeText, - "alias3": schema.FieldTypeText, - "alias4": schema.FieldTypeText, + "id": core.FieldTypeText, + "alias1": core.FieldTypeText, + "alias2": core.FieldTypeText, + "alias3": core.FieldTypeText, + "alias4": core.FieldTypeText, }, }, } for _, s := range scenarios { - result, err := app.Dao().CreateViewSchema(s.query) + t.Run(s.name, func(t *testing.T) { + result, err := app.CreateViewFields(s.query) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - continue - } - - if hasErr { - continue - } - - if len(s.expectFields) != len(result.Fields()) { - serialized, _ := json.Marshal(result) - t.Errorf("[%s] Expected %d fields, got %d: \n%s", s.name, len(s.expectFields), len(result.Fields()), serialized) - continue - } - - for name, typ := range s.expectFields { - field := result.GetFieldByName(name) - - if field == nil { - t.Errorf("[%s] Expected to find field %s, got nil", s.name, name) - continue + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } - if field.Type != typ { - t.Errorf("[%s] Expected field %s to be %q, got %s", s.name, name, typ, field.Type) - continue + if hasErr { + return } - } + + if len(s.expectFields) != len(result) { + serialized, _ := json.Marshal(result) + t.Fatalf("Expected %d fields, got %d: \n%s", len(s.expectFields), len(result), serialized) + } + + for name, typ := range s.expectFields { + field := result.GetByName(name) + + if field == nil { + t.Fatalf("Expected to find field %s, got nil", name) + } + + if field.Type() != typ { + t.Fatalf("Expected field %s to be %q, got %q", name, typ, field.Type()) + } + } + }) } ensureNoTempViews(app, t) @@ -499,7 +509,7 @@ func TestFindRecordByViewFile(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - prevCollection, err := app.Dao().FindCollectionByNameOrId("demo1") + prevCollection, err := app.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } @@ -509,22 +519,20 @@ func TestFindRecordByViewFile(t *testing.T) { // create collection view mocks fileOneAlias := "file_one one0" fileManyAlias := "file_many many0" - mockCollections := make([]*models.Collection, 0, totalLevels) + mockCollections := make([]*core.Collection, 0, totalLevels) for i := 0; i <= totalLevels; i++ { - view := new(models.Collection) - view.Type = models.CollectionTypeView + view := new(core.Collection) + view.Type = core.CollectionTypeView view.Name = fmt.Sprintf("_test_view%d", i) - view.SetOptions(&models.CollectionViewOptions{ - Query: fmt.Sprintf( - "select id, %s, %s from %s", - fileOneAlias, - fileManyAlias, - prevCollection.Name, - ), - }) + view.ViewQuery = fmt.Sprintf( + "select id, %s, %s from %s", + fileOneAlias, + fileManyAlias, + prevCollection.Name, + ) // save view - if err := app.Dao().SaveCollection(view); err != nil { + if err := app.Save(view); err != nil { t.Fatalf("Failed to save view%d: %v", i, err) } @@ -586,7 +594,6 @@ func TestFindRecordByViewFile(t *testing.T) { false, expectedRecordId, }, - { "last view collection before the recursion limit (single file)", mockCollections[totalLevels-2].Name, @@ -606,24 +613,25 @@ func TestFindRecordByViewFile(t *testing.T) { } for _, s := range scenarios { - record, err := app.Dao().FindRecordByViewFile( - s.collectionNameOrId, - s.fileFieldName, - s.filename, - ) + t.Run(s.name, func(t *testing.T) { + record, err := app.FindRecordByViewFile( + s.collectionNameOrId, + s.fileFieldName, + s.filename, + ) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - continue - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } - if hasErr { - continue - } + if hasErr { + return + } - if record.Id != s.expectRecordId { - t.Errorf("[%s] Expected recordId %q, got %q", s.name, s.expectRecordId, record.Id) - } + if record.Id != s.expectRecordId { + t.Fatalf("Expected recordId %q, got %q", s.expectRecordId, record.Id) + } + }) } } diff --git a/daos/admin.go b/daos/admin.go deleted file mode 100644 index 5b80882b..00000000 --- a/daos/admin.go +++ /dev/null @@ -1,128 +0,0 @@ -package daos - -import ( - "errors" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/security" -) - -// AdminQuery returns a new Admin select query. -func (dao *Dao) AdminQuery() *dbx.SelectQuery { - return dao.ModelQuery(&models.Admin{}) -} - -// FindAdminById finds the admin with the provided id. -func (dao *Dao) FindAdminById(id string) (*models.Admin, error) { - model := &models.Admin{} - - err := dao.AdminQuery(). - AndWhere(dbx.HashExp{"id": id}). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - -// FindAdminByEmail finds the admin with the provided email address. -func (dao *Dao) FindAdminByEmail(email string) (*models.Admin, error) { - model := &models.Admin{} - - err := dao.AdminQuery(). - AndWhere(dbx.HashExp{"email": email}). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - -// FindAdminByToken finds the admin associated with the provided JWT. -// -// Returns an error if the JWT is invalid or expired. -func (dao *Dao) FindAdminByToken(token string, baseTokenKey string) (*models.Admin, error) { - // @todo consider caching the unverified claims - unverifiedClaims, err := security.ParseUnverifiedJWT(token) - if err != nil { - return nil, err - } - - // check required claims - id, _ := unverifiedClaims["id"].(string) - if id == "" { - return nil, errors.New("missing or invalid token claims") - } - - admin, err := dao.FindAdminById(id) - if err != nil || admin == nil { - return nil, err - } - - verificationKey := admin.TokenKey + baseTokenKey - - // verify token signature - if _, err := security.ParseJWT(token, verificationKey); err != nil { - return nil, err - } - - return admin, nil -} - -// TotalAdmins returns the number of existing admin records. -func (dao *Dao) TotalAdmins() (int, error) { - var total int - - err := dao.AdminQuery().Select("count(*)").Row(&total) - - return total, err -} - -// IsAdminEmailUnique checks if the provided email address is not -// already in use by other admins. -func (dao *Dao) IsAdminEmailUnique(email string, excludeIds ...string) bool { - if email == "" { - return false - } - - query := dao.AdminQuery().Select("count(*)"). - AndWhere(dbx.HashExp{"email": email}). - Limit(1) - - if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { - query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) - } - - var exists bool - - return query.Row(&exists) == nil && !exists -} - -// DeleteAdmin deletes the provided Admin model. -// -// Returns an error if there is only 1 admin. -func (dao *Dao) DeleteAdmin(admin *models.Admin) error { - total, err := dao.TotalAdmins() - if err != nil { - return err - } - - if total == 1 { - return errors.New("you cannot delete the only existing admin") - } - - return dao.Delete(admin) -} - -// SaveAdmin upserts the provided Admin model. -func (dao *Dao) SaveAdmin(admin *models.Admin) error { - return dao.Save(admin) -} diff --git a/daos/admin_test.go b/daos/admin_test.go deleted file mode 100644 index 89ab8e8b..00000000 --- a/daos/admin_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package daos_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestAdminQuery(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - expected := "SELECT {{_admins}}.* FROM `_admins`" - - sql := app.Dao().AdminQuery().Build().SQL() - if sql != expected { - t.Errorf("Expected sql %s, got %s", expected, sql) - } -} - -func TestFindAdminById(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - id string - expectError bool - }{ - {" ", true}, - {"missing", true}, - {"9q2trqumvlyr3bd", false}, - } - - for i, scenario := range scenarios { - admin, err := app.Dao().FindAdminById(scenario.id) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if admin != nil && admin.Id != scenario.id { - t.Errorf("(%d) Expected admin with id %s, got %s", i, scenario.id, admin.Id) - } - } -} - -func TestFindAdminByEmail(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - email string - expectError bool - }{ - {"", true}, - {"invalid", true}, - {"missing@example.com", true}, - {"test@example.com", false}, - } - - for i, scenario := range scenarios { - admin, err := app.Dao().FindAdminByEmail(scenario.email) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && admin.Email != scenario.email { - t.Errorf("(%d) Expected admin with email %s, got %s", i, scenario.email, admin.Email) - } - } -} - -func TestFindAdminByToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - token string - baseKey string - expectedEmail string - expectError bool - }{ - // invalid auth token - { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MDk5MTY2MX0.qrbkI2TITtFKMP6vrATrBVKPGjEiDIBeQ0mlqPGMVeY", - app.Settings().AdminAuthToken.Secret, - "", - true, - }, - // expired token - { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MDk5MTY2MX0.I7w8iktkleQvC7_UIRpD7rNzcU4OnF7i7SFIUu6lD_4", - app.Settings().AdminAuthToken.Secret, - "", - true, - }, - // wrong base token (password reset token secret instead of auth secret) - { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - app.Settings().AdminPasswordResetToken.Secret, - "", - true, - }, - // valid token - { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8", - app.Settings().AdminAuthToken.Secret, - "test@example.com", - false, - }, - } - - for i, scenario := range scenarios { - admin, err := app.Dao().FindAdminByToken(scenario.token, scenario.baseKey) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && admin.Email != scenario.expectedEmail { - t.Errorf("(%d) Expected admin model %s, got %s", i, scenario.expectedEmail, admin.Email) - } - } -} - -func TestTotalAdmins(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - result1, err := app.Dao().TotalAdmins() - if err != nil { - t.Fatal(err) - } - if result1 != 3 { - t.Fatalf("Expected 3 admins, got %d", result1) - } - - // delete all - app.Dao().DB().NewQuery("delete from {{_admins}}").Execute() - - result2, err := app.Dao().TotalAdmins() - if err != nil { - t.Fatal(err) - } - if result2 != 0 { - t.Fatalf("Expected 0 admins, got %d", result2) - } -} - -func TestIsAdminEmailUnique(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - email string - excludeId string - expected bool - }{ - {"", "", false}, - {"test@example.com", "", false}, - {"test2@example.com", "", false}, - {"test3@example.com", "", false}, - {"new@example.com", "", true}, - {"test@example.com", "sywbhecnh46rhm0", true}, - } - - for i, scenario := range scenarios { - result := app.Dao().IsAdminEmailUnique(scenario.email, scenario.excludeId) - if result != scenario.expected { - t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) - } - } -} - -func TestDeleteAdmin(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // try to delete unsaved admin model - deleteErr0 := app.Dao().DeleteAdmin(&models.Admin{}) - if deleteErr0 == nil { - t.Fatal("Expected error, got nil") - } - - admin1, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - admin2, err := app.Dao().FindAdminByEmail("test2@example.com") - if err != nil { - t.Fatal(err) - } - admin3, err := app.Dao().FindAdminByEmail("test3@example.com") - if err != nil { - t.Fatal(err) - } - - deleteErr1 := app.Dao().DeleteAdmin(admin1) - if deleteErr1 != nil { - t.Fatal(deleteErr1) - } - - deleteErr2 := app.Dao().DeleteAdmin(admin2) - if deleteErr2 != nil { - t.Fatal(deleteErr2) - } - - // cannot delete the only remaining admin - deleteErr3 := app.Dao().DeleteAdmin(admin3) - if deleteErr3 == nil { - t.Fatal("Expected delete error, got nil") - } - - total, _ := app.Dao().TotalAdmins() - if total != 1 { - t.Fatalf("Expected only 1 admin, got %d", total) - } -} - -func TestSaveAdmin(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create - newAdmin := &models.Admin{} - newAdmin.Email = "new@example.com" - newAdmin.SetPassword("123456") - saveErr1 := app.Dao().SaveAdmin(newAdmin) - if saveErr1 != nil { - t.Fatal(saveErr1) - } - if newAdmin.Id == "" { - t.Fatal("Expected admin id to be set") - } - - // update - existingAdmin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - updatedEmail := "test_update@example.com" - existingAdmin.Email = updatedEmail - saveErr2 := app.Dao().SaveAdmin(existingAdmin) - if saveErr2 != nil { - t.Fatal(saveErr2) - } - existingAdmin, _ = app.Dao().FindAdminById(existingAdmin.Id) - if existingAdmin.Email != updatedEmail { - t.Fatalf("Expected admin email to be %s, got %s", updatedEmail, existingAdmin.Email) - } -} diff --git a/daos/base.go b/daos/base.go deleted file mode 100644 index 5698c421..00000000 --- a/daos/base.go +++ /dev/null @@ -1,372 +0,0 @@ -// Package daos handles common PocketBase DB model manipulations. -// -// Think of daos as DB repository and service layer in one. -package daos - -import ( - "errors" - "fmt" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" -) - -// New creates a new Dao instance with the provided db builder -// (for both async and sync db operations). -func New(db dbx.Builder) *Dao { - return NewMultiDB(db, db) -} - -// NewMultiDB creates a new Dao instance with the provided dedicated -// async and sync db builders. -func NewMultiDB(concurrentDB, nonconcurrentDB dbx.Builder) *Dao { - return &Dao{ - concurrentDB: concurrentDB, - nonconcurrentDB: nonconcurrentDB, - MaxLockRetries: 8, - ModelQueryTimeout: 30 * time.Second, - } -} - -// Dao handles various db operations. -// -// You can think of Dao as a repository and service layer in one. -type Dao struct { - // in a transaction both refer to the same *dbx.TX instance - concurrentDB dbx.Builder - nonconcurrentDB dbx.Builder - - // MaxLockRetries specifies the default max "database is locked" auto retry attempts. - MaxLockRetries int - - // ModelQueryTimeout is the default max duration of a running ModelQuery(). - // - // This field has no effect if an explicit query context is already specified. - ModelQueryTimeout time.Duration - - // write hooks - BeforeCreateFunc func(eventDao *Dao, m models.Model, action func() error) error - AfterCreateFunc func(eventDao *Dao, m models.Model) error - BeforeUpdateFunc func(eventDao *Dao, m models.Model, action func() error) error - AfterUpdateFunc func(eventDao *Dao, m models.Model) error - BeforeDeleteFunc func(eventDao *Dao, m models.Model, action func() error) error - AfterDeleteFunc func(eventDao *Dao, m models.Model) error -} - -// DB returns the default dao db builder (*dbx.DB or *dbx.TX). -// -// Currently the default db builder is dao.concurrentDB but that may change in the future. -func (dao *Dao) DB() dbx.Builder { - return dao.ConcurrentDB() -} - -// ConcurrentDB returns the dao concurrent (aka. multiple open connections) -// db builder (*dbx.DB or *dbx.TX). -// -// In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance. -func (dao *Dao) ConcurrentDB() dbx.Builder { - return dao.concurrentDB -} - -// NonconcurrentDB returns the dao nonconcurrent (aka. single open connection) -// db builder (*dbx.DB or *dbx.TX). -// -// In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance. -func (dao *Dao) NonconcurrentDB() dbx.Builder { - return dao.nonconcurrentDB -} - -// Clone returns a new Dao with the same configuration options as the current one. -func (dao *Dao) Clone() *Dao { - clone := *dao - - return &clone -} - -// WithoutHooks returns a new Dao with the same configuration options -// as the current one, but without create/update/delete hooks. -func (dao *Dao) WithoutHooks() *Dao { - clone := dao.Clone() - - clone.BeforeCreateFunc = nil - clone.AfterCreateFunc = nil - clone.BeforeUpdateFunc = nil - clone.AfterUpdateFunc = nil - clone.BeforeDeleteFunc = nil - clone.AfterDeleteFunc = nil - - return clone -} - -// ModelQuery creates a new preconfigured select query with preset -// SELECT, FROM and other common fields based on the provided model. -func (dao *Dao) ModelQuery(m models.Model) *dbx.SelectQuery { - tableName := m.TableName() - - return dao.DB(). - Select("{{" + tableName + "}}.*"). - From(tableName). - WithBuildHook(func(query *dbx.Query) { - query.WithExecHook(execLockRetry(dao.ModelQueryTimeout, dao.MaxLockRetries)) - }) -} - -// FindById finds a single db record with the specified id and -// scans the result into m. -func (dao *Dao) FindById(m models.Model, id string) error { - return dao.ModelQuery(m).Where(dbx.HashExp{"id": id}).Limit(1).One(m) -} - -type afterCallGroup struct { - Model models.Model - EventDao *Dao - Action string -} - -// RunInTransaction wraps fn into a transaction. -// -// It is safe to nest RunInTransaction calls as long as you use the txDao. -func (dao *Dao) RunInTransaction(fn func(txDao *Dao) error) error { - switch txOrDB := dao.NonconcurrentDB().(type) { - case *dbx.Tx: - // nested transactions are not supported by default - // so execute the function within the current transaction - // --- - // create a new dao with the same hooks to avoid semaphore deadlock when nesting - txDao := New(txOrDB) - txDao.MaxLockRetries = dao.MaxLockRetries - txDao.ModelQueryTimeout = dao.ModelQueryTimeout - txDao.BeforeCreateFunc = dao.BeforeCreateFunc - txDao.BeforeUpdateFunc = dao.BeforeUpdateFunc - txDao.BeforeDeleteFunc = dao.BeforeDeleteFunc - txDao.AfterCreateFunc = dao.AfterCreateFunc - txDao.AfterUpdateFunc = dao.AfterUpdateFunc - txDao.AfterDeleteFunc = dao.AfterDeleteFunc - - return fn(txDao) - case *dbx.DB: - afterCalls := []afterCallGroup{} - - txError := txOrDB.Transactional(func(tx *dbx.Tx) error { - txDao := New(tx) - - if dao.BeforeCreateFunc != nil { - txDao.BeforeCreateFunc = func(eventDao *Dao, m models.Model, action func() error) error { - return dao.BeforeCreateFunc(eventDao, m, action) - } - } - if dao.BeforeUpdateFunc != nil { - txDao.BeforeUpdateFunc = func(eventDao *Dao, m models.Model, action func() error) error { - return dao.BeforeUpdateFunc(eventDao, m, action) - } - } - if dao.BeforeDeleteFunc != nil { - txDao.BeforeDeleteFunc = func(eventDao *Dao, m models.Model, action func() error) error { - return dao.BeforeDeleteFunc(eventDao, m, action) - } - } - - if dao.AfterCreateFunc != nil { - txDao.AfterCreateFunc = func(eventDao *Dao, m models.Model) error { - afterCalls = append(afterCalls, afterCallGroup{m, eventDao, "create"}) - return nil - } - } - if dao.AfterUpdateFunc != nil { - txDao.AfterUpdateFunc = func(eventDao *Dao, m models.Model) error { - afterCalls = append(afterCalls, afterCallGroup{m, eventDao, "update"}) - return nil - } - } - if dao.AfterDeleteFunc != nil { - txDao.AfterDeleteFunc = func(eventDao *Dao, m models.Model) error { - afterCalls = append(afterCalls, afterCallGroup{m, eventDao, "delete"}) - return nil - } - } - - return fn(txDao) - }) - if txError != nil { - return txError - } - - // execute after event calls on successful transaction - // (note: using the non-transaction dao to allow following queries in the after hooks) - var errs []error - for _, call := range afterCalls { - var err error - switch call.Action { - case "create": - err = dao.AfterCreateFunc(dao, call.Model) - case "update": - err = dao.AfterUpdateFunc(dao, call.Model) - case "delete": - err = dao.AfterDeleteFunc(dao, call.Model) - } - - if err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return fmt.Errorf("after transaction errors: %w", errors.Join(errs...)) - } - - return nil - } - - return errors.New("failed to start transaction (unknown dao.NonconcurrentDB() instance)") -} - -// Delete deletes the provided model. -func (dao *Dao) Delete(m models.Model) error { - if !m.HasId() { - return errors.New("ID is not set") - } - - return dao.lockRetry(func(retryDao *Dao) error { - action := func() error { - if err := retryDao.NonconcurrentDB().Model(m).Delete(); err != nil { - return err - } - - if retryDao.AfterDeleteFunc != nil { - retryDao.AfterDeleteFunc(retryDao, m) - } - - return nil - } - - if retryDao.BeforeDeleteFunc != nil { - return retryDao.BeforeDeleteFunc(retryDao, m, action) - } - - return action() - }) -} - -// Save persists the provided model in the database. -// -// If m.IsNew() is true, the method will perform a create, otherwise an update. -// To explicitly mark a model for update you can use m.MarkAsNotNew(). -func (dao *Dao) Save(m models.Model) error { - if m.IsNew() { - return dao.lockRetry(func(retryDao *Dao) error { - return retryDao.create(m) - }) - } - - return dao.lockRetry(func(retryDao *Dao) error { - return retryDao.update(m) - }) -} - -func (dao *Dao) update(m models.Model) error { - if !m.HasId() { - return errors.New("ID is not set") - } - - if m.GetCreated().IsZero() { - m.RefreshCreated() - } - - m.RefreshUpdated() - - action := func() error { - if v, ok := any(m).(models.ColumnValueMapper); ok { - dataMap := v.ColumnValueMap() - - _, err := dao.NonconcurrentDB().Update( - m.TableName(), - dataMap, - dbx.HashExp{"id": m.GetId()}, - ).Execute() - - if err != nil { - return err - } - } else if err := dao.NonconcurrentDB().Model(m).Update(); err != nil { - return err - } - - if dao.AfterUpdateFunc != nil { - return dao.AfterUpdateFunc(dao, m) - } - - return nil - } - - if dao.BeforeUpdateFunc != nil { - return dao.BeforeUpdateFunc(dao, m, action) - } - - return action() -} - -func (dao *Dao) create(m models.Model) error { - if !m.HasId() { - // auto generate id - m.RefreshId() - } - - // mark the model as "new" since the model now always has an ID - m.MarkAsNew() - - if m.GetCreated().IsZero() { - m.RefreshCreated() - } - - if m.GetUpdated().IsZero() { - m.RefreshUpdated() - } - - action := func() error { - if v, ok := any(m).(models.ColumnValueMapper); ok { - dataMap := v.ColumnValueMap() - if _, ok := dataMap["id"]; !ok { - dataMap["id"] = m.GetId() - } - - _, err := dao.NonconcurrentDB().Insert(m.TableName(), dataMap).Execute() - if err != nil { - return err - } - } else if err := dao.NonconcurrentDB().Model(m).Insert(); err != nil { - return err - } - - // clears the "new" model flag - m.MarkAsNotNew() - - if dao.AfterCreateFunc != nil { - return dao.AfterCreateFunc(dao, m) - } - - return nil - } - - if dao.BeforeCreateFunc != nil { - return dao.BeforeCreateFunc(dao, m, action) - } - - return action() -} - -func (dao *Dao) lockRetry(op func(retryDao *Dao) error) error { - retryDao := dao - - return baseLockRetry(func(attempt int) error { - if attempt == 2 { - // assign new Dao without the before hooks to avoid triggering - // the already fired before callbacks multiple times - retryDao = NewMultiDB(dao.concurrentDB, dao.nonconcurrentDB) - retryDao.AfterCreateFunc = dao.AfterCreateFunc - retryDao.AfterUpdateFunc = dao.AfterUpdateFunc - retryDao.AfterDeleteFunc = dao.AfterDeleteFunc - } - - return op(retryDao) - }, dao.MaxLockRetries) -} diff --git a/daos/base_retry_test.go b/daos/base_retry_test.go deleted file mode 100644 index 72b80478..00000000 --- a/daos/base_retry_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package daos - -import ( - "errors" - "testing" -) - -func TestGetDefaultRetryInterval(t *testing.T) { - t.Parallel() - - if i := getDefaultRetryInterval(-1); i.Milliseconds() != 1000 { - t.Fatalf("Expected 1000ms, got %v", i) - } - - if i := getDefaultRetryInterval(999); i.Milliseconds() != 1000 { - t.Fatalf("Expected 1000ms, got %v", i) - } - - if i := getDefaultRetryInterval(3); i.Milliseconds() != 500 { - t.Fatalf("Expected 500ms, got %v", i) - } -} - -func TestBaseLockRetry(t *testing.T) { - t.Parallel() - - scenarios := []struct { - err error - failUntilAttempt int - expectedAttempts int - }{ - {nil, 3, 1}, - {errors.New("test"), 3, 1}, - {errors.New("database is locked"), 3, 3}, - } - - for i, s := range scenarios { - lastAttempt := 0 - - err := baseLockRetry(func(attempt int) error { - lastAttempt = attempt - - if attempt < s.failUntilAttempt { - return s.err - } - - return nil - }, s.failUntilAttempt+2) - - if lastAttempt != s.expectedAttempts { - t.Errorf("[%d] Expected lastAttempt to be %d, got %d", i, s.expectedAttempts, lastAttempt) - } - - if s.failUntilAttempt == s.expectedAttempts && err != nil { - t.Errorf("[%d] Expected nil, got err %v", i, err) - continue - } - - if s.failUntilAttempt != s.expectedAttempts && s.err != nil && err == nil { - t.Errorf("[%d] Expected error %q, got nil", i, s.err) - continue - } - } -} diff --git a/daos/base_test.go b/daos/base_test.go deleted file mode 100644 index 9de1ba9d..00000000 --- a/daos/base_test.go +++ /dev/null @@ -1,870 +0,0 @@ -package daos_test - -import ( - "errors" - "testing" - "time" - - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestNew(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - dao := daos.New(testApp.DB()) - - if dao.DB() != testApp.DB() { - t.Fatal("The 2 db instances are different") - } -} - -func TestNewMultiDB(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - dao := daos.NewMultiDB(testApp.Dao().ConcurrentDB(), testApp.Dao().NonconcurrentDB()) - - if dao.DB() != testApp.Dao().ConcurrentDB() { - t.Fatal("[db-concurrentDB] The 2 db instances are different") - } - - if dao.ConcurrentDB() != testApp.Dao().ConcurrentDB() { - t.Fatal("[concurrentDB-concurrentDB] The 2 db instances are different") - } - - if dao.NonconcurrentDB() != testApp.Dao().NonconcurrentDB() { - t.Fatal("[nonconcurrentDB-nonconcurrentDB] The 2 db instances are different") - } -} - -func TestDaoClone(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - hookCalls := map[string]int{} - - dao := daos.NewMultiDB(testApp.Dao().ConcurrentDB(), testApp.Dao().NonconcurrentDB()) - dao.MaxLockRetries = 1 - dao.ModelQueryTimeout = 2 - dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeDeleteFunc"]++ - return action() - } - dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeUpdateFunc"]++ - return action() - } - dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeCreateFunc"]++ - return action() - } - dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterDeleteFunc"]++ - return nil - } - dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterUpdateFunc"]++ - return nil - } - dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterCreateFunc"]++ - return nil - } - - clone := dao.Clone() - clone.MaxLockRetries = 3 - clone.ModelQueryTimeout = 4 - clone.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["NewAfterCreateFunc"]++ - return nil - } - - if dao.MaxLockRetries == clone.MaxLockRetries { - t.Fatal("Expected different MaxLockRetries") - } - - if dao.ModelQueryTimeout == clone.ModelQueryTimeout { - t.Fatal("Expected different ModelQueryTimeout") - } - - emptyAction := func() error { return nil } - - // trigger hooks - dao.BeforeDeleteFunc(nil, nil, emptyAction) - dao.BeforeUpdateFunc(nil, nil, emptyAction) - dao.BeforeCreateFunc(nil, nil, emptyAction) - dao.AfterDeleteFunc(nil, nil) - dao.AfterUpdateFunc(nil, nil) - dao.AfterCreateFunc(nil, nil) - clone.BeforeDeleteFunc(nil, nil, emptyAction) - clone.BeforeUpdateFunc(nil, nil, emptyAction) - clone.BeforeCreateFunc(nil, nil, emptyAction) - clone.AfterDeleteFunc(nil, nil) - clone.AfterUpdateFunc(nil, nil) - clone.AfterCreateFunc(nil, nil) - - expectations := []struct { - hook string - total int - }{ - {"BeforeDeleteFunc", 2}, - {"BeforeUpdateFunc", 2}, - {"BeforeCreateFunc", 2}, - {"AfterDeleteFunc", 2}, - {"AfterUpdateFunc", 2}, - {"AfterCreateFunc", 1}, - {"NewAfterCreateFunc", 1}, - } - - for _, e := range expectations { - if hookCalls[e.hook] != e.total { - t.Errorf("Expected %s to be caleed %d", e.hook, e.total) - } - } -} - -func TestDaoWithoutHooks(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - hookCalls := map[string]int{} - - dao := daos.NewMultiDB(testApp.Dao().ConcurrentDB(), testApp.Dao().NonconcurrentDB()) - dao.MaxLockRetries = 1 - dao.ModelQueryTimeout = 2 - dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeDeleteFunc"]++ - return action() - } - dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeUpdateFunc"]++ - return action() - } - dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - hookCalls["BeforeCreateFunc"]++ - return action() - } - dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterDeleteFunc"]++ - return nil - } - dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterUpdateFunc"]++ - return nil - } - dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - hookCalls["AfterCreateFunc"]++ - return nil - } - - new := dao.WithoutHooks() - - if new.MaxLockRetries != dao.MaxLockRetries { - t.Fatalf("Expected MaxLockRetries %d, got %d", new.Clone().MaxLockRetries, dao.MaxLockRetries) - } - - if new.ModelQueryTimeout != dao.ModelQueryTimeout { - t.Fatalf("Expected ModelQueryTimeout %d, got %d", new.Clone().ModelQueryTimeout, dao.ModelQueryTimeout) - } - - if new.BeforeDeleteFunc != nil { - t.Fatal("Expected BeforeDeleteFunc to be nil") - } - - if new.BeforeUpdateFunc != nil { - t.Fatal("Expected BeforeUpdateFunc to be nil") - } - - if new.BeforeCreateFunc != nil { - t.Fatal("Expected BeforeCreateFunc to be nil") - } - - if new.AfterDeleteFunc != nil { - t.Fatal("Expected AfterDeleteFunc to be nil") - } - - if new.AfterUpdateFunc != nil { - t.Fatal("Expected AfterUpdateFunc to be nil") - } - - if new.AfterCreateFunc != nil { - t.Fatal("Expected AfterCreateFunc to be nil") - } -} - -func TestDaoModelQuery(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - dao := daos.New(testApp.DB()) - - scenarios := []struct { - model models.Model - expected string - }{ - { - &models.Collection{}, - "SELECT {{_collections}}.* FROM `_collections`", - }, - { - &models.Admin{}, - "SELECT {{_admins}}.* FROM `_admins`", - }, - { - &models.Request{}, - "SELECT {{_requests}}.* FROM `_requests`", - }, - } - - for i, scenario := range scenarios { - sql := dao.ModelQuery(scenario.model).Build().SQL() - if sql != scenario.expected { - t.Errorf("(%d) Expected select %s, got %s", i, scenario.expected, sql) - } - } -} - -func TestDaoModelQueryCancellation(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - dao := daos.New(testApp.DB()) - - m := &models.Admin{} - - if err := dao.ModelQuery(m).One(m); err != nil { - t.Fatalf("Failed to execute control query: %v", err) - } - - dao.ModelQueryTimeout = 0 * time.Millisecond - if err := dao.ModelQuery(m).One(m); err == nil { - t.Fatal("Expected to be cancelled, got nil") - } -} - -func TestDaoFindById(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - scenarios := []struct { - model models.Model - id string - expectError bool - }{ - // missing id - { - &models.Collection{}, - "missing", - true, - }, - // existing collection id - { - &models.Collection{}, - "wsmn24bux7wo113", - false, - }, - // existing admin id - { - &models.Admin{}, - "sbmbsdb40jyxf7h", - false, - }, - } - - for i, scenario := range scenarios { - err := testApp.Dao().FindById(scenario.model, scenario.id) - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected %v, got %v", i, scenario.expectError, err) - } - - if !scenario.expectError && scenario.id != scenario.model.GetId() { - t.Errorf("(%d) Expected model with id %v, got %v", i, scenario.id, scenario.model.GetId()) - } - } -} - -func TestDaoRunInTransaction(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - // failed nested transaction - testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error { - admin, _ := txDao.FindAdminByEmail("test@example.com") - - return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error { - if err := tx2Dao.DeleteAdmin(admin); err != nil { - t.Fatal(err) - } - return errors.New("test error") - }) - }) - - // admin should still exist - admin1, _ := testApp.Dao().FindAdminByEmail("test@example.com") - if admin1 == nil { - t.Fatal("Expected admin test@example.com to not be deleted") - } - - // successful nested transaction - testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error { - admin, _ := txDao.FindAdminByEmail("test@example.com") - - return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error { - return tx2Dao.DeleteAdmin(admin) - }) - }) - - // admin should have been deleted - admin2, _ := testApp.Dao().FindAdminByEmail("test@example.com") - if admin2 != nil { - t.Fatalf("Expected admin test@example.com to be deleted, found %v", admin2) - } -} - -func TestDaoSaveCreate(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model := &models.Admin{} - model.Email = "test_new@example.com" - model.Avatar = 8 - if err := testApp.Dao().Save(model); err != nil { - t.Fatal(err) - } - - // refresh - model, _ = testApp.Dao().FindAdminByEmail("test_new@example.com") - - if model.Avatar != 8 { - t.Fatalf("Expected model avatar field to be 8, got %v", model.Avatar) - } - - expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"} - for _, h := range expectedHooks { - if v, ok := testApp.EventCalls[h]; !ok || v != 1 { - t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) - } - } -} - -func TestDaoSaveWithInsertId(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model := &models.Admin{} - model.Id = "test" - model.Email = "test_new@example.com" - model.MarkAsNew() - if err := testApp.Dao().Save(model); err != nil { - t.Fatal(err) - } - - // refresh - model, _ = testApp.Dao().FindAdminById("test") - - if model == nil { - t.Fatal("Failed to find admin with id 'test'") - } - - expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"} - for _, h := range expectedHooks { - if v, ok := testApp.EventCalls[h]; !ok || v != 1 { - t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) - } - } -} - -func TestDaoSaveUpdate(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - model.Avatar = 8 - if err := testApp.Dao().Save(model); err != nil { - t.Fatal(err) - } - - // refresh - model, _ = testApp.Dao().FindAdminByEmail("test@example.com") - - if model.Avatar != 8 { - t.Fatalf("Expected model avatar field to be updated to 8, got %v", model.Avatar) - } - - expectedHooks := []string{"OnModelBeforeUpdate", "OnModelAfterUpdate"} - for _, h := range expectedHooks { - if v, ok := testApp.EventCalls[h]; !ok || v != 1 { - t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) - } - } -} - -type dummyColumnValueMapper struct { - models.Admin -} - -func (a *dummyColumnValueMapper) ColumnValueMap() map[string]any { - return map[string]any{ - "email": a.Email, - "passwordHash": a.PasswordHash, - "tokenKey": "custom_token_key", - } -} - -func TestDaoSaveWithColumnValueMapper(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model := &dummyColumnValueMapper{} - model.Id = "test_mapped_id" // explicitly set an id - model.Email = "test_mapped_create@example.com" - model.TokenKey = "test_unmapped_token_key" // not used in the map - model.SetPassword("123456") - model.MarkAsNew() - if err := testApp.Dao().Save(model); err != nil { - t.Fatal(err) - } - - createdModel, _ := testApp.Dao().FindAdminById("test_mapped_id") - if createdModel == nil { - t.Fatal("[create] Failed to find model with id 'test_mapped_id'") - } - if createdModel.Email != model.Email { - t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email) - } - if createdModel.TokenKey != "custom_token_key" { - t.Fatalf("Expected model with tokenKey %q, got %q", "custom_token_key", createdModel.TokenKey) - } - - model.Email = "test_mapped_update@example.com" - model.Avatar = 9 // not mapped and expect to be ignored - if err := testApp.Dao().Save(model); err != nil { - t.Fatal(err) - } - - updatedModel, _ := testApp.Dao().FindAdminById("test_mapped_id") - if updatedModel == nil { - t.Fatal("[update] Failed to find model with id 'test_mapped_id'") - } - if updatedModel.Email != model.Email { - t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email) - } - if updatedModel.Avatar != 0 { - t.Fatalf("Expected model avatar 0, got %v", updatedModel.Avatar) - } -} - -func TestDaoDelete(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - if err := testApp.Dao().Delete(model); err != nil { - t.Fatal(err) - } - - model, _ = testApp.Dao().FindAdminByEmail("test@example.com") - if model != nil { - t.Fatalf("Expected model to be deleted, found %v", model) - } - - expectedHooks := []string{"OnModelBeforeDelete", "OnModelAfterDelete"} - for _, h := range expectedHooks { - if v, ok := testApp.EventCalls[h]; !ok || v != 1 { - t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) - } - } -} - -func TestDaoRetryCreate(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - // init mock retry dao - retryBeforeCreateHookCalls := 0 - retryAfterCreateHookCalls := 0 - retryDao := daos.New(testApp.DB()) - retryDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeCreateHookCalls++ - return errors.New("database is locked") - } - retryDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - retryAfterCreateHookCalls++ - return nil - } - - model := &models.Admin{Email: "new@example.com"} - if err := retryDao.Save(model); err != nil { - t.Fatalf("Expected nil after retry, got error: %v", err) - } - - // the before hook is expected to be called only once because - // it is ignored after the first "database is locked" error - if retryBeforeCreateHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeCreateHookCalls) - } - - if retryAfterCreateHookCalls != 1 { - t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterCreateHookCalls) - } - - // with non-locking error - retryBeforeCreateHookCalls = 0 - retryAfterCreateHookCalls = 0 - retryDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeCreateHookCalls++ - return errors.New("non-locking error") - } - - dummy := &models.Admin{Email: "test@example.com"} - if err := retryDao.Save(dummy); err == nil { - t.Fatal("Expected error, got nil") - } - - if retryBeforeCreateHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeCreateHookCalls) - } - - if retryAfterCreateHookCalls != 0 { - t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterCreateHookCalls) - } -} - -func TestDaoRetryUpdate(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - model, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - // init mock retry dao - retryBeforeUpdateHookCalls := 0 - retryAfterUpdateHookCalls := 0 - retryDao := daos.New(testApp.DB()) - retryDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeUpdateHookCalls++ - return errors.New("database is locked") - } - retryDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - retryAfterUpdateHookCalls++ - return nil - } - - if err := retryDao.Save(model); err != nil { - t.Fatalf("Expected nil after retry, got error: %v", err) - } - - // the before hook is expected to be called only once because - // it is ignored after the first "database is locked" error - if retryBeforeUpdateHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeUpdateHookCalls) - } - - if retryAfterUpdateHookCalls != 1 { - t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterUpdateHookCalls) - } - - // with non-locking error - retryBeforeUpdateHookCalls = 0 - retryAfterUpdateHookCalls = 0 - retryDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeUpdateHookCalls++ - return errors.New("non-locking error") - } - - if err := retryDao.Save(model); err == nil { - t.Fatal("Expected error, got nil") - } - - if retryBeforeUpdateHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeUpdateHookCalls) - } - - if retryAfterUpdateHookCalls != 0 { - t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterUpdateHookCalls) - } -} - -func TestDaoRetryDelete(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - // init mock retry dao - retryBeforeDeleteHookCalls := 0 - retryAfterDeleteHookCalls := 0 - retryDao := daos.New(testApp.DB()) - retryDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeDeleteHookCalls++ - return errors.New("database is locked") - } - retryDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - retryAfterDeleteHookCalls++ - return nil - } - - model, _ := retryDao.FindAdminByEmail("test@example.com") - if err := retryDao.Delete(model); err != nil { - t.Fatalf("Expected nil after retry, got error: %v", err) - } - - // the before hook is expected to be called only once because - // it is ignored after the first "database is locked" error - if retryBeforeDeleteHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeDeleteHookCalls) - } - - if retryAfterDeleteHookCalls != 1 { - t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterDeleteHookCalls) - } - - // with non-locking error - retryBeforeDeleteHookCalls = 0 - retryAfterDeleteHookCalls = 0 - retryDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - retryBeforeDeleteHookCalls++ - return errors.New("non-locking error") - } - - dummy := &models.Admin{} - dummy.RefreshId() - dummy.MarkAsNotNew() - if err := retryDao.Delete(dummy); err == nil { - t.Fatal("Expected error, got nil") - } - - if retryBeforeDeleteHookCalls != 1 { - t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeDeleteHookCalls) - } - - if retryAfterDeleteHookCalls != 0 { - t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterDeleteHookCalls) - } -} - -func TestDaoBeforeHooksError(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - baseDao := testApp.Dao() - - baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - return errors.New("before_create") - } - baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - return errors.New("before_update") - } - baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - return errors.New("before_delete") - } - - existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - // test create error - // --- - newModel := &models.Admin{} - if err := baseDao.Save(newModel); err.Error() != "before_create" { - t.Fatalf("Expected before_create error, got %v", err) - } - - // test update error - // --- - if err := baseDao.Save(existingModel); err.Error() != "before_update" { - t.Fatalf("Expected before_update error, got %v", err) - } - - // test delete error - // --- - if err := baseDao.Delete(existingModel); err.Error() != "before_delete" { - t.Fatalf("Expected before_delete error, got %v", err) - } -} - -func TestDaoTransactionHooksCallsOnFailure(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - beforeCreateFuncCalls := 0 - beforeUpdateFuncCalls := 0 - beforeDeleteFuncCalls := 0 - afterCreateFuncCalls := 0 - afterUpdateFuncCalls := 0 - afterDeleteFuncCalls := 0 - - baseDao := testApp.Dao() - - baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeCreateFuncCalls++ - return action() - } - baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeUpdateFuncCalls++ - return action() - } - baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeDeleteFuncCalls++ - return action() - } - - baseDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - afterCreateFuncCalls++ - return nil - } - baseDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - afterUpdateFuncCalls++ - return nil - } - baseDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - afterDeleteFuncCalls++ - return nil - } - - existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - baseDao.RunInTransaction(func(txDao1 *daos.Dao) error { - return txDao1.RunInTransaction(func(txDao2 *daos.Dao) error { - // test create - // --- - newModel := &models.Admin{} - newModel.Email = "test_new1@example.com" - newModel.SetPassword("123456") - if err := txDao2.Save(newModel); err != nil { - t.Fatal(err) - } - - // test update (twice) - // --- - if err := txDao2.Save(existingModel); err != nil { - t.Fatal(err) - } - if err := txDao2.Save(existingModel); err != nil { - t.Fatal(err) - } - - // test delete - // --- - if err := txDao2.Delete(existingModel); err != nil { - t.Fatal(err) - } - - return errors.New("test_tx_error") - }) - }) - - if beforeCreateFuncCalls != 1 { - t.Fatalf("Expected beforeCreateFuncCalls to be called 1 times, got %d", beforeCreateFuncCalls) - } - if beforeUpdateFuncCalls != 2 { - t.Fatalf("Expected beforeUpdateFuncCalls to be called 2 times, got %d", beforeUpdateFuncCalls) - } - if beforeDeleteFuncCalls != 1 { - t.Fatalf("Expected beforeDeleteFuncCalls to be called 1 times, got %d", beforeDeleteFuncCalls) - } - if afterCreateFuncCalls != 0 { - t.Fatalf("Expected afterCreateFuncCalls to be called 0 times, got %d", afterCreateFuncCalls) - } - if afterUpdateFuncCalls != 0 { - t.Fatalf("Expected afterUpdateFuncCalls to be called 0 times, got %d", afterUpdateFuncCalls) - } - if afterDeleteFuncCalls != 0 { - t.Fatalf("Expected afterDeleteFuncCalls to be called 0 times, got %d", afterDeleteFuncCalls) - } -} - -func TestDaoTransactionHooksCallsOnSuccess(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - beforeCreateFuncCalls := 0 - beforeUpdateFuncCalls := 0 - beforeDeleteFuncCalls := 0 - afterCreateFuncCalls := 0 - afterUpdateFuncCalls := 0 - afterDeleteFuncCalls := 0 - - baseDao := testApp.Dao() - - baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeCreateFuncCalls++ - return action() - } - baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeUpdateFuncCalls++ - return action() - } - baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - beforeDeleteFuncCalls++ - return action() - } - - baseDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) error { - afterCreateFuncCalls++ - return nil - } - baseDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { - afterUpdateFuncCalls++ - return nil - } - baseDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { - afterDeleteFuncCalls++ - return nil - } - - existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - baseDao.RunInTransaction(func(txDao1 *daos.Dao) error { - return txDao1.RunInTransaction(func(txDao2 *daos.Dao) error { - // test create - // --- - newModel := &models.Admin{} - newModel.Email = "test_new1@example.com" - newModel.SetPassword("123456") - if err := txDao2.Save(newModel); err != nil { - t.Fatal(err) - } - - // test update (twice) - // --- - if err := txDao2.Save(existingModel); err != nil { - t.Fatal(err) - } - if err := txDao2.Save(existingModel); err != nil { - t.Fatal(err) - } - - // test delete - // --- - if err := txDao2.Delete(existingModel); err != nil { - t.Fatal(err) - } - - return nil - }) - }) - - if beforeCreateFuncCalls != 1 { - t.Fatalf("Expected beforeCreateFuncCalls to be called 1 times, got %d", beforeCreateFuncCalls) - } - if beforeUpdateFuncCalls != 2 { - t.Fatalf("Expected beforeUpdateFuncCalls to be called 2 times, got %d", beforeUpdateFuncCalls) - } - if beforeDeleteFuncCalls != 1 { - t.Fatalf("Expected beforeDeleteFuncCalls to be called 1 times, got %d", beforeDeleteFuncCalls) - } - if afterCreateFuncCalls != 1 { - t.Fatalf("Expected afterCreateFuncCalls to be called 1 times, got %d", afterCreateFuncCalls) - } - if afterUpdateFuncCalls != 2 { - t.Fatalf("Expected afterUpdateFuncCalls to be called 2 times, got %d", afterUpdateFuncCalls) - } - if afterDeleteFuncCalls != 1 { - t.Fatalf("Expected afterDeleteFuncCalls to be called 1 times, got %d", afterDeleteFuncCalls) - } -} diff --git a/daos/collection.go b/daos/collection.go deleted file mode 100644 index 6a241e74..00000000 --- a/daos/collection.go +++ /dev/null @@ -1,500 +0,0 @@ -package daos - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/list" -) - -// CollectionQuery returns a new Collection select query. -func (dao *Dao) CollectionQuery() *dbx.SelectQuery { - return dao.ModelQuery(&models.Collection{}) -} - -// FindCollectionsByType finds all collections by the given type. -func (dao *Dao) FindCollectionsByType(collectionType string) ([]*models.Collection, error) { - collections := []*models.Collection{} - - err := dao.CollectionQuery(). - AndWhere(dbx.HashExp{"type": collectionType}). - OrderBy("created ASC"). - All(&collections) - - if err != nil { - return nil, err - } - - return collections, nil -} - -// FindCollectionByNameOrId finds a single collection by its name (case insensitive) or id. -func (dao *Dao) FindCollectionByNameOrId(nameOrId string) (*models.Collection, error) { - model := &models.Collection{} - - err := dao.CollectionQuery(). - AndWhere(dbx.NewExp("[[id]] = {:id} OR LOWER([[name]])={:name}", dbx.Params{ - "id": nameOrId, - "name": strings.ToLower(nameOrId), - })). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - -// IsCollectionNameUnique checks that there is no existing collection -// with the provided name (case insensitive!). -// -// Note: case insensitive check because the name is used also as a table name for the records. -func (dao *Dao) IsCollectionNameUnique(name string, excludeIds ...string) bool { - if name == "" { - return false - } - - query := dao.CollectionQuery(). - Select("count(*)"). - AndWhere(dbx.NewExp("LOWER([[name]])={:name}", dbx.Params{"name": strings.ToLower(name)})). - Limit(1) - - if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { - query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) - } - - var exists bool - - return query.Row(&exists) == nil && !exists -} - -// FindCollectionReferences returns information for all -// relation schema fields referencing the provided collection. -// -// If the provided collection has reference to itself then it will be -// also included in the result. To exclude it, pass the collection id -// as the excludeId argument. -func (dao *Dao) FindCollectionReferences(collection *models.Collection, excludeIds ...string) (map[*models.Collection][]*schema.SchemaField, error) { - collections := []*models.Collection{} - - query := dao.CollectionQuery() - - if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { - query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) - } - - if err := query.All(&collections); err != nil { - return nil, err - } - - result := map[*models.Collection][]*schema.SchemaField{} - - for _, c := range collections { - for _, f := range c.Schema.Fields() { - if f.Type != schema.FieldTypeRelation { - continue - } - f.InitOptions() - options, _ := f.Options.(*schema.RelationOptions) - if options != nil && options.CollectionId == collection.Id { - result[c] = append(result[c], f) - } - } - } - - return result, nil -} - -// DeleteCollection deletes the provided Collection model. -// This method automatically deletes the related collection records table. -// -// NB! The collection cannot be deleted, if: -// - is system collection (aka. collection.System is true) -// - is referenced as part of a relation field in another collection -func (dao *Dao) DeleteCollection(collection *models.Collection) error { - if collection.System { - return fmt.Errorf("system collection %q cannot be deleted", collection.Name) - } - - // ensure that there aren't any existing references. - // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction - result, err := dao.FindCollectionReferences(collection, collection.Id) - if err != nil { - return err - } - if total := len(result); total > 0 { - names := make([]string, 0, len(result)) - for ref := range result { - names = append(names, ref.Name) - } - return fmt.Errorf("the collection %q has external relation field references (%s)", collection.Name, strings.Join(names, ", ")) - } - - return dao.RunInTransaction(func(txDao *Dao) error { - // delete the related view or records table - if collection.IsView() { - if err := txDao.DeleteView(collection.Name); err != nil { - return err - } - } else { - if err := txDao.DeleteTable(collection.Name); err != nil { - return err - } - } - - // trigger views resave to check for dependencies - if err := txDao.resaveViewsWithChangedSchema(collection.Id); err != nil { - return fmt.Errorf("the collection has a view dependency - %w", err) - } - - return txDao.Delete(collection) - }) -} - -// SaveCollection persists the provided Collection model and updates -// its related records table schema. -// -// If collection.IsNew() is true, the method will perform a create, otherwise an update. -// To explicitly mark a collection for update you can use collection.MarkAsNotNew(). -func (dao *Dao) SaveCollection(collection *models.Collection) error { - var oldCollection *models.Collection - - if !collection.IsNew() { - // get the existing collection state to compare with the new one - // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction - var findErr error - oldCollection, findErr = dao.FindCollectionByNameOrId(collection.Id) - if findErr != nil { - return findErr - } - } - - txErr := dao.RunInTransaction(func(txDao *Dao) error { - // set default collection type - if collection.Type == "" { - collection.Type = models.CollectionTypeBase - } - - switch collection.Type { - case models.CollectionTypeView: - if err := txDao.saveViewCollection(collection, oldCollection); err != nil { - return err - } - default: - // persist the collection model - if err := txDao.Save(collection); err != nil { - return err - } - - // sync the changes with the related records table - if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil { - return err - } - } - - return nil - }) - - if txErr != nil { - return txErr - } - - // trigger an update for all views with changed schema as a result of the current collection save - // (ignoring view errors to allow users to update the query from the UI) - dao.resaveViewsWithChangedSchema(collection.Id) - - return nil -} - -// ImportCollections imports the provided collections list within a single transaction. -// -// NB1! If deleteMissing is set, all local collections and schema fields, that are not present -// in the imported configuration, WILL BE DELETED (including their related records data). -// -// NB2! This method doesn't perform validations on the imported collections data! -// If you need validations, use [forms.CollectionsImport]. -func (dao *Dao) ImportCollections( - importedCollections []*models.Collection, - deleteMissing bool, - afterSync func(txDao *Dao, mappedImported, mappedExisting map[string]*models.Collection) error, -) error { - if len(importedCollections) == 0 { - return errors.New("no collections to import") - } - - return dao.RunInTransaction(func(txDao *Dao) error { - existingCollections := []*models.Collection{} - if err := txDao.CollectionQuery().OrderBy("updated ASC").All(&existingCollections); err != nil { - return err - } - mappedExisting := make(map[string]*models.Collection, len(existingCollections)) - for _, existing := range existingCollections { - mappedExisting[existing.GetId()] = existing - } - - mappedImported := make(map[string]*models.Collection, len(importedCollections)) - for _, imported := range importedCollections { - // generate id if not set - if !imported.HasId() { - imported.MarkAsNew() - imported.RefreshId() - } - - // set default type if missing - if imported.Type == "" { - imported.Type = models.CollectionTypeBase - } - - if existing, ok := mappedExisting[imported.GetId()]; ok { - imported.MarkAsNotNew() - - // preserve original created date - if !existing.Created.IsZero() { - imported.Created = existing.Created - } - - // extend existing schema - if !deleteMissing { - schemaClone, _ := existing.Schema.Clone() - for _, f := range imported.Schema.Fields() { - schemaClone.AddField(f) // add or replace - } - imported.Schema = *schemaClone - } - } else { - imported.MarkAsNew() - } - - mappedImported[imported.GetId()] = imported - } - - // delete old collections not available in the new configuration - // (before saving the imports in case a deleted collection name is being reused) - if deleteMissing { - for _, existing := range existingCollections { - if mappedImported[existing.GetId()] != nil { - continue // exist - } - - if existing.System { - return fmt.Errorf("system collection %q cannot be deleted", existing.Name) - } - - // delete the related records table or view - if existing.IsView() { - if err := txDao.DeleteView(existing.Name); err != nil { - return err - } - } else { - if err := txDao.DeleteTable(existing.Name); err != nil { - return err - } - } - - // delete the collection - if err := txDao.Delete(existing); err != nil { - return err - } - } - } - - // upsert imported collections - for _, imported := range importedCollections { - if err := txDao.Save(imported); err != nil { - return err - } - } - - // sync record tables - for _, imported := range importedCollections { - if imported.IsView() { - continue - } - - existing := mappedExisting[imported.GetId()] - - if err := txDao.SyncRecordTableSchema(imported, existing); err != nil { - return err - } - } - - // sync views - for _, imported := range importedCollections { - if !imported.IsView() { - continue - } - - existing := mappedExisting[imported.GetId()] - - if err := txDao.saveViewCollection(imported, existing); err != nil { - return err - } - } - - if afterSync != nil { - if err := afterSync(txDao, mappedImported, mappedExisting); err != nil { - return err - } - } - - return nil - }) -} - -// saveViewCollection persists the provided View collection changes: -// - deletes the old related SQL view (if any) -// - creates a new SQL view with the latest newCollection.Options.Query -// - generates a new schema based on newCollection.Options.Query -// - updates newCollection.Schema based on the generated view table info and query -// - saves the newCollection -// -// This method returns an error if newCollection is not a "view". -func (dao *Dao) saveViewCollection(newCollection, oldCollection *models.Collection) error { - if !newCollection.IsView() { - return errors.New("not a view collection") - } - - return dao.RunInTransaction(func(txDao *Dao) error { - query := newCollection.ViewOptions().Query - - // generate collection schema from the query - viewSchema, err := txDao.CreateViewSchema(query) - if err != nil { - return err - } - - // delete old renamed view - if oldCollection != nil { - if err := txDao.DeleteView(oldCollection.Name); err != nil { - return err - } - } - - // wrap view query if necessary - query, err = txDao.normalizeViewQueryId(query) - if err != nil { - return fmt.Errorf("failed to normalize view query id: %w", err) - } - - // (re)create the view - if err := txDao.SaveView(newCollection.Name, query); err != nil { - return err - } - - newCollection.Schema = viewSchema - - return txDao.Save(newCollection) - }) -} - -// @todo consider removing once custom id types are supported -// -// normalizeViewQueryId wraps (if necessary) the provided view query -// with a subselect to ensure that the id column is a text since -// currently we don't support non-string model ids -// (see https://github.com/pocketbase/pocketbase/issues/3110). -func (dao *Dao) normalizeViewQueryId(query string) (string, error) { - query = strings.Trim(strings.TrimSpace(query), ";") - - parsed, err := dao.parseQueryToFields(query) - if err != nil { - return "", err - } - - needWrapping := true - - idField := parsed[schema.FieldNameId] - if idField != nil && idField.field != nil && - idField.field.Type != schema.FieldTypeJson && - idField.field.Type != schema.FieldTypeNumber && - idField.field.Type != schema.FieldTypeBool { - needWrapping = false - } - - if !needWrapping { - return query, nil // no changes needed - } - - // raw parse to preserve the columns order - rawParsed := new(identifiersParser) - if err := rawParsed.parse(query); err != nil { - return "", err - } - - columns := make([]string, 0, len(rawParsed.columns)) - for _, col := range rawParsed.columns { - if col.alias == schema.FieldNameId { - columns = append(columns, fmt.Sprintf("cast([[%s]] as text) [[%s]]", col.alias, col.alias)) - } else { - columns = append(columns, "[["+col.alias+"]]") - } - } - - query = fmt.Sprintf("SELECT %s FROM (%s)", strings.Join(columns, ","), query) - - return query, nil -} - -// resaveViewsWithChangedSchema updates all view collections with changed schemas. -func (dao *Dao) resaveViewsWithChangedSchema(excludeIds ...string) error { - collections, err := dao.FindCollectionsByType(models.CollectionTypeView) - if err != nil { - return err - } - - return dao.RunInTransaction(func(txDao *Dao) error { - for _, collection := range collections { - if len(excludeIds) > 0 && list.ExistInSlice(collection.Id, excludeIds) { - continue - } - - // clone the existing schema so that it is safe for temp modifications - oldSchema, err := collection.Schema.Clone() - if err != nil { - return err - } - - // generate a new schema from the query - newSchema, err := txDao.CreateViewSchema(collection.ViewOptions().Query) - if err != nil { - return err - } - - // unset the schema field ids to exclude from the comparison - for _, f := range oldSchema.Fields() { - f.Id = "" - } - for _, f := range newSchema.Fields() { - f.Id = "" - } - - encodedNewSchema, err := json.Marshal(newSchema) - if err != nil { - return err - } - - encodedOldSchema, err := json.Marshal(oldSchema) - if err != nil { - return err - } - - if bytes.EqualFold(encodedNewSchema, encodedOldSchema) { - continue // no changes - } - - if err := txDao.saveViewCollection(collection, nil); err != nil { - return err - } - } - - return nil - }) -} diff --git a/daos/collection_test.go b/daos/collection_test.go deleted file mode 100644 index 62ad42ae..00000000 --- a/daos/collection_test.go +++ /dev/null @@ -1,813 +0,0 @@ -package daos_test - -import ( - "encoding/json" - "errors" - "strings" - "testing" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestCollectionQuery(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - expected := "SELECT {{_collections}}.* FROM `_collections`" - - sql := app.Dao().CollectionQuery().Build().SQL() - if sql != expected { - t.Errorf("Expected sql %s, got %s", expected, sql) - } -} - -func TestFindCollectionsByType(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionType string - expectError bool - expectTotal int - }{ - {"", false, 0}, - {"unknown", false, 0}, - {models.CollectionTypeAuth, false, 3}, - {models.CollectionTypeBase, false, 5}, - } - - for i, scenario := range scenarios { - collections, err := app.Dao().FindCollectionsByType(scenario.collectionType) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("[%d] Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if len(collections) != scenario.expectTotal { - t.Errorf("[%d] Expected %d collections, got %d", i, scenario.expectTotal, len(collections)) - } - - for _, c := range collections { - if c.Type != scenario.collectionType { - t.Errorf("[%d] Expected collection with type %s, got %s: \n%v", i, scenario.collectionType, c.Type, c) - } - } - } -} - -func TestFindCollectionByNameOrId(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - nameOrId string - expectError bool - }{ - {"", true}, - {"missing", true}, - {"wsmn24bux7wo113", false}, - {"demo1", false}, - {"DEMO1", false}, // case insensitive check - } - - for i, scenario := range scenarios { - model, err := app.Dao().FindCollectionByNameOrId(scenario.nameOrId) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("[%d] Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if model != nil && model.Id != scenario.nameOrId && !strings.EqualFold(model.Name, scenario.nameOrId) { - t.Errorf("[%d] Expected model with identifier %s, got %v", i, scenario.nameOrId, model) - } - } -} - -func TestIsCollectionNameUnique(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - excludeId string - expected bool - }{ - {"", "", false}, - {"demo1", "", false}, - {"Demo1", "", false}, - {"new", "", true}, - {"demo1", "wsmn24bux7wo113", true}, - } - - for i, scenario := range scenarios { - result := app.Dao().IsCollectionNameUnique(scenario.name, scenario.excludeId) - if result != scenario.expected { - t.Errorf("[%d] Expected %v, got %v", i, scenario.expected, result) - } - } -} - -func TestFindCollectionReferences(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo3") - if err != nil { - t.Fatal(err) - } - - result, err := app.Dao().FindCollectionReferences( - collection, - collection.Id, - // test whether "nonempty" exclude ids condition will be skipped - "", - "", - ) - if err != nil { - t.Fatal(err) - } - - if len(result) != 1 { - t.Fatalf("Expected 1 collection, got %d: %v", len(result), result) - } - - expectedFields := []string{ - "rel_one_no_cascade", - "rel_one_no_cascade_required", - "rel_one_cascade", - "rel_one_unique", - "rel_many_no_cascade", - "rel_many_no_cascade_required", - "rel_many_cascade", - "rel_many_unique", - } - - for col, fields := range result { - if col.Name != "demo4" { - t.Fatalf("Expected collection demo4, got %s", col.Name) - } - if len(fields) != len(expectedFields) { - t.Fatalf("Expected fields %v, got %v", expectedFields, fields) - } - for i, f := range fields { - if !list.ExistInSlice(f.Name, expectedFields) { - t.Fatalf("[%d] Didn't expect field %v", i, f) - } - } - } -} - -func TestDeleteCollection(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - colUnsaved := &models.Collection{} - - colAuth, err := app.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - colReferenced, err := app.Dao().FindCollectionByNameOrId("demo2") - if err != nil { - t.Fatal(err) - } - - colSystem, err := app.Dao().FindCollectionByNameOrId("demo3") - if err != nil { - t.Fatal(err) - } - colSystem.System = true - if err := app.Dao().Save(colSystem); err != nil { - t.Fatal(err) - } - - colBase, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - colView1, err := app.Dao().FindCollectionByNameOrId("view1") - if err != nil { - t.Fatal(err) - } - - colView2, err := app.Dao().FindCollectionByNameOrId("view2") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - model *models.Collection - expectError bool - }{ - {colUnsaved, true}, - {colReferenced, true}, - {colSystem, true}, - {colBase, true}, // depend on view1, view2 and view2 - {colView1, true}, // view2 depend on it - {colView2, false}, - {colView1, false}, // no longer has dependent collections - {colBase, false}, // no longer has dependent views - {colAuth, false}, // should delete also its related external auths - } - - for i, s := range scenarios { - err := app.Dao().DeleteCollection(s.model) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%d] Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } - - if hasErr { - continue - } - - if app.Dao().HasTable(s.model.Name) { - t.Errorf("[%d] Expected table/view %s to be deleted", i, s.model.Name) - } - - // check if the external auths were deleted - if s.model.IsAuth() { - var total int - err := app.Dao().ExternalAuthQuery(). - Select("count(*)"). - AndWhere(dbx.HashExp{"collectionId": s.model.Id}). - Row(&total) - - if err != nil || total > 0 { - t.Fatalf("[%d] Expected external auths to be deleted, got %v (%v)", i, total, err) - } - } - } -} - -func TestSaveCollectionCreate(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection := &models.Collection{ - Name: "new_test", - Type: models.CollectionTypeBase, - Schema: schema.NewSchema( - &schema.SchemaField{ - Type: schema.FieldTypeText, - Name: "test", - }, - ), - } - - err := app.Dao().SaveCollection(collection) - if err != nil { - t.Fatal(err) - } - - if collection.Id == "" { - t.Fatal("Expected collection id to be set") - } - - // check if the records table was created - hasTable := app.Dao().HasTable(collection.Name) - if !hasTable { - t.Fatalf("Expected records table %s to be created", collection.Name) - } - - // check if the records table has the schema fields - columns, err := app.Dao().TableColumns(collection.Name) - if err != nil { - t.Fatal(err) - } - expectedColumns := []string{"id", "created", "updated", "test"} - if len(columns) != len(expectedColumns) { - t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) - } - for i, c := range columns { - if !list.ExistInSlice(c, expectedColumns) { - t.Fatalf("[%d] Didn't expect record column %s", i, c) - } - } -} - -func TestSaveCollectionUpdate(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo3") - if err != nil { - t.Fatal(err) - } - - // rename an existing schema field and add a new one - oldField := collection.Schema.GetFieldByName("title") - oldField.Name = "title_update" - collection.Schema.AddField(&schema.SchemaField{ - Type: schema.FieldTypeText, - Name: "test", - }) - - saveErr := app.Dao().SaveCollection(collection) - if saveErr != nil { - t.Fatal(saveErr) - } - - // check if the records table has the schema fields - expectedColumns := []string{"id", "created", "updated", "title_update", "test", "files"} - columns, err := app.Dao().TableColumns(collection.Name) - if err != nil { - t.Fatal(err) - } - if len(columns) != len(expectedColumns) { - t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) - } - for i, c := range columns { - if !list.ExistInSlice(c, expectedColumns) { - t.Fatalf("[%d] Didn't expect record column %s", i, c) - } - } -} - -// indirect update of a field used in view should cause view(s) update -func TestSaveCollectionIndirectViewsUpdate(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - // update MaxSelect fields - { - relMany := collection.Schema.GetFieldByName("rel_many") - relManyOpt := relMany.Options.(*schema.RelationOptions) - relManyOpt.MaxSelect = types.Pointer(1) - - fileOne := collection.Schema.GetFieldByName("file_one") - fileOneOpt := fileOne.Options.(*schema.FileOptions) - fileOneOpt.MaxSelect = 10 - - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - } - - // check view1 schema - { - view1, err := app.Dao().FindCollectionByNameOrId("view1") - if err != nil { - t.Fatal(err) - } - - relMany := view1.Schema.GetFieldByName("rel_many") - relManyOpt := relMany.Options.(*schema.RelationOptions) - if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 { - t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect) - } - - fileOne := view1.Schema.GetFieldByName("file_one") - fileOneOpt := fileOne.Options.(*schema.FileOptions) - if fileOneOpt.MaxSelect != 10 { - t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOneOpt.MaxSelect) - } - } - - // check view2 schema - { - view2, err := app.Dao().FindCollectionByNameOrId("view2") - if err != nil { - t.Fatal(err) - } - - relMany := view2.Schema.GetFieldByName("rel_many") - relManyOpt := relMany.Options.(*schema.RelationOptions) - if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 { - t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect) - } - } -} - -func TestSaveCollectionViewWrapping(t *testing.T) { - t.Parallel() - - viewName := "test_wrapping" - - scenarios := []struct { - name string - query string - expected string - }{ - { - "no wrapping - text field", - "select text as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)", - }, - { - "no wrapping - id field", - "select text as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)", - }, - { - "no wrapping - relation field", - "select rel_one as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1)", - }, - { - "no wrapping - select field", - "select select_many as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1)", - }, - { - "no wrapping - email field", - "select email as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1)", - }, - { - "no wrapping - datetime field", - "select datetime as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1)", - }, - { - "no wrapping - url field", - "select url as id, bool from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1)", - }, - { - "wrapping - bool field", - "select bool as id, text as txt, url from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT cast(`id` as text) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1))", - }, - { - "wrapping - bool field (different order)", - "select text as txt, url, bool as id from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,cast(`id` as text) `id` FROM (select text as txt, url, bool as id from demo1))", - }, - { - "wrapping - json field", - "select json as id, text, url from demo1", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT cast(`id` as text) `id`,`text`,`url` FROM (select json as id, text, url from demo1))", - }, - { - "wrapping - numeric id", - "select 1 as id", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT cast(`id` as text) `id` FROM (select 1 as id))", - }, - { - "wrapping - expresion", - "select ('test') as id", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT cast(`id` as text) `id` FROM (select ('test') as id))", - }, - { - "no wrapping - cast as text", - "select cast('test' as text) as id", - "CREATE VIEW `test_wrapping` AS SELECT * FROM (select cast('test' as text) as id)", - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection := &models.Collection{ - Name: viewName, - Type: models.CollectionTypeView, - Options: types.JsonMap{ - "query": s.query, - }, - } - - err := app.Dao().SaveCollection(collection) - if err != nil { - t.Fatal(err) - } - - var sql string - - rowErr := app.Dao().DB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}"). - Bind(dbx.Params{"name": viewName}). - Row(&sql) - if rowErr != nil { - t.Fatalf("Failed to retrieve view sql: %v", rowErr) - } - - if sql != s.expected { - t.Fatalf("Expected query \n%v, \ngot \n%v", s.expected, sql) - } - }) - } -} - -func TestImportCollections(t *testing.T) { - t.Parallel() - - totalCollections := 11 - - scenarios := []struct { - name string - jsonData string - deleteMissing bool - beforeRecordsSync func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error - expectError bool - expectCollectionsCount int - beforeTestFunc func(testApp *tests.TestApp, resultCollections []*models.Collection) - afterTestFunc func(testApp *tests.TestApp, resultCollections []*models.Collection) - }{ - { - name: "empty collections", - jsonData: `[]`, - expectError: true, - expectCollectionsCount: totalCollections, - }, - { - name: "minimal collection import", - jsonData: `[ - {"name": "import_test1", "schema": [{"name":"test", "type": "text"}]}, - {"name": "import_test2", "type": "auth"} - ]`, - deleteMissing: false, - expectError: false, - expectCollectionsCount: totalCollections + 2, - }, - { - name: "minimal collection import + failed beforeRecordsSync", - jsonData: `[ - {"name": "import_test", "schema": [{"name":"test", "type": "text"}]} - ]`, - beforeRecordsSync: func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error { - return errors.New("test_error") - }, - deleteMissing: false, - expectError: true, - expectCollectionsCount: totalCollections, - }, - { - name: "minimal collection import + successful beforeRecordsSync", - jsonData: `[ - {"name": "import_test", "schema": [{"name":"test", "type": "text"}]} - ]`, - beforeRecordsSync: func(txDao *daos.Dao, mappedImported, mappedExisting map[string]*models.Collection) error { - return nil - }, - deleteMissing: false, - expectError: false, - expectCollectionsCount: totalCollections + 1, - }, - { - name: "new + update + delete system collection", - jsonData: `[ - { - "id":"wsmn24bux7wo113", - "name":"demo", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - }, - { - "name": "import1", - "schema": [ - { - "name":"active", - "type":"bool" - } - ] - } - ]`, - deleteMissing: true, - expectError: true, - expectCollectionsCount: totalCollections, - }, - { - name: "new + update + delete non-system collection", - jsonData: `[ - { - "id": "kpv709sk2lqbqk8", - "system": true, - "name": "nologin", - "type": "auth", - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": [], - "manageRule": "@request.auth.collectionName = 'users'", - "minPasswordLength": 8, - "onlyEmailDomains": [], - "requireEmail": true - }, - "listRule": "", - "viewRule": "", - "createRule": "", - "updateRule": "", - "deleteRule": "", - "schema": [ - { - "id": "x8zzktwe", - "name": "name", - "type": "text", - "system": false, - "required": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": "" - } - } - ] - }, - { - "id":"wsmn24bux7wo113", - "name":"demo1_rename", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - }, - { - "id": "test_deleted_collection_name_reuse", - "name": "demo2", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "id": "test_new_view", - "name": "new_view", - "type": "view", - "options": { - "query": "select id from demo2" - } - } - ]`, - deleteMissing: true, - expectError: false, - expectCollectionsCount: 4, - }, - { - name: "test with deleteMissing: false", - jsonData: `[ - { - "id":"wsmn24bux7wo113", - "name":"demo1", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - }, - { - "id":"_2hlxbmp", - "name":"field_with_duplicate_id", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - }, - { - "id":"abcd_import", - "name":"new_field", - "type":"text" - } - ] - }, - { - "name": "new_import", - "schema": [ - { - "id":"abcd_import", - "name":"active", - "type":"bool" - } - ] - } - ]`, - deleteMissing: false, - expectError: false, - expectCollectionsCount: totalCollections + 1, - afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) { - expectedCollectionFields := map[string]int{ - "nologin": 1, - "demo1": 15, - "demo2": 2, - "demo3": 2, - "demo4": 13, - "demo5": 6, - "new_import": 1, - } - for name, expectedCount := range expectedCollectionFields { - collection, err := testApp.Dao().FindCollectionByNameOrId(name) - if err != nil { - t.Fatal(err) - } - - if totalFields := len(collection.Schema.Fields()); totalFields != expectedCount { - t.Errorf("Expected %d %q fields, got %d", expectedCount, collection.Name, totalFields) - } - } - }, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - importedCollections := []*models.Collection{} - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), &importedCollections) - if loadErr != nil { - t.Fatalf("Failed to load data: %v", loadErr) - } - - err := testApp.Dao().ImportCollections(importedCollections, s.deleteMissing, s.beforeRecordsSync) - - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) - } - - // check collections count - collections := []*models.Collection{} - if err := testApp.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - if len(collections) != s.expectCollectionsCount { - t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections)) - } - - if s.afterTestFunc != nil { - s.afterTestFunc(testApp, collections) - } - }) - } -} diff --git a/daos/external_auth.go b/daos/external_auth.go deleted file mode 100644 index a5cfa79e..00000000 --- a/daos/external_auth.go +++ /dev/null @@ -1,88 +0,0 @@ -package daos - -import ( - "errors" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" -) - -// ExternalAuthQuery returns a new ExternalAuth select query. -func (dao *Dao) ExternalAuthQuery() *dbx.SelectQuery { - return dao.ModelQuery(&models.ExternalAuth{}) -} - -// FindAllExternalAuthsByRecord returns all ExternalAuth models -// linked to the provided auth record. -func (dao *Dao) FindAllExternalAuthsByRecord(authRecord *models.Record) ([]*models.ExternalAuth, error) { - auths := []*models.ExternalAuth{} - - err := dao.ExternalAuthQuery(). - AndWhere(dbx.HashExp{ - "collectionId": authRecord.Collection().Id, - "recordId": authRecord.Id, - }). - OrderBy("created ASC"). - All(&auths) - - if err != nil { - return nil, err - } - - return auths, nil -} - -// FindExternalAuthByRecordAndProvider returns the first available -// ExternalAuth model for the specified record data and provider. -func (dao *Dao) FindExternalAuthByRecordAndProvider(authRecord *models.Record, provider string) (*models.ExternalAuth, error) { - model := &models.ExternalAuth{} - - err := dao.ExternalAuthQuery(). - AndWhere(dbx.HashExp{ - "collectionId": authRecord.Collection().Id, - "recordId": authRecord.Id, - "provider": provider, - }). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - -// FindFirstExternalAuthByExpr returns the first available -// ExternalAuth model that satisfies the non-nil expression. -func (dao *Dao) FindFirstExternalAuthByExpr(expr dbx.Expression) (*models.ExternalAuth, error) { - model := &models.ExternalAuth{} - - err := dao.ExternalAuthQuery(). - AndWhere(dbx.Not(dbx.HashExp{"providerId": ""})). // exclude empty providerIds - AndWhere(expr). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - -// SaveExternalAuth upserts the provided ExternalAuth model. -func (dao *Dao) SaveExternalAuth(model *models.ExternalAuth) error { - // extra check the model data in case the provider's API response - // has changed and no longer returns the expected fields - if model.CollectionId == "" || model.RecordId == "" || model.Provider == "" || model.ProviderId == "" { - return errors.New("Missing required ExternalAuth fields.") - } - - return dao.Save(model) -} - -// DeleteExternalAuth deletes the provided ExternalAuth model. -func (dao *Dao) DeleteExternalAuth(model *models.ExternalAuth) error { - return dao.Delete(model) -} diff --git a/daos/external_auth_test.go b/daos/external_auth_test.go deleted file mode 100644 index 17e55aeb..00000000 --- a/daos/external_auth_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package daos_test - -import ( - "testing" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestExternalAuthQuery(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - expected := "SELECT {{_externalAuths}}.* FROM `_externalAuths`" - - sql := app.Dao().ExternalAuthQuery().Build().SQL() - if sql != expected { - t.Errorf("Expected sql %s, got %s", expected, sql) - } -} - -func TestFindAllExternalAuthsByRecord(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - userId string - expectedCount int - }{ - {"oap640cot4yru2s", 0}, - {"4q1xlclmfloku33", 2}, - } - - for i, s := range scenarios { - record, err := app.Dao().FindRecordById("users", s.userId) - if err != nil { - t.Errorf("(%d) Unexpected record fetch error %v", i, err) - continue - } - - auths, err := app.Dao().FindAllExternalAuthsByRecord(record) - if err != nil { - t.Errorf("(%d) Unexpected auths fetch error %v", i, err) - continue - } - - if len(auths) != s.expectedCount { - t.Errorf("(%d) Expected %d auths, got %d", i, s.expectedCount, len(auths)) - } - - for _, auth := range auths { - if auth.RecordId != record.Id { - t.Errorf("(%d) Expected all auths to be linked to record id %s, got %v", i, record.Id, auth) - } - } - } -} - -func TestFindFirstExternalAuthByExpr(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - expr dbx.Expression - expectedId string - }{ - {dbx.HashExp{"provider": "github", "providerId": ""}, ""}, - {dbx.HashExp{"provider": "github", "providerId": "id1"}, ""}, - {dbx.HashExp{"provider": "github", "providerId": "id2"}, ""}, - {dbx.HashExp{"provider": "google", "providerId": "test123"}, "clmflokuq1xl341"}, - {dbx.HashExp{"provider": "gitlab", "providerId": "test123"}, "dlmflokuq1xl342"}, - } - - for i, s := range scenarios { - auth, err := app.Dao().FindFirstExternalAuthByExpr(s.expr) - - hasErr := err != nil - expectErr := s.expectedId == "" - if hasErr != expectErr { - t.Errorf("(%d) Expected hasErr %v, got %v", i, expectErr, err) - continue - } - - if auth != nil && auth.Id != s.expectedId { - t.Errorf("(%d) Expected external auth with ID %s, got \n%v", i, s.expectedId, auth) - } - } -} - -func TestFindExternalAuthByRecordAndProvider(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - userId string - provider string - expectedId string - }{ - {"bgs820n361vj1qd", "google", ""}, - {"4q1xlclmfloku33", "google", "clmflokuq1xl341"}, - {"4q1xlclmfloku33", "gitlab", "dlmflokuq1xl342"}, - } - - for i, s := range scenarios { - record, err := app.Dao().FindRecordById("users", s.userId) - if err != nil { - t.Errorf("(%d) Unexpected record fetch error %v", i, err) - continue - } - - auth, err := app.Dao().FindExternalAuthByRecordAndProvider(record, s.provider) - - hasErr := err != nil - expectErr := s.expectedId == "" - if hasErr != expectErr { - t.Errorf("(%d) Expected hasErr %v, got %v", i, expectErr, err) - continue - } - - if auth != nil && auth.Id != s.expectedId { - t.Errorf("(%d) Expected external auth with ID %s, got \n%v", i, s.expectedId, auth) - } - } -} - -func TestSaveExternalAuth(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // save with empty provider data - emptyAuth := &models.ExternalAuth{} - if err := app.Dao().SaveExternalAuth(emptyAuth); err == nil { - t.Fatal("Expected error, got nil") - } - - auth := &models.ExternalAuth{ - RecordId: "o1y0dd0spd786md", - CollectionId: "v851q4r790rhknl", - Provider: "test", - ProviderId: "test_id", - } - - if err := app.Dao().SaveExternalAuth(auth); err != nil { - t.Fatal(err) - } - - // check if it was really saved - foundAuth, err := app.Dao().FindFirstExternalAuthByExpr(dbx.HashExp{ - "collectionId": "v851q4r790rhknl", - "provider": "test", - "providerId": "test_id", - }) - if err != nil { - t.Fatal(err) - } - - if auth.Id != foundAuth.Id { - t.Fatalf("Expected ExternalAuth with id %s, got \n%v", auth.Id, foundAuth) - } -} - -func TestDeleteExternalAuth(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - record, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33") - if err != nil { - t.Fatal(err) - } - - auths, err := app.Dao().FindAllExternalAuthsByRecord(record) - if err != nil { - t.Fatal(err) - } - - for _, auth := range auths { - if err := app.Dao().DeleteExternalAuth(auth); err != nil { - t.Fatalf("Failed to delete the ExternalAuth relation, got \n%v", err) - } - } - - // check if the relations were really deleted - newAuths, err := app.Dao().FindAllExternalAuthsByRecord(record) - if err != nil { - t.Fatal(err) - } - - if len(newAuths) != 0 { - t.Fatalf("Expected all record %s ExternalAuth relations to be deleted, got \n%v", record.Id, newAuths) - } -} diff --git a/daos/log_test.go b/daos/log_test.go deleted file mode 100644 index 5fc9549d..00000000 --- a/daos/log_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package daos_test - -import ( - "encoding/json" - "testing" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestLogQuery(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - expected := "SELECT {{_logs}}.* FROM `_logs`" - - sql := app.Dao().LogQuery().Build().SQL() - if sql != expected { - t.Errorf("Expected sql %s, got %s", expected, sql) - } -} - -func TestFindLogById(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - tests.MockLogsData(app) - - scenarios := []struct { - id string - expectError bool - }{ - {"", true}, - {"invalid", true}, - {"00000000-9f38-44fb-bf82-c8f53b310d91", true}, - {"873f2133-9f38-44fb-bf82-c8f53b310d91", false}, - } - - for i, scenario := range scenarios { - admin, err := app.LogsDao().FindLogById(scenario.id) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if admin != nil && admin.Id != scenario.id { - t.Errorf("(%d) Expected admin with id %s, got %s", i, scenario.id, admin.Id) - } - } -} - -func TestLogsStats(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - tests.MockLogsData(app) - - expected := `[{"total":1,"date":"2022-05-01 10:00:00.000Z"},{"total":1,"date":"2022-05-02 10:00:00.000Z"}]` - - now := time.Now().UTC().Format(types.DefaultDateLayout) - exp := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": now}) - result, err := app.LogsDao().LogsStats(exp) - if err != nil { - t.Fatal(err) - } - - encoded, _ := json.Marshal(result) - if string(encoded) != expected { - t.Fatalf("Expected %s, got %s", expected, string(encoded)) - } -} - -func TestDeleteOldLogs(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - tests.MockLogsData(app) - - scenarios := []struct { - date string - expectedTotal int - }{ - {"2022-01-01 10:00:00.000Z", 2}, // no logs to delete before that time - {"2022-05-01 11:00:00.000Z", 1}, // only 1 log should have left - {"2022-05-03 11:00:00.000Z", 0}, // no more logs should have left - {"2022-05-04 11:00:00.000Z", 0}, // no more logs should have left - } - - for i, scenario := range scenarios { - date, dateErr := time.Parse(types.DefaultDateLayout, scenario.date) - if dateErr != nil { - t.Errorf("(%d) Date error %v", i, dateErr) - } - - deleteErr := app.LogsDao().DeleteOldLogs(date) - if deleteErr != nil { - t.Errorf("(%d) Delete error %v", i, deleteErr) - } - - // check total remaining logs - var total int - countErr := app.LogsDao().LogQuery().Select("count(*)").Row(&total) - if countErr != nil { - t.Errorf("(%d) Count error %v", i, countErr) - } - - if total != scenario.expectedTotal { - t.Errorf("(%d) Expected %d remaining logs, got %d", i, scenario.expectedTotal, total) - } - } -} - -func TestSaveLog(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - tests.MockLogsData(app) - - // create new log - newLog := &models.Log{} - newLog.Level = -4 - newLog.Data = types.JsonMap{} - createErr := app.LogsDao().SaveLog(newLog) - if createErr != nil { - t.Fatal(createErr) - } - - // check if it was really created - existingLog, fetchErr := app.LogsDao().FindLogById(newLog.Id) - if fetchErr != nil { - t.Fatal(fetchErr) - } - - existingLog.Level = 4 - updateErr := app.LogsDao().SaveLog(existingLog) - if updateErr != nil { - t.Fatal(updateErr) - } - // refresh instance to check if it was really updated - existingLog, _ = app.LogsDao().FindLogById(existingLog.Id) - if existingLog.Level != 4 { - t.Fatalf("Expected log level to be %d, got %d", 4, existingLog.Level) - } -} diff --git a/daos/param.go b/daos/param.go deleted file mode 100644 index 23ba07dd..00000000 --- a/daos/param.go +++ /dev/null @@ -1,73 +0,0 @@ -package daos - -import ( - "encoding/json" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" -) - -// ParamQuery returns a new Param select query. -func (dao *Dao) ParamQuery() *dbx.SelectQuery { - return dao.ModelQuery(&models.Param{}) -} - -// FindParamByKey finds the first Param model with the provided key. -func (dao *Dao) FindParamByKey(key string) (*models.Param, error) { - param := &models.Param{} - - err := dao.ParamQuery(). - AndWhere(dbx.HashExp{"key": key}). - Limit(1). - One(param) - - if err != nil { - return nil, err - } - - return param, nil -} - -// SaveParam creates or updates a Param model by the provided key-value pair. -// The value argument will be encoded as json string. -// -// If `optEncryptionKey` is provided it will encrypt the value before storing it. -func (dao *Dao) SaveParam(key string, value any, optEncryptionKey ...string) error { - param, _ := dao.FindParamByKey(key) - if param == nil { - param = &models.Param{Key: key} - } - - normalizedValue := value - - // encrypt if optEncryptionKey is set - if len(optEncryptionKey) > 0 && optEncryptionKey[0] != "" { - encoded, encodingErr := json.Marshal(value) - if encodingErr != nil { - return encodingErr - } - - encryptVal, encryptErr := security.Encrypt(encoded, optEncryptionKey[0]) - if encryptErr != nil { - return encryptErr - } - - normalizedValue = encryptVal - } - - encodedValue := types.JsonRaw{} - if err := encodedValue.Scan(normalizedValue); err != nil { - return err - } - - param.Value = encodedValue - - return dao.Save(param) -} - -// DeleteParam deletes the provided Param model. -func (dao *Dao) DeleteParam(param *models.Param) error { - return dao.Delete(param) -} diff --git a/daos/param_test.go b/daos/param_test.go deleted file mode 100644 index ef1b8144..00000000 --- a/daos/param_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package daos_test - -import ( - "encoding/json" - "testing" - - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestParamQuery(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - expected := "SELECT {{_params}}.* FROM `_params`" - - sql := app.Dao().ParamQuery().Build().SQL() - if sql != expected { - t.Errorf("Expected sql %s, got %s", expected, sql) - } -} - -func TestFindParamByKey(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - key string - expectError bool - }{ - {"", true}, - {"missing", true}, - {models.ParamAppSettings, false}, - } - - for i, scenario := range scenarios { - param, err := app.Dao().FindParamByKey(scenario.key) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if param != nil && param.Key != scenario.key { - t.Errorf("(%d) Expected param with identifier %s, got %v", i, scenario.key, param.Key) - } - } -} - -func TestSaveParam(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - key string - value any - }{ - {"", "demo"}, - {"test", nil}, - {"test", ""}, - {"test", 1}, - {"test", 123}, - {models.ParamAppSettings, map[string]any{"test": 123}}, - } - - for i, scenario := range scenarios { - err := app.Dao().SaveParam(scenario.key, scenario.value) - if err != nil { - t.Errorf("(%d) %v", i, err) - } - - jsonRaw := types.JsonRaw{} - jsonRaw.Scan(scenario.value) - encodedScenarioValue, err := jsonRaw.MarshalJSON() - if err != nil { - t.Errorf("(%d) Encoded error %v", i, err) - } - - // check if the param was really saved - param, _ := app.Dao().FindParamByKey(scenario.key) - encodedParamValue, err := param.Value.MarshalJSON() - if err != nil { - t.Errorf("(%d) Encoded error %v", i, err) - } - - if string(encodedParamValue) != string(encodedScenarioValue) { - t.Errorf("(%d) Expected the two values to be equal, got %v vs %v", i, string(encodedParamValue), string(encodedScenarioValue)) - } - } -} - -func TestSaveParamEncrypted(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - encryptionKey := security.RandomString(32) - data := map[string]int{"test": 123} - expected := map[string]int{} - - err := app.Dao().SaveParam("test", data, encryptionKey) - if err != nil { - t.Fatal(err) - } - - // check if the param was really saved - param, _ := app.Dao().FindParamByKey("test") - - // decrypt - decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey) - if decryptErr != nil { - t.Fatal(decryptErr) - } - - // decode - decryptedDecodeErr := json.Unmarshal(decrypted, &expected) - if decryptedDecodeErr != nil { - t.Fatal(decryptedDecodeErr) - } - - // check if the decoded value is correct - if len(expected) != len(data) || expected["test"] != data["test"] { - t.Fatalf("Expected %v, got %v", expected, data) - } -} - -func TestDeleteParam(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // unsaved param - err1 := app.Dao().DeleteParam(&models.Param{}) - if err1 == nil { - t.Fatal("Expected error, got nil") - } - - // existing param - param, _ := app.Dao().FindParamByKey(models.ParamAppSettings) - err2 := app.Dao().DeleteParam(param) - if err2 != nil { - t.Fatalf("Expected nil, got error %v", err2) - } - - // check if it was really deleted - paramCheck, _ := app.Dao().FindParamByKey(models.ParamAppSettings) - if paramCheck != nil { - t.Fatalf("Expected param to be deleted, got %v", paramCheck) - } -} diff --git a/daos/record.go b/daos/record.go deleted file mode 100644 index 9e2f532a..00000000 --- a/daos/record.go +++ /dev/null @@ -1,776 +0,0 @@ -package daos - -import ( - "context" - "database/sql" - "errors" - "fmt" - "sort" - "strings" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/resolvers" - "github.com/pocketbase/pocketbase/tools/inflector" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/search" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" - "github.com/spf13/cast" -) - -// RecordQuery returns a new Record select query from a collection model, id or name. -// -// In case a collection id or name is provided and that collection doesn't -// actually exists, the generated query will be created with a cancelled context -// and will fail once an executor (Row(), One(), All(), etc.) is called. -func (dao *Dao) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuery { - var tableName string - var collection *models.Collection - var collectionErr error - switch c := collectionModelOrIdentifier.(type) { - case *models.Collection: - collection = c - tableName = collection.Name - case models.Collection: - collection = &c - tableName = collection.Name - case string: - collection, collectionErr = dao.FindCollectionByNameOrId(c) - if collection != nil { - tableName = collection.Name - } - default: - collectionErr = errors.New("unsupported collection identifier, must be collection model, id or name") - } - - // update with some fake table name for easier debugging - if tableName == "" { - tableName = "@@__invalidCollectionModelOrIdentifier" - } - - selectCols := fmt.Sprintf("%s.*", dao.DB().QuoteSimpleColumnName(tableName)) - - query := dao.DB().Select(selectCols).From(tableName) - - // in case of an error attach a new context and cancel it immediately with the error - if collectionErr != nil { - // @todo consider changing to WithCancelCause when upgrading - // the min Go requirement to 1.20, so that we can pass the error - ctx, cancelFunc := context.WithCancel(context.Background()) - query.WithContext(ctx) - cancelFunc() - } - - return query.WithBuildHook(func(q *dbx.Query) { - q.WithExecHook(execLockRetry(dao.ModelQueryTimeout, dao.MaxLockRetries)). - WithOneHook(func(q *dbx.Query, a any, op func(b any) error) error { - switch v := a.(type) { - case *models.Record: - if v == nil { - return op(a) - } - - row := dbx.NullStringMap{} - if err := op(&row); err != nil { - return err - } - - record := models.NewRecordFromNullStringMap(collection, row) - - *v = *record - - return nil - default: - return op(a) - } - }). - WithAllHook(func(q *dbx.Query, sliceA any, op func(sliceB any) error) error { - switch v := sliceA.(type) { - case *[]*models.Record: - if v == nil { - return op(sliceA) - } - - rows := []dbx.NullStringMap{} - if err := op(&rows); err != nil { - return err - } - - records := models.NewRecordsFromNullStringMaps(collection, rows) - - *v = records - - return nil - case *[]models.Record: - if v == nil { - return op(sliceA) - } - - rows := []dbx.NullStringMap{} - if err := op(&rows); err != nil { - return err - } - - records := models.NewRecordsFromNullStringMaps(collection, rows) - - nonPointers := make([]models.Record, len(records)) - for i, r := range records { - nonPointers[i] = *r - } - - *v = nonPointers - - return nil - default: - return op(sliceA) - } - }) - }) -} - -// FindRecordById finds the Record model by its id. -func (dao *Dao) FindRecordById( - collectionNameOrId string, - recordId string, - optFilters ...func(q *dbx.SelectQuery) error, -) (*models.Record, error) { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, err - } - - query := dao.RecordQuery(collection). - AndWhere(dbx.HashExp{collection.Name + ".id": recordId}) - - for _, filter := range optFilters { - if filter == nil { - continue - } - if err := filter(query); err != nil { - return nil, err - } - } - - record := &models.Record{} - - if err := query.Limit(1).One(record); err != nil { - return nil, err - } - - return record, nil -} - -// FindRecordsByIds finds all Record models by the provided ids. -// If no records are found, returns an empty slice. -func (dao *Dao) FindRecordsByIds( - collectionNameOrId string, - recordIds []string, - optFilters ...func(q *dbx.SelectQuery) error, -) ([]*models.Record, error) { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, err - } - - query := dao.RecordQuery(collection). - AndWhere(dbx.In( - collection.Name+".id", - list.ToInterfaceSlice(recordIds)..., - )) - - for _, filter := range optFilters { - if filter == nil { - continue - } - if err := filter(query); err != nil { - return nil, err - } - } - - records := make([]*models.Record, 0, len(recordIds)) - - if err := query.All(&records); err != nil { - return nil, err - } - - return records, nil -} - -// FindRecordsByExpr finds all records by the specified db expression. -// -// Returns all collection records if no expressions are provided. -// -// Returns an empty slice if no records are found. -// -// Example: -// -// expr1 := dbx.HashExp{"email": "test@example.com"} -// expr2 := dbx.NewExp("LOWER(username) = {:username}", dbx.Params{"username": "test"}) -// dao.FindRecordsByExpr("example", expr1, expr2) -func (dao *Dao) FindRecordsByExpr(collectionNameOrId string, exprs ...dbx.Expression) ([]*models.Record, error) { - query := dao.RecordQuery(collectionNameOrId) - - // add only the non-nil expressions - for _, expr := range exprs { - if expr != nil { - query.AndWhere(expr) - } - } - - var records []*models.Record - - if err := query.All(&records); err != nil { - return nil, err - } - - return records, nil -} - -// FindFirstRecordByData returns the first found record matching -// the provided key-value pair. -func (dao *Dao) FindFirstRecordByData( - collectionNameOrId string, - key string, - value any, -) (*models.Record, error) { - record := &models.Record{} - - err := dao.RecordQuery(collectionNameOrId). - AndWhere(dbx.HashExp{inflector.Columnify(key): value}). - Limit(1). - One(record) - if err != nil { - return nil, err - } - - return record, nil -} - -// FindRecordsByFilter returns limit number of records matching the -// provided string filter. -// -// NB! Use the last "params" argument to bind untrusted user variables! -// -// The sort argument is optional and can be empty string OR the same format -// used in the web APIs, eg. "-created,title". -// -// If the limit argument is <= 0, no limit is applied to the query and -// all matching records are returned. -// -// Example: -// -// dao.FindRecordsByFilter( -// "posts", -// "title ~ {:title} && visible = {:visible}", -// "-created", -// 10, -// 0, -// dbx.Params{"title": "lorem ipsum", "visible": true} -// ) -func (dao *Dao) FindRecordsByFilter( - collectionNameOrId string, - filter string, - sort string, - limit int, - offset int, - params ...dbx.Params, -) ([]*models.Record, error) { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, err - } - - q := dao.RecordQuery(collection) - - // build a fields resolver and attach the generated conditions to the query - // --- - resolver := resolvers.NewRecordFieldResolver( - dao, - collection, // the base collection - nil, // no request data - true, // allow searching hidden/protected fields like "email" - ) - - expr, err := search.FilterData(filter).BuildExpr(resolver, params...) - if err != nil || expr == nil { - return nil, errors.New("invalid or empty filter expression") - } - q.AndWhere(expr) - - if sort != "" { - for _, sortField := range search.ParseSortFromString(sort) { - expr, err := sortField.BuildExpr(resolver) - if err != nil { - return nil, err - } - if expr != "" { - q.AndOrderBy(expr) - } - } - } - - resolver.UpdateQuery(q) // attaches any adhoc joins and aliases - // --- - - if offset > 0 { - q.Offset(int64(offset)) - } - - if limit > 0 { - q.Limit(int64(limit)) - } - - records := []*models.Record{} - - if err := q.All(&records); err != nil { - return nil, err - } - - return records, nil -} - -// FindFirstRecordByFilter returns the first available record matching the provided filter. -// -// NB! Use the last params argument to bind untrusted user variables! -// -// Example: -// -// dao.FindFirstRecordByFilter("posts", "slug={:slug} && status='public'", dbx.Params{"slug": "test"}) -func (dao *Dao) FindFirstRecordByFilter( - collectionNameOrId string, - filter string, - params ...dbx.Params, -) (*models.Record, error) { - result, err := dao.FindRecordsByFilter(collectionNameOrId, filter, "", 1, 0, params...) - if err != nil { - return nil, err - } - - if len(result) == 0 { - return nil, sql.ErrNoRows - } - - return result[0], nil -} - -// IsRecordValueUnique checks if the provided key-value pair is a unique Record value. -// -// For correctness, if the collection is "auth" and the key is "username", -// the unique check will be case insensitive. -// -// NB! Array values (eg. from multiple select fields) are matched -// as a serialized json strings (eg. `["a","b"]`), so the value uniqueness -// depends on the elements order. Or in other words the following values -// are considered different: `[]string{"a","b"}` and `[]string{"b","a"}` -func (dao *Dao) IsRecordValueUnique( - collectionNameOrId string, - key string, - value any, - excludeIds ...string, -) bool { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return false - } - - var expr dbx.Expression - if collection.IsAuth() && key == schema.FieldNameUsername { - expr = dbx.NewExp("LOWER([["+schema.FieldNameUsername+"]])={:username}", dbx.Params{ - "username": strings.ToLower(cast.ToString(value)), - }) - } else { - var normalizedVal any - switch val := value.(type) { - case []string: - normalizedVal = append(types.JsonArray[string]{}, val...) - case []any: - normalizedVal = append(types.JsonArray[any]{}, val...) - default: - normalizedVal = val - } - - expr = dbx.HashExp{inflector.Columnify(key): normalizedVal} - } - - query := dao.RecordQuery(collection). - Select("count(*)"). - AndWhere(expr). - Limit(1) - - if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 { - query.AndWhere(dbx.NotIn(collection.Name+".id", list.ToInterfaceSlice(uniqueExcludeIds)...)) - } - - var exists bool - - return query.Row(&exists) == nil && !exists -} - -// FindAuthRecordByToken finds the auth record associated with the provided JWT. -// -// Returns an error if the JWT is invalid, expired or not associated to an auth collection record. -func (dao *Dao) FindAuthRecordByToken(token string, baseTokenKey string) (*models.Record, error) { - unverifiedClaims, err := security.ParseUnverifiedJWT(token) - if err != nil { - return nil, err - } - - // check required claims - id, _ := unverifiedClaims["id"].(string) - collectionId, _ := unverifiedClaims["collectionId"].(string) - if id == "" || collectionId == "" { - return nil, errors.New("missing or invalid token claims") - } - - record, err := dao.FindRecordById(collectionId, id) - if err != nil { - return nil, err - } - - if !record.Collection().IsAuth() { - return nil, errors.New("the token is not associated to an auth collection record") - } - - verificationKey := record.TokenKey() + baseTokenKey - - // verify token signature - if _, err := security.ParseJWT(token, verificationKey); err != nil { - return nil, err - } - - return record, nil -} - -// FindAuthRecordByEmail finds the auth record associated with the provided email. -// -// Returns an error if it is not an auth collection or the record is not found. -func (dao *Dao) FindAuthRecordByEmail(collectionNameOrId string, email string) (*models.Record, error) { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, fmt.Errorf("failed to fetch auth collection %q (%w)", collectionNameOrId, err) - } - if !collection.IsAuth() { - return nil, fmt.Errorf("%q is not an auth collection", collectionNameOrId) - } - - record := &models.Record{} - - err = dao.RecordQuery(collection). - AndWhere(dbx.HashExp{schema.FieldNameEmail: email}). - Limit(1). - One(record) - if err != nil { - return nil, err - } - - return record, nil -} - -// FindAuthRecordByUsername finds the auth record associated with the provided username (case insensitive). -// -// Returns an error if it is not an auth collection or the record is not found. -func (dao *Dao) FindAuthRecordByUsername(collectionNameOrId string, username string) (*models.Record, error) { - collection, err := dao.FindCollectionByNameOrId(collectionNameOrId) - if err != nil { - return nil, fmt.Errorf("failed to fetch auth collection %q (%w)", collectionNameOrId, err) - } - if !collection.IsAuth() { - return nil, fmt.Errorf("%q is not an auth collection", collectionNameOrId) - } - - record := &models.Record{} - - err = dao.RecordQuery(collection). - AndWhere(dbx.NewExp("LOWER([["+schema.FieldNameUsername+"]])={:username}", dbx.Params{ - "username": strings.ToLower(username), - })). - Limit(1). - One(record) - if err != nil { - return nil, err - } - - return record, nil -} - -// SuggestUniqueAuthRecordUsername checks if the provided username is unique -// and return a new "unique" username with appended random numeric part -// (eg. "existingName" -> "existingName583"). -// -// The same username will be returned if the provided string is already unique. -func (dao *Dao) SuggestUniqueAuthRecordUsername( - collectionNameOrId string, - baseUsername string, - excludeIds ...string, -) string { - username := baseUsername - - for i := 0; i < 10; i++ { // max 10 attempts - isUnique := dao.IsRecordValueUnique( - collectionNameOrId, - schema.FieldNameUsername, - username, - excludeIds..., - ) - if isUnique { - break // already unique - } - username = baseUsername + security.RandomStringWithAlphabet(3+i, "123456789") - } - - return username -} - -// 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.Admin is set. -// -// The returned error indicate that something unexpected happened during -// the check (eg. invalid rule or db error). -// -// The method always return false on invalid access rule or db error. -// -// Example: -// -// requestInfo := apis.RequestInfo(c /* echo.Context */) -// record, _ := dao.FindRecordById("example", "RECORD_ID") -// rule := types.Pointer("@request.auth.id != '' || status = 'public'") -// // ... or use one of the record collection's rule, eg. record.Collection().ViewRule -// -// if ok, _ := dao.CanAccessRecord(record, requestInfo, rule); ok { ... } -func (dao *Dao) CanAccessRecord(record *models.Record, requestInfo *models.RequestInfo, accessRule *string) (bool, error) { - if requestInfo.Admin != nil { - // admins can access everything - return true, nil - } - - if accessRule == nil { - // only admins can access this record - return false, nil - } - - if *accessRule == "" { - // empty public rule, aka. everyone can access - return true, nil - } - - var exists bool - - query := dao.RecordQuery(record.Collection()). - Select("(1)"). - AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id}) - - // parse and apply the access rule filter - resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestInfo, true) - expr, err := search.FilterData(*accessRule).BuildExpr(resolver) - if err != nil { - return false, err - } - resolver.UpdateQuery(query) - query.AndWhere(expr) - - if err := query.Limit(1).Row(&exists); err != nil && !errors.Is(err, sql.ErrNoRows) { - return false, err - } - - return exists, nil -} - -// SaveRecord persists the provided Record model in the database. -// -// If record.IsNew() is true, the method will perform a create, otherwise an update. -// To explicitly mark a record for update you can use record.MarkAsNotNew(). -func (dao *Dao) SaveRecord(record *models.Record) error { - if record.Collection().IsAuth() { - if record.Username() == "" { - return errors.New("unable to save auth record without username") - } - - // Cross-check that the auth record id is unique for all auth collections. - // This is to make sure that the filter `@request.auth.id` always returns a unique id. - authCollections, err := dao.FindCollectionsByType(models.CollectionTypeAuth) - if err != nil { - return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err) - } - for _, collection := range authCollections { - if record.Collection().Id == collection.Id { - continue // skip current collection (sqlite will do the check for us) - } - isUnique := dao.IsRecordValueUnique(collection.Id, schema.FieldNameId, record.Id) - if !isUnique { - return errors.New("the auth record ID must be unique across all auth collections") - } - } - } - - return dao.Save(record) -} - -// DeleteRecord deletes the provided Record model. -// -// This method will also cascade the delete operation to all linked -// relational records (delete or unset, depending on the rel settings). -// -// The delete operation may fail if the record is part of a required -// reference in another record (aka. cannot be deleted or unset). -func (dao *Dao) DeleteRecord(record *models.Record) error { - // fetch rel references (if any) - // - // note: the select is outside of the transaction to minimize - // SQLITE_BUSY errors when mixing read&write in a single transaction - refs, err := dao.FindCollectionReferences(record.Collection()) - if err != nil { - return err - } - - return dao.RunInTransaction(func(txDao *Dao) error { - // manually trigger delete on any linked external auth to ensure - // that the `OnModel*` hooks are triggered - if record.Collection().IsAuth() { - // note: the select is outside of the transaction to minimize - // SQLITE_BUSY errors when mixing read&write in a single transaction - externalAuths, err := dao.FindAllExternalAuthsByRecord(record) - if err != nil { - return err - } - for _, auth := range externalAuths { - if err := txDao.DeleteExternalAuth(auth); err != nil { - return err - } - } - } - - // delete the record before the relation references to ensure that there - // will be no "A<->B" relations to prevent deadlock when calling DeleteRecord recursively - if err := txDao.Delete(record); err != nil { - return err - } - - return txDao.cascadeRecordDelete(record, refs) - }) -} - -// cascadeRecordDelete triggers cascade deletion for the provided references. -// -// NB! This method is expected to be called inside a transaction. -func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.Collection][]*schema.SchemaField) error { - // @todo consider changing refs to a slice - // - // Sort the refs keys to ensure that the cascade events firing order is always the same. - // This is not necessary for the operation to function correctly but it helps having deterministic output during testing. - sortedRefKeys := make([]*models.Collection, 0, len(refs)) - for k := range refs { - sortedRefKeys = append(sortedRefKeys, k) - } - sort.Slice(sortedRefKeys, func(i, j int) bool { - return sortedRefKeys[i].Name < sortedRefKeys[j].Name - }) - - for _, refCollection := range sortedRefKeys { - fields, ok := refs[refCollection] - - if refCollection.IsView() || !ok { - continue // skip missing or view collections - } - - for _, field := range fields { - recordTableName := inflector.Columnify(refCollection.Name) - prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name) - - query := dao.RecordQuery(refCollection) - - if opt, ok := field.Options.(schema.MultiValuer); !ok || !opt.IsMultiple() { - query.AndWhere(dbx.HashExp{prefixedFieldName: mainRecord.Id}) - } else { - query.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf( - `SELECT 1 FROM json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) {{__je__}} WHERE [[__je__.value]]={:jevalue}`, - prefixedFieldName, prefixedFieldName, prefixedFieldName, - ), dbx.Params{ - "jevalue": mainRecord.Id, - }))) - } - - if refCollection.Id == mainRecord.Collection().Id { - query.AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id})) - } - - // trigger cascade for each batchSize rel items until there is none - batchSize := 4000 - rows := make([]dbx.NullStringMap, 0, batchSize) - for { - if err := query.Limit(int64(batchSize)).All(&rows); err != nil { - return err - } - - total := len(rows) - if total == 0 { - break - } - - refRecords := models.NewRecordsFromNullStringMaps(refCollection, rows) - - err := dao.deleteRefRecords(mainRecord, refRecords, field) - if err != nil { - return err - } - - if total < batchSize { - break // no more items - } - - rows = rows[:0] // keep allocated memory - } - } - } - - return nil -} - -// deleteRefRecords checks if related records has to be deleted (if `CascadeDelete` is set) -// OR -// just unset the record id from any relation field values (if they are not required). -// -// NB! This method is expected to be called inside a transaction. -func (dao *Dao) deleteRefRecords(mainRecord *models.Record, refRecords []*models.Record, field *schema.SchemaField) error { - options, _ := field.Options.(*schema.RelationOptions) - if options == nil { - return errors.New("relation field options are not initialized") - } - - for _, refRecord := range refRecords { - ids := refRecord.GetStringSlice(field.Name) - - // unset the record id - for i := len(ids) - 1; i >= 0; i-- { - if ids[i] == mainRecord.Id { - ids = append(ids[:i], ids[i+1:]...) - break - } - } - - // cascade delete the reference - // (only if there are no other active references in case of multiple select) - if options.CascadeDelete && len(ids) == 0 { - if err := dao.DeleteRecord(refRecord); err != nil { - return err - } - // no further actions are needed (the reference is deleted) - continue - } - - if field.Required && len(ids) == 0 { - return fmt.Errorf("the record cannot be deleted because it is part of a required reference in record %s (%s collection)", refRecord.Id, refRecord.Collection().Name) - } - - // save the reference changes - refRecord.Set(field.Name, field.PrepareValue(ids)) - if err := dao.SaveRecord(refRecord); err != nil { - return err - } - } - - return nil -} diff --git a/daos/record_table_sync.go b/daos/record_table_sync.go deleted file mode 100644 index 83f45370..00000000 --- a/daos/record_table_sync.go +++ /dev/null @@ -1,361 +0,0 @@ -package daos - -import ( - "fmt" - "strconv" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/dbutils" - "github.com/pocketbase/pocketbase/tools/security" -) - -// SyncRecordTableSchema compares the two provided collections -// and applies the necessary related record table changes. -// -// If `oldCollection` is null, then only `newCollection` is used to create the record table. -func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error { - return dao.RunInTransaction(func(txDao *Dao) error { - // create - // ----------------------------------------------------------- - if oldCollection == nil { - cols := map[string]string{ - schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL", - schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL", - schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL", - } - - if newCollection.IsAuth() { - cols[schema.FieldNameUsername] = "TEXT NOT NULL" - cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameTokenKey] = "TEXT NOT NULL" - cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL" - cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameLastLoginAlertSentAt] = "TEXT DEFAULT '' NOT NULL" - } - - // ensure that the new collection has an id - if !newCollection.HasId() { - newCollection.RefreshId() - newCollection.MarkAsNew() - } - - tableName := newCollection.Name - - // add schema field definitions - for _, field := range newCollection.Schema.Fields() { - cols[field.Name] = field.ColDefinition() - } - - // create table - if _, err := txDao.DB().CreateTable(tableName, cols).Execute(); err != nil { - return err - } - - // add named unique index on the email and tokenKey columns - if newCollection.IsAuth() { - _, err := txDao.DB().NewQuery(fmt.Sprintf( - ` - CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]); - CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != ''; - CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]); - `, - newCollection.Id, tableName, - newCollection.Id, tableName, - newCollection.Id, tableName, - )).Execute() - if err != nil { - return err - } - } - - return txDao.createCollectionIndexes(newCollection) - } - - // update - // ----------------------------------------------------------- - oldTableName := oldCollection.Name - newTableName := newCollection.Name - oldSchema := oldCollection.Schema - newSchema := newCollection.Schema - deletedFieldNames := []string{} - renamedFieldNames := map[string]string{} - - // drop old indexes (if any) - if err := txDao.dropCollectionIndex(oldCollection); err != nil { - return err - } - - // check for renamed table - if !strings.EqualFold(oldTableName, newTableName) { - _, err := txDao.DB().RenameTable("{{"+oldTableName+"}}", "{{"+newTableName+"}}").Execute() - if err != nil { - return err - } - } - - // check for deleted columns - for _, oldField := range oldSchema.Fields() { - if f := newSchema.GetFieldById(oldField.Id); f != nil { - continue // exist - } - - _, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute() - if err != nil { - return fmt.Errorf("failed to drop column %s - %w", oldField.Name, err) - } - - deletedFieldNames = append(deletedFieldNames, oldField.Name) - } - - // check for new or renamed columns - toRename := map[string]string{} - for _, field := range newSchema.Fields() { - oldField := oldSchema.GetFieldById(field.Id) - // Note: - // We are using a temporary column name when adding or renaming columns - // to ensure that there are no name collisions in case there is - // names switch/reuse of existing columns (eg. name, title -> title, name). - // This way we are always doing 1 more rename operation but it provides better dev experience. - - if oldField == nil { - tempName := field.Name + security.PseudorandomString(5) - toRename[tempName] = field.Name - - // add - _, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute() - if err != nil { - return fmt.Errorf("failed to add column %s - %w", field.Name, err) - } - } else if oldField.Name != field.Name { - tempName := field.Name + security.PseudorandomString(5) - toRename[tempName] = field.Name - - // rename - _, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute() - if err != nil { - return fmt.Errorf("failed to rename column %s - %w", oldField.Name, err) - } - - renamedFieldNames[oldField.Name] = field.Name - } - } - - // set the actual columns name - for tempName, actualName := range toRename { - _, err := txDao.DB().RenameColumn(newTableName, tempName, actualName).Execute() - if err != nil { - return err - } - } - - if err := txDao.normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection); err != nil { - return err - } - - return txDao.createCollectionIndexes(newCollection) - }) -} - -func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection *models.Collection) error { - if newCollection.IsView() || oldCollection == nil { - return nil // view or not an update - } - - return dao.RunInTransaction(func(txDao *Dao) 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 := txDao.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 txDao.DB().NewQuery("PRAGMA writable_schema = RESET").Execute() - - for _, newField := range newCollection.Schema.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 - var isOldMultiple bool - if oldField := oldCollection.Schema.GetFieldById(newField.Id); oldField != nil { - if opt, ok := oldField.Options.(schema.MultiValuer); ok { - isOldMultiple = opt.IsMultiple() - } - } - - var isNewMultiple bool - if opt, ok := newField.Options.(schema.MultiValuer); ok { - isNewMultiple = opt.IsMultiple() - } - - if isOldMultiple == isNewMultiple { - 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 - // ------------------------------------------------------- - - originalName := newField.Name - tempName := "_" + newField.Name + security.PseudorandomString(5) - - _, err := txDao.DB().AddColumn(newCollection.Name, tempName, newField.ColDefinition()).Execute() - if err != nil { - return err - } - - var copyQuery *dbx.Query - - if !isOldMultiple && isNewMultiple { - // single -> multiple (convert to array) - copyQuery = txDao.DB().NewQuery(fmt.Sprintf( - `UPDATE {{%s}} set [[%s]] = ( - CASE - WHEN COALESCE([[%s]], '') = '' - THEN '[]' - ELSE ( - CASE - WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' - THEN [[%s]] - ELSE json_array([[%s]]) - END - ) - END - )`, - newCollection.Name, - tempName, - originalName, - originalName, - originalName, - originalName, - originalName, - )) - } else { - // multiple -> single (keep only the last element) - // - // note: for file fields the actual file objects are not - // deleted allowing additional custom handling via migration - copyQuery = txDao.DB().NewQuery(fmt.Sprintf( - `UPDATE {{%s}} set [[%s]] = ( - CASE - WHEN COALESCE([[%s]], '[]') = '[]' - THEN '' - ELSE ( - CASE - WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' - THEN COALESCE(json_extract([[%s]], '$[#-1]'), '') - ELSE [[%s]] - END - ) - END - )`, - newCollection.Name, - tempName, - originalName, - originalName, - originalName, - originalName, - originalName, - )) - } - - // copy the normalized values - if _, err := copyQuery.Execute(); err != nil { - return err - } - - // drop the original column - if _, err := txDao.DB().DropColumn(newCollection.Name, originalName).Execute(); err != nil { - return err - } - - // rename the new column back to the original - if _, err := txDao.DB().RenameColumn(newCollection.Name, tempName, originalName).Execute(); err != nil { - return err - } - } - - // revert the pragma and reload the schema - _, revertErr := txDao.DB().NewQuery("PRAGMA writable_schema = RESET").Execute() - - return revertErr - }) -} - -func (dao *Dao) dropCollectionIndex(collection *models.Collection) error { - if collection.IsView() { - return nil // views don't have indexes - } - - return dao.RunInTransaction(func(txDao *Dao) error { - for _, raw := range collection.Indexes { - parsed := dbutils.ParseIndex(raw) - - if !parsed.IsValid() { - continue - } - - if _, err := txDao.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil { - return err - } - } - - return nil - }) -} - -func (dao *Dao) createCollectionIndexes(collection *models.Collection) error { - if collection.IsView() { - return nil // views don't have indexes - } - - return dao.RunInTransaction(func(txDao *Dao) error { - // drop new indexes in case a duplicated index name is used - if err := txDao.dropCollectionIndex(collection); err != nil { - return err - } - - // upsert new indexes - // - // note: we are returning validation errors because the indexes cannot be - // validated in a form, aka. before persisting the related collection - // record table changes - errs := validation.Errors{} - for i, idx := range collection.Indexes { - parsed := dbutils.ParseIndex(idx) - - // ensure that the index is always for the current collection - parsed.TableName = collection.Name - - if !parsed.IsValid() { - errs[strconv.Itoa(i)] = validation.NewError( - "validation_invalid_index_expression", - "Invalid CREATE INDEX expression.", - ) - continue - } - - if _, err := txDao.DB().NewQuery(parsed.Build()).Execute(); err != nil { - errs[strconv.Itoa(i)] = validation.NewError( - "validation_invalid_index_expression", - fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()), - ) - continue - } - } - - if len(errs) > 0 { - return validation.Errors{"indexes": errs} - } - - return nil - }) -} diff --git a/daos/record_test.go b/daos/record_test.go deleted file mode 100644 index 12797128..00000000 --- a/daos/record_test.go +++ /dev/null @@ -1,1368 +0,0 @@ -package daos_test - -import ( - "context" - "database/sql" - "errors" - "regexp" - "strings" - "testing" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestRecordQueryWithDifferentCollectionValues(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - name any - collection any - expectedTotal int - expectError bool - }{ - {"with nil value", nil, 0, true}, - {"with invalid or missing collection id/name", "missing", 0, true}, - {"with pointer model", collection, 3, false}, - {"with value model", *collection, 3, false}, - {"with name", "demo1", 3, false}, - {"with id", "wsmn24bux7wo113", 3, false}, - } - - for _, s := range scenarios { - var records []*models.Record - err := app.Dao().RecordQuery(s.collection).All(&records) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasError %v, got %v", s.name, s.expectError, hasErr) - continue - } - - if total := len(records); total != s.expectedTotal { - t.Errorf("[%s] Expected %d records, got %d", s.name, s.expectedTotal, total) - } - } -} - -func TestRecordQueryOneWithRecord(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - id := "84nmscqy84lsi1t" - - q := app.Dao().RecordQuery(collection). - Where(dbx.HashExp{"id": id}) - - record := &models.Record{} - if err := q.One(record); err != nil { - t.Fatal(err) - } - - if record.GetString("id") != id { - t.Fatalf("Expected record with id %q, got %q", id, record.GetString("id")) - } -} - -func TestRecordQueryAllWithRecordsSlices(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - id1 := "84nmscqy84lsi1t" - id2 := "al1h9ijdeojtsjy" - - { - records := []models.Record{} - - q := app.Dao().RecordQuery(collection). - Where(dbx.HashExp{"id": []any{id1, id2}}). - OrderBy("created asc") - - if err := q.All(&records); err != nil { - t.Fatal(err) - } - - if len(records) != 2 { - t.Fatalf("Expected %d records, got %d", 2, len(records)) - } - - if records[0].Id != id1 { - t.Fatalf("Expected record with id %q, got %q", id1, records[0].Id) - } - - if records[1].Id != id2 { - t.Fatalf("Expected record with id %q, got %q", id2, records[1].Id) - } - } - - { - records := []*models.Record{} - - q := app.Dao().RecordQuery(collection). - Where(dbx.HashExp{"id": []any{id1, id2}}). - OrderBy("created asc") - - if err := q.All(&records); err != nil { - t.Fatal(err) - } - - if len(records) != 2 { - t.Fatalf("Expected %d records, got %d", 2, len(records)) - } - - if records[0].Id != id1 { - t.Fatalf("Expected record with id %q, got %q", id1, records[0].Id) - } - - if records[1].Id != id2 { - t.Fatalf("Expected record with id %q, got %q", id2, records[1].Id) - } - } -} - -func TestFindRecordById(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - id string - filter1 func(q *dbx.SelectQuery) error - filter2 func(q *dbx.SelectQuery) error - expectError bool - }{ - {"demo2", "missing", nil, nil, true}, - {"missing", "0yxhwia2amd8gec", nil, nil, true}, - {"demo2", "0yxhwia2amd8gec", nil, nil, false}, - {"demo2", "0yxhwia2amd8gec", func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"title": "missing"}) - return nil - }, nil, true}, - {"demo2", "0yxhwia2amd8gec", func(q *dbx.SelectQuery) error { - return errors.New("test error") - }, nil, true}, - {"demo2", "0yxhwia2amd8gec", func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"title": "test3"}) - return nil - }, nil, false}, - {"demo2", "0yxhwia2amd8gec", func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"title": "test3"}) - return nil - }, func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"active": false}) - return nil - }, true}, - {"sz5l5z67tg7gku0", "0yxhwia2amd8gec", func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"title": "test3"}) - return nil - }, func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"active": true}) - return nil - }, false}, - } - - for i, scenario := range scenarios { - record, err := app.Dao().FindRecordById( - scenario.collectionIdOrName, - scenario.id, - scenario.filter1, - scenario.filter2, - ) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if record != nil && record.Id != scenario.id { - t.Errorf("(%d) Expected record with id %s, got %s", i, scenario.id, record.Id) - } - } -} - -func TestFindRecordsByIds(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - ids []string - filter1 func(q *dbx.SelectQuery) error - filter2 func(q *dbx.SelectQuery) error - expectTotal int - expectError bool - }{ - {"demo2", []string{}, nil, nil, 0, false}, - {"demo2", []string{""}, nil, nil, 0, false}, - {"demo2", []string{"missing"}, nil, nil, 0, false}, - {"missing", []string{"0yxhwia2amd8gec"}, nil, nil, 0, true}, - {"demo2", []string{"0yxhwia2amd8gec"}, nil, nil, 1, false}, - {"sz5l5z67tg7gku0", []string{"0yxhwia2amd8gec"}, nil, nil, 1, false}, - { - "demo2", - []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, - nil, - nil, - 2, - false, - }, - { - "demo2", - []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, - func(q *dbx.SelectQuery) error { - return nil // empty filter - }, - func(q *dbx.SelectQuery) error { - return errors.New("test error") - }, - 0, - true, - }, - { - "demo2", - []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, - func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"active": true}) - return nil - }, - nil, - 1, - false, - }, - { - "sz5l5z67tg7gku0", - []string{"0yxhwia2amd8gec", "llvuca81nly1qls"}, - func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.HashExp{"active": true}) - return nil - }, - func(q *dbx.SelectQuery) error { - q.AndWhere(dbx.Not(dbx.HashExp{"title": ""})) - return nil - }, - 1, - false, - }, - } - - for i, scenario := range scenarios { - records, err := app.Dao().FindRecordsByIds( - scenario.collectionIdOrName, - scenario.ids, - scenario.filter1, - scenario.filter2, - ) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if len(records) != scenario.expectTotal { - t.Errorf("(%d) Expected %d records, got %d", i, scenario.expectTotal, len(records)) - continue - } - - for _, r := range records { - if !list.ExistInSlice(r.Id, scenario.ids) { - t.Errorf("(%d) Couldn't find id %s in %v", i, r.Id, scenario.ids) - } - } - } -} - -func TestFindRecordsByExpr(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - expressions []dbx.Expression - expectIds []string - expectError bool - }{ - { - "missing", - nil, - []string{}, - true, - }, - { - "demo2", - nil, - []string{ - "achvryl401bhse3", - "llvuca81nly1qls", - "0yxhwia2amd8gec", - }, - false, - }, - { - "demo2", - []dbx.Expression{ - nil, - dbx.HashExp{"id": "123"}, - }, - []string{}, - false, - }, - { - "sz5l5z67tg7gku0", - []dbx.Expression{ - dbx.Like("title", "test").Match(true, true), - dbx.HashExp{"active": true}, - }, - []string{ - "achvryl401bhse3", - "0yxhwia2amd8gec", - }, - false, - }, - } - - for i, scenario := range scenarios { - records, err := app.Dao().FindRecordsByExpr(scenario.collectionIdOrName, scenario.expressions...) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - } - - if len(records) != len(scenario.expectIds) { - t.Errorf("(%d) Expected %d records, got %d", i, len(scenario.expectIds), len(records)) - continue - } - - for _, r := range records { - if !list.ExistInSlice(r.Id, scenario.expectIds) { - t.Errorf("(%d) Couldn't find id %s in %v", i, r.Id, scenario.expectIds) - } - } - } -} - -func TestFindFirstRecordByData(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - key string - value any - expectId string - expectError bool - }{ - { - "missing", - "id", - "llvuca81nly1qls", - "llvuca81nly1qls", - true, - }, - { - "demo2", - "", - "llvuca81nly1qls", - "", - true, - }, - { - "demo2", - "id", - "invalid", - "", - true, - }, - { - "demo2", - "id", - "llvuca81nly1qls", - "llvuca81nly1qls", - false, - }, - { - "sz5l5z67tg7gku0", - "title", - "test3", - "0yxhwia2amd8gec", - false, - }, - } - - for i, scenario := range scenarios { - record, err := app.Dao().FindFirstRecordByData(scenario.collectionIdOrName, scenario.key, scenario.value) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && record.Id != scenario.expectId { - t.Errorf("(%d) Expected record with id %s, got %v", i, scenario.expectId, record.Id) - } - } -} - -func TestFindRecordsByFilter(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - collectionIdOrName string - filter string - sort string - limit int - offset int - params []dbx.Params - expectError bool - expectRecordIds []string - }{ - { - "missing collection", - "missing", - "id != ''", - "", - 0, - 0, - nil, - true, - nil, - }, - { - "missing filter", - "demo2", - "", - "", - 0, - 0, - nil, - true, - nil, - }, - { - "invalid filter", - "demo2", - "someMissingField > 1", - "", - 0, - 0, - nil, - true, - nil, - }, - { - "simple filter", - "demo2", - "id != ''", - "", - 0, - 0, - nil, - false, - []string{ - "llvuca81nly1qls", - "achvryl401bhse3", - "0yxhwia2amd8gec", - }, - }, - { - "multi-condition filter with sort", - "demo2", - "id != '' && active=true", - "-created,title", - -1, // should behave the same as 0 - 0, - nil, - false, - []string{ - "0yxhwia2amd8gec", - "achvryl401bhse3", - }, - }, - { - "with limit and offset", - "demo2", - "id != ''", - "title", - 2, - 1, - nil, - false, - []string{ - "achvryl401bhse3", - "0yxhwia2amd8gec", - }, - }, - { - "with placeholder params", - "demo2", - "active = {:active}", - "", - 10, - 0, - []dbx.Params{{"active": false}}, - false, - []string{ - "llvuca81nly1qls", - }, - }, - { - "with json filter and sort", - "demo4", - "json_object != null && json_object.a.b = 'test'", - "-json_object.a", - 10, - 0, - []dbx.Params{{"active": false}}, - false, - []string{ - "i9naidtvr6qsgb4", - }, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - records, err := app.Dao().FindRecordsByFilter( - s.collectionIdOrName, - s.filter, - s.sort, - s.limit, - s.offset, - s.params..., - ) - - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("[%s] Expected hasErr to be %v, got %v (%v)", s.name, s.expectError, hasErr, err) - } - - if hasErr { - return - } - - if len(records) != len(s.expectRecordIds) { - t.Fatalf("[%s] Expected %d records, got %d", s.name, len(s.expectRecordIds), len(records)) - } - - for i, id := range s.expectRecordIds { - if id != records[i].Id { - t.Fatalf("[%s] Expected record with id %q, got %q at index %d", s.name, id, records[i].Id, i) - } - } - }) - } -} - -func TestFindFirstRecordByFilter(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - collectionIdOrName string - filter string - params []dbx.Params - expectError bool - expectRecordId string - }{ - { - "missing collection", - "missing", - "id != ''", - nil, - true, - "", - }, - { - "missing filter", - "demo2", - "", - nil, - true, - "", - }, - { - "invalid filter", - "demo2", - "someMissingField > 1", - nil, - true, - "", - }, - { - "valid filter but no matches", - "demo2", - "id = 'test'", - nil, - true, - "", - }, - { - "valid filter and multiple matches", - "demo2", - "id != ''", - nil, - false, - "llvuca81nly1qls", - }, - { - "with placeholder params", - "demo2", - "active = {:active}", - []dbx.Params{{"active": false}}, - false, - "llvuca81nly1qls", - }, - } - - for _, s := range scenarios { - record, err := app.Dao().FindFirstRecordByFilter(s.collectionIdOrName, s.filter, s.params...) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", s.name, s.expectError, hasErr, err) - continue - } - - if hasErr { - continue - } - - if record.Id != s.expectRecordId { - t.Errorf("[%s] Expected record with id %q, got %q", s.name, s.expectRecordId, record.Id) - } - } -} - -func TestCanAccessRecord(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - authRecord, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - record, err := app.Dao().FindRecordById("demo1", "imy661ixudk5izi") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - name string - record *models.Record - requestInfo *models.RequestInfo - rule *string - expected bool - expectError bool - }{ - { - "as admin with nil rule", - record, - &models.RequestInfo{ - Admin: admin, - }, - nil, - true, - false, - }, - { - "as admin with non-empty rule", - record, - &models.RequestInfo{ - Admin: admin, - }, - types.Pointer("id = ''"), // the filter rule should be ignored - true, - false, - }, - { - "as admin with invalid rule", - record, - &models.RequestInfo{ - Admin: admin, - }, - types.Pointer("id ?!@ 1"), // the filter rule should be ignored - true, - false, - }, - { - "as guest with nil rule", - record, - &models.RequestInfo{}, - nil, - false, - false, - }, - { - "as guest with empty rule", - record, - &models.RequestInfo{}, - types.Pointer(""), - true, - false, - }, - { - "as guest with invalid rule", - record, - &models.RequestInfo{}, - types.Pointer("id ?!@ 1"), - false, - true, - }, - { - "as guest with mismatched rule", - record, - &models.RequestInfo{}, - types.Pointer("@request.auth.id != ''"), - false, - false, - }, - { - "as guest with matched rule", - record, - &models.RequestInfo{ - Data: map[string]any{"test": 1}, - }, - types.Pointer("@request.auth.id != '' || @request.data.test = 1"), - true, - false, - }, - { - "as auth record with nil rule", - record, - &models.RequestInfo{ - AuthRecord: authRecord, - }, - nil, - false, - false, - }, - { - "as auth record with empty rule", - record, - &models.RequestInfo{ - AuthRecord: authRecord, - }, - types.Pointer(""), - true, - false, - }, - { - "as auth record with invalid rule", - record, - &models.RequestInfo{ - AuthRecord: authRecord, - }, - types.Pointer("id ?!@ 1"), - false, - true, - }, - { - "as auth record with mismatched rule", - record, - &models.RequestInfo{ - AuthRecord: authRecord, - Data: map[string]any{"test": 1}, - }, - types.Pointer("@request.auth.id != '' && @request.data.test > 1"), - false, - false, - }, - { - "as auth record with matched rule", - record, - &models.RequestInfo{ - AuthRecord: authRecord, - Data: map[string]any{"test": 2}, - }, - types.Pointer("@request.auth.id != '' && @request.data.test > 1"), - true, - false, - }, - } - - for _, s := range scenarios { - result, err := app.Dao().CanAccessRecord(s.record, s.requestInfo, s.rule) - - if result != s.expected { - t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - } - } -} - -func TestIsRecordValueUnique(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - testManyRelsId1 := "bgs820n361vj1qd" - testManyRelsId2 := "4q1xlclmfloku33" - testManyRelsId3 := "oap640cot4yru2s" - - scenarios := []struct { - collectionIdOrName string - key string - value any - excludeIds []string - expected bool - }{ - {"demo2", "", "", nil, false}, - {"demo2", "", "", []string{""}, false}, - {"demo2", "missing", "unique", nil, false}, - {"demo2", "title", "unique", nil, true}, - {"demo2", "title", "unique", []string{}, true}, - {"demo2", "title", "unique", []string{""}, true}, - {"demo2", "title", "test1", []string{""}, false}, - {"demo2", "title", "test1", []string{"llvuca81nly1qls"}, true}, - {"demo1", "rel_many", []string{testManyRelsId3}, nil, false}, - {"wsmn24bux7wo113", "rel_many", []any{testManyRelsId3}, []string{""}, false}, - {"wsmn24bux7wo113", "rel_many", []any{testManyRelsId3}, []string{"84nmscqy84lsi1t"}, true}, - // mixed json array order - {"demo1", "rel_many", []string{testManyRelsId1, testManyRelsId3, testManyRelsId2}, nil, true}, - // username special case-insensitive match - {"users", "username", "test2_username", nil, false}, - {"users", "username", "TEST2_USERNAME", nil, false}, - {"users", "username", "new_username", nil, true}, - {"users", "username", "TEST2_USERNAME", []string{"oap640cot4yru2s"}, true}, - } - - for i, scenario := range scenarios { - result := app.Dao().IsRecordValueUnique( - scenario.collectionIdOrName, - scenario.key, - scenario.value, - scenario.excludeIds..., - ) - - if result != scenario.expected { - t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) - } - } -} - -func TestFindAuthRecordByToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - token string - baseKey string - expectedEmail string - expectError bool - }{ - // invalid auth token - { - "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.H2KKcIXiAfxvuXMFzizo1SgsinDP4hcWhD3pYoP4Nqw", - app.Settings().RecordAuthToken.Secret, - "", - true, - }, - // expired token - { - "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoxNjQwOTkxNjYxfQ.HqvpCpM0RAk3Qu9PfCMuZsk_DKh9UYuzFLwXBMTZd1w", - app.Settings().RecordAuthToken.Secret, - "", - true, - }, - // wrong base key (password reset token secret instead of auth secret) - { - "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - app.Settings().RecordPasswordResetToken.Secret, - "", - true, - }, - // valid token and base key but with deleted/missing collection - { - "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoibWlzc2luZyIsImV4cCI6MjIwODk4NTI2MX0.0oEHQpdpHp0Nb3VN8La0ssg-SjwWKiRl_k1mUGxdKlU", - app.Settings().RecordAuthToken.Secret, - "test@example.com", - true, - }, - // valid token - { - "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", - app.Settings().RecordAuthToken.Secret, - "test@example.com", - false, - }, - } - - for i, scenario := range scenarios { - record, err := app.Dao().FindAuthRecordByToken(scenario.token, scenario.baseKey) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && record.Email() != scenario.expectedEmail { - t.Errorf("(%d) Expected record model %s, got %s", i, scenario.expectedEmail, record.Email()) - } - } -} - -func TestFindAuthRecordByEmail(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - email string - expectError bool - }{ - {"missing", "test@example.com", true}, - {"demo2", "test@example.com", true}, - {"users", "missing@example.com", true}, - {"users", "test@example.com", false}, - {"clients", "test2@example.com", false}, - } - - for i, scenario := range scenarios { - record, err := app.Dao().FindAuthRecordByEmail(scenario.collectionIdOrName, scenario.email) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && record.Email() != scenario.email { - t.Errorf("(%d) Expected record with email %s, got %s", i, scenario.email, record.Email()) - } - } -} - -func TestFindAuthRecordByUsername(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - username string - expectError bool - }{ - {"missing", "test_username", true}, - {"demo2", "test_username", true}, - {"users", "missing", true}, - {"users", "test2_username", false}, - {"users", "TEST2_USERNAME", false}, // case insensitive check - {"clients", "clients43362", false}, - } - - for i, scenario := range scenarios { - record, err := app.Dao().FindAuthRecordByUsername(scenario.collectionIdOrName, scenario.username) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) - continue - } - - if !scenario.expectError && !strings.EqualFold(record.Username(), scenario.username) { - t.Errorf("(%d) Expected record with username %s, got %s", i, scenario.username, record.Username()) - } - } -} - -func TestSuggestUniqueAuthRecordUsername(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - collectionIdOrName string - baseUsername string - expectedPattern string - }{ - // missing collection - {"missing", "test2_username", `^test2_username\d{12}$`}, - // not an auth collection - {"demo2", "test2_username", `^test2_username\d{12}$`}, - // auth collection with unique base username - {"users", "new_username", `^new_username$`}, - {"users", "NEW_USERNAME", `^NEW_USERNAME$`}, - // auth collection with existing username - {"users", "test2_username", `^test2_username\d{3}$`}, - {"users", "TEST2_USERNAME", `^TEST2_USERNAME\d{3}$`}, - } - - for i, scenario := range scenarios { - username := app.Dao().SuggestUniqueAuthRecordUsername( - scenario.collectionIdOrName, - scenario.baseUsername, - ) - - pattern, err := regexp.Compile(scenario.expectedPattern) - if err != nil { - t.Errorf("[%d] Invalid username pattern %q: %v", i, scenario.expectedPattern, err) - } - if !pattern.MatchString(username) { - t.Fatalf("Expected username to match %s, got username %s", scenario.expectedPattern, username) - } - } -} - -func TestSaveRecord(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo2") - - // create - // --- - r1 := models.NewRecord(collection) - r1.Set("title", "test_new") - err1 := app.Dao().SaveRecord(r1) - if err1 != nil { - t.Fatal(err1) - } - newR1, _ := app.Dao().FindFirstRecordByData(collection.Id, "title", "test_new") - if newR1 == nil || newR1.Id != r1.Id || newR1.GetString("title") != r1.GetString("title") { - t.Fatalf("Expected to find record %v, got %v", r1, newR1) - } - - // update - // --- - r2, _ := app.Dao().FindFirstRecordByData(collection.Id, "id", "0yxhwia2amd8gec") - r2.Set("title", "test_update") - err2 := app.Dao().SaveRecord(r2) - if err2 != nil { - t.Fatal(err2) - } - newR2, _ := app.Dao().FindFirstRecordByData(collection.Id, "title", "test_update") - if newR2 == nil || newR2.Id != r2.Id || newR2.GetString("title") != r2.GetString("title") { - t.Fatalf("Expected to find record %v, got %v", r2, newR2) - } -} - -func TestSaveRecordWithIdFromOtherCollection(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - baseCollection, _ := app.Dao().FindCollectionByNameOrId("demo2") - authCollection, _ := app.Dao().FindCollectionByNameOrId("nologin") - - // base collection test - r1 := models.NewRecord(baseCollection) - r1.Set("title", "test_new") - r1.Set("id", "mk5fmymtx4wsprk") // existing id of demo3 record - r1.MarkAsNew() - if err := app.Dao().SaveRecord(r1); err != nil { - t.Fatalf("Expected nil, got error %v", err) - } - - // auth collection test - r2 := models.NewRecord(authCollection) - r2.Set("username", "test_new") - r2.Set("id", "gk390qegs4y47wn") // existing id of "clients" record - r2.MarkAsNew() - if err := app.Dao().SaveRecord(r2); err == nil { - t.Fatal("Expected error, got nil") - } - - // try again with unique id - r2.Set("id", "unique_id") - if err := app.Dao().SaveRecord(r2); err != nil { - t.Fatalf("Expected nil, got error %v", err) - } -} - -func TestDeleteRecord(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - demoCollection, _ := app.Dao().FindCollectionByNameOrId("demo2") - - // delete unsaved record - // --- - rec0 := models.NewRecord(demoCollection) - if err := app.Dao().DeleteRecord(rec0); err == nil { - t.Fatal("(rec0) Didn't expect to succeed deleting unsaved record") - } - - // delete existing record + external auths - // --- - rec1, _ := app.Dao().FindRecordById("users", "4q1xlclmfloku33") - if err := app.Dao().DeleteRecord(rec1); err != nil { - t.Fatalf("(rec1) Expected nil, got error %v", err) - } - // check if it was really deleted - if refreshed, _ := app.Dao().FindRecordById(rec1.Collection().Id, rec1.Id); refreshed != nil { - t.Fatalf("(rec1) Expected record to be deleted, got %v", refreshed) - } - // check if the external auths were deleted - if auths, _ := app.Dao().FindAllExternalAuthsByRecord(rec1); len(auths) > 0 { - t.Fatalf("(rec1) Expected external auths to be deleted, got %v", auths) - } - - // delete existing record while being part of a non-cascade required relation - // --- - rec2, _ := app.Dao().FindRecordById("demo3", "7nwo8tuiatetxdm") - if err := app.Dao().DeleteRecord(rec2); err == nil { - t.Fatalf("(rec2) Expected error, got nil") - } - - // delete existing record + cascade - // --- - calledQueries := []string{} - app.Dao().NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { - calledQueries = append(calledQueries, sql) - } - app.Dao().ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { - calledQueries = append(calledQueries, sql) - } - app.Dao().NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - calledQueries = append(calledQueries, sql) - } - app.Dao().ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - calledQueries = append(calledQueries, sql) - } - rec3, _ := app.Dao().FindRecordById("users", "oap640cot4yru2s") - // delete - if err := app.Dao().DeleteRecord(rec3); err != nil { - t.Fatalf("(rec3) Expected nil, got error %v", err) - } - // check if it was really deleted - rec3, _ = app.Dao().FindRecordById(rec3.Collection().Id, rec3.Id) - if rec3 != nil { - t.Fatalf("(rec3) Expected record to be deleted, got %v", rec3) - } - // check if the operation cascaded - rel, _ := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t") - if rel != nil { - t.Fatalf("(rec3) Expected the delete to cascade, found relation %v", rel) - } - // ensure that the json rel fields were prefixed - joinedQueries := strings.Join(calledQueries, " ") - expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='" - if !strings.Contains(joinedQueries, expectedRelManyPart) { - t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries) - } - expectedRelOnePart := "SELECT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`='" - if !strings.Contains(joinedQueries, expectedRelOnePart) { - t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries) - } -} - -func TestDeleteRecordBatchProcessing(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - if err := createMockBatchProcessingData(app.Dao()); err != nil { - t.Fatal(err) - } - - // find and delete the first c1 record to trigger cascade - mainRecord, _ := app.Dao().FindRecordById("c1", "a") - if err := app.Dao().DeleteRecord(mainRecord); err != nil { - t.Fatal(err) - } - - // check if the main record was deleted - _, err := app.Dao().FindRecordById(mainRecord.Collection().Id, mainRecord.Id) - if err == nil { - t.Fatal("The main record wasn't deleted") - } - - // check if the c1 b rel field were updated - c1RecordB, err := app.Dao().FindRecordById("c1", "b") - if err != nil || c1RecordB.GetString("rel") != "" { - t.Fatalf("Expected c1RecordB.rel to be nil, got %v", c1RecordB.GetString("rel")) - } - - // check if the c2 rel fields were updated - c2Records, err := app.Dao().FindRecordsByExpr("c2", nil) - if err != nil || len(c2Records) == 0 { - t.Fatalf("Failed to fetch c2 records: %v", err) - } - for _, r := range c2Records { - ids := r.GetStringSlice("rel") - if len(ids) != 1 || ids[0] != "b" { - t.Fatalf("Expected only 'b' rel id, got %v", ids) - } - } - - // check if all c3 relations were deleted - c3Records, err := app.Dao().FindRecordsByExpr("c3", nil) - if err != nil { - t.Fatalf("Failed to fetch c3 records: %v", err) - } - if total := len(c3Records); total != 0 { - t.Fatalf("Expected c3 records to be deleted, found %d", total) - } -} - -func createMockBatchProcessingData(dao *daos.Dao) error { - // create mock collection without relation - c1 := &models.Collection{} - c1.Id = "c1" - c1.Name = c1.Id - c1.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "text", - Type: schema.FieldTypeText, - }, - // self reference - &schema.SchemaField{ - Name: "rel", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(1), - CollectionId: "c1", - CascadeDelete: false, // should unset all rel fields - }, - }, - ) - if err := dao.SaveCollection(c1); err != nil { - return err - } - - // create mock collection with a multi-rel field - c2 := &models.Collection{} - c2.Id = "c2" - c2.Name = c2.Id - c2.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "rel", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(10), - CollectionId: "c1", - CascadeDelete: false, // should unset all rel fields - }, - }, - ) - if err := dao.SaveCollection(c2); err != nil { - return err - } - - // create mock collection with a single-rel field - c3 := &models.Collection{} - c3.Id = "c3" - c3.Name = c3.Id - c3.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "rel", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(1), - CollectionId: "c1", - CascadeDelete: true, // should delete all c3 records - }, - }, - ) - if err := dao.SaveCollection(c3); err != nil { - return err - } - - // insert mock records - c1RecordA := models.NewRecord(c1) - c1RecordA.Id = "a" - c1RecordA.Set("rel", c1RecordA.Id) // self reference - if err := dao.Save(c1RecordA); err != nil { - return err - } - c1RecordB := models.NewRecord(c1) - c1RecordB.Id = "b" - c1RecordB.Set("rel", c1RecordA.Id) // rel to another record from the same collection - if err := dao.Save(c1RecordB); err != nil { - return err - } - for i := 0; i < 4500; i++ { - c2Record := models.NewRecord(c2) - c2Record.Set("rel", []string{c1RecordA.Id, c1RecordB.Id}) - if err := dao.Save(c2Record); err != nil { - return err - } - - c3Record := models.NewRecord(c3) - c3Record.Set("rel", c1RecordA.Id) - if err := dao.Save(c3Record); err != nil { - return err - } - } - - // set the same id as the relation for at least 1 record - // to check whether the correct condition will be added - c3Record := models.NewRecord(c3) - c3Record.Set("rel", c1RecordA.Id) - c3Record.Id = c1RecordA.Id - if err := dao.Save(c3Record); err != nil { - return err - } - - return nil -} diff --git a/daos/settings.go b/daos/settings.go deleted file mode 100644 index f4830e7b..00000000 --- a/daos/settings.go +++ /dev/null @@ -1,63 +0,0 @@ -package daos - -import ( - "encoding/json" - "errors" - - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tools/security" -) - -// FindSettings returns and decode the serialized app settings param value. -// -// The method will first try to decode the param value without decryption. -// If it fails and optEncryptionKey is set, it will try again by first -// decrypting the value and then decode it again. -// -// Returns an error if it fails to decode the stored serialized param value. -func (dao *Dao) FindSettings(optEncryptionKey ...string) (*settings.Settings, error) { - param, err := dao.FindParamByKey(models.ParamAppSettings) - if err != nil { - return nil, err - } - - result := settings.New() - - // try first without decryption - plainDecodeErr := json.Unmarshal(param.Value, result) - - // failed, try to decrypt - if plainDecodeErr != nil { - var encryptionKey string - if len(optEncryptionKey) > 0 && optEncryptionKey[0] != "" { - encryptionKey = optEncryptionKey[0] - } - - // load without decrypt has failed and there is no encryption key to use for decrypt - if encryptionKey == "" { - return nil, errors.New("failed to load the stored app settings - missing or invalid encryption key") - } - - // decrypt - decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey) - if decryptErr != nil { - return nil, decryptErr - } - - // decode again - decryptedDecodeErr := json.Unmarshal(decrypted, result) - if decryptedDecodeErr != nil { - return nil, decryptedDecodeErr - } - } - - return result, nil -} - -// SaveSettings persists the specified settings configuration. -// -// If optEncryptionKey is set, then the stored serialized value will be encrypted with it. -func (dao *Dao) SaveSettings(newSettings *settings.Settings, optEncryptionKey ...string) error { - return dao.SaveParam(models.ParamAppSettings, newSettings, optEncryptionKey...) -} diff --git a/daos/settings_test.go b/daos/settings_test.go deleted file mode 100644 index 9ae52449..00000000 --- a/daos/settings_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package daos_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestSaveAndFindSettings(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - encryptionKey := security.PseudorandomString(32) - - // change unencrypted app settings - app.Settings().Meta.AppName = "save_unencrypted" - if err := app.Dao().SaveSettings(app.Settings()); err != nil { - t.Fatal(err) - } - - // check if the change was persisted - s1, err := app.Dao().FindSettings() - if err != nil { - t.Fatalf("Failed to fetch settings: %v", err) - } - if s1.Meta.AppName != "save_unencrypted" { - t.Fatalf("Expected settings to be changed with app name %q, got \n%v", "save_unencrypted", s1) - } - - // make another change but this time provide an encryption key - app.Settings().Meta.AppName = "save_encrypted" - if err := app.Dao().SaveSettings(app.Settings(), encryptionKey); err != nil { - t.Fatal(err) - } - - // try to fetch the settings without encryption key (should fail) - if s2, err := app.Dao().FindSettings(); err == nil { - t.Fatalf("Expected FindSettings to fail without an encryption key, got \n%v", s2) - } - - // try again but this time with an encryption key - s3, err := app.Dao().FindSettings(encryptionKey) - if err != nil { - t.Fatalf("Failed to fetch settings with an encryption key %s: %v", encryptionKey, err) - } - if s3.Meta.AppName != "save_encrypted" { - t.Fatalf("Expected settings to be changed with app name %q, got \n%v", "save_encrypted", s3) - } -} diff --git a/daos/table_test.go b/daos/table_test.go deleted file mode 100644 index 8e7d9980..00000000 --- a/daos/table_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package daos_test - -import ( - "context" - "database/sql" - "encoding/json" - "testing" - "time" - - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/list" -) - -func TestHasTable(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - tableName string - expected bool - }{ - {"", false}, - {"test", false}, - {"_admins", true}, - {"demo3", true}, - {"DEMO3", true}, // table names are case insensitives by default - {"view1", true}, // view - } - - for i, scenario := range scenarios { - result := app.Dao().HasTable(scenario.tableName) - if result != scenario.expected { - t.Errorf("[%d] Expected %v, got %v", i, scenario.expected, result) - } - } -} - -func TestTableColumns(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - tableName string - expected []string - }{ - {"", nil}, - {"_params", []string{"id", "key", "value", "created", "updated"}}, - } - - for i, s := range scenarios { - columns, _ := app.Dao().TableColumns(s.tableName) - - if len(columns) != len(s.expected) { - t.Errorf("[%d] Expected columns %v, got %v", i, s.expected, columns) - continue - } - - for _, c := range columns { - if !list.ExistInSlice(c, s.expected) { - t.Errorf("[%d] Didn't expect column %s", i, c) - } - } - } -} - -func TestTableInfo(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - tableName string - expected string - }{ - {"", "null"}, - {"missing", "null"}, - { - "_admins", - `[{"PK":1,"Index":0,"Name":"id","Type":"TEXT","NotNull":false,"DefaultValue":null},{"PK":0,"Index":1,"Name":"avatar","Type":"INTEGER","NotNull":true,"DefaultValue":0},{"PK":0,"Index":2,"Name":"email","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":3,"Name":"tokenKey","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":4,"Name":"passwordHash","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":5,"Name":"lastResetSentAt","Type":"TEXT","NotNull":true,"DefaultValue":""},{"PK":0,"Index":6,"Name":"created","Type":"TEXT","NotNull":true,"DefaultValue":""},{"PK":0,"Index":7,"Name":"updated","Type":"TEXT","NotNull":true,"DefaultValue":""}]`, - }, - } - - for i, s := range scenarios { - rows, _ := app.Dao().TableInfo(s.tableName) - - raw, _ := json.Marshal(rows) - rawStr := string(raw) - - if rawStr != s.expected { - t.Errorf("[%d] Expected \n%v, \ngot \n%v", i, s.expected, rawStr) - } - } -} - -func TestDeleteTable(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - tableName string - expectError bool - }{ - {"", true}, - {"test", false}, // missing tables are ignored - {"_admins", false}, - {"demo3", false}, - } - - for i, s := range scenarios { - err := app.Dao().DeleteTable(s.tableName) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%d] Expected hasErr %v, got %v", i, s.expectError, hasErr) - } - } -} - -func TestVacuum(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - calledQueries := []string{} - app.DB().QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { - calledQueries = append(calledQueries, sql) - } - app.DB().ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - calledQueries = append(calledQueries, sql) - } - - if err := app.Dao().Vacuum(); err != nil { - t.Fatal(err) - } - - if total := len(calledQueries); total != 1 { - t.Fatalf("Expected 1 query, got %d", total) - } - - if calledQueries[0] != "VACUUM" { - t.Fatalf("Expected VACUUM query, got %s", calledQueries[0]) - } -} - -func TestTableIndexes(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - table string - expectError bool - expectIndexes []string - }{ - { - "missing", - false, - nil, - }, - { - "demo2", - false, - []string{"idx_demo2_created", "idx_unique_demo2_title", "idx_demo2_active"}, - }, - } - - for _, s := range scenarios { - result, err := app.Dao().TableIndexes(s.table) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v", s.table, s.expectError, hasErr) - } - - if len(s.expectIndexes) != len(result) { - t.Errorf("[%s] Expected %d indexes, got %d:\n%v", s.table, len(s.expectIndexes), len(result), result) - continue - } - - for _, name := range s.expectIndexes { - if result[name] == "" { - t.Errorf("[%s] Missing index %q in \n%v", s.table, name, result) - } - } - } -} diff --git a/examples/base/main.go b/examples/base/main.go index 13371422..84cae004 100644 --- a/examples/base/main.go +++ b/examples/base/main.go @@ -2,10 +2,10 @@ package main import ( "log" + "net/http" "os" "path/filepath" "strings" - "time" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" @@ -13,6 +13,7 @@ import ( "github.com/pocketbase/pocketbase/plugins/ghupdate" "github.com/pocketbase/pocketbase/plugins/jsvm" "github.com/pocketbase/pocketbase/plugins/migratecmd" + "github.com/pocketbase/pocketbase/tools/hook" ) func main() { @@ -42,7 +43,7 @@ func main() { app.RootCmd.PersistentFlags().IntVar( &hooksPool, "hooksPool", - 25, + 20, "the total prewarm goja.Runtime instances for the JS app hooks execution", ) @@ -78,21 +79,13 @@ func main() { "fallback the request to index.html on missing static path (eg. when pretty urls are used with SPA)", ) - var queryTimeout int - app.RootCmd.PersistentFlags().IntVar( - &queryTimeout, - "queryTimeout", - 30, - "the default SELECT queries timeout in seconds", - ) - app.RootCmd.ParseFlags(os.Args[1:]) // --------------------------------------------------------------- // Plugins and hooks: // --------------------------------------------------------------- - // load jsvm (hooks and migrations) + // load jsvm (pb_hooks and pb_migrations) jsvm.MustRegister(app, jsvm.Config{ MigrationsDir: migrationsDir, HooksDir: hooksDir, @@ -110,17 +103,18 @@ func main() { // GitHub selfupdate ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{}) - app.OnAfterBootstrap().PreAdd(func(e *core.BootstrapEvent) error { - app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second - return nil - }) + // static route to serves files from the provided public dir + // (if publicDir exists and the route path is not already defined) + app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{ + Func: func(e *core.ServeEvent) error { + if !e.Router.HasRoute(http.MethodGet, "/{path...}") { + e.Router.GET("/{path...}", apis.Static(os.DirFS(publicDir), indexFallback)) + } - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - // serves static files from the provided public dir (if exists) - e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDir), indexFallback)) - return nil + return e.Next() + }, + Priority: 999, // execute as latest as possible to allow users to provide their own route }) - if err := app.Start(); err != nil { log.Fatal(err) } diff --git a/forms/admin_login.go b/forms/admin_login.go deleted file mode 100644 index da4631a7..00000000 --- a/forms/admin_login.go +++ /dev/null @@ -1,80 +0,0 @@ -package forms - -import ( - "database/sql" - "errors" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" -) - -// AdminLogin is an admin email/pass login form. -type AdminLogin struct { - app core.App - dao *daos.Dao - - Identity string `form:"identity" json:"identity"` - Password string `form:"password" json:"password"` -} - -// NewAdminLogin creates a new [AdminLogin] form initialized with -// the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewAdminLogin(app core.App) *AdminLogin { - return &AdminLogin{ - app: app, - dao: app.Dao(), - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *AdminLogin) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *AdminLogin) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Identity, validation.Required, validation.Length(1, 255), is.EmailFormat), - validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), - ) -} - -// Submit validates and submits the admin form. -// On success returns the authorized admin model. -// -// You can optionally provide a list of InterceptorFunc to -// further modify the form behavior before persisting it. -func (form *AdminLogin) Submit(interceptors ...InterceptorFunc[*models.Admin]) (*models.Admin, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - admin, fetchErr := form.dao.FindAdminByEmail(form.Identity) - - // ignore not found errors to allow custom fetch implementations - if fetchErr != nil && !errors.Is(fetchErr, sql.ErrNoRows) { - return nil, fetchErr - } - - interceptorsErr := runInterceptors(admin, func(m *models.Admin) error { - admin = m - - if admin == nil || !admin.ValidatePassword(form.Password) { - return errors.New("Invalid login credentials.") - } - - return nil - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return admin, nil -} diff --git a/forms/admin_login_test.go b/forms/admin_login_test.go deleted file mode 100644 index 3fa4580f..00000000 --- a/forms/admin_login_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package forms_test - -import ( - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestAdminLoginValidateAndSubmit(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - form := forms.NewAdminLogin(app) - - scenarios := []struct { - email string - password string - expectError bool - }{ - {"", "", true}, - {"", "1234567890", true}, - {"test@example.com", "", true}, - {"test", "test", true}, - {"missing@example.com", "1234567890", true}, - {"test@example.com", "123456789", true}, - {"test@example.com", "1234567890", false}, - } - - for i, s := range scenarios { - form.Identity = s.email - form.Password = s.password - - admin, err := form.Submit() - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - if !s.expectError && admin == nil { - t.Errorf("(%d) Expected admin model to be returned, got nil", i) - } - - if admin != nil && admin.Email != s.email { - t.Errorf("(%d) Expected admin with email %s to be returned, got %v", i, s.email, admin) - } - } -} - -func TestAdminLoginInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - form := forms.NewAdminLogin(testApp) - form.Identity = "test@example.com" - form.Password = "123456" - var interceptorAdmin *models.Admin - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptor1Called = true - return next(admin) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptorAdmin = admin - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorAdmin == nil || interceptorAdmin.Email != form.Identity { - t.Fatalf("Expected Admin model with email %s, got %v", form.Identity, interceptorAdmin) - } -} diff --git a/forms/admin_password_reset_confirm.go b/forms/admin_password_reset_confirm.go deleted file mode 100644 index 0c5ee11f..00000000 --- a/forms/admin_password_reset_confirm.go +++ /dev/null @@ -1,96 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" -) - -// AdminPasswordResetConfirm is an admin password reset confirmation form. -type AdminPasswordResetConfirm struct { - app core.App - dao *daos.Dao - - Token string `form:"token" json:"token"` - Password string `form:"password" json:"password"` - PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` -} - -// NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm { - return &AdminPasswordResetConfirm{ - app: app, - dao: app.Dao(), - } -} - -// SetDao replaces the form Dao instance with the provided one. -// -// This is useful if you want to use a specific transaction Dao instance -// instead of the default app.Dao(). -func (form *AdminPasswordResetConfirm) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *AdminPasswordResetConfirm) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), - validation.Field(&form.Password, validation.Required, validation.Length(10, 72)), - validation.Field(&form.PasswordConfirm, validation.Required, validation.By(validators.Compare(form.Password))), - ) -} - -func (form *AdminPasswordResetConfirm) checkToken(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - admin, err := form.dao.FindAdminByToken(v, form.app.Settings().AdminPasswordResetToken.Secret) - if err != nil || admin == nil { - return validation.NewError("validation_invalid_token", "Invalid or expired token.") - } - - return nil -} - -// Submit validates and submits the admin password reset confirmation form. -// On success returns the updated admin model associated to `form.Token`. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *AdminPasswordResetConfirm) Submit(interceptors ...InterceptorFunc[*models.Admin]) (*models.Admin, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - admin, err := form.dao.FindAdminByToken( - form.Token, - form.app.Settings().AdminPasswordResetToken.Secret, - ) - if err != nil { - return nil, err - } - - if err := admin.SetPassword(form.Password); err != nil { - return nil, err - } - - interceptorsErr := runInterceptors(admin, func(m *models.Admin) error { - admin = m - return form.dao.SaveAdmin(m) - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return admin, nil -} diff --git a/forms/admin_password_reset_confirm_test.go b/forms/admin_password_reset_confirm_test.go deleted file mode 100644 index 583fce65..00000000 --- a/forms/admin_password_reset_confirm_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package forms_test - -import ( - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestAdminPasswordResetConfirmValidateAndSubmit(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - form := forms.NewAdminPasswordResetConfirm(app) - - scenarios := []struct { - token string - password string - passwordConfirm string - expectError bool - }{ - {"", "", "", true}, - {"", "123", "", true}, - {"", "", "123", true}, - {"test", "", "", true}, - {"test", "123", "", true}, - {"test", "123", "123", true}, - { - // expired - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTY0MDk5MTY2MX0.GLwCOsgWTTEKXTK-AyGW838de1OeZGIjfHH0FoRLqZg", - "1234567890", - "1234567890", - true, - }, - { - // valid with mismatched passwords - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc", - "1234567890", - "1234567891", - true, - }, - { - // valid with matching passwords - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc", - "1234567891", - "1234567891", - false, - }, - } - - for i, s := range scenarios { - form.Token = s.token - form.Password = s.password - form.PasswordConfirm = s.passwordConfirm - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - interceptorCalls++ - return next(m) - } - } - - admin, err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if s.expectError { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } - - if s.expectError { - continue - } - - claims, _ := security.ParseUnverifiedJWT(s.token) - tokenAdminId := claims["id"] - - if admin.Id != tokenAdminId { - t.Errorf("(%d) Expected admin with id %s to be returned, got %v", i, tokenAdminId, admin) - } - - if !admin.ValidatePassword(form.Password) { - t.Errorf("(%d) Expected the admin password to have been updated to %q", i, form.Password) - } - } -} - -func TestAdminPasswordResetConfirmInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - admin, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewAdminPasswordResetConfirm(testApp) - form.Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4MTYwMH0.kwFEler6KSMKJNstuaSDvE1QnNdCta5qSnjaIQ0hhhc" - form.Password = "1234567891" - form.PasswordConfirm = "1234567891" - interceptorTokenKey := admin.TokenKey - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptor1Called = true - return next(admin) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptorTokenKey = admin.TokenKey - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorTokenKey == admin.TokenKey { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} diff --git a/forms/admin_password_reset_request.go b/forms/admin_password_reset_request.go deleted file mode 100644 index b0568ae7..00000000 --- a/forms/admin_password_reset_request.go +++ /dev/null @@ -1,89 +0,0 @@ -package forms - -import ( - "errors" - "fmt" - "time" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/types" -) - -// AdminPasswordResetRequest is an admin password reset request form. -type AdminPasswordResetRequest struct { - app core.App - dao *daos.Dao - resendThreshold float64 // in seconds - - Email string `form:"email" json:"email"` -} - -// NewAdminPasswordResetRequest creates a new [AdminPasswordResetRequest] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest { - return &AdminPasswordResetRequest{ - app: app, - dao: app.Dao(), - resendThreshold: 120, // 2min - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *AdminPasswordResetRequest) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -// -// This method doesn't verify that admin with `form.Email` exists (this is done on Submit). -func (form *AdminPasswordResetRequest) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Email, - validation.Required, - validation.Length(1, 255), - is.EmailFormat, - ), - ) -} - -// Submit validates and submits the form. -// On success sends a password reset email to the `form.Email` admin. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *AdminPasswordResetRequest) Submit(interceptors ...InterceptorFunc[*models.Admin]) error { - if err := form.Validate(); err != nil { - return err - } - - admin, err := form.dao.FindAdminByEmail(form.Email) - if err != nil { - return fmt.Errorf("Failed to fetch admin with email %s: %w", form.Email, err) - } - - now := time.Now().UTC() - lastResetSentAt := admin.LastResetSentAt.Time() - if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { - return errors.New("You have already requested a password reset.") - } - - return runInterceptors(admin, func(m *models.Admin) error { - if err := mails.SendAdminPasswordReset(form.app, m); err != nil { - return err - } - - // update last sent timestamp - m.LastResetSentAt = types.NowDateTime() - - return form.dao.SaveAdmin(m) - }, interceptors...) -} diff --git a/forms/admin_password_reset_request_test.go b/forms/admin_password_reset_request_test.go deleted file mode 100644 index 6d69ccc3..00000000 --- a/forms/admin_password_reset_request_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package forms_test - -import ( - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestAdminPasswordResetRequestValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - form := forms.NewAdminPasswordResetRequest(testApp) - - scenarios := []struct { - email string - expectError bool - }{ - {"", true}, - {"", true}, - {"invalid", true}, - {"missing@example.com", true}, - {"test@example.com", false}, - {"test@example.com", true}, // already requested - } - - for i, s := range scenarios { - testApp.TestMailer.TotalSend = 0 // reset - form.Email = s.email - - adminBefore, _ := testApp.Dao().FindAdminByEmail(s.email) - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - interceptorCalls++ - return next(m) - } - } - - err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if s.expectError { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - adminAfter, _ := testApp.Dao().FindAdminByEmail(s.email) - - if !s.expectError && (adminBefore.LastResetSentAt == adminAfter.LastResetSentAt || adminAfter.LastResetSentAt.IsZero()) { - t.Errorf("(%d) Expected admin.LastResetSentAt to change, got %q", i, adminAfter.LastResetSentAt) - } - - expectedMails := 1 - if s.expectError { - expectedMails = 0 - } - if testApp.TestMailer.TotalSend != expectedMails { - t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) - } - } -} - -func TestAdminPasswordResetRequestInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - admin, err := testApp.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewAdminPasswordResetRequest(testApp) - form.Email = admin.Email - interceptorLastResetSentAt := admin.LastResetSentAt - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptor1Called = true - return next(admin) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(admin *models.Admin) error { - interceptorLastResetSentAt = admin.LastResetSentAt - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorLastResetSentAt.String() != admin.LastResetSentAt.String() { - t.Fatalf("Expected the form model to NOT be filled before calling the interceptors") - } -} diff --git a/forms/admin_upsert.go b/forms/admin_upsert.go deleted file mode 100644 index 4afcb67e..00000000 --- a/forms/admin_upsert.go +++ /dev/null @@ -1,123 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" -) - -// AdminUpsert is a [models.Admin] upsert (create/update) form. -type AdminUpsert struct { - app core.App - dao *daos.Dao - admin *models.Admin - - Id string `form:"id" json:"id"` - Avatar int `form:"avatar" json:"avatar"` - Email string `form:"email" json:"email"` - Password string `form:"password" json:"password"` - PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` -} - -// NewAdminUpsert creates a new [AdminUpsert] form with initializer -// config created from the provided [core.App] and [models.Admin] instances -// (for create you could pass a pointer to an empty Admin - `&models.Admin{}`). -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert { - form := &AdminUpsert{ - app: app, - dao: app.Dao(), - admin: admin, - } - - // load defaults - form.Id = admin.Id - form.Avatar = admin.Avatar - form.Email = admin.Email - - return form -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *AdminUpsert) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *AdminUpsert) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Id, - validation.When( - form.admin.IsNew(), - validation.Length(models.DefaultIdLength, models.DefaultIdLength), - validation.Match(idRegex), - validation.By(validators.UniqueId(form.dao, form.admin.TableName())), - ).Else(validation.In(form.admin.Id)), - ), - validation.Field( - &form.Avatar, - validation.Min(0), - validation.Max(9), - ), - validation.Field( - &form.Email, - validation.Required, - validation.Length(1, 255), - is.EmailFormat, - validation.By(form.checkUniqueEmail), - ), - validation.Field( - &form.Password, - validation.When(form.admin.IsNew(), validation.Required), - validation.Length(10, 72), - ), - validation.Field( - &form.PasswordConfirm, - validation.When(form.Password != "", validation.Required), - validation.By(validators.Compare(form.Password)), - ), - ) -} - -func (form *AdminUpsert) checkUniqueEmail(value any) error { - v, _ := value.(string) - - if form.dao.IsAdminEmailUnique(v, form.admin.Id) { - return nil - } - - return validation.NewError("validation_admin_email_exists", "Admin email already exists.") -} - -// Submit validates the form and upserts the form admin model. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc[*models.Admin]) error { - if err := form.Validate(); err != nil { - return err - } - - // custom insertion id can be set only on create - if form.admin.IsNew() && form.Id != "" { - form.admin.MarkAsNew() - form.admin.SetId(form.Id) - } - - form.admin.Avatar = form.Avatar - form.admin.Email = form.Email - - if form.Password != "" { - form.admin.SetPassword(form.Password) - } - - return runInterceptors(form.admin, func(admin *models.Admin) error { - return form.dao.SaveAdmin(admin) - }, interceptors...) -} diff --git a/forms/admin_upsert_test.go b/forms/admin_upsert_test.go deleted file mode 100644 index bb502842..00000000 --- a/forms/admin_upsert_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "fmt" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestNewAdminUpsert(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin := &models.Admin{} - admin.Avatar = 3 - admin.Email = "new@example.com" - - form := forms.NewAdminUpsert(app, admin) - - // test defaults - if form.Avatar != admin.Avatar { - t.Errorf("Expected Avatar %d, got %d", admin.Avatar, form.Avatar) - } - if form.Email != admin.Email { - t.Errorf("Expected Email %q, got %q", admin.Email, form.Email) - } -} - -func TestAdminUpsertValidateAndSubmit(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - id string - jsonData string - expectError bool - }{ - { - // create empty - "", - `{}`, - true, - }, - { - // update empty - "sywbhecnh46rhm0", - `{}`, - false, - }, - { - // create failure - existing email - "", - `{ - "email": "test@example.com", - "password": "1234567890", - "passwordConfirm": "1234567890" - }`, - true, - }, - { - // create failure - passwords mismatch - "", - `{ - "email": "test_new@example.com", - "password": "1234567890", - "passwordConfirm": "1234567891" - }`, - true, - }, - { - // create success - "", - `{ - "email": "test_new@example.com", - "password": "1234567890", - "passwordConfirm": "1234567890" - }`, - false, - }, - { - // update failure - existing email - "sywbhecnh46rhm0", - `{ - "email": "test2@example.com" - }`, - true, - }, - { - // update failure - mismatching passwords - "sywbhecnh46rhm0", - `{ - "password": "1234567890", - "passwordConfirm": "1234567891" - }`, - true, - }, - { - // update success - new email - "sywbhecnh46rhm0", - `{ - "email": "test_update@example.com" - }`, - false, - }, - { - // update success - new password - "sywbhecnh46rhm0", - `{ - "password": "1234567890", - "passwordConfirm": "1234567890" - }`, - false, - }, - } - - for i, s := range scenarios { - isCreate := true - admin := &models.Admin{} - if s.id != "" { - isCreate = false - admin, _ = app.Dao().FindAdminById(s.id) - } - initialTokenKey := admin.TokenKey - - form := forms.NewAdminUpsert(app, admin) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - - err := form.Submit(func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - interceptorCalls++ - return next(m) - } - }) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - foundAdmin, _ := app.Dao().FindAdminByEmail(form.Email) - - if !s.expectError && isCreate && foundAdmin == nil { - t.Errorf("(%d) Expected admin to be created, got nil", i) - continue - } - - expectInterceptorCall := 1 - if s.expectError { - expectInterceptorCall = 0 - } - if interceptorCalls != expectInterceptorCall { - t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) - } - - if s.expectError { - continue // skip persistence check - } - - if foundAdmin.Email != form.Email { - t.Errorf("(%d) Expected email %s, got %s", i, form.Email, foundAdmin.Email) - } - - if foundAdmin.Avatar != form.Avatar { - t.Errorf("(%d) Expected avatar %d, got %d", i, form.Avatar, foundAdmin.Avatar) - } - - if form.Password != "" && initialTokenKey == foundAdmin.TokenKey { - t.Errorf("(%d) Expected token key to be renewed when setting a new password", i) - } - } -} - -func TestAdminUpsertSubmitInterceptors(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin := &models.Admin{} - form := forms.NewAdminUpsert(app, admin) - form.Email = "test_new@example.com" - form.Password = "1234567890" - form.PasswordConfirm = form.Password - - testErr := errors.New("test_error") - interceptorAdminEmail := "" - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - interceptor1Called = true - return next(m) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Admin]) forms.InterceptorNextFunc[*models.Admin] { - return func(m *models.Admin) error { - interceptorAdminEmail = admin.Email // to check if the record was filled - interceptor2Called = true - return testErr - } - } - - err := form.Submit(interceptor1, interceptor2) - if err != testErr { - t.Fatalf("Expected error %v, got %v", testErr, err) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorAdminEmail != form.Email { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} - -func TestAdminUpsertWithCustomId(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - existingAdmin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - name string - jsonData string - collection *models.Admin - expectError bool - }{ - { - "empty data", - "{}", - &models.Admin{}, - false, - }, - { - "empty id", - `{"id":""}`, - &models.Admin{}, - false, - }, - { - "id < 15 chars", - `{"id":"a23"}`, - &models.Admin{}, - true, - }, - { - "id > 15 chars", - `{"id":"a234567890123456"}`, - &models.Admin{}, - true, - }, - { - "id = 15 chars (invalid chars)", - `{"id":"a@3456789012345"}`, - &models.Admin{}, - true, - }, - { - "id = 15 chars (valid chars)", - `{"id":"a23456789012345"}`, - &models.Admin{}, - false, - }, - { - "changing the id of an existing item", - `{"id":"b23456789012345"}`, - existingAdmin, - true, - }, - { - "using the same existing item id", - `{"id":"` + existingAdmin.Id + `"}`, - existingAdmin, - false, - }, - { - "skipping the id for existing item", - `{}`, - existingAdmin, - false, - }, - } - - for i, scenario := range scenarios { - form := forms.NewAdminUpsert(app, scenario.collection) - if form.Email == "" { - form.Email = fmt.Sprintf("test_id_%d@example.com", i) - } - form.Password = "1234567890" - form.PasswordConfirm = form.Password - - // load data - loadErr := json.Unmarshal([]byte(scenario.jsonData), form) - if loadErr != nil { - t.Errorf("[%s] Failed to load form data: %v", scenario.name, loadErr) - continue - } - - submitErr := form.Submit() - hasErr := submitErr != nil - - if hasErr != scenario.expectError { - t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasErr, submitErr) - } - - if !hasErr && form.Id != "" { - _, err := app.Dao().FindAdminById(form.Id) - if err != nil { - t.Errorf("[%s] Expected to find record with id %s, got %v", scenario.name, form.Id, err) - } - } - } -} diff --git a/forms/backup_create.go b/forms/backup_create.go deleted file mode 100644 index d7737027..00000000 --- a/forms/backup_create.go +++ /dev/null @@ -1,79 +0,0 @@ -package forms - -import ( - "context" - "regexp" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" -) - -var backupNameRegex = regexp.MustCompile(`^[a-z0-9_-]+\.zip$`) - -// BackupCreate is a request form for creating a new app backup. -type BackupCreate struct { - app core.App - ctx context.Context - - Name string `form:"name" json:"name"` -} - -// NewBackupCreate creates new BackupCreate request form. -func NewBackupCreate(app core.App) *BackupCreate { - return &BackupCreate{ - app: app, - ctx: context.Background(), - } -} - -// SetContext replaces the default form context with the provided one. -func (form *BackupCreate) SetContext(ctx context.Context) { - form.ctx = ctx -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *BackupCreate) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Name, - validation.Length(1, 100), - validation.Match(backupNameRegex), - validation.By(form.checkUniqueName), - ), - ) -} - -func (form *BackupCreate) checkUniqueName(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - fsys, err := form.app.NewBackupsFilesystem() - if err != nil { - return err - } - defer fsys.Close() - - fsys.SetContext(form.ctx) - - if exists, err := fsys.Exists(v); err != nil || exists { - return validation.NewError("validation_backup_name_exists", "The backup file name is invalid or already exists.") - } - - return nil -} - -// Submit validates the form and creates the app backup. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before creating the backup. -func (form *BackupCreate) Submit(interceptors ...InterceptorFunc[string]) error { - if err := form.Validate(); err != nil { - return err - } - - return runInterceptors(form.Name, func(name string) error { - return form.app.CreateBackup(form.ctx, name) - }, interceptors...) -} diff --git a/forms/backup_create_test.go b/forms/backup_create_test.go deleted file mode 100644 index 82112ca4..00000000 --- a/forms/backup_create_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package forms_test - -import ( - "strings" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/tests" -) - -func TestBackupCreateValidateAndSubmit(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - backupName string - expectedErrors []string - }{ - { - "invalid length", - strings.Repeat("a", 97) + ".zip", - []string{"name"}, - }, - { - "valid length + invalid format", - strings.Repeat("a", 96), - []string{"name"}, - }, - { - "valid length + valid format", - strings.Repeat("a", 96) + ".zip", - []string{}, - }, - { - "auto generated name", - "", - []string{}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - fsys, err := app.NewBackupsFilesystem() - if err != nil { - t.Fatal(err) - } - defer fsys.Close() - - form := forms.NewBackupCreate(app) - form.Name = s.backupName - - result := form.Submit() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - return - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Fatalf("Missing expected error key %q in %v", k, errs) - } - } - - // retrieve all created backup files - files, err := fsys.List("") - if err != nil { - t.Fatal("Failed to retrieve backup files") - return - } - - if result != nil { - if total := len(files); total != 0 { - t.Fatalf("Didn't expected backup files, found %d", total) - } - return - } - - if total := len(files); total != 1 { - t.Fatalf("Expected 1 backup file, got %d", total) - return - } - - if s.backupName == "" { - prefix := "pb_backup_" - if !strings.HasPrefix(files[0].Key, prefix) { - t.Fatalf("Expected the backup file, to have prefix %q: %q", prefix, files[0].Key) - } - } else if s.backupName != files[0].Key { - t.Fatalf("Expected backup file %q, got %q", s.backupName, files[0].Key) - } - }) - } -} diff --git a/forms/backup_upload.go b/forms/backup_upload.go deleted file mode 100644 index 8056e691..00000000 --- a/forms/backup_upload.go +++ /dev/null @@ -1,85 +0,0 @@ -package forms - -import ( - "context" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/tools/filesystem" -) - -// BackupUpload is a request form for uploading a new app backup. -type BackupUpload struct { - app core.App - ctx context.Context - - File *filesystem.File `json:"file"` -} - -// NewBackupUpload creates new BackupUpload request form. -func NewBackupUpload(app core.App) *BackupUpload { - return &BackupUpload{ - app: app, - ctx: context.Background(), - } -} - -// SetContext replaces the default form upload context with the provided one. -func (form *BackupUpload) SetContext(ctx context.Context) { - form.ctx = ctx -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *BackupUpload) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.File, - validation.Required, - validation.By(validators.UploadedFileMimeType([]string{"application/zip"})), - validation.By(form.checkUniqueName), - ), - ) -} - -func (form *BackupUpload) checkUniqueName(value any) error { - v, _ := value.(*filesystem.File) - if v == nil { - return nil // nothing to check - } - - fsys, err := form.app.NewBackupsFilesystem() - if err != nil { - return err - } - defer fsys.Close() - - fsys.SetContext(form.ctx) - - if exists, err := fsys.Exists(v.OriginalName); err != nil || exists { - return validation.NewError("validation_backup_name_exists", "Backup file with the specified name already exists.") - } - - return nil -} - -// Submit validates the form and upload the backup file. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before uploading the backup. -func (form *BackupUpload) Submit(interceptors ...InterceptorFunc[*filesystem.File]) error { - if err := form.Validate(); err != nil { - return err - } - - return runInterceptors(form.File, func(file *filesystem.File) error { - fsys, err := form.app.NewBackupsFilesystem() - if err != nil { - return err - } - - fsys.SetContext(form.ctx) - - return fsys.UploadFile(file, file.OriginalName) - }, interceptors...) -} diff --git a/forms/backup_upload_test.go b/forms/backup_upload_test.go deleted file mode 100644 index f92ceada..00000000 --- a/forms/backup_upload_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package forms_test - -import ( - "archive/zip" - "bytes" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/filesystem" -) - -func TestBackupUploadValidateAndSubmit(t *testing.T) { - t.Parallel() - - var zb bytes.Buffer - zw := zip.NewWriter(&zb) - if err := zw.Close(); err != nil { - t.Fatal(err) - } - - f0, _ := filesystem.NewFileFromBytes([]byte("test"), "existing") - f1, _ := filesystem.NewFileFromBytes([]byte("456"), "nozip") - f2, _ := filesystem.NewFileFromBytes(zb.Bytes(), "existing") - f3, _ := filesystem.NewFileFromBytes(zb.Bytes(), "zip") - - scenarios := []struct { - name string - file *filesystem.File - expectedErrors []string - }{ - { - "missing file", - nil, - []string{"file"}, - }, - { - "non-zip file", - f1, - []string{"file"}, - }, - { - "zip file with non-unique name", - f2, - []string{"file"}, - }, - { - "zip file with unique name", - f3, - []string{}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - fsys, err := app.NewBackupsFilesystem() - if err != nil { - t.Fatal(err) - } - defer fsys.Close() - // create a dummy backup file to simulate existing backups - if err := fsys.UploadFile(f0, f0.OriginalName); err != nil { - t.Fatal(err) - } - - form := forms.NewBackupUpload(app) - form.File = s.file - - result := form.Submit() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Fatalf("Missing expected error key %q in %v", k, errs) - } - } - - expectedFiles := []*filesystem.File{f0} - if result == nil { - expectedFiles = append(expectedFiles, s.file) - } - - // retrieve all uploaded backup files - files, err := fsys.List("") - if err != nil { - t.Fatal("Failed to retrieve backup files") - } - - if len(files) != len(expectedFiles) { - t.Fatalf("Expected %d files, got %d", len(expectedFiles), len(files)) - } - - for _, ef := range expectedFiles { - exists := false - for _, f := range files { - if f.Key == ef.OriginalName { - exists = true - break - } - } - if !exists { - t.Fatalf("Missing expected backup file %v", ef.OriginalName) - } - } - }) - } -} diff --git a/forms/base.go b/forms/base.go deleted file mode 100644 index 64c18883..00000000 --- a/forms/base.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package models implements various services used for request data -// validation and applying changes to existing DB models through the app Dao. -package forms - -import ( - "regexp" -) - -// base ID value regex pattern -var idRegex = regexp.MustCompile(`^[^\@\#\$\&\|\.\,\'\"\\\/\s]+$`) - -// InterceptorNextFunc is a interceptor handler function. -// Usually used in combination with InterceptorFunc. -type InterceptorNextFunc[T any] func(t T) error - -// InterceptorFunc defines a single interceptor function that -// will execute the provided next func handler. -type InterceptorFunc[T any] func(next InterceptorNextFunc[T]) InterceptorNextFunc[T] - -// runInterceptors executes the provided list of interceptors. -func runInterceptors[T any]( - data T, - next InterceptorNextFunc[T], - interceptors ...InterceptorFunc[T], -) error { - for i := len(interceptors) - 1; i >= 0; i-- { - next = interceptors[i](next) - } - - return next(data) -} diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go deleted file mode 100644 index eb39a676..00000000 --- a/forms/collection_upsert.go +++ /dev/null @@ -1,540 +0,0 @@ -package forms - -import ( - "encoding/json" - "fmt" - "regexp" - "strconv" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/resolvers" - "github.com/pocketbase/pocketbase/tools/dbutils" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/search" - "github.com/pocketbase/pocketbase/tools/types" -) - -var collectionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`) - -// CollectionUpsert is a [models.Collection] upsert (create/update) form. -type CollectionUpsert struct { - app core.App - dao *daos.Dao - collection *models.Collection - - Id string `form:"id" json:"id"` - Type string `form:"type" json:"type"` - Name string `form:"name" json:"name"` - System bool `form:"system" json:"system"` - Schema schema.Schema `form:"schema" json:"schema"` - Indexes types.JsonArray[string] `form:"indexes" json:"indexes"` - ListRule *string `form:"listRule" json:"listRule"` - ViewRule *string `form:"viewRule" json:"viewRule"` - CreateRule *string `form:"createRule" json:"createRule"` - UpdateRule *string `form:"updateRule" json:"updateRule"` - DeleteRule *string `form:"deleteRule" json:"deleteRule"` - Options types.JsonMap `form:"options" json:"options"` -} - -// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer -// config created from the provided [core.App] and [models.Collection] instances -// (for create you could pass a pointer to an empty Collection - `&models.Collection{}`). -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert { - form := &CollectionUpsert{ - app: app, - dao: app.Dao(), - collection: collection, - } - - // load defaults - form.Id = form.collection.Id - form.Type = form.collection.Type - form.Name = form.collection.Name - form.System = form.collection.System - form.Indexes = form.collection.Indexes - form.ListRule = form.collection.ListRule - form.ViewRule = form.collection.ViewRule - form.CreateRule = form.collection.CreateRule - form.UpdateRule = form.collection.UpdateRule - form.DeleteRule = form.collection.DeleteRule - form.Options = form.collection.Options - - if form.Type == "" { - form.Type = models.CollectionTypeBase - } - - clone, _ := form.collection.Schema.Clone() - if clone != nil && form.Type != models.CollectionTypeView { - form.Schema = *clone - } else { - form.Schema = schema.Schema{} - } - - return form -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *CollectionUpsert) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *CollectionUpsert) Validate() error { - isAuth := form.Type == models.CollectionTypeAuth - isView := form.Type == models.CollectionTypeView - - // generate schema from the query (overwriting any explicit user defined schema) - if isView { - options := models.CollectionViewOptions{} - if err := decodeOptions(form.Options, &options); err != nil { - return err - } - form.Schema, _ = form.dao.CreateViewSchema(options.Query) - } - - return validation.ValidateStruct(form, - validation.Field( - &form.Id, - validation.When( - form.collection.IsNew(), - validation.Length(models.DefaultIdLength, models.DefaultIdLength), - validation.Match(idRegex), - validation.By(validators.UniqueId(form.dao, form.collection.TableName())), - ).Else(validation.In(form.collection.Id)), - ), - validation.Field( - &form.System, - validation.By(form.ensureNoSystemFlagChange), - ), - validation.Field( - &form.Type, - validation.Required, - validation.In( - models.CollectionTypeBase, - models.CollectionTypeAuth, - models.CollectionTypeView, - ), - validation.By(form.ensureNoTypeChange), - ), - validation.Field( - &form.Name, - validation.Required, - validation.Length(1, 255), - validation.Match(collectionNameRegex), - validation.By(form.ensureNoSystemNameChange), - validation.By(form.checkUniqueName), - validation.By(form.checkForVia), - ), - // validates using the type's own validation rules + some collection's specifics - validation.Field( - &form.Schema, - validation.By(form.checkMinSchemaFields), - validation.By(form.ensureNoSystemFieldsChange), - validation.By(form.ensureNoFieldsTypeChange), - validation.By(form.checkRelationFields), - validation.When(isAuth, validation.By(form.ensureNoAuthFieldName)), - ), - validation.Field(&form.ListRule, validation.By(form.checkRule)), - validation.Field(&form.ViewRule, validation.By(form.checkRule)), - validation.Field( - &form.CreateRule, - validation.When(isView, validation.Nil), - validation.By(form.checkRule), - ), - validation.Field( - &form.UpdateRule, - validation.When(isView, validation.Nil), - validation.By(form.checkRule), - ), - validation.Field( - &form.DeleteRule, - validation.When(isView, validation.Nil), - validation.By(form.checkRule), - ), - validation.Field(&form.Indexes, validation.By(form.checkIndexes)), - validation.Field(&form.Options, validation.By(form.checkOptions)), - ) -} - -func (form *CollectionUpsert) checkForVia(value any) error { - v, _ := value.(string) - if v == "" { - return nil - } - - if strings.Contains(strings.ToLower(v), "_via_") { - return validation.NewError("validation_invalid_name", "The name of the collection cannot contain '_via_'.") - } - - return nil -} - -func (form *CollectionUpsert) checkUniqueName(value any) error { - v, _ := value.(string) - - // ensure unique collection name - if !form.dao.IsCollectionNameUnique(v, form.collection.Id) { - return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") - } - - // ensure that the collection name doesn't collide with the id of any collection - if form.dao.FindById(&models.Collection{}, v) == nil { - return validation.NewError("validation_collection_name_id_duplicate", "The name must not match an existing collection id.") - } - - return nil -} - -func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error { - v, _ := value.(string) - - if !form.collection.IsNew() && form.collection.System && v != form.collection.Name { - return validation.NewError("validation_collection_system_name_change", "System collections cannot be renamed.") - } - - return nil -} - -func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error { - v, _ := value.(bool) - - if !form.collection.IsNew() && v != form.collection.System { - return validation.NewError("validation_collection_system_flag_change", "System collection state cannot be changed.") - } - - return nil -} - -func (form *CollectionUpsert) ensureNoTypeChange(value any) error { - v, _ := value.(string) - - if !form.collection.IsNew() && v != form.collection.Type { - return validation.NewError("validation_collection_type_change", "Collection type cannot be changed.") - } - - return nil -} - -func (form *CollectionUpsert) ensureNoFieldsTypeChange(value any) error { - v, _ := value.(schema.Schema) - - for i, field := range v.Fields() { - oldField := form.collection.Schema.GetFieldById(field.Id) - - if oldField != nil && oldField.Type != field.Type { - return validation.Errors{fmt.Sprint(i): validation.NewError( - "validation_field_type_change", - "Field type cannot be changed.", - )} - } - } - - return nil -} - -func (form *CollectionUpsert) checkRelationFields(value any) error { - v, _ := value.(schema.Schema) - - for i, field := range v.Fields() { - if field.Type != schema.FieldTypeRelation { - continue - } - - options, _ := field.Options.(*schema.RelationOptions) - if options == nil { - return validation.Errors{fmt.Sprint(i): validation.Errors{ - "options": validation.NewError( - "validation_schema_invalid_relation_field_options", - "The relation field has invalid field options.", - )}, - } - } - - // prevent collectionId change - oldField := form.collection.Schema.GetFieldById(field.Id) - if oldField != nil { - oldOptions, _ := oldField.Options.(*schema.RelationOptions) - if oldOptions != nil && oldOptions.CollectionId != options.CollectionId { - return validation.Errors{fmt.Sprint(i): validation.Errors{ - "options": validation.Errors{ - "collectionId": validation.NewError( - "validation_field_relation_change", - "The relation collection cannot be changed.", - ), - }}, - } - } - } - - relCollection, _ := form.dao.FindCollectionByNameOrId(options.CollectionId) - - // validate collectionId - if relCollection == nil || relCollection.Id != options.CollectionId { - return validation.Errors{fmt.Sprint(i): validation.Errors{ - "options": validation.Errors{ - "collectionId": validation.NewError( - "validation_field_invalid_relation", - "The relation collection doesn't exist.", - ), - }}, - } - } - - // allow only views to have relations to other views - // (see https://github.com/pocketbase/pocketbase/issues/3000) - if form.Type != models.CollectionTypeView && relCollection.IsView() { - return validation.Errors{fmt.Sprint(i): validation.Errors{ - "options": validation.Errors{ - "collectionId": validation.NewError( - "validation_field_non_view_base_relation_collection", - "Non view collections are not allowed to have a view relation.", - ), - }}, - } - } - } - - return nil -} - -func (form *CollectionUpsert) ensureNoAuthFieldName(value any) error { - v, _ := value.(schema.Schema) - - if form.Type != models.CollectionTypeAuth { - return nil // not an auth collection - } - - authFieldNames := schema.AuthFieldNames() - // exclude the meta RecordUpsert form fields - authFieldNames = append(authFieldNames, "password", "passwordConfirm", "oldPassword") - - errs := validation.Errors{} - for i, field := range v.Fields() { - if list.ExistInSlice(field.Name, authFieldNames) { - errs[fmt.Sprint(i)] = validation.Errors{ - "name": validation.NewError( - "validation_reserved_auth_field_name", - "The field name is reserved and cannot be used.", - ), - } - } - } - - if len(errs) > 0 { - return errs - } - - return nil -} - -func (form *CollectionUpsert) checkMinSchemaFields(value any) error { - v, _ := value.(schema.Schema) - - switch form.Type { - case models.CollectionTypeAuth, models.CollectionTypeView: - return nil // no schema fields constraint - default: - if len(v.Fields()) == 0 { - return validation.ErrRequired - } - } - - return nil -} - -func (form *CollectionUpsert) ensureNoSystemFieldsChange(value any) error { - v, _ := value.(schema.Schema) - - for _, oldField := range form.collection.Schema.Fields() { - if !oldField.System { - continue - } - - newField := v.GetFieldById(oldField.Id) - - if newField == nil || oldField.String() != newField.String() { - return validation.NewError("validation_system_field_change", "System fields cannot be deleted or changed.") - } - } - - return nil -} - -func (form *CollectionUpsert) checkRule(value any) error { - v, _ := value.(*string) - if v == nil || *v == "" { - return nil // nothing to check - } - - dummy := *form.collection - dummy.Type = form.Type - dummy.Schema = form.Schema - dummy.System = form.System - dummy.Options = form.Options - - r := resolvers.NewRecordFieldResolver(form.dao, &dummy, nil, true) - - _, err := search.FilterData(*v).BuildExpr(r) - if err != nil { - return validation.NewError("validation_invalid_rule", "Invalid filter rule. Raw error: "+err.Error()) - } - - return nil -} - -func (form *CollectionUpsert) checkIndexes(value any) error { - v, _ := value.(types.JsonArray[string]) - - if form.Type == models.CollectionTypeView && len(v) > 0 { - return validation.NewError( - "validation_indexes_not_supported", - "The collection doesn't support indexes.", - ) - } - - for i, rawIndex := range v { - parsed := dbutils.ParseIndex(rawIndex) - - if !parsed.IsValid() { - return validation.Errors{ - strconv.Itoa(i): validation.NewError( - "validation_invalid_index_expression", - "Invalid CREATE INDEX expression.", - ), - } - } - - // note: we don't check the index table because it is always - // overwritten by the daos.SyncRecordTableSchema to allow - // easier partial modifications (eg. changing only the collection name). - // if !strings.EqualFold(parsed.TableName, form.Name) { - // return validation.Errors{ - // strconv.Itoa(i): validation.NewError( - // "validation_invalid_index_table", - // fmt.Sprintf("The index table must be the same as the collection name."), - // ), - // } - // } - } - - return nil -} - -func (form *CollectionUpsert) checkOptions(value any) error { - v, _ := value.(types.JsonMap) - - switch form.Type { - case models.CollectionTypeAuth: - options := models.CollectionAuthOptions{} - if err := decodeOptions(v, &options); err != nil { - return err - } - - // check the generic validations - if err := options.Validate(); err != nil { - return err - } - - // additional form specific validations - if err := form.checkRule(options.ManageRule); err != nil { - return validation.Errors{"manageRule": err} - } - case models.CollectionTypeView: - options := models.CollectionViewOptions{} - if err := decodeOptions(v, &options); err != nil { - return err - } - - // check the generic validations - if err := options.Validate(); err != nil { - return err - } - - // check the query option - if _, err := form.dao.CreateViewSchema(options.Query); err != nil { - return validation.Errors{ - "query": validation.NewError( - "validation_invalid_view_query", - fmt.Sprintf("Invalid query - %s", err.Error()), - ), - } - } - } - - return nil -} - -func decodeOptions(options types.JsonMap, result any) error { - raw, err := options.MarshalJSON() - if err != nil { - return validation.NewError("validation_invalid_options", "Invalid options.") - } - - if err := json.Unmarshal(raw, result); err != nil { - return validation.NewError("validation_invalid_options", "Invalid options.") - } - - return nil -} - -// Submit validates the form and upserts the form's Collection model. -// -// On success the related record table schema will be auto updated. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc[*models.Collection]) error { - if err := form.Validate(); err != nil { - return err - } - - if form.collection.IsNew() { - // type can be set only on create - form.collection.Type = form.Type - - // system flag can be set only on create - form.collection.System = form.System - - // custom insertion id can be set only on create - if form.Id != "" { - form.collection.MarkAsNew() - form.collection.SetId(form.Id) - } - } - - // system collections cannot be renamed - if form.collection.IsNew() || !form.collection.System { - form.collection.Name = form.Name - } - - // view schema is autogenerated on save and cannot have indexes - if !form.collection.IsView() { - form.collection.Schema = form.Schema - - // normalize indexes format - form.collection.Indexes = make(types.JsonArray[string], len(form.Indexes)) - for i, rawIdx := range form.Indexes { - form.collection.Indexes[i] = dbutils.ParseIndex(rawIdx).Build() - } - } - - form.collection.ListRule = form.ListRule - form.collection.ViewRule = form.ViewRule - form.collection.CreateRule = form.CreateRule - form.collection.UpdateRule = form.UpdateRule - form.collection.DeleteRule = form.DeleteRule - form.collection.SetOptions(form.Options) - - return runInterceptors(form.collection, func(collection *models.Collection) error { - return form.dao.SaveCollection(collection) - }, interceptors...) -} diff --git a/forms/collection_upsert_test.go b/forms/collection_upsert_test.go deleted file mode 100644 index 5de00332..00000000 --- a/forms/collection_upsert_test.go +++ /dev/null @@ -1,827 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/dbutils" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/spf13/cast" -) - -func TestNewCollectionUpsert(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection := &models.Collection{} - collection.Name = "test_name" - collection.Type = "test_type" - collection.System = true - listRule := "test_list" - collection.ListRule = &listRule - viewRule := "test_view" - collection.ViewRule = &viewRule - createRule := "test_create" - collection.CreateRule = &createRule - updateRule := "test_update" - collection.UpdateRule = &updateRule - deleteRule := "test_delete" - collection.DeleteRule = &deleteRule - collection.Schema = schema.NewSchema(&schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }) - - form := forms.NewCollectionUpsert(app, collection) - - if form.Name != collection.Name { - t.Errorf("Expected Name %q, got %q", collection.Name, form.Name) - } - - if form.Type != collection.Type { - t.Errorf("Expected Type %q, got %q", collection.Type, form.Type) - } - - if form.System != collection.System { - t.Errorf("Expected System %v, got %v", collection.System, form.System) - } - - if form.ListRule != collection.ListRule { - t.Errorf("Expected ListRule %v, got %v", collection.ListRule, form.ListRule) - } - - if form.ViewRule != collection.ViewRule { - t.Errorf("Expected ViewRule %v, got %v", collection.ViewRule, form.ViewRule) - } - - if form.CreateRule != collection.CreateRule { - t.Errorf("Expected CreateRule %v, got %v", collection.CreateRule, form.CreateRule) - } - - if form.UpdateRule != collection.UpdateRule { - t.Errorf("Expected UpdateRule %v, got %v", collection.UpdateRule, form.UpdateRule) - } - - if form.DeleteRule != collection.DeleteRule { - t.Errorf("Expected DeleteRule %v, got %v", collection.DeleteRule, form.DeleteRule) - } - - // store previous state and modify the collection schema to verify - // that the form.Schema is a deep clone - loadedSchema, _ := collection.Schema.MarshalJSON() - collection.Schema.AddField(&schema.SchemaField{ - Name: "new_field", - Type: schema.FieldTypeBool, - }) - - formSchema, _ := form.Schema.MarshalJSON() - - if string(formSchema) != string(loadedSchema) { - t.Errorf("Expected Schema %v, got %v", string(loadedSchema), string(formSchema)) - } -} - -func TestCollectionUpsertValidateAndSubmit(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - testName string - existingName string - jsonData string - expectedErrors []string - }{ - {"empty create (base)", "", "{}", []string{"name", "schema"}}, - {"empty create (auth)", "", `{"type":"auth"}`, []string{"name"}}, - {"empty create (view)", "", `{"type":"view"}`, []string{"name", "options"}}, - {"empty update", "demo2", "{}", []string{}}, - { - "collection and field with _via_ names", - "", - `{ - "name": "a_via_b", - "schema": [ - {"name":"c_via_d","type":"text"} - ] - }`, - []string{"name", "schema"}, - }, - { - "create failure", - "", - `{ - "name": "test ?!@#$", - "type": "invalid", - "system": true, - "schema": [ - {"name":"","type":"text"} - ], - "listRule": "missing = '123'", - "viewRule": "missing = '123'", - "createRule": "missing = '123'", - "updateRule": "missing = '123'", - "deleteRule": "missing = '123'", - "indexes": ["create index '' on '' ()"] - }`, - []string{"name", "type", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule", "indexes"}, - }, - { - "create failure - existing name", - "", - `{ - "name": "demo1", - "system": true, - "schema": [ - {"name":"test","type":"text"} - ], - "listRule": "test='123'", - "viewRule": "test='123'", - "createRule": "test='123'", - "updateRule": "test='123'", - "deleteRule": "test='123'" - }`, - []string{"name"}, - }, - { - "create failure - existing internal table", - "", - `{ - "name": "_admins", - "schema": [ - {"name":"test","type":"text"} - ] - }`, - []string{"name"}, - }, - { - "create failure - name starting with underscore", - "", - `{ - "name": "_test_new", - "schema": [ - {"name":"test","type":"text"} - ] - }`, - []string{"name"}, - }, - { - "create failure - duplicated field names (case insensitive)", - "", - `{ - "name": "test_new", - "schema": [ - {"name":"test","type":"text"}, - {"name":"tESt","type":"text"} - ] - }`, - []string{"schema"}, - }, - { - "create failure - check auth options validators", - "", - `{ - "name": "test_new", - "type": "auth", - "schema": [ - {"name":"test","type":"text"} - ], - "options": { "minPasswordLength": 3 } - }`, - []string{"options"}, - }, - { - "create failure - check view options validators", - "", - `{ - "name": "test_new", - "type": "view", - "options": { "query": "invalid query" } - }`, - []string{"options"}, - }, - { - "create success", - "", - `{ - "name": "test_new", - "type": "auth", - "system": true, - "schema": [ - {"id":"a123456","name":"test1","type":"text"}, - {"id":"b123456","name":"test2","type":"email"}, - { - "name":"test3", - "type":"relation", - "options":{ - "collectionId":"v851q4r790rhknl", - "displayFields":["name","id","created","updated","username","email","emailVisibility","verified"] - } - } - ], - "listRule": "test1='123' && verified = true", - "viewRule": "test1='123' && emailVisibility = true", - "createRule": "test1='123' && email != ''", - "updateRule": "test1='123' && username != ''", - "deleteRule": "test1='123' && id != ''", - "indexes": ["create index idx_test_new on anything (test1)"] - }`, - []string{}, - }, - { - "update failure - changing field type", - "test_new", - `{ - "schema": [ - {"id":"a123456","name":"test1","type":"url"}, - {"id":"b123456","name":"test2","type":"bool"} - ], - "indexes": ["create index idx_test_new on test_new (test1)", "invalid"] - }`, - []string{"schema", "indexes"}, - }, - { - "update success - rename fields to existing field names (aka. reusing field names)", - "test_new", - `{ - "schema": [ - {"id":"a123456","name":"test2","type":"text"}, - {"id":"b123456","name":"test1","type":"email"} - ] - }`, - []string{}, - }, - { - "update failure - existing name", - "demo2", - `{"name": "demo3"}`, - []string{"name"}, - }, - { - "update failure - changing system collection", - "nologin", - `{ - "name": "update", - "system": false, - "schema": [ - {"id":"koih1lqx","name":"abc","type":"text"} - ], - "listRule": "abc = '123'", - "viewRule": "abc = '123'", - "createRule": "abc = '123'", - "updateRule": "abc = '123'", - "deleteRule": "abc = '123'" - }`, - []string{"name", "system"}, - }, - { - "update failure - changing collection type", - "demo3", - `{ - "type": "auth" - }`, - []string{"type"}, - }, - { - "update failure - changing relation collection", - "users", - `{ - "schema": [ - { - "id": "lkeigvv3", - "name": "rel", - "type": "relation", - "options": { - "collectionId": "wzlqyes4orhoygb", - "cascadeDelete": false, - "maxSelect": 1, - "displayFields": null - } - } - ] - }`, - []string{"schema"}, - }, - { - "update failure - all fields", - "demo2", - `{ - "name": "test ?!@#$", - "type": "invalid", - "system": true, - "schema": [ - {"name":"","type":"text"} - ], - "listRule": "missing = '123'", - "viewRule": "missing = '123'", - "createRule": "missing = '123'", - "updateRule": "missing = '123'", - "deleteRule": "missing = '123'", - "options": {"test": 123}, - "indexes": ["create index '' from demo2 on (id)"] - }`, - []string{"name", "type", "system", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule", "indexes"}, - }, - { - "update success - update all fields", - "clients", - `{ - "name": "demo_update", - "type": "auth", - "schema": [ - {"id":"_2hlxbmp","name":"test","type":"text"} - ], - "listRule": "test='123' && verified = true", - "viewRule": "test='123' && emailVisibility = true", - "createRule": "test='123' && email != ''", - "updateRule": "test='123' && username != ''", - "deleteRule": "test='123' && id != ''", - "options": {"minPasswordLength": 10}, - "indexes": [ - "create index idx_clients_test1 on anything (id, email, test)", - "create unique index idx_clients_test2 on clients (id, username, email)" - ] - }`, - []string{}, - }, - // (fail due to filters old field references) - { - "update failure - rename the schema field of the last updated collection", - "demo_update", - `{ - "schema": [ - {"id":"_2hlxbmp","name":"test_renamed","type":"text"} - ] - }`, - []string{"listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, - }, - // (cleared filter references) - { - "update success - rename the schema field of the last updated collection", - "demo_update", - `{ - "schema": [ - {"id":"_2hlxbmp","name":"test_renamed","type":"text"} - ], - "listRule": null, - "viewRule": null, - "createRule": null, - "updateRule": null, - "deleteRule": null, - "indexes": [] - }`, - []string{}, - }, - { - "update success - system collection", - "nologin", - `{ - "listRule": "name='123'", - "viewRule": "name='123'", - "createRule": "name='123'", - "updateRule": "name='123'", - "deleteRule": "name='123'" - }`, - []string{}, - }, - - // view tests - // ----------------------------------------------------------- - { - "base->view relation", - "", - `{ - "name": "test_view_relation", - "type": "base", - "schema": [ - { - "name": "test", - "type": "relation", - "options":{ - "collectionId": "v9gwnfh02gjq1q0" - } - } - ] - }`, - []string{"schema"}, // not allowed - }, - { - "auth->view relation", - "", - `{ - "name": "test_view_relation", - "type": "auth", - "schema": [ - { - "name": "test", - "type": "relation", - "options": { - "collectionId": "v9gwnfh02gjq1q0" - } - } - ] - }`, - []string{"schema"}, // not allowed - }, - { - "view->view relation", - "", - `{ - "name": "test_view_relation", - "type": "view", - "options": { - "query": "select view1.id, view1.id as rel from view1" - } - }`, - []string{}, // allowed - }, - { - "view create failure", - "", - `{ - "name": "upsert_view", - "type": "view", - "listRule": "id='123' && verified = true", - "viewRule": "id='123' && emailVisibility = true", - "schema": [ - {"id":"abc123","name":"some invalid field name that will be overwritten !@#$","type":"bool"} - ], - "options": { - "query": "select id, email from users; drop table _admins;" - }, - "indexes": ["create index idx_test_view on upsert_view (id)"] - }`, - []string{ - "listRule", - "viewRule", - "options", - "indexes", // views don't have indexes - }, - }, - { - "view create success", - "", - `{ - "name": "upsert_view", - "type": "view", - "listRule": "id='123' && verified = true", - "viewRule": "id='123' && emailVisibility = true", - "schema": [ - {"id":"abc123","name":"some invalid field name that will be overwritten !@#$","type":"bool"} - ], - "options": { - "query": "select id, emailVisibility, verified from users" - } - }`, - []string{ - // "schema", should be overwritten by an autogenerated from the query - }, - }, - { - "view update failure (schema autogeneration and rule fields check)", - "upsert_view", - `{ - "name": "upsert_view_2", - "listRule": "id='456' && verified = true", - "viewRule": "id='456'", - "createRule": "id='123'", - "updateRule": "id='123'", - "deleteRule": "id='123'", - "schema": [ - {"id":"abc123","name":"verified","type":"bool"} - ], - "options": { - "query": "select 1 as id" - } - }`, - []string{ - "listRule", // missing field (ignoring the old or explicit schema) - "createRule", // not allowed - "updateRule", // not allowed - "deleteRule", // not allowed - }, - }, - { - "view update failure (check query identifiers format)", - "upsert_view", - `{ - "listRule": null, - "viewRule": null, - "options": { - "query": "select 1 as id, 2 as [invalid!@#]" - } - }`, - []string{ - "schema", // should fail due to invalid field name - }, - }, - { - "view update success", - "upsert_view", - `{ - "listRule": null, - "viewRule": null, - "options": { - "query": "select 1 as id, 2 as valid" - } - }`, - []string{}, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - collection := &models.Collection{} - if s.existingName != "" { - var err error - collection, err = app.Dao().FindCollectionByNameOrId(s.existingName) - if err != nil { - t.Fatal(err) - } - } - - form := forms.NewCollectionUpsert(app, collection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Fatalf("Failed to load form data: %v", loadErr) - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Collection]) forms.InterceptorNextFunc[*models.Collection] { - return func(c *models.Collection) error { - interceptorCalls++ - return next(c) - } - } - - // parse errors - result := form.Submit(interceptor) - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - } - - // check interceptor calls - expectInterceptorCalls := 1 - if len(s.expectedErrors) > 0 { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Fatalf("Expected interceptor to be called %d, got %d", expectInterceptorCalls, interceptorCalls) - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Fatalf("Missing expected error key %q in %v", k, errs) - } - } - - if len(s.expectedErrors) > 0 { - return - } - - collection, _ = app.Dao().FindCollectionByNameOrId(form.Name) - if collection == nil { - t.Fatalf("Expected to find collection %q, got nil", form.Name) - } - - if form.Name != collection.Name { - t.Fatalf("Expected Name %q, got %q", collection.Name, form.Name) - } - - if form.Type != collection.Type { - t.Fatalf("Expected Type %q, got %q", collection.Type, form.Type) - } - - if form.System != collection.System { - t.Fatalf("Expected System %v, got %v", collection.System, form.System) - } - - if cast.ToString(form.ListRule) != cast.ToString(collection.ListRule) { - t.Fatalf("Expected ListRule %v, got %v", collection.ListRule, form.ListRule) - } - - if cast.ToString(form.ViewRule) != cast.ToString(collection.ViewRule) { - t.Fatalf("Expected ViewRule %v, got %v", collection.ViewRule, form.ViewRule) - } - - if cast.ToString(form.CreateRule) != cast.ToString(collection.CreateRule) { - t.Fatalf("Expected CreateRule %v, got %v", collection.CreateRule, form.CreateRule) - } - - if cast.ToString(form.UpdateRule) != cast.ToString(collection.UpdateRule) { - t.Fatalf("Expected UpdateRule %v, got %v", collection.UpdateRule, form.UpdateRule) - } - - if cast.ToString(form.DeleteRule) != cast.ToString(collection.DeleteRule) { - t.Fatalf("Expected DeleteRule %v, got %v", collection.DeleteRule, form.DeleteRule) - } - - rawFormSchema, _ := form.Schema.MarshalJSON() - rawCollectionSchema, _ := collection.Schema.MarshalJSON() - - if len(form.Schema.Fields()) != len(collection.Schema.Fields()) { - t.Fatalf("Expected Schema \n%v, \ngot \n%v", string(rawCollectionSchema), string(rawFormSchema)) - } - - for _, f := range form.Schema.Fields() { - if collection.Schema.GetFieldByName(f.Name) == nil { - t.Fatalf("Missing field %s \nin \n%v", f.Name, string(rawFormSchema)) - } - } - - // check indexes (if any) - allIndexes, _ := app.Dao().TableIndexes(form.Name) - for _, formIdx := range form.Indexes { - parsed := dbutils.ParseIndex(formIdx) - parsed.TableName = form.Name - normalizedIdx := parsed.Build() - - var exists bool - for _, idx := range allIndexes { - if dbutils.ParseIndex(idx).Build() == normalizedIdx { - exists = true - continue - } - } - - if !exists { - t.Fatalf("Missing index %s \nin \n%v", normalizedIdx, allIndexes) - } - } - }) - } -} - -func TestCollectionUpsertSubmitInterceptors(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo2") - if err != nil { - t.Fatal(err) - } - - form := forms.NewCollectionUpsert(app, collection) - form.Name = "test_new" - - testErr := errors.New("test_error") - interceptorCollectionName := "" - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Collection]) forms.InterceptorNextFunc[*models.Collection] { - return func(c *models.Collection) error { - interceptor1Called = true - return next(c) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Collection]) forms.InterceptorNextFunc[*models.Collection] { - return func(c *models.Collection) error { - interceptorCollectionName = collection.Name // to check if the record was filled - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorCollectionName != form.Name { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} - -func TestCollectionUpsertWithCustomId(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - existingCollection, err := app.Dao().FindCollectionByNameOrId("demo2") - if err != nil { - t.Fatal(err) - } - - newCollection := func() *models.Collection { - return &models.Collection{ - Name: "c_" + security.PseudorandomString(4), - Schema: existingCollection.Schema, - } - } - - scenarios := []struct { - name string - jsonData string - collection *models.Collection - expectError bool - }{ - { - "empty data", - "{}", - newCollection(), - false, - }, - { - "empty id", - `{"id":""}`, - newCollection(), - false, - }, - { - "id < 15 chars", - `{"id":"a23"}`, - newCollection(), - true, - }, - { - "id > 15 chars", - `{"id":"a234567890123456"}`, - newCollection(), - true, - }, - { - "id = 15 chars (invalid chars)", - `{"id":"a@3456789012345"}`, - newCollection(), - true, - }, - { - "id = 15 chars (valid chars)", - `{"id":"a23456789012345"}`, - newCollection(), - false, - }, - { - "changing the id of an existing item", - `{"id":"b23456789012345"}`, - existingCollection, - true, - }, - { - "using the same existing item id", - `{"id":"` + existingCollection.Id + `"}`, - existingCollection, - false, - }, - { - "skipping the id for existing item", - `{}`, - existingCollection, - false, - }, - } - - for _, s := range scenarios { - form := forms.NewCollectionUpsert(app, s.collection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("[%s] Failed to load form data: %v", s.name, loadErr) - continue - } - - submitErr := form.Submit() - hasErr := submitErr != nil - - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", s.name, s.expectError, hasErr, submitErr) - } - - if !hasErr && form.Id != "" { - _, err := app.Dao().FindCollectionByNameOrId(form.Id) - if err != nil { - t.Errorf("[%s] Expected to find record with id %s, got %v", s.name, form.Id, err) - } - } - } -} diff --git a/forms/collections_import.go b/forms/collections_import.go deleted file mode 100644 index bd17833a..00000000 --- a/forms/collections_import.go +++ /dev/null @@ -1,132 +0,0 @@ -package forms - -import ( - "encoding/json" - "fmt" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" -) - -// CollectionsImport is a form model to bulk import -// (create, replace and delete) collections from a user provided list. -type CollectionsImport struct { - app core.App - dao *daos.Dao - - Collections []*models.Collection `form:"collections" json:"collections"` - DeleteMissing bool `form:"deleteMissing" json:"deleteMissing"` -} - -// NewCollectionsImport creates a new [CollectionsImport] form with -// initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewCollectionsImport(app core.App) *CollectionsImport { - return &CollectionsImport{ - app: app, - dao: app.Dao(), - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *CollectionsImport) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *CollectionsImport) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Collections, validation.Required), - ) -} - -// Submit applies the import, aka.: -// - imports the form collections (create or replace) -// - sync the collection changes with their related records table -// - ensures the integrity of the imported structure (aka. run validations for each collection) -// - if [form.DeleteMissing] is set, deletes all local collections that are not found in the imports list -// -// All operations are wrapped in a single transaction that are -// rollbacked on the first encountered error. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc[[]*models.Collection]) error { - if err := form.Validate(); err != nil { - return err - } - - return runInterceptors(form.Collections, func(collections []*models.Collection) error { - return form.dao.RunInTransaction(func(txDao *daos.Dao) error { - importErr := txDao.ImportCollections( - collections, - form.DeleteMissing, - form.afterSync, - ) - if importErr == nil { - return nil - } - - // validation failure - if err, ok := importErr.(validation.Errors); ok { - return err - } - - // generic/db failure - return validation.Errors{"collections": validation.NewError( - "collections_import_failure", - "Failed to import the collections configuration. Raw error:\n"+importErr.Error(), - )} - }) - }, interceptors...) -} - -func (form *CollectionsImport) afterSync(txDao *daos.Dao, mappedNew, mappedOld map[string]*models.Collection) error { - // refresh the actual persisted collections list - refreshedCollections := []*models.Collection{} - if err := txDao.CollectionQuery().OrderBy("updated ASC").All(&refreshedCollections); err != nil { - return err - } - - // trigger the validator for each existing collection to - // ensure that the app is not left in a broken state - for _, collection := range refreshedCollections { - upsertModel := mappedOld[collection.GetId()] - if upsertModel == nil { - upsertModel = collection - } - upsertModel.MarkAsNotNew() - - upsertForm := NewCollectionUpsert(form.app, upsertModel) - upsertForm.SetDao(txDao) - - // load form fields with the refreshed collection state - upsertForm.Id = collection.Id - upsertForm.Type = collection.Type - upsertForm.Name = collection.Name - upsertForm.System = collection.System - upsertForm.ListRule = collection.ListRule - upsertForm.ViewRule = collection.ViewRule - upsertForm.CreateRule = collection.CreateRule - upsertForm.UpdateRule = collection.UpdateRule - upsertForm.DeleteRule = collection.DeleteRule - upsertForm.Schema = collection.Schema - upsertForm.Options = collection.Options - - if err := upsertForm.Validate(); err != nil { - // serialize the validation error(s) - serializedErr, _ := json.MarshalIndent(err, "", " ") - - return validation.Errors{"collections": validation.NewError( - "collections_import_validate_failure", - fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", collection.Name, collection.Id, serializedErr), - )} - } - } - - return nil -} diff --git a/forms/collections_import_test.go b/forms/collections_import_test.go deleted file mode 100644 index 33d15ab8..00000000 --- a/forms/collections_import_test.go +++ /dev/null @@ -1,511 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestCollectionsImportValidate(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - form := forms.NewCollectionsImport(app) - - scenarios := []struct { - collections []*models.Collection - expectError bool - }{ - {nil, true}, - {[]*models.Collection{}, true}, - {[]*models.Collection{{}}, false}, - } - - for i, s := range scenarios { - form.Collections = s.collections - - err := form.Validate() - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} - -func TestCollectionsImportSubmit(t *testing.T) { - t.Parallel() - - totalCollections := 11 - - scenarios := []struct { - name string - jsonData string - expectError bool - expectCollectionsCount int - expectEvents map[string]int - }{ - { - name: "empty collections", - jsonData: `{ - "deleteMissing": true, - "collections": [] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: nil, - }, - { - name: "one of the collections has invalid data", - jsonData: `{ - "collections": [ - { - "name": "import1", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "name": "import 2", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: map[string]int{ - "OnModelBeforeCreate": 2, - }, - }, - { - name: "test empty base collection schema", - jsonData: `{ - "collections": [ - { - "name": "import1" - }, - { - "name": "import2", - "type": "auth" - } - ] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: map[string]int{ - "OnModelBeforeCreate": 2, - }, - }, - { - name: "all imported collections has valid data", - jsonData: `{ - "collections": [ - { - "name": "import1", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "name": "import2", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "name": "import3", - "type": "auth" - } - ] - }`, - expectError: false, - expectCollectionsCount: totalCollections + 3, - expectEvents: map[string]int{ - "OnModelBeforeCreate": 3, - "OnModelAfterCreate": 3, - }, - }, - { - name: "new collection with existing name", - jsonData: `{ - "collections": [ - { - "name": "demo2", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: map[string]int{ - "OnModelBeforeCreate": 1, - }, - }, - { - name: "delete system + modified + new collection", - jsonData: `{ - "deleteMissing": true, - "collections": [ - { - "id":"sz5l5z67tg7gku0", - "name":"demo2", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - }, - { - "name": "import1", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: map[string]int{ - "OnModelBeforeDelete": 1, - }, - }, - { - name: "modified + new collection", - jsonData: `{ - "collections": [ - { - "id":"sz5l5z67tg7gku0", - "name":"demo2_rename", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title_new", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - }, - { - "name": "import1", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "name": "import2", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: false, - expectCollectionsCount: totalCollections + 2, - expectEvents: map[string]int{ - "OnModelBeforeUpdate": 1, - "OnModelAfterUpdate": 1, - "OnModelBeforeCreate": 2, - "OnModelAfterCreate": 2, - }, - }, - { - name: "delete non-system + modified + new collection", - jsonData: `{ - "deleteMissing": true, - "collections": [ - { - "id": "kpv709sk2lqbqk8", - "system": true, - "name": "nologin", - "type": "auth", - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": [], - "manageRule": "@request.auth.collectionName = 'users'", - "minPasswordLength": 8, - "onlyEmailDomains": [], - "requireEmail": true - }, - "listRule": "", - "viewRule": "", - "createRule": "", - "updateRule": "", - "deleteRule": "", - "schema": [ - { - "id": "x8zzktwe", - "name": "name", - "type": "text", - "system": false, - "required": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": "" - } - } - ] - }, - { - "id":"sz5l5z67tg7gku0", - "name":"demo2", - "schema":[ - { - "id":"_2hlxbmp", - "name":"title", - "type":"text", - "system":false, - "required":true, - "unique":false, - "options":{ - "min":3, - "max":null, - "pattern":"" - } - } - ] - }, - { - "id": "test_deleted_collection_name_reuse", - "name": "demo1", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: false, - expectCollectionsCount: 3, - expectEvents: map[string]int{ - "OnModelBeforeUpdate": 2, - "OnModelAfterUpdate": 2, - "OnModelBeforeCreate": 1, - "OnModelAfterCreate": 1, - "OnModelBeforeDelete": totalCollections - 2, - "OnModelAfterDelete": totalCollections - 2, - }, - }, - { - name: "lazy system table name error", - jsonData: `{ - "collections": [ - { - "name": "_admins", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - } - ] - }`, - expectError: true, - expectCollectionsCount: totalCollections, - expectEvents: map[string]int{ - "OnModelBeforeCreate": 1, - }, - }, - { - name: "lazy view evaluation", - jsonData: `{ - "collections": [ - { - "name": "view_before", - "type": "view", - "options": { - "query": "select id, active from base_test" - } - }, - { - "name": "base_test", - "schema": [ - { - "id":"fz6iql2m", - "name":"active", - "type":"bool" - } - ] - }, - { - "name": "view_after_new", - "type": "view", - "options": { - "query": "select id, active from base_test" - } - }, - { - "name": "view_after_old", - "type": "view", - "options": { - "query": "select id from demo1" - } - } - ] - }`, - expectError: false, - expectCollectionsCount: totalCollections + 4, - expectEvents: map[string]int{ - "OnModelBeforeUpdate": 3, - "OnModelAfterUpdate": 3, - "OnModelBeforeCreate": 4, - "OnModelAfterCreate": 4, - }, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - form := forms.NewCollectionsImport(testApp) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Fatalf("Failed to load form data: %v", loadErr) - } - - err := form.Submit() - - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) - } - - // check collections count - collections := []*models.Collection{} - if err := testApp.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - if len(collections) != s.expectCollectionsCount { - t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections)) - } - - // check events - if len(testApp.EventCalls) > len(s.expectEvents) { - t.Fatalf("Expected events %v, got %v", s.expectEvents, testApp.EventCalls) - } - for event, expectedCalls := range s.expectEvents { - actualCalls := testApp.EventCalls[event] - if actualCalls != expectedCalls { - t.Fatalf("Expected event %s to be called %d, got %d", event, expectedCalls, actualCalls) - } - } - }) - } -} - -func TestCollectionsImportSubmitInterceptors(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collections := []*models.Collection{} - if err := app.Dao().CollectionQuery().All(&collections); err != nil { - t.Fatal(err) - } - - form := forms.NewCollectionsImport(app) - form.Collections = collections - - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[[]*models.Collection]) forms.InterceptorNextFunc[[]*models.Collection] { - return func(imports []*models.Collection) error { - interceptor1Called = true - return next(imports) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[[]*models.Collection]) forms.InterceptorNextFunc[[]*models.Collection] { - return func(imports []*models.Collection) error { - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } -} diff --git a/forms/realtime_subscribe.go b/forms/realtime_subscribe.go deleted file mode 100644 index fc852fc8..00000000 --- a/forms/realtime_subscribe.go +++ /dev/null @@ -1,23 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" -) - -// RealtimeSubscribe is a realtime subscriptions request form. -type RealtimeSubscribe struct { - ClientId string `form:"clientId" json:"clientId"` - Subscriptions []string `form:"subscriptions" json:"subscriptions"` -} - -// NewRealtimeSubscribe creates new RealtimeSubscribe request form. -func NewRealtimeSubscribe() *RealtimeSubscribe { - return &RealtimeSubscribe{} -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RealtimeSubscribe) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)), - ) -} diff --git a/forms/realtime_subscribe_test.go b/forms/realtime_subscribe_test.go deleted file mode 100644 index d4f8b1e7..00000000 --- a/forms/realtime_subscribe_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package forms_test - -import ( - "strings" - "testing" - - "github.com/pocketbase/pocketbase/forms" -) - -func TestRealtimeSubscribeValidate(t *testing.T) { - t.Parallel() - - scenarios := []struct { - clientId string - expectError bool - }{ - {"", true}, - {strings.Repeat("a", 256), true}, - {"test", false}, - } - - for i, s := range scenarios { - form := forms.NewRealtimeSubscribe() - form.ClientId = s.clientId - - err := form.Validate() - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} diff --git a/forms/record_email_change_confirm.go b/forms/record_email_change_confirm.go deleted file mode 100644 index 79da8b92..00000000 --- a/forms/record_email_change_confirm.go +++ /dev/null @@ -1,145 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/security" -) - -// RecordEmailChangeConfirm is an auth record email change confirmation form. -type RecordEmailChangeConfirm struct { - app core.App - dao *daos.Dao - collection *models.Collection - - Token string `form:"token" json:"token"` - Password string `form:"password" json:"password"` -} - -// NewRecordEmailChangeConfirm creates a new [RecordEmailChangeConfirm] form -// initialized with from the provided [core.App] and [models.Collection] instances. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordEmailChangeConfirm(app core.App, collection *models.Collection) *RecordEmailChangeConfirm { - return &RecordEmailChangeConfirm{ - app: app, - dao: app.Dao(), - collection: collection, - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordEmailChangeConfirm) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordEmailChangeConfirm) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Token, - validation.Required, - validation.By(form.checkToken), - ), - validation.Field( - &form.Password, - validation.Required, - validation.Length(1, 100), - validation.By(form.checkPassword), - ), - ) -} - -func (form *RecordEmailChangeConfirm) checkToken(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - authRecord, _, err := form.parseToken(v) - if err != nil { - return err - } - - if authRecord.Collection().Id != form.collection.Id { - return validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") - } - - return nil -} - -func (form *RecordEmailChangeConfirm) checkPassword(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - authRecord, _, _ := form.parseToken(form.Token) - if authRecord == nil || !authRecord.ValidatePassword(v) { - return validation.NewError("validation_invalid_password", "Missing or invalid auth record password.") - } - - return nil -} - -func (form *RecordEmailChangeConfirm) parseToken(token string) (*models.Record, string, error) { - // check token payload - claims, _ := security.ParseUnverifiedJWT(token) - newEmail, _ := claims["newEmail"].(string) - if newEmail == "" { - return nil, "", validation.NewError("validation_invalid_token_payload", "Invalid token payload - newEmail must be set.") - } - - // ensure that there aren't other users with the new email - if !form.dao.IsRecordValueUnique(form.collection.Id, schema.FieldNameEmail, newEmail) { - return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail) - } - - // verify that the token is not expired and its signature is valid - authRecord, err := form.dao.FindAuthRecordByToken( - token, - form.app.Settings().RecordEmailChangeToken.Secret, - ) - if err != nil || authRecord == nil { - return nil, "", validation.NewError("validation_invalid_token", "Invalid or expired token.") - } - - return authRecord, newEmail, nil -} - -// Submit validates and submits the auth record email change confirmation form. -// On success returns the updated auth record associated to `form.Token`. -// -// You can optionally provide a list of InterceptorFunc to -// further modify the form behavior before persisting it. -func (form *RecordEmailChangeConfirm) Submit(interceptors ...InterceptorFunc[*models.Record]) (*models.Record, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - authRecord, newEmail, err := form.parseToken(form.Token) - if err != nil { - return nil, err - } - - authRecord.SetEmail(newEmail) - authRecord.SetVerified(true) - - // @todo consider removing if not necessary anymore - authRecord.RefreshTokenKey() // invalidate old tokens - - interceptorsErr := runInterceptors(authRecord, func(m *models.Record) error { - authRecord = m - return form.dao.SaveRecord(m) - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return authRecord, nil -} diff --git a/forms/record_email_change_confirm_test.go b/forms/record_email_change_confirm_test.go deleted file mode 100644 index 90ca6f86..00000000 --- a/forms/record_email_change_confirm_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestRecordEmailChangeConfirmValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectedErrors []string - }{ - // empty payload - {"{}", []string{"token", "password"}}, - // empty data - { - `{"token": "", "password": ""}`, - []string{"token", "password"}, - }, - // invalid token payload - { - `{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.quDgaCi2rGTRx3qO06CrFvHdeCua_5J7CCVWSaFhkus", - "password": "123456" - }`, - []string{"token", "password"}, - }, - // expired token - { - `{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MTYwOTQ1NTY2MX0.n1OJXJEACMNPT9aMTO48cVJexIiZEtHsz4UNBIfMcf4", - "password": "123456" - }`, - []string{"token", "password"}, - }, - // existing new email - { - `{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4NTI2MX0.Q_o6zpc2URggTU0mWv2CS0rIPbQhFdmrjZ-ASwHh1Ww", - "password": "1234567890" - }`, - []string{"token", "password"}, - }, - // wrong confirmation password - { - `{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4NTI2MX0.hmR7Ye23C68tS1LgHgYgT7NBJczTad34kzcT4sqW3FY", - "password": "123456" - }`, - []string{"password"}, - }, - // valid data - { - `{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4NTI2MX0.hmR7Ye23C68tS1LgHgYgT7NBJczTad34kzcT4sqW3FY", - "password": "1234567890" - }`, - []string{}, - }, - } - - for i, s := range scenarios { - form := forms.NewRecordEmailChangeConfirm(testApp, authCollection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - record, err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if len(s.expectedErrors) > 0 { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - // parse errors - errs, ok := err.(validation.Errors) - if !ok && err != nil { - t.Errorf("(%d) Failed to parse errors %v", i, err) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - } - } - - if len(errs) > 0 { - continue - } - - claims, _ := security.ParseUnverifiedJWT(form.Token) - newEmail, _ := claims["newEmail"].(string) - - // check whether the user was updated - // --- - if record.Email() != newEmail { - t.Errorf("(%d) Expected record email %q, got %q", i, newEmail, record.Email()) - } - - if !record.Verified() { - t.Errorf("(%d) Expected record to be verified, got false", i) - } - - // shouldn't validate second time due to refreshed record token - if err := form.Validate(); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestRecordEmailChangeConfirmInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordEmailChangeConfirm(testApp, authCollection) - form.Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MjIwODk4NTI2MX0.hmR7Ye23C68tS1LgHgYgT7NBJczTad34kzcT4sqW3FY" - form.Password = "1234567890" - interceptorEmail := authRecord.Email() - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorEmail = record.Email() - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorEmail == authRecord.Email() { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} diff --git a/forms/record_email_change_request.go b/forms/record_email_change_request.go deleted file mode 100644 index f849290a..00000000 --- a/forms/record_email_change_request.go +++ /dev/null @@ -1,75 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// RecordEmailChangeRequest is an auth record email change request form. -type RecordEmailChangeRequest struct { - app core.App - dao *daos.Dao - record *models.Record - - NewEmail string `form:"newEmail" json:"newEmail"` -} - -// NewRecordEmailChangeRequest creates a new [RecordEmailChangeRequest] form -// initialized with from the provided [core.App] and [models.Record] instances. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordEmailChangeRequest(app core.App, record *models.Record) *RecordEmailChangeRequest { - return &RecordEmailChangeRequest{ - app: app, - dao: app.Dao(), - record: record, - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordEmailChangeRequest) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordEmailChangeRequest) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.NewEmail, - validation.Required, - validation.Length(1, 255), - is.EmailFormat, - validation.By(form.checkUniqueEmail), - ), - ) -} - -func (form *RecordEmailChangeRequest) checkUniqueEmail(value any) error { - v, _ := value.(string) - - if !form.dao.IsRecordValueUnique(form.record.Collection().Id, schema.FieldNameEmail, v) { - return validation.NewError("validation_record_email_invalid", "User email already exists or it is invalid.") - } - - return nil -} - -// Submit validates and sends the change email request. -// -// You can optionally provide a list of InterceptorFunc to -// further modify the form behavior before persisting it. -func (form *RecordEmailChangeRequest) Submit(interceptors ...InterceptorFunc[*models.Record]) error { - if err := form.Validate(); err != nil { - return err - } - - return runInterceptors(form.record, func(m *models.Record) error { - return mails.SendRecordChangeEmail(form.app, m, form.NewEmail) - }, interceptors...) -} diff --git a/forms/record_email_change_request_test.go b/forms/record_email_change_request_test.go deleted file mode 100644 index c6e8e9d3..00000000 --- a/forms/record_email_change_request_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestRecordEmailChangeRequestValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - user, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectedErrors []string - }{ - // empty payload - {"{}", []string{"newEmail"}}, - // empty data - { - `{"newEmail": ""}`, - []string{"newEmail"}, - }, - // invalid email - { - `{"newEmail": "invalid"}`, - []string{"newEmail"}, - }, - // existing email token - { - `{"newEmail": "test2@example.com"}`, - []string{"newEmail"}, - }, - // valid new email - { - `{"newEmail": "test_new@example.com"}`, - []string{}, - }, - } - - for i, s := range scenarios { - testApp.TestMailer.TotalSend = 0 // reset - form := forms.NewRecordEmailChangeRequest(testApp, user) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if len(s.expectedErrors) > 0 { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - // parse errors - errs, ok := err.(validation.Errors) - if !ok && err != nil { - t.Errorf("(%d) Failed to parse errors %v", i, err) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - } - } - - expectedMails := 1 - if len(s.expectedErrors) > 0 { - expectedMails = 0 - } - if testApp.TestMailer.TotalSend != expectedMails { - t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) - } - } -} - -func TestRecordEmailChangeRequestInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordEmailChangeRequest(testApp, authRecord) - form.NewEmail = "test_new@example.com" - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } -} diff --git a/forms/record_oauth2_login.go b/forms/record_oauth2_login.go deleted file mode 100644 index 6747e1ba..00000000 --- a/forms/record_oauth2_login.go +++ /dev/null @@ -1,294 +0,0 @@ -package forms - -import ( - "context" - "errors" - "fmt" - "time" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/auth" - "github.com/pocketbase/pocketbase/tools/security" - "golang.org/x/oauth2" -) - -// RecordOAuth2LoginData defines the OA -type RecordOAuth2LoginData struct { - ExternalAuth *models.ExternalAuth - Record *models.Record - OAuth2User *auth.AuthUser - ProviderClient auth.Provider -} - -// BeforeOAuth2RecordCreateFunc defines a callback function that will -// be called before OAuth2 new Record creation. -type BeforeOAuth2RecordCreateFunc func(createForm *RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error - -// RecordOAuth2Login is an auth record OAuth2 login form. -type RecordOAuth2Login struct { - app core.App - dao *daos.Dao - collection *models.Collection - - beforeOAuth2RecordCreateFunc BeforeOAuth2RecordCreateFunc - - // Optional auth record that will be used if no external - // auth relation is found (if it is from the same collection) - loggedAuthRecord *models.Record - - // The name of the OAuth2 client provider (eg. "google") - Provider string `form:"provider" json:"provider"` - - // The authorization code returned from the initial request. - Code string `form:"code" json:"code"` - - // The optional PKCE code verifier as part of the code_challenge sent with the initial request. - CodeVerifier string `form:"codeVerifier" json:"codeVerifier"` - - // The redirect url sent with the initial request. - RedirectUrl string `form:"redirectUrl" json:"redirectUrl"` - - // Additional data that will be used for creating a new auth record - // if an existing OAuth2 account doesn't exist. - CreateData map[string]any `form:"createData" json:"createData"` -} - -// NewRecordOAuth2Login creates a new [RecordOAuth2Login] form with -// initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordOAuth2Login(app core.App, collection *models.Collection, optAuthRecord *models.Record) *RecordOAuth2Login { - form := &RecordOAuth2Login{ - app: app, - dao: app.Dao(), - collection: collection, - loggedAuthRecord: optAuthRecord, - } - - return form -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordOAuth2Login) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// SetBeforeNewRecordCreateFunc sets a before OAuth2 record create callback handler. -func (form *RecordOAuth2Login) SetBeforeNewRecordCreateFunc(f BeforeOAuth2RecordCreateFunc) { - form.beforeOAuth2RecordCreateFunc = f -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordOAuth2Login) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Provider, validation.Required, validation.By(form.checkProviderName)), - validation.Field(&form.Code, validation.Required), - validation.Field(&form.RedirectUrl, validation.Required), - ) -} - -func (form *RecordOAuth2Login) checkProviderName(value any) error { - name, _ := value.(string) - - config, ok := form.app.Settings().NamedAuthProviderConfigs()[name] - if !ok || !config.Enabled { - return validation.NewError("validation_invalid_provider", fmt.Sprintf("%q is missing or is not enabled.", name)) - } - - return nil -} - -// Submit validates and submits the form. -// -// If an auth record doesn't exist, it will make an attempt to create it -// based on the fetched OAuth2 profile data via a local [RecordUpsert] form. -// You can intercept/modify the Record create form with [form.SetBeforeNewRecordCreateFunc()]. -// -// You can also optionally provide a list of InterceptorFunc to -// further modify the form behavior before persisting it. -// -// On success returns the authorized record model and the fetched provider's data. -func (form *RecordOAuth2Login) Submit( - interceptors ...InterceptorFunc[*RecordOAuth2LoginData], -) (*models.Record, *auth.AuthUser, error) { - if err := form.Validate(); err != nil { - return nil, nil, err - } - - if !form.collection.AuthOptions().AllowOAuth2Auth { - return nil, nil, errors.New("OAuth2 authentication is not allowed for the auth collection.") - } - - provider, err := auth.NewProviderByName(form.Provider) - if err != nil { - return nil, nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - provider.SetContext(ctx) - - // load provider configuration - providerConfig := form.app.Settings().NamedAuthProviderConfigs()[form.Provider] - if err := providerConfig.SetupProvider(provider); err != nil { - return nil, nil, err - } - - provider.SetRedirectUrl(form.RedirectUrl) - - var opts []oauth2.AuthCodeOption - - if provider.PKCE() { - opts = append(opts, oauth2.SetAuthURLParam("code_verifier", form.CodeVerifier)) - } - - // fetch token - token, err := provider.FetchToken(form.Code, opts...) - if err != nil { - return nil, nil, err - } - - // fetch external auth user - authUser, err := provider.FetchAuthUser(token) - if err != nil { - return nil, nil, err - } - - var authRecord *models.Record - - // check for existing relation with the auth record - rel, _ := form.dao.FindFirstExternalAuthByExpr(dbx.HashExp{ - "collectionId": form.collection.Id, - "provider": form.Provider, - "providerId": authUser.Id, - }) - switch { - case rel != nil: - authRecord, err = form.dao.FindRecordById(form.collection.Id, rel.RecordId) - if err != nil { - return nil, authUser, err - } - case form.loggedAuthRecord != nil && form.loggedAuthRecord.Collection().Id == form.collection.Id: - // fallback to the logged auth record (if any) - authRecord = form.loggedAuthRecord - case authUser.Email != "": - // look for an existing auth record by the external auth record's email - authRecord, _ = form.dao.FindAuthRecordByEmail(form.collection.Id, authUser.Email) - } - - interceptorData := &RecordOAuth2LoginData{ - ExternalAuth: rel, - Record: authRecord, - OAuth2User: authUser, - ProviderClient: provider, - } - - interceptorsErr := runInterceptors(interceptorData, func(newData *RecordOAuth2LoginData) error { - return form.submit(newData) - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorData.OAuth2User, interceptorsErr - } - - return interceptorData.Record, interceptorData.OAuth2User, nil -} - -func (form *RecordOAuth2Login) submit(data *RecordOAuth2LoginData) error { - return form.dao.RunInTransaction(func(txDao *daos.Dao) error { - if data.Record == nil { - data.Record = models.NewRecord(form.collection) - data.Record.RefreshId() - data.Record.MarkAsNew() - createForm := NewRecordUpsert(form.app, data.Record) - createForm.SetFullManageAccess(true) - createForm.SetDao(txDao) - if data.OAuth2User.Username != "" && - len(data.OAuth2User.Username) >= 3 && - len(data.OAuth2User.Username) <= 150 && - usernameRegex.MatchString(data.OAuth2User.Username) { - createForm.Username = form.dao.SuggestUniqueAuthRecordUsername( - form.collection.Id, - data.OAuth2User.Username, - ) - } - - // load custom data - createForm.LoadData(form.CreateData) - - // load the OAuth2 user data - createForm.Email = data.OAuth2User.Email - createForm.Verified = true // mark as verified as long as it matches the OAuth2 data (even if the email is empty) - - // generate a random password if not explicitly set - if createForm.Password == "" { - createForm.Password = security.RandomString(30) - createForm.PasswordConfirm = createForm.Password - } - - if form.beforeOAuth2RecordCreateFunc != nil { - if err := form.beforeOAuth2RecordCreateFunc(createForm, data.Record, data.OAuth2User); err != nil { - return err - } - } - - // create the new auth record - if err := createForm.Submit(); err != nil { - return err - } - } else { - isLoggedAuthRecord := form.loggedAuthRecord != nil && - form.loggedAuthRecord.Id == data.Record.Id && - form.loggedAuthRecord.Collection().Id == data.Record.Collection().Id - - // set random password for users with unverified email - // (this is in case a malicious actor has registered via password using the user email) - if !isLoggedAuthRecord && data.Record.Email() != "" && !data.Record.Verified() { - data.Record.SetPassword(security.RandomString(30)) - if err := txDao.SaveRecord(data.Record); err != nil { - return err - } - } - - // update the existing auth record empty email if the data.OAuth2User has one - // (this is in case previously the auth record was created - // with an OAuth2 provider that didn't return an email address) - if data.Record.Email() == "" && data.OAuth2User.Email != "" { - data.Record.SetEmail(data.OAuth2User.Email) - if err := txDao.SaveRecord(data.Record); err != nil { - return err - } - } - - // update the existing auth record verified state - // (only if the auth record doesn't have an email or the auth record email match with the one in data.OAuth2User) - if !data.Record.Verified() && (data.Record.Email() == "" || data.Record.Email() == data.OAuth2User.Email) { - data.Record.SetVerified(true) - if err := txDao.SaveRecord(data.Record); err != nil { - return err - } - } - } - - // create ExternalAuth relation if missing - if data.ExternalAuth == nil { - data.ExternalAuth = &models.ExternalAuth{ - CollectionId: data.Record.Collection().Id, - RecordId: data.Record.Id, - Provider: form.Provider, - ProviderId: data.OAuth2User.Id, - } - if err := txDao.SaveExternalAuth(data.ExternalAuth); err != nil { - return err - } - } - - return nil - }) -} diff --git a/forms/record_oauth2_login_test.go b/forms/record_oauth2_login_test.go deleted file mode 100644 index 5a4c0744..00000000 --- a/forms/record_oauth2_login_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/tests" -) - -func TestUserOauth2LoginValidate(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - testName string - collectionName string - jsonData string - expectedErrors []string - }{ - { - "empty payload", - "users", - "{}", - []string{"provider", "code", "redirectUrl"}, - }, - { - "empty data", - "users", - `{"provider":"","code":"","codeVerifier":"","redirectUrl":""}`, - []string{"provider", "code", "redirectUrl"}, - }, - { - "missing provider", - "users", - `{"provider":"missing","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, - []string{"provider"}, - }, - { - "disabled provider", - "users", - `{"provider":"github","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, - []string{"provider"}, - }, - { - "enabled provider", - "users", - `{"provider":"gitlab","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, - []string{}, - }, - { - "[#3689] any redirectUrl value", - "users", - `{"provider":"gitlab","code":"123","codeVerifier":"123","redirectUrl":"something"}`, - []string{}, - }, - } - - for _, s := range scenarios { - authCollection, _ := app.Dao().FindCollectionByNameOrId(s.collectionName) - if authCollection == nil { - t.Errorf("[%s] Failed to fetch auth collection", s.testName) - } - - form := forms.NewRecordOAuth2Login(app, authCollection, nil) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("[%s] Failed to load form data: %v", s.testName, loadErr) - continue - } - - err := form.Validate() - - // parse errors - errs, ok := err.(validation.Errors) - if !ok && err != nil { - t.Errorf("[%s] Failed to parse errors %v", s.testName, err) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", s.testName, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", s.testName, k, errs) - } - } - } -} - -// @todo consider mocking a Oauth2 provider to test Submit diff --git a/forms/record_password_login.go b/forms/record_password_login.go deleted file mode 100644 index 85f8caae..00000000 --- a/forms/record_password_login.go +++ /dev/null @@ -1,95 +0,0 @@ -package forms - -import ( - "database/sql" - "errors" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" -) - -// RecordPasswordLogin is record username/email + password login form. -type RecordPasswordLogin struct { - app core.App - dao *daos.Dao - collection *models.Collection - - Identity string `form:"identity" json:"identity"` - Password string `form:"password" json:"password"` -} - -// NewRecordPasswordLogin creates a new [RecordPasswordLogin] form initialized -// with from the provided [core.App] and [models.Collection] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordPasswordLogin(app core.App, collection *models.Collection) *RecordPasswordLogin { - return &RecordPasswordLogin{ - app: app, - dao: app.Dao(), - collection: collection, - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordPasswordLogin) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordPasswordLogin) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Identity, validation.Required, validation.Length(1, 255)), - validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), - ) -} - -// Submit validates and submits the form. -// On success returns the authorized record model. -// -// You can optionally provide a list of InterceptorFunc to -// further modify the form behavior before persisting it. -func (form *RecordPasswordLogin) Submit(interceptors ...InterceptorFunc[*models.Record]) (*models.Record, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - authOptions := form.collection.AuthOptions() - - var authRecord *models.Record - var fetchErr error - - isEmail := is.EmailFormat.Validate(form.Identity) == nil - - if isEmail { - if authOptions.AllowEmailAuth { - authRecord, fetchErr = form.dao.FindAuthRecordByEmail(form.collection.Id, form.Identity) - } - } else if authOptions.AllowUsernameAuth { - authRecord, fetchErr = form.dao.FindAuthRecordByUsername(form.collection.Id, form.Identity) - } - - // ignore not found errors to allow custom fetch implementations - if fetchErr != nil && !errors.Is(fetchErr, sql.ErrNoRows) { - return nil, fetchErr - } - - interceptorsErr := runInterceptors(authRecord, func(m *models.Record) error { - authRecord = m - - if authRecord == nil || !authRecord.ValidatePassword(form.Password) { - return errors.New("Invalid login credentials.") - } - - return nil - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return authRecord, nil -} diff --git a/forms/record_password_login_test.go b/forms/record_password_login_test.go deleted file mode 100644 index 3892dc48..00000000 --- a/forms/record_password_login_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package forms_test - -import ( - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" -) - -func TestRecordPasswordLoginValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - scenarios := []struct { - testName string - collectionName string - identity string - password string - expectError bool - }{ - { - "empty data", - "users", - "", - "", - true, - }, - - // username - { - "existing username + wrong password", - "users", - "users75657", - "invalid", - true, - }, - { - "missing username + valid password", - "users", - "clients57772", // not in the "users" collection - "1234567890", - true, - }, - { - "existing username + valid password but in restricted username auth collection", - "clients", - "clients57772", - "1234567890", - true, - }, - { - "existing username + valid password but in restricted username and email auth collection", - "nologin", - "test_username", - "1234567890", - true, - }, - { - "existing username + valid password", - "users", - "users75657", - "1234567890", - false, - }, - - // email - { - "existing email + wrong password", - "users", - "test@example.com", - "invalid", - true, - }, - { - "missing email + valid password", - "users", - "test_missing@example.com", - "1234567890", - true, - }, - { - "existing username + valid password but in restricted username auth collection", - "clients", - "test@example.com", - "1234567890", - false, - }, - { - "existing username + valid password but in restricted username and email auth collection", - "nologin", - "test@example.com", - "1234567890", - true, - }, - { - "existing email + valid password", - "users", - "test@example.com", - "1234567890", - false, - }, - } - - for _, s := range scenarios { - authCollection, err := testApp.Dao().FindCollectionByNameOrId(s.collectionName) - if err != nil { - t.Errorf("[%s] Failed to fetch auth collection: %v", s.testName, err) - } - - form := forms.NewRecordPasswordLogin(testApp, authCollection) - form.Identity = s.identity - form.Password = s.password - - record, err := form.Submit() - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", s.testName, s.expectError, hasErr, err) - continue - } - - if hasErr { - continue - } - - if record.Email() != s.identity && record.Username() != s.identity { - t.Errorf("[%s] Expected record with identity %q, got \n%v", s.testName, s.identity, record) - } - } -} - -func TestRecordPasswordLoginInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordPasswordLogin(testApp, authCollection) - form.Identity = "test@example.com" - form.Password = "123456" - var interceptorRecord *models.Record - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorRecord = record - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorRecord == nil || interceptorRecord.Email() != form.Identity { - t.Fatalf("Expected auth Record model with email %s, got %v", form.Identity, interceptorRecord) - } -} diff --git a/forms/record_password_reset_confirm.go b/forms/record_password_reset_confirm.go deleted file mode 100644 index 370322cd..00000000 --- a/forms/record_password_reset_confirm.go +++ /dev/null @@ -1,118 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/spf13/cast" -) - -// RecordPasswordResetConfirm is an auth record password reset confirmation form. -type RecordPasswordResetConfirm struct { - app core.App - collection *models.Collection - dao *daos.Dao - - Token string `form:"token" json:"token"` - Password string `form:"password" json:"password"` - PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` -} - -// NewRecordPasswordResetConfirm creates a new [RecordPasswordResetConfirm] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordPasswordResetConfirm(app core.App, collection *models.Collection) *RecordPasswordResetConfirm { - return &RecordPasswordResetConfirm{ - app: app, - dao: app.Dao(), - collection: collection, - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordPasswordResetConfirm) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordPasswordResetConfirm) Validate() error { - minPasswordLength := form.collection.AuthOptions().MinPasswordLength - - return validation.ValidateStruct(form, - validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), - validation.Field(&form.Password, validation.Required, validation.Length(minPasswordLength, 100)), - validation.Field(&form.PasswordConfirm, validation.Required, validation.By(validators.Compare(form.Password))), - ) -} - -func (form *RecordPasswordResetConfirm) checkToken(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - record, err := form.dao.FindAuthRecordByToken( - v, - form.app.Settings().RecordPasswordResetToken.Secret, - ) - if err != nil || record == nil { - return validation.NewError("validation_invalid_token", "Invalid or expired token.") - } - - if record.Collection().Id != form.collection.Id { - return validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") - } - - return nil -} - -// Submit validates and submits the form. -// On success returns the updated auth record associated to `form.Token`. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *RecordPasswordResetConfirm) Submit(interceptors ...InterceptorFunc[*models.Record]) (*models.Record, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - authRecord, err := form.dao.FindAuthRecordByToken( - form.Token, - form.app.Settings().RecordPasswordResetToken.Secret, - ) - if err != nil { - return nil, err - } - - if err := authRecord.SetPassword(form.Password); err != nil { - return nil, err - } - - if !authRecord.Verified() { - payload, err := security.ParseUnverifiedJWT(form.Token) - if err != nil { - return nil, err - } - - // mark as verified if the email hasn't changed - if authRecord.Email() == cast.ToString(payload["email"]) { - authRecord.SetVerified(true) - } - } - - interceptorsErr := runInterceptors(authRecord, func(m *models.Record) error { - authRecord = m - return form.dao.SaveRecord(authRecord) - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return authRecord, nil -} diff --git a/forms/record_password_reset_confirm_test.go b/forms/record_password_reset_confirm_test.go deleted file mode 100644 index 18fb7a6d..00000000 --- a/forms/record_password_reset_confirm_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestRecordPasswordResetConfirmValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectedErrors []string - }{ - // empty data (Validate call check) - { - `{}`, - []string{"token", "password", "passwordConfirm"}, - }, - // expired token - { - `{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoxNjQwOTkxNjYxfQ.TayHoXkOTM0w8InkBEb86npMJEaf6YVUrxrRmMgFjeY", - "password":"12345678", - "passwordConfirm":"12345678" - }`, - []string{"token"}, - }, - // valid token but invalid passwords lengths - { - `{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"1234567", - "passwordConfirm":"1234567" - }`, - []string{"password"}, - }, - // valid token but mismatched passwordConfirm - { - `{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345679" - }`, - []string{"passwordConfirm"}, - }, - // valid token and password - { - `{ - "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg", - "password":"12345678", - "passwordConfirm":"12345678" - }`, - []string{}, - }, - } - - for i, s := range scenarios { - form := forms.NewRecordPasswordResetConfirm(testApp, authCollection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - record, submitErr := form.Submit(interceptor) - - // parse errors - errs, ok := submitErr.(validation.Errors) - if !ok && submitErr != nil { - t.Errorf("(%d) Failed to parse errors %v", i, submitErr) - continue - } - - // check interceptor calls - expectInterceptorCalls := 1 - if len(s.expectedErrors) > 0 { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - } - } - - if len(errs) > 0 || len(s.expectedErrors) > 0 { - continue - } - - claims, _ := security.ParseUnverifiedJWT(form.Token) - tokenRecordId := claims["id"] - - if record.Id != tokenRecordId { - t.Errorf("(%d) Expected record with id %s, got %v", i, tokenRecordId, record) - } - - if !record.LastResetSentAt().IsZero() { - t.Errorf("(%d) Expected record.LastResetSentAt to be empty, got %v", i, record.LastResetSentAt()) - } - - if !record.ValidatePassword(form.Password) { - t.Errorf("(%d) Expected the record password to have been updated to %q", i, form.Password) - } - } -} - -func TestRecordPasswordResetConfirmInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordPasswordResetConfirm(testApp, authCollection) - form.Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.R_4FOSUHIuJQ5Crl3PpIPCXMsoHzuTaNlccpXg_3FOg" - form.Password = "1234567890" - form.PasswordConfirm = "1234567890" - interceptorTokenKey := authRecord.TokenKey() - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorTokenKey = record.TokenKey() - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorTokenKey == authRecord.TokenKey() { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} diff --git a/forms/record_password_reset_request.go b/forms/record_password_reset_request.go deleted file mode 100644 index 0abda397..00000000 --- a/forms/record_password_reset_request.go +++ /dev/null @@ -1,92 +0,0 @@ -package forms - -import ( - "errors" - "fmt" - "time" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" -) - -// RecordPasswordResetRequest is an auth record reset password request form. -type RecordPasswordResetRequest struct { - app core.App - dao *daos.Dao - collection *models.Collection - resendThreshold float64 // in seconds - - Email string `form:"email" json:"email"` -} - -// NewRecordPasswordResetRequest creates a new [RecordPasswordResetRequest] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordPasswordResetRequest(app core.App, collection *models.Collection) *RecordPasswordResetRequest { - return &RecordPasswordResetRequest{ - app: app, - dao: app.Dao(), - collection: collection, - resendThreshold: 120, // 2 min - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordPasswordResetRequest) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -// -// This method doesn't check whether auth record with `form.Email` exists (this is done on Submit). -func (form *RecordPasswordResetRequest) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Email, - validation.Required, - validation.Length(1, 255), - is.EmailFormat, - ), - ) -} - -// Submit validates and submits the form. -// On success, sends a password reset email to the `form.Email` auth record. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *RecordPasswordResetRequest) Submit(interceptors ...InterceptorFunc[*models.Record]) error { - if err := form.Validate(); err != nil { - return err - } - - authRecord, err := form.dao.FindAuthRecordByEmail(form.collection.Id, form.Email) - if err != nil { - return fmt.Errorf("Failed to fetch %s record with email %s: %w", form.collection.Id, form.Email, err) - } - - now := time.Now().UTC() - lastResetSentAt := authRecord.LastResetSentAt().Time() - if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { - return errors.New("You've already requested a password reset.") - } - - return runInterceptors(authRecord, func(m *models.Record) error { - if err := mails.SendRecordPasswordReset(form.app, m); err != nil { - return err - } - - // update last sent timestamp - m.Set(schema.FieldNameLastResetSentAt, types.NowDateTime()) - - return form.dao.SaveRecord(m) - }, interceptors...) -} diff --git a/forms/record_password_reset_request_test.go b/forms/record_password_reset_request_test.go deleted file mode 100644 index 2dc52052..00000000 --- a/forms/record_password_reset_request_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - "time" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestRecordPasswordResetRequestSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectError bool - }{ - // empty field (Validate call check) - { - `{"email":""}`, - true, - }, - // invalid email field (Validate call check) - { - `{"email":"invalid"}`, - true, - }, - // nonexisting user - { - `{"email":"missing@example.com"}`, - true, - }, - // existing user - { - `{"email":"test@example.com"}`, - false, - }, - // existing user - reached send threshod - { - `{"email":"test@example.com"}`, - true, - }, - } - - now := types.NowDateTime() - time.Sleep(1 * time.Millisecond) - - for i, s := range scenarios { - testApp.TestMailer.TotalSend = 0 // reset - form := forms.NewRecordPasswordResetRequest(testApp, authCollection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if s.expectError { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - expectedMails := 1 - if s.expectError { - expectedMails = 0 - } - if testApp.TestMailer.TotalSend != expectedMails { - t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) - } - - if s.expectError { - continue - } - - // check whether LastResetSentAt was updated - user, err := testApp.Dao().FindAuthRecordByEmail(authCollection.Id, form.Email) - if err != nil { - t.Errorf("(%d) Expected user with email %q to exist, got nil", i, form.Email) - continue - } - - if user.LastResetSentAt().Time().Sub(now.Time()) < 0 { - t.Errorf("(%d) Expected LastResetSentAt to be after %v, got %v", i, now, user.LastResetSentAt()) - } - } -} - -func TestRecordPasswordResetRequestInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordPasswordResetRequest(testApp, authCollection) - form.Email = authRecord.Email() - interceptorLastResetSentAt := authRecord.LastResetSentAt() - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorLastResetSentAt = record.LastResetSentAt() - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorLastResetSentAt.String() != authRecord.LastResetSentAt().String() { - t.Fatalf("Expected the form model to NOT be filled before calling the interceptors") - } -} diff --git a/forms/record_upsert.go b/forms/record_upsert.go index cdfe972b..72373b9e 100644 --- a/forms/record_upsert.go +++ b/forms/record_upsert.go @@ -1,935 +1,289 @@ package forms import ( - "encoding/json" + "context" "errors" "fmt" - "log/slog" - "net/http" - "regexp" - "strings" + "slices" validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/core/validators" "github.com/pocketbase/pocketbase/tools/security" "github.com/spf13/cast" ) -// username value regex pattern -var usernameRegex = regexp.MustCompile(`^[\w][\w\.\-]*$`) +const ( + accessLevelDefault = iota + accessLevelManager + accessLevelSuperuser +) -// RecordUpsert is a [models.Record] upsert (create/update) form. type RecordUpsert struct { - app core.App - dao *daos.Dao - manageAccess bool - record *models.Record + ctx context.Context + app core.App + record *core.Record + accessLevel int - filesToUpload map[string][]*filesystem.File - filesToDelete []string // names list - - // base model fields - Id string `json:"id"` - - // auth collection fields - // --- - Username string `json:"username"` - Email string `json:"email"` - EmailVisibility bool `json:"emailVisibility"` - Verified bool `json:"verified"` - Password string `json:"password"` - PasswordConfirm string `json:"passwordConfirm"` - OldPassword string `json:"oldPassword"` - // --- - - data map[string]any + // extra password fields + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` + OldPassword string `form:"oldPassword" json:"oldPassword"` } -// NewRecordUpsert creates a new [RecordUpsert] form with initializer -// config created from the provided [core.App] and [models.Record] instances -// (for create you could pass a pointer to an empty Record - models.NewRecord(collection)). -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert { +// NewRecordUpsert creates a new [RecordUpsert] form from the provided [core.App] and [core.Record] instances +// (for create you could pass a pointer to an empty Record - core.NewRecord(collection)). +func NewRecordUpsert(app core.App, record *core.Record) *RecordUpsert { form := &RecordUpsert{ - app: app, - dao: app.Dao(), - record: record, - filesToDelete: []string{}, - filesToUpload: map[string][]*filesystem.File{}, + ctx: context.Background(), + app: app, + record: record, } - form.loadFormDefaults() - return form } -// Data returns the loaded form's data. -func (form *RecordUpsert) Data() map[string]any { - return form.data +// SetContext assigns ctx as context of the current form. +func (form *RecordUpsert) SetContext(ctx context.Context) { + form.ctx = ctx } -// SetFullManageAccess sets the manageAccess bool flag of the current -// form to enable/disable directly changing some system record fields -// (often used with auth collection records). -func (form *RecordUpsert) SetFullManageAccess(fullManageAccess bool) { - form.manageAccess = fullManageAccess -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordUpsert) SetDao(dao *daos.Dao) { - form.dao = dao -} - -func (form *RecordUpsert) loadFormDefaults() { - form.Id = form.record.Id - - if form.record.Collection().IsAuth() { - form.Username = form.record.Username() - form.Email = form.record.Email() - form.EmailVisibility = form.record.EmailVisibility() - form.Verified = form.record.Verified() - } - - form.data = map[string]any{} - for _, field := range form.record.Collection().Schema.Fields() { - form.data[field.Name] = form.record.Get(field.Name) - } -} - -func (form *RecordUpsert) getContentType(r *http.Request) string { - t := r.Header.Get("Content-Type") - for i, c := range t { - if c == ' ' || c == ';' { - return t[:i] - } - } - return t -} - -func (form *RecordUpsert) extractRequestData( - r *http.Request, - keyPrefix string, -) (map[string]any, map[string][]*filesystem.File, error) { - switch form.getContentType(r) { - case "application/json": - return form.extractJsonData(r, keyPrefix) - case "multipart/form-data": - return form.extractMultipartFormData(r, keyPrefix) - default: - return nil, nil, errors.New("unsupported request content-type") - } -} - -func (form *RecordUpsert) extractJsonData( - r *http.Request, - keyPrefix string, -) (map[string]any, map[string][]*filesystem.File, error) { - data := map[string]any{} - - err := rest.CopyJsonBody(r, &data) - - if keyPrefix != "" { - parts := strings.Split(keyPrefix, ".") - for _, part := range parts { - if data[part] == nil { - break - } - if v, ok := data[part].(map[string]any); ok { - data = v - } - } - } - - return data, nil, err -} - -func (form *RecordUpsert) extractMultipartFormData( - r *http.Request, - keyPrefix string, -) (map[string]any, map[string][]*filesystem.File, error) { - // parse form data (if not already) - if err := r.ParseMultipartForm(rest.DefaultMaxMemory); err != nil { - return nil, nil, err - } - - data := map[string]any{} - filesToUpload := map[string][]*filesystem.File{} - arraybleFieldTypes := schema.ArraybleFieldTypes() - - for fullKey, values := range r.PostForm { - key := fullKey - if keyPrefix != "" { - key = strings.TrimPrefix(key, keyPrefix+".") - } - - if len(values) == 0 { - data[key] = nil - continue - } - - // special case for multipart json encoded fields - if key == rest.MultipartJsonKey { - for _, v := range values { - if err := json.Unmarshal([]byte(v), &data); err != nil { - form.app.Logger().Debug("Failed to decode @json value into the data map", "error", err, "value", v) - } - } - continue - } - - field := form.record.Collection().Schema.GetFieldByName(key) - if field != nil && list.ExistInSlice(field.Type, arraybleFieldTypes) { - data[key] = values - } else { - data[key] = values[0] - } - } - - // load uploaded files (if any) - for _, field := range form.record.Collection().Schema.Fields() { - if field.Type != schema.FieldTypeFile { - continue // not a file field - } - - key := field.Name - fullKey := key - if keyPrefix != "" { - fullKey = keyPrefix + "." + key - } - - files, err := rest.FindUploadedFiles(r, fullKey) - if err != nil || len(files) == 0 { - if err != nil && err != http.ErrMissingFile { - form.app.Logger().Debug( - "Uploaded file error", - slog.String("key", fullKey), - slog.String("error", err.Error()), - ) - } - - // skip invalid or missing file(s) - continue - } - - filesToUpload[key] = append(filesToUpload[key], files...) - } - - return data, filesToUpload, nil -} - -// LoadRequest extracts the json or multipart/form-data request data -// and lods it into the form. +// SetApp replaces the current form app instance. // -// File upload is supported only via multipart/form-data. -func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error { - requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix) - if err != nil { - return err - } - - if err := form.LoadData(requestData); err != nil { - return err - } - - for key, files := range uploadedFiles { - form.AddFiles(key, files...) - } - - return nil +// This could be used for example if you want to change at later stage +// before submission to change from regular -> transactional app instance. +func (form *RecordUpsert) SetApp(app core.App) { + form.app = app } -// FilesToUpload returns the parsed request files ready for upload. -func (form *RecordUpsert) FilesToUpload() map[string][]*filesystem.File { - return form.filesToUpload +// SetRecord replaces the current form record instance. +func (form *RecordUpsert) SetRecord(record *core.Record) { + form.record = record } -// FilesToUpload returns the parsed request filenames ready to be deleted. -func (form *RecordUpsert) FilesToDelete() []string { - return form.filesToDelete +// ResetAccess resets the form access level to the accessLevelDefault. +func (form *RecordUpsert) ResetAccess() { + form.accessLevel = accessLevelDefault } -// AddFiles adds the provided file(s) to the specified file field. -// -// If the file field is a SINGLE-value file field (aka. "Max Select = 1"), -// then the newly added file will REPLACE the existing one. -// In this case if you pass more than 1 files only the first one will be assigned. -// -// If the file field is a MULTI-value file field (aka. "Max Select > 1"), -// then the newly added file(s) will be APPENDED to the existing one(s). -// -// Example -// -// f1, _ := filesystem.NewFileFromPath("/path/to/file1.txt") -// f2, _ := filesystem.NewFileFromPath("/path/to/file2.txt") -// form.AddFiles("documents", f1, f2) -func (form *RecordUpsert) AddFiles(key string, files ...*filesystem.File) error { - field := form.record.Collection().Schema.GetFieldByName(key) - if field == nil || field.Type != schema.FieldTypeFile { - return errors.New("invalid field key") - } - - options, ok := field.Options.(*schema.FileOptions) - if !ok { - return errors.New("failed to initilize field options") - } - - if len(files) == 0 { - return nil // nothing to upload - } - - if form.filesToUpload == nil { - form.filesToUpload = map[string][]*filesystem.File{} - } - - oldNames := list.ToUniqueStringSlice(form.data[key]) - - if options.MaxSelect == 1 { - // mark previous file(s) for deletion before replacing - if len(oldNames) > 0 { - form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...)) - } - - // replace - form.filesToUpload[key] = []*filesystem.File{files[0]} - form.data[key] = field.PrepareValue(files[0].Name) - } else { - // append - form.filesToUpload[key] = append(form.filesToUpload[key], files...) - for _, f := range files { - oldNames = append(oldNames, f.Name) - } - form.data[key] = field.PrepareValue(oldNames) - } - - return nil +// GrantManagerAccess updates the form access level to "manager" allowing +// directly changing some system record fields (often used with auth collection records). +func (form *RecordUpsert) GrantManagerAccess() { + form.accessLevel = accessLevelManager } -// RemoveFiles removes a single or multiple file from the specified file field. -// -// NB! If filesToDelete is not set it will remove all existing files -// assigned to the file field (including those assigned with AddFiles)! -// -// Example -// -// // mark only only 2 files for removal -// form.RemoveFiles("documents", "file1_aw4bdrvws6.txt", "file2_xwbs36bafv.txt") -// -// // mark all "documents" files for removal -// form.RemoveFiles("documents") -func (form *RecordUpsert) RemoveFiles(key string, toDelete ...string) error { - field := form.record.Collection().Schema.GetFieldByName(key) - if field == nil || field.Type != schema.FieldTypeFile { - return errors.New("invalid field key") - } - - existing := list.ToUniqueStringSlice(form.data[key]) - - // mark all files for deletion - if len(toDelete) == 0 { - toDelete = make([]string, len(existing)) - copy(toDelete, existing) - } - - // check for existing files - for i := len(existing) - 1; i >= 0; i-- { - if list.ExistInSlice(existing[i], toDelete) { - form.filesToDelete = append(form.filesToDelete, existing[i]) - existing = append(existing[:i], existing[i+1:]...) - } - } - - // check for newly uploaded files - for i := len(form.filesToUpload[key]) - 1; i >= 0; i-- { - f := form.filesToUpload[key][i] - if list.ExistInSlice(f.Name, toDelete) { - form.filesToUpload[key] = append(form.filesToUpload[key][:i], form.filesToUpload[key][i+1:]...) - } - } - - form.data[key] = field.PrepareValue(existing) - - return nil +// GrantSuperuserAccess updates the form access level to "superuser" allowing +// directly changing all system record fields, including those marked as "Hidden". +func (form *RecordUpsert) GrantSuperuserAccess() { + form.accessLevel = accessLevelSuperuser } -// LoadData loads and normalizes the provided regular record data fields into the form. -func (form *RecordUpsert) LoadData(requestData map[string]any) error { - // load base system fields - if v, ok := requestData[schema.FieldNameId]; ok { - form.Id = cast.ToString(v) - } +// HasManageAccess reports whether the form has "manager" or "superuser" level access. +func (form *RecordUpsert) HasManageAccess() bool { + return form.accessLevel == accessLevelManager || form.accessLevel == accessLevelSuperuser +} - // load auth system fields - if form.record.Collection().IsAuth() { - if v, ok := requestData[schema.FieldNameUsername]; ok { - form.Username = cast.ToString(v) - } - if v, ok := requestData[schema.FieldNameEmail]; ok { - form.Email = cast.ToString(v) - } - if v, ok := requestData[schema.FieldNameEmailVisibility]; ok { - form.EmailVisibility = cast.ToBool(v) - } - if v, ok := requestData[schema.FieldNameVerified]; ok { - form.Verified = cast.ToBool(v) - } - if v, ok := requestData["password"]; ok { +// Load loads the provided data into the form and the related record. +func (form *RecordUpsert) Load(data map[string]any) { + excludeFields := []string{core.FieldNameExpand} + + isAuth := form.record.Collection().IsAuth() + + // load the special auth form fields + if isAuth { + if v, ok := data["password"]; ok { form.Password = cast.ToString(v) } - if v, ok := requestData["passwordConfirm"]; ok { + if v, ok := data["passwordConfirm"]; ok { form.PasswordConfirm = cast.ToString(v) } - if v, ok := requestData["oldPassword"]; ok { + if v, ok := data["oldPassword"]; ok { form.OldPassword = cast.ToString(v) } + + excludeFields = append(excludeFields, "passwordConfirm", "oldPassword") // skip non-schema password fields } - // replace modifiers (if any) - requestData = form.record.ReplaceModifers(requestData) - - // create a shallow copy of form.data - var extendedData = make(map[string]any, len(form.data)) - for k, v := range form.data { - extendedData[k] = v - } - - // extend form.data with the request data - rawData, err := json.Marshal(requestData) - if err != nil { - return err - } - if err := json.Unmarshal(rawData, &extendedData); err != nil { - return err - } - - for _, field := range form.record.Collection().Schema.Fields() { - key := field.Name - value := field.PrepareValue(extendedData[key]) - - if field.Type != schema.FieldTypeFile { - form.data[key] = value + for k, v := range data { + if slices.Contains(excludeFields, k) { continue } - // ----------------------------------------------------------- - // Delete previously uploaded file(s) - // ----------------------------------------------------------- + // set only known collection fields + field := form.record.SetIfFieldExists(k, v) - oldNames := form.record.GetStringSlice(key) - submittedNames := list.ToUniqueStringSlice(value) - - // ensure that all submitted names are existing to prevent accidental files deletions - if len(submittedNames) > len(oldNames) || len(list.SubtractSlice(submittedNames, oldNames)) != 0 { - return validation.Errors{ - key: validation.NewError( - "validation_unknown_filenames", - "The field contains unknown filenames.", - ), - } - } - - // if empty value was set, mark all previously uploaded files for deletion - // otherwise check for "deleted" (aka. unsubmitted) file names - if len(submittedNames) == 0 && len(oldNames) > 0 { - form.RemoveFiles(key) - } else if len(oldNames) > 0 { - toDelete := []string{} - - for _, name := range oldNames { - // submitted as a modifier or a new array - if !list.ExistInSlice(name, submittedNames) { - toDelete = append(toDelete, name) - continue - } - } - - if len(toDelete) > 0 { - form.RemoveFiles(key, toDelete...) - } - } - - // allow file key reasignments for file names sorting - // (only if all submitted values already exists) - if len(submittedNames) > 0 && len(list.SubtractSlice(submittedNames, oldNames)) == 0 { - form.data[key] = submittedNames + // restore original value if hidden field (with exception of the auth "password") + // + // note: this is an extra measure against loading hidden fields + // but usually is not used by the default route handlers since + // they filter additionally the data before calling Load + if form.accessLevel != accessLevelSuperuser && field != nil && field.GetHidden() && (!isAuth || field.GetName() != core.FieldNamePassword) { + form.record.SetRaw(field.GetName(), form.record.Original().GetRaw(field.GetName())) } } - - return nil } -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordUpsert) Validate() error { - // base form fields validator - baseFieldsRules := []*validation.FieldRules{ - validation.Field( - &form.Id, - validation.When( - form.record.IsNew(), - validation.Length(models.DefaultIdLength, models.DefaultIdLength), - validation.Match(idRegex), - validation.By(validators.UniqueId(form.dao, form.record.TableName())), - ).Else(validation.In(form.record.Id)), - ), +func (form *RecordUpsert) validateFormFields() error { + isAuth := form.record.Collection().IsAuth() + if !isAuth { + return nil } - // auth fields validators - if form.record.Collection().IsAuth() { - baseFieldsRules = append(baseFieldsRules, - validation.Field( - &form.Username, - // require only on update, because on create we fallback to auto generated username - validation.When(!form.record.IsNew(), validation.Required), - validation.Length(3, 150), - validation.Match(usernameRegex), - validation.By(form.checkUniqueUsername), - ), - validation.Field( - &form.Email, - validation.When( - form.record.Collection().AuthOptions().RequireEmail, - validation.Required, - ), - // don't allow direct email change (or unset) if the form doesn't have manage access permissions + isNew := form.record.IsNew() + + original := form.record.Original() + + validateData := map[string]any{ + "email": form.record.Email(), + "verified": form.record.Verified(), + "password": form.Password, + "passwordConfirm": form.PasswordConfirm, + "oldPassword": form.OldPassword, + } + + return validation.Validate(validateData, + validation.Map( + validation.Key( + "email", + // don't allow direct email updates if the form doesn't have manage access permissions // (aka. allow only admin or authorized auth models to directly update the field) validation.When( - !form.record.IsNew() && !form.manageAccess, - validation.In(form.record.Email()), + !isNew && !form.HasManageAccess(), + validation.By(validators.Equal(original.Email())), ), - validation.Length(1, 255), - is.EmailFormat, - validation.By(form.checkEmailDomain), - validation.By(form.checkUniqueEmail), ), - validation.Field( - &form.Verified, + validation.Key( + "verified", // don't allow changing verified if the form doesn't have manage access permissions // (aka. allow only admin or authorized auth models to directly change the field) validation.When( - !form.manageAccess, - validation.In(form.record.Verified()), + !form.HasManageAccess(), + validation.By(validators.Equal(original.Verified())), ), ), - validation.Field( - &form.Password, + validation.Key( + "password", validation.When( - (form.record.IsNew() || form.PasswordConfirm != "" || form.OldPassword != ""), + (isNew || form.PasswordConfirm != "" || form.OldPassword != ""), validation.Required, ), - validation.Length(form.record.Collection().AuthOptions().MinPasswordLength, 72), ), - validation.Field( - &form.PasswordConfirm, + validation.Key( + "passwordConfirm", validation.When( - (form.record.IsNew() || form.Password != "" || form.OldPassword != ""), + (isNew || form.Password != "" || form.OldPassword != ""), validation.Required, ), - validation.By(validators.Compare(form.Password)), + validation.By(validators.Equal(form.Password)), ), - validation.Field( - &form.OldPassword, + validation.Key( + "oldPassword", // require old password only on update when: - // - form.manageAccess is not set + // - form.HasManageAccess() is not satisfied // - changing the existing password validation.When( - !form.record.IsNew() && !form.manageAccess && (form.Password != "" || form.PasswordConfirm != ""), + !isNew && !form.HasManageAccess() && (form.Password != "" || form.PasswordConfirm != ""), validation.Required, validation.By(form.checkOldPassword), ), ), - ) - } - - if err := validation.ValidateStruct(form, baseFieldsRules...); err != nil { - return err - } - - // record data validator - return validators.NewRecordDataValidator( - form.dao, - form.record, - form.filesToUpload, - ).Validate(form.data) -} - -func (form *RecordUpsert) checkUniqueUsername(value any) error { - v, _ := value.(string) - if v == "" { - return nil - } - - isUnique := form.dao.IsRecordValueUnique( - form.record.Collection().Id, - schema.FieldNameUsername, - v, - form.record.Id, + ), ) - if !isUnique { - return validation.NewError("validation_invalid_username", "The username is invalid or already in use.") - } - - return nil -} - -func (form *RecordUpsert) checkUniqueEmail(value any) error { - v, _ := value.(string) - if v == "" { - return nil - } - - isUnique := form.dao.IsRecordValueUnique( - form.record.Collection().Id, - schema.FieldNameEmail, - v, - form.record.Id, - ) - if !isUnique { - return validation.NewError("validation_invalid_email", "The email is invalid or already in use.") - } - - return nil -} - -func (form *RecordUpsert) checkEmailDomain(value any) error { - val, _ := value.(string) - if val == "" { - return nil // nothing to check - } - - domain := val[strings.LastIndex(val, "@")+1:] - only := form.record.Collection().AuthOptions().OnlyEmailDomains - except := form.record.Collection().AuthOptions().ExceptEmailDomains - - // only domains check - if len(only) > 0 && !list.ExistInSlice(domain, only) { - return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.") - } - - // except domains check - if len(except) > 0 && list.ExistInSlice(domain, except) { - return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.") - } - - return nil } func (form *RecordUpsert) checkOldPassword(value any) error { v, _ := value.(string) - if v == "" { + if v == "" || form.record.IsNew() { return nil // nothing to check } - if !form.record.ValidatePassword(v) { + if !form.record.Original().ValidatePassword(v) { return validation.NewError("validation_invalid_old_password", "Missing or invalid old password.") } return nil } -func (form *RecordUpsert) ValidateAndFill() error { - if err := form.Validate(); err != nil { - return err - } - - isNew := form.record.IsNew() - - // custom insertion id can be set only on create - if isNew && form.Id != "" { - form.record.SetId(form.Id) - form.record.MarkAsNew() - } - - // set auth fields - if form.record.Collection().IsAuth() { - // generate a default username during create (if missing) - if form.record.IsNew() && form.Username == "" { - baseUsername := form.record.Collection().Name + security.RandomStringWithAlphabet(5, "123456789") - form.Username = form.dao.SuggestUniqueAuthRecordUsername(form.record.Collection().Id, baseUsername) - } - - if form.Username != "" { - if err := form.record.SetUsername(form.Username); err != nil { - return err - } - } - - if isNew || form.manageAccess { - if err := form.record.SetEmail(form.Email); err != nil { - return err - } - } - - if err := form.record.SetEmailVisibility(form.EmailVisibility); err != nil { - return err - } - - if form.manageAccess { - if err := form.record.SetVerified(form.Verified); err != nil { - return err - } - } - - if form.Password != "" && form.Password == form.PasswordConfirm { - if err := form.record.SetPassword(form.Password); err != nil { - return err - } - } - } - - // bulk load the remaining form data - form.record.Load(form.data) - - return nil -} - -// DrySubmit performs a form submit within a transaction and reverts it. -// For actual record persistence, check the `form.Submit()` method. +// @todo consider removing and executing the Create API rule without dummy insert. // -// This method doesn't handle file uploads/deletes or trigger any app events! -func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error { +// DrySubmit performs a temp form submit within a transaction and reverts it at the end. +// For actual record persistence, check the [RecordUpsert.Submit()] method. +// +// This method doesn't perform validations, handle file uploads/deletes or trigger app save events! +func (form *RecordUpsert) DrySubmit(callback func(txApp core.App, drySavedRecord *core.Record) error) error { isNew := form.record.IsNew() - if err := form.ValidateAndFill(); err != nil { - return err + clone := form.record.Clone() + + // set an id if it doesn't have already + // (the value doesn't matter; it is used only during the manual delete/update rollback) + if clone.IsNew() && clone.Id == "" { + clone.Id = "_temp_" + security.PseudorandomString(15) } - var dryDao *daos.Dao - if form.dao.ConcurrentDB() == form.dao.NonconcurrentDB() { - // it is already in a transaction and therefore use the app concurrent db pool - // to prevent "transaction has already been committed or rolled back" error - dryDao = daos.New(form.app.Dao().ConcurrentDB()) - } else { - // otherwise use the form noncurrent dao db pool - dryDao = daos.New(form.dao.NonconcurrentDB()) + app := form.app.UnsafeWithoutHooks() + + _, isTransactional := app.DB().(*dbx.Tx) + if !isTransactional { + return app.RunInTransaction(func(txApp core.App) error { + tx, ok := txApp.DB().(*dbx.Tx) + if !ok { + return errors.New("failed to get transaction db") + } + defer tx.Rollback() + + if err := txApp.SaveNoValidateWithContext(form.ctx, clone); err != nil { + return validators.NormalizeUniqueIndexError(err, clone.Collection().Name, clone.Collection().Fields.FieldNames()) + } + + if callback != nil { + return callback(txApp, clone) + } + + return nil + }) } - return dryDao.RunInTransaction(func(txDao *daos.Dao) error { - tx, ok := txDao.DB().(*dbx.Tx) - if !ok { - return errors.New("failed to get transaction db") - } - defer tx.Rollback() + // already in a transaction + // (manual rollback to avoid starting another transaction) + // --------------------------------------------------------------- + err := app.SaveNoValidateWithContext(form.ctx, clone) + if err != nil { + return validators.NormalizeUniqueIndexError(err, clone.Collection().Name, clone.Collection().Fields.FieldNames()) + } - if err := txDao.SaveRecord(form.record); err != nil { - return form.prepareError(err) - } - - // restore record isNew state + manualRollback := func() error { if isNew { - form.record.MarkAsNew() - } - - if callback != nil { - return callback(txDao) - } - - return nil - }) -} - -// Submit validates the form and upserts the form Record model. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record]) error { - if err := form.ValidateAndFill(); err != nil { - return err - } - - return runInterceptors(form.record, func(record *models.Record) error { - form.record = record - - if !form.record.HasId() { - form.record.RefreshId() - form.record.MarkAsNew() - } - - dao := form.dao.Clone() - - // upload new files (if any) - // - // note: executed after the default BeforeCreateFunc and BeforeUpdateFunc hook actions - // to allow uploading AFTER the before app model hooks (eg. in case of an id change) - // but BEFORE the actual record db persistence - // --- - dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - newAction := func() error { - if m.TableName() == form.record.TableName() && m.GetId() == form.record.GetId() { - if err := form.processFilesToUpload(); err != nil { - return err - } - } - - return action() + err = app.DeleteWithContext(form.ctx, clone) + if err != nil { + return fmt.Errorf("failed to rollback dry submit created record: %w", err) } - - if form.dao.BeforeCreateFunc != nil { - return form.dao.BeforeCreateFunc(eventDao, m, newAction) - } - - return newAction() - } - - dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model, action func() error) error { - newAction := func() error { - if m.TableName() == form.record.TableName() && m.GetId() == form.record.GetId() { - if err := form.processFilesToUpload(); err != nil { - return err - } - } - - return action() - } - - if form.dao.BeforeUpdateFunc != nil { - return form.dao.BeforeUpdateFunc(eventDao, m, newAction) - } - - return newAction() - } - // --- - - // persist the record model - if err := dao.SaveRecord(form.record); err != nil { - return form.prepareError(err) - } - - // delete old files (if any) - // - // for now fail silently to avoid reupload when `form.Submit()` - // is called manually (aka. not from an api request)... - if err := form.processFilesToDelete(); err != nil { - form.app.Logger().Debug( - "Failed to delete old files", - slog.String("error", err.Error()), - ) - } - - return nil - }, interceptors...) -} - -func (form *RecordUpsert) processFilesToUpload() error { - if len(form.filesToUpload) == 0 { - return nil // no parsed file fields - } - - if !form.record.HasId() { - return errors.New("the record doesn't have an id") - } - - fs, err := form.app.NewFilesystem() - if err != nil { - return err - } - defer fs.Close() - - var uploadErrors []error // list of upload errors - var uploaded []string // list of uploaded file paths - - for fieldKey := range form.filesToUpload { - for i, file := range form.filesToUpload[fieldKey] { - path := form.record.BaseFilesPath() + "/" + file.Name - if err := fs.UploadFile(file, path); err == nil { - // keep track of the already uploaded file - uploaded = append(uploaded, path) - } else { - // store the upload error - uploadErrors = append(uploadErrors, fmt.Errorf("file %d: %v", i, err)) - } - } - } - - if len(uploadErrors) > 0 { - // cleanup - try to delete the successfully uploaded files (if any) - form.deleteFilesByNamesList(uploaded) - - return fmt.Errorf("failed to upload all files: %v", uploadErrors) - } - - return nil -} - -func (form *RecordUpsert) processFilesToDelete() (err error) { - form.filesToDelete, err = form.deleteFilesByNamesList(form.filesToDelete) - return -} - -// deleteFiles deletes a list of record files by their names. -// Returns the failed/remaining files. -func (form *RecordUpsert) deleteFilesByNamesList(filenames []string) ([]string, error) { - if len(filenames) == 0 { - return filenames, nil // nothing to delete - } - - if !form.record.HasId() { - return filenames, errors.New("the record doesn't have an id") - } - - fs, err := form.app.NewFilesystem() - if err != nil { - return filenames, err - } - defer fs.Close() - - var deleteErrors []error - - for i := len(filenames) - 1; i >= 0; i-- { - filename := filenames[i] - path := form.record.BaseFilesPath() + "/" + filename - - if err := fs.Delete(path); err == nil { - // remove the deleted file from the list - filenames = append(filenames[:i], filenames[i+1:]...) - - // try to delete the related file thumbs (if any) - fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/") } else { - // store the delete error - deleteErrors = append(deleteErrors, fmt.Errorf("file %d: %v", i, err)) - } - } - - if len(deleteErrors) > 0 { - return filenames, fmt.Errorf("failed to delete all files: %v", deleteErrors) - } - - return filenames, nil -} - -// prepareError parses the provided error and tries to return -// user-friendly validation error(s). -func (form *RecordUpsert) prepareError(err error) error { - msg := strings.ToLower(err.Error()) - - validationErrs := validation.Errors{} - - // check for unique constraint failure - if strings.Contains(msg, "unique constraint failed") { - msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ") - - c := form.record.Collection() - for _, f := range c.Schema.Fields() { - // blank space to unify multi-columns lookup - if strings.Contains(msg+" ", strings.ToLower(c.Name+"."+f.Name)) { - validationErrs[f.Name] = validation.NewError("validation_not_unique", "Value must be unique") + clone.Load(clone.Original().FieldsData()) + err = app.SaveNoValidateWithContext(form.ctx, clone) + if err != nil { + return fmt.Errorf("failed to rollback dry submit updated record: %w", err) } } + + return nil } - if len(validationErrs) > 0 { - return validationErrs + if callback != nil { + return errors.Join(callback(app, clone), manualRollback()) } - return err + return manualRollback() +} + +// Submit validates the form specific validations and attempts to save the form record. +func (form *RecordUpsert) Submit() error { + err := form.validateFormFields() + if err != nil { + return err + } + + // run record validations and persist in db + return form.app.SaveWithContext(form.ctx, form.record) } diff --git a/forms/record_upsert_test.go b/forms/record_upsert_test.go index 6a730f81..bd88629e 100644 --- a/forms/record_upsert_test.go +++ b/forms/record_upsert_test.go @@ -4,1224 +4,894 @@ import ( "bytes" "encoding/json" "errors" - "fmt" - "net/http" - "net/http/httptest" + "maps" "os" "path/filepath" "strings" "testing" - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/rest" - "github.com/pocketbase/pocketbase/tools/types" ) -func hasRecordFile(app core.App, record *models.Record, filename string) bool { - fs, _ := app.NewFilesystem() - defer fs.Close() +func TestRecordUpsertLoad(t *testing.T) { + t.Parallel() - fileKey := filepath.Join( - record.Collection().Id, - record.Id, - filename, - ) + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() - exists, _ := fs.Exists(fileKey) - - return exists -} - -func TestNewRecordUpsert(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo2") - record := models.NewRecord(collection) - record.Set("title", "test_value") - - form := forms.NewRecordUpsert(app, record) - - val := form.Data()["title"] - if val != "test_value" { - t.Errorf("Expected record data to be loaded, got %v", form.Data()) - } -} - -func TestRecordUpsertLoadRequestUnsupported(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - record, err := app.Dao().FindRecordById("demo2", "0yxhwia2amd8gec") + demo1Col, err := testApp.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - testData := "title=test123" - - form := forms.NewRecordUpsert(app, record) - req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(testData)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) - - if err := form.LoadRequest(req, ""); err == nil { - t.Fatal("Expected LoadRequest to fail, got nil") - } -} - -func TestRecordUpsertLoadRequestJson(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - record, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t") + usersCol, err := testApp.FindCollectionByNameOrId("users") if err != nil { t.Fatal(err) } - testData := map[string]any{ - "a": map[string]any{ - "b": map[string]any{ - "id": "test_id", - "text": "test123", - "unknown": "test456", - // file fields unset/delete - "file_one": nil, - "file_many.0": "", // delete by index - "file_many-": []string{"test_MaWC6mWyrP.txt", "test_tC1Yc87DfC.txt"}, // multiple delete with modifier - "file_many.300_WlbFWSGmW9.png": nil, // delete by filename - "file_many.2": "test.png", // should be ignored - }, - }, - } - - form := forms.NewRecordUpsert(app, record) - jsonBody, _ := json.Marshal(testData) - req := httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(jsonBody)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - loadErr := form.LoadRequest(req, "a.b") - if loadErr != nil { - t.Fatal(loadErr) - } - - if form.Id != "test_id" { - t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id) - } - - if v, ok := form.Data()["text"]; !ok || v != "test123" { - t.Fatalf("Expect title field to be %q, got %q", "test123", v) - } - - if v, ok := form.Data()["unknown"]; ok { - t.Fatalf("Didn't expect unknown field to be set, got %v", v) - } - - fileOne, ok := form.Data()["file_one"] - if !ok { - t.Fatal("Expect file_one field to be set") - } - if fileOne != "" { - t.Fatalf("Expect file_one field to be empty string, got %v", fileOne) - } - - fileMany, ok := form.Data()["file_many"] - if !ok || fileMany == nil { - t.Fatal("Expect file_many field to be set") - } - manyfilesRemains := len(list.ToUniqueStringSlice(fileMany)) - if manyfilesRemains != 1 { - t.Fatalf("Expect only 1 file_many to remain, got \n%v", fileMany) - } -} - -func TestRecordUpsertLoadRequestMultipart(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - record, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "a.b.id": "test_id", - "a.b.text": "test123", - "a.b.unknown": "test456", - "a.b." + rest.MultipartJsonKey: `{"json":["a","b"],"email":"test3@example.com"}`, - // file fields unset/delete - "a.b.file_one-": "test_d61b33QdDU.txt", // delete with modifier - "a.b.file_many.0": "", // delete by index - "a.b.file_many-": "test_tC1Yc87DfC.txt", // delete with modifier - "a.b.file_many.300_WlbFWSGmW9.png": "", // delete by filename - "a.b.file_many.2": "test.png", // should be ignored - }, "a.b.file_many") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, record) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - loadErr := form.LoadRequest(req, "a.b") - if loadErr != nil { - t.Fatal(loadErr) - } - - if form.Id != "test_id" { - t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id) - } - - if v, ok := form.Data()["text"]; !ok || v != "test123" { - t.Fatalf("Expect text field to be %q, got %q", "test123", v) - } - - if v, ok := form.Data()["unknown"]; ok { - t.Fatalf("Didn't expect unknown field to be set, got %v", v) - } - - if v, ok := form.Data()["email"]; !ok || v != "test3@example.com" { - t.Fatalf("Expect email field to be %q, got %q", "test3@example.com", v) - } - - rawJsonValue, ok := form.Data()["json"].(types.JsonRaw) - if !ok { - t.Fatal("Expect json field to be set") - } - expectedJsonValue := `["a","b"]` - if rawJsonValue.String() != expectedJsonValue { - t.Fatalf("Expect json field %v, got %v", expectedJsonValue, rawJsonValue) - } - - fileOne, ok := form.Data()["file_one"] - if !ok { - t.Fatal("Expect file_one field to be set") - } - if fileOne != "" { - t.Fatalf("Expect file_one field to be empty string, got %v", fileOne) - } - - fileMany, ok := form.Data()["file_many"] - if !ok || fileMany == nil { - t.Fatal("Expect file_many field to be set") - } - manyfilesRemains := len(list.ToUniqueStringSlice(fileMany)) - expectedRemains := 3 // 5 old; 3 deleted and 1 new uploaded - if manyfilesRemains != expectedRemains { - t.Fatalf("Expect file_many to be %d, got %d (%v)", expectedRemains, manyfilesRemains, fileMany) - } -} - -func TestRecordUpsertLoadData(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - record, err := app.Dao().FindRecordById("demo2", "llvuca81nly1qls") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, record) - - loadErr := form.LoadData(map[string]any{ - "title": "test_new", - "active": true, - }) - if loadErr != nil { - t.Fatal(loadErr) - } - - if v, ok := form.Data()["title"]; !ok || v != "test_new" { - t.Fatalf("Expect title field to be %v, got %v", "test_new", v) - } - - if v, ok := form.Data()["active"]; !ok || v != true { - t.Fatalf("Expect active field to be %v, got %v", true, v) - } -} - -func TestRecordUpsertDrySubmitFailure(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo1") - recordBefore, err := app.Dao().FindRecordById(collection.Id, "al1h9ijdeojtsjy") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "title": "abc", - "rel_one": "missing", - }) - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, recordBefore) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - callbackCalls := 0 - - // ensure that validate is triggered - // --- - result := form.DrySubmit(func(txDao *daos.Dao) error { - callbackCalls++ - return nil - }) - if result == nil { - t.Fatal("Expected error, got nil") - } - if callbackCalls != 0 { - t.Fatalf("Expected callbackCalls to be 0, got %d", callbackCalls) - } - - // ensure that the record changes weren't persisted - // --- - recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id) - if err != nil { - t.Fatal(err) - } - - if recordAfter.GetString("title") == "abc" { - t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetString("title"), "abc") - } - - if recordAfter.GetString("rel_one") == "missing" { - t.Fatalf("Expected record.rel_one to be %s, got %s", recordBefore.GetString("rel_one"), "missing") - } -} - -func TestRecordUpsertDrySubmitSuccess(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo1") - recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "title": "dry_test", - "file_one": "", - }, "file_many") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, recordBefore) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - callbackCalls := 0 - - result := form.DrySubmit(func(txDao *daos.Dao) error { - callbackCalls++ - return nil - }) - if result != nil { - t.Fatalf("Expected nil, got error %v", result) - } - - // ensure callback was called - if callbackCalls != 1 { - t.Fatalf("Expected callbackCalls to be 1, got %d", callbackCalls) - } - - // ensure that the record changes weren't persisted - recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id) - if err != nil { - t.Fatal(err) - } - if recordAfter.GetString("title") == "dry_test" { - t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetString("title"), "dry_test") - } - if recordAfter.GetString("file_one") == "" { - t.Fatal("Expected record.file_one to not be changed, got empty string") - } - - // file wasn't removed - if !hasRecordFile(app, recordAfter, recordAfter.GetString("file_one")) { - t.Fatal("file_one file should not have been deleted") - } -} - -func TestRecordUpsertDrySubmitWithNestedTx(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo1") - recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "title": "dry_test", - }) - if err != nil { - t.Fatal(err) - } - - txErr := app.Dao().RunInTransaction(func(txDao *daos.Dao) error { - form := forms.NewRecordUpsert(app, recordBefore) - form.SetDao(txDao) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - callbackCalls := 0 - - result := form.DrySubmit(func(innerTxDao *daos.Dao) error { - callbackCalls++ - return nil - }) - if result != nil { - t.Fatalf("Expected nil, got error %v", result) - } - - // ensure callback was called - if callbackCalls != 1 { - t.Fatalf("Expected callbackCalls to be 1, got %d", callbackCalls) - } - - // ensure that the original txDao can still be used after the DrySubmit rollback - if _, err := txDao.FindRecordById(collection.Id, recordBefore.Id); err != nil { - t.Fatalf("Expected the dry submit rollback to not affect the outer tx context, got %v", err) - } - - // ensure that the record changes weren't persisted - recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id) - if err != nil { - t.Fatal(err) - } - if recordAfter.GetString("title") == "dry_test" { - t.Fatalf("Expected record.title to be %v, got %v", recordBefore.GetString("title"), "dry_test") - } - - return nil - }) - if txErr != nil { - t.Fatalf("Nested transactions failure: %v", txErr) - } -} - -func TestRecordUpsertSubmitFailure(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo1") - if err != nil { - t.Fatal(err) - } - - recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "text": "abc", - "bool": "false", - "select_one": "invalid", - "file_many": "invalid", - "email": "invalid", - }, "file_one") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, recordBefore) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - // ensure that validate is triggered - // --- - result := form.Submit(interceptor) - if result == nil { - t.Fatal("Expected error, got nil") - } - - // check interceptor calls - // --- - if interceptorCalls != 0 { - t.Fatalf("Expected interceptor to be called 0 times, got %d", interceptorCalls) - } - - // ensure that the record changes weren't persisted - // --- - recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id) - if err != nil { - t.Fatal(err) - } - - if v := recordAfter.Get("text"); v == "abc" { - t.Fatalf("Expected record.text not to change, got %v", v) - } - if v := recordAfter.Get("bool"); v == false { - t.Fatalf("Expected record.bool not to change, got %v", v) - } - if v := recordAfter.Get("select_one"); v == "invalid" { - t.Fatalf("Expected record.select_one not to change, got %v", v) - } - if v := recordAfter.Get("email"); v == "invalid" { - t.Fatalf("Expected record.email not to change, got %v", v) - } - if v := recordAfter.GetStringSlice("file_many"); len(v) != 5 { - t.Fatalf("Expected record.file_many not to change, got %v", v) - } - - // ensure the files weren't removed - for _, f := range recordAfter.GetStringSlice("file_many") { - if !hasRecordFile(app, recordAfter, f) { - t.Fatal("file_many file should not have been deleted") - } - } -} - -func TestRecordUpsertSubmitSuccess(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo1") - recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - formData, mp, err := tests.MockMultipartData(map[string]string{ - "text": "test_save", - "bool": "true", - "select_one": "optionA", - "file_one": "", - }, "file_many.1", "file_many") // replace + new file - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, recordBefore) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - result := form.Submit(interceptor) - if result != nil { - t.Fatalf("Expected nil, got error %v", result) - } - - // check interceptor calls - // --- - if interceptorCalls != 1 { - t.Fatalf("Expected interceptor to be called 1 time, got %d", interceptorCalls) - } - - // ensure that the record changes were persisted - // --- - recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id) - if err != nil { - t.Fatal(err) - } - - if v := recordAfter.GetString("text"); v != "test_save" { - t.Fatalf("Expected record.text to be %v, got %v", v, "test_save") - } - - if hasRecordFile(app, recordAfter, recordAfter.GetString("file_one")) { - t.Fatal("Expected record.file_one to be deleted") - } - - fileMany := (recordAfter.GetStringSlice("file_many")) - if len(fileMany) != 6 { // 1 replace + 1 new - t.Fatalf("Expected 6 record.file_many, got %d (%v)", len(fileMany), fileMany) - } - for _, f := range fileMany { - if !hasRecordFile(app, recordAfter, f) { - t.Fatalf("Expected file %q to exist", f) - } - } -} - -func TestRecordUpsertSubmitInterceptors(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo3") - record, err := app.Dao().FindRecordById(collection.Id, "mk5fmymtx4wsprk") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, record) - form.Data()["title"] = "test_new" - - testErr := errors.New("test_error") - interceptorRecordTitle := "" - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptor1Called = true - return next(r) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorRecordTitle = record.GetString("title") // to check if the record was filled - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorRecordTitle != form.Data()["title"].(string) { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} - -func TestRecordUpsertWithCustomId(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, err := app.Dao().FindCollectionByNameOrId("demo3") - if err != nil { - t.Fatal(err) - } - - existingRecord, err := app.Dao().FindRecordById(collection.Id, "mk5fmymtx4wsprk") + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") if err != nil { t.Fatal(err) } scenarios := []struct { - name string - data map[string]string - record *models.Record - expectError bool + name string + data map[string]any + record *core.Record + managerAccessLevel bool + superuserAccessLevel bool + expected []string + notExpected []string }{ { - "empty data", - map[string]string{}, - models.NewRecord(collection), - false, - }, - { - "empty id", - map[string]string{"id": ""}, - models.NewRecord(collection), - false, - }, - { - "id < 15 chars", - map[string]string{"id": "a23"}, - models.NewRecord(collection), - true, - }, - { - "id > 15 chars", - map[string]string{"id": "a234567890123456"}, - models.NewRecord(collection), - true, - }, - { - "id = 15 chars (invalid chars)", - map[string]string{"id": "a@3456789012345"}, - models.NewRecord(collection), - true, - }, - { - "id = 15 chars (valid chars)", - map[string]string{"id": "a23456789012345"}, - models.NewRecord(collection), - false, - }, - { - "changing the id of an existing record", - map[string]string{"id": "b23456789012345"}, - existingRecord, - true, - }, - { - "using the same existing record id", - map[string]string{"id": existingRecord.Id}, - existingRecord, - false, - }, - { - "skipping the id for existing record", - map[string]string{}, - existingRecord, - false, - }, - } - - for _, scenario := range scenarios { - formData, mp, err := tests.MockMultipartData(scenario.data) - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordUpsert(app, scenario.record) - req := httptest.NewRequest(http.MethodGet, "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) - form.LoadRequest(req, "") - - dryErr := form.DrySubmit(nil) - hasDryErr := dryErr != nil - - submitErr := form.Submit() - hasSubmitErr := submitErr != nil - - if hasDryErr != hasSubmitErr { - t.Errorf("[%s] Expected hasDryErr and hasSubmitErr to have the same value, got %v vs %v", scenario.name, hasDryErr, hasSubmitErr) - } - - if hasSubmitErr != scenario.expectError { - t.Errorf("[%s] Expected hasSubmitErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasSubmitErr, submitErr) - } - - if id, ok := scenario.data["id"]; ok && id != "" && !hasSubmitErr { - _, err := app.Dao().FindRecordById(collection.Id, id) - if err != nil { - t.Errorf("[%s] Expected to find record with id %s, got %v", scenario.name, id, err) - } - } - } -} - -func TestRecordUpsertAuthRecord(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - name string - existingId string - data map[string]any - manageAccess bool - expectError bool - }{ - { - "empty create data", - "", - map[string]any{}, - false, - true, - }, - { - "empty update data", - "4q1xlclmfloku33", - map[string]any{}, - false, - false, - }, - { - "minimum valid create data", - "", - map[string]any{ - "password": "12345678", - "passwordConfirm": "12345678", + name: "base collection record", + data: map[string]any{ + "text": "test_text", + "custom": "123", // should be ignored + "number": "456", // should be normalized by the setter + "select_many+": []string{"optionB", "optionC"}, // test modifier fields + "created": "2022-01:01 10:00:00.000Z", // should be ignored + // ignore special auth fields + "oldPassword": "123", + "password": "456", + "passwordConfirm": "789", + }, + record: core.NewRecord(demo1Col), + expected: []string{ + `"text":"test_text"`, + `"number":456`, + `"select_many":["optionB","optionC"]`, + `"password":""`, + `"oldPassword":""`, + `"passwordConfirm":""`, + `"created":""`, + `"updated":""`, + `"json":null`, + }, + notExpected: []string{ + `"custom"`, + `"select_many-"`, + `"select_many+"`, }, - false, - false, }, { - "create with all allowed auth fields", - "", - map[string]any{ - "username": "test_new-a.b", - "email": "test_new@example.com", - "emailVisibility": true, - "password": "12345678", - "passwordConfirm": "12345678", + name: "auth collection record", + data: map[string]any{ + "email": "test@example.com", + // special auth fields + "oldPassword": "123", + "password": "456", + "passwordConfirm": "789", }, - false, - false, - }, - - // username - { - "invalid username characters", - "", - map[string]any{ - "username": "test abc!@#", - "password": "12345678", - "passwordConfirm": "12345678", + record: core.NewRecord(usersCol), + expected: []string{ + `"email":"test@example.com"`, + `"oldPassword":"123"`, + `"password":"456"`, + `"passwordConfirm":"789"`, }, - false, - true, }, { - "invalid username length (less than 3)", - "", - map[string]any{ - "username": "ab", - "password": "12345678", - "passwordConfirm": "12345678", + name: "hidden fields (manager)", + data: map[string]any{ + "email": "test@example.com", + "tokenKey": "abc", // should be ignored + // special auth fields + "password": "456", + "oldPassword": "123", + "passwordConfirm": "789", + }, + managerAccessLevel: true, + record: core.NewRecord(usersCol), + expected: []string{ + `"email":"test@example.com"`, + `"tokenKey":""`, + `"password":"456"`, + `"oldPassword":"123"`, + `"passwordConfirm":"789"`, }, - false, - true, }, { - "invalid username length (more than 150)", - "", - map[string]any{ - "username": strings.Repeat("a", 151), - "password": "12345678", - "passwordConfirm": "12345678", + name: "hidden fields (superuser)", + data: map[string]any{ + "email": "test@example.com", + "tokenKey": "abc", + // special auth fields + "password": "456", + "oldPassword": "123", + "passwordConfirm": "789", }, - false, - true, - }, - - // verified - { - "try to set verified without managed access", - "", - map[string]any{ - "verified": true, - "password": "12345678", - "passwordConfirm": "12345678", + superuserAccessLevel: true, + record: core.NewRecord(usersCol), + expected: []string{ + `"email":"test@example.com"`, + `"tokenKey":"abc"`, + `"password":"456"`, + `"oldPassword":"123"`, + `"passwordConfirm":"789"`, }, - false, - true, }, { - "try to update verified without managed access", - "4q1xlclmfloku33", - map[string]any{ - "verified": true, + name: "with file field", + data: map[string]any{ + "file_one": file, + "url": file, // should be ignored for non-file fields }, - false, - true, - }, - { - "set verified with managed access", - "", - map[string]any{ - "verified": true, - "password": "12345678", - "passwordConfirm": "12345678", + record: core.NewRecord(demo1Col), + expected: []string{ + `"file_one":{`, + `"originalName":"test.txt"`, + `"url":""`, }, - true, - false, - }, - { - "update verified with managed access", - "4q1xlclmfloku33", - map[string]any{ - "verified": true, - }, - true, - false, - }, - - // email - { - "try to update email without managed access", - "4q1xlclmfloku33", - map[string]any{ - "email": "test_update@example.com", - }, - false, - true, - }, - { - "update email with managed access", - "4q1xlclmfloku33", - map[string]any{ - "email": "test_update@example.com", - }, - true, - false, - }, - - // password - { - "trigger the password validations if only oldPassword is set", - "4q1xlclmfloku33", - map[string]any{ - "oldPassword": "1234567890", - }, - false, - true, - }, - { - "trigger the password validations if only passwordConfirm is set", - "4q1xlclmfloku33", - map[string]any{ - "passwordConfirm": "1234567890", - }, - false, - true, - }, - { - "try to update password without managed access", - "4q1xlclmfloku33", - map[string]any{ - "password": "1234567890", - "passwordConfirm": "1234567890", - }, - false, - true, - }, - { - "update password without managed access but with oldPassword", - "4q1xlclmfloku33", - map[string]any{ - "oldPassword": "1234567890", - "password": "1234567890", - "passwordConfirm": "1234567890", - }, - false, - false, - }, - { - "update email with managed access (without oldPassword)", - "4q1xlclmfloku33", - map[string]any{ - "password": "1234567890", - "passwordConfirm": "1234567890", - }, - true, - false, }, } for _, s := range scenarios { - collection, err := app.Dao().FindCollectionByNameOrId("users") + t.Run(s.name, func(t *testing.T) { + form := forms.NewRecordUpsert(testApp, s.record) + + if s.managerAccessLevel { + form.GrantManagerAccess() + } + + if s.superuserAccessLevel { + form.GrantSuperuserAccess() + } + + // ensure that the form access level was updated + if !form.HasManageAccess() && (s.superuserAccessLevel || s.managerAccessLevel) { + t.Fatalf("Expected the form to have manage access level (manager or superuser)") + } + + form.Load(s.data) + + loaded := map[string]any{ + "oldPassword": form.OldPassword, + "password": form.Password, + "passwordConfirm": form.PasswordConfirm, + } + maps.Copy(loaded, s.record.FieldsData()) + maps.Copy(loaded, s.record.CustomData()) + + raw, err := json.Marshal(loaded) + if err != nil { + t.Fatalf("Failed to serialize data: %v", err) + } + + rawStr := string(raw) + + for _, str := range s.expected { + if !strings.Contains(rawStr, str) { + t.Fatalf("Couldn't find %q in \n%v", str, rawStr) + } + } + + for _, str := range s.notExpected { + if strings.Contains(rawStr, str) { + t.Fatalf("Didn't expect %q in \n%v", str, rawStr) + } + } + }) + } +} + +func TestRecordUpsertDrySubmitFailure(t *testing.T) { + runTest := func(t *testing.T, testApp core.App) { + col, err := testApp.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - record := models.NewRecord(collection) - if s.existingId != "" { - var err error - record, err = app.Dao().FindRecordById(collection.Id, s.existingId) - if err != nil { - t.Errorf("[%s] Failed to fetch auth record with id %s", s.name, s.existingId) - continue - } + originalId := "imy661ixudk5izi" + + record, err := testApp.FindRecordById(col, originalId) + if err != nil { + t.Fatal(err) } - form := forms.NewRecordUpsert(app, record) - form.SetFullManageAccess(s.manageAccess) - if err := form.LoadData(s.data); err != nil { - t.Errorf("[%s] Failed to load form data", s.name) - continue + oldRaw, err := json.Marshal(record) + if err != nil { + t.Fatal(err) } - submitErr := form.Submit() - - hasErr := submitErr != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, submitErr) + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) } - if !hasErr && record.Username() == "" { - t.Errorf("[%s] Expected username to be set, got empty string: \n%v", s.name, record) + form := forms.NewRecordUpsert(testApp, record) + form.Load(map[string]any{ + "text": "test_update", + "file_one": file, + "select_one": "!invalid", // should be allowed even if invalid since validations are not executed + }) + + calls := "" + testApp.OnRecordValidate(col.Name).BindFunc(func(e *core.RecordEvent) error { + calls += "a" // shouldn't be called + return e.Next() + }) + + result := form.DrySubmit(func(txApp core.App, drySavedRecord *core.Record) error { + calls += "b" + return errors.New("error...") + }) + + if result == nil { + t.Fatal("Expected DrySubmit error, got nil") } + + if calls != "b" { + t.Fatalf("Expected calls %q, got %q", "ab", calls) + } + + // refresh the record to ensure that the changes weren't persisted + record, err = testApp.FindRecordById(col, originalId) + if err != nil { + t.Fatalf("Expected record with the original id %q to exist, got\n%v", originalId, record.PublicExport()) + } + + newRaw, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldRaw, newRaw) { + t.Fatalf("Expected record\n%s\ngot\n%s", oldRaw, newRaw) + } + + testFilesCount(t, testApp, record, 0) } + + t.Run("without parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + runTest(t, testApp) + }) + + t.Run("with parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + testApp.RunInTransaction(func(txApp core.App) error { + runTest(t, txApp) + return nil + }) + }) } -func TestRecordUpsertUniqueValidator(t *testing.T) { +func TestRecordUpsertDrySubmitCreateSuccess(t *testing.T) { + runTest := func(t *testing.T, testApp core.App) { + col, err := testApp.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + record := core.NewRecord(col) + + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(testApp, record) + form.Load(map[string]any{ + "id": "test", + "text": "test_update", + "file_one": file, + "select_one": "!invalid", // should be allowed even if invalid since validations are not executed + }) + + calls := "" + testApp.OnRecordValidate(col.Name).BindFunc(func(e *core.RecordEvent) error { + calls += "a" // shouldn't be called + return e.Next() + }) + + result := form.DrySubmit(func(txApp core.App, drySavedRecord *core.Record) error { + calls += "b" + return nil + }) + + if result != nil { + t.Fatalf("Expected DrySubmit success, got error: %v", result) + } + + if calls != "b" { + t.Fatalf("Expected calls %q, got %q", "ab", calls) + } + + // refresh the record to ensure that the changes weren't persisted + _, err = testApp.FindRecordById(col, record.Id) + if err == nil { + t.Fatal("Expected the created record to be deleted") + } + + testFilesCount(t, testApp, record, 0) + } + + t.Run("without parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + runTest(t, testApp) + }) + + t.Run("with parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + testApp.RunInTransaction(func(txApp core.App) error { + runTest(t, txApp) + return nil + }) + }) +} + +func TestRecordUpsertDrySubmitUpdateSuccess(t *testing.T) { + runTest := func(t *testing.T, testApp core.App) { + col, err := testApp.FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + record, err := testApp.FindRecordById(col, "imy661ixudk5izi") + if err != nil { + t.Fatal(err) + } + + oldRaw, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(testApp, record) + form.Load(map[string]any{ + "text": "test_update", + "file_one": file, + }) + + calls := "" + testApp.OnRecordValidate(col.Name).BindFunc(func(e *core.RecordEvent) error { + calls += "a" // shouldn't be called + return e.Next() + }) + + result := form.DrySubmit(func(txApp core.App, drySavedRecord *core.Record) error { + calls += "b" + return nil + }) + + if result != nil { + t.Fatalf("Expected DrySubmit success, got error: %v", result) + } + + if calls != "b" { + t.Fatalf("Expected calls %q, got %q", "ab", calls) + } + + // refresh the record to ensure that the changes weren't persisted + record, err = testApp.FindRecordById(col, record.Id) + if err != nil { + t.Fatal(err) + } + + newRaw, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldRaw, newRaw) { + t.Fatalf("Expected record\n%s\ngot\n%s", oldRaw, newRaw) + } + + testFilesCount(t, testApp, record, 0) + } + + t.Run("without parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + runTest(t, testApp) + }) + + t.Run("with parent transaction", func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + testApp.RunInTransaction(func(txApp core.App) error { + runTest(t, txApp) + return nil + }) + }) +} + +func TestRecordUpsertSubmitValidations(t *testing.T) { + t.Parallel() + app, _ := tests.NewTestApp() defer app.Cleanup() - // create a dummy collection - collection := &models.Collection{ - Name: "test", - Schema: schema.NewSchema( - &schema.SchemaField{ - Type: "text", - Name: "fieldA", - }, - &schema.SchemaField{ - Type: "text", - Name: "fieldB", - }, - &schema.SchemaField{ - Type: "text", - Name: "fieldC", - }, - ), - Indexes: types.JsonArray[string]{ - // the field case shouldn't matter - "create unique index unique_single_idx on test (fielda)", - "create unique index unique_combined_idx on test (fieldb, FIELDC)", - }, - } - if err := app.Dao().SaveCollection(collection); err != nil { + demo2Col, err := app.FindCollectionByNameOrId("demo2") + if err != nil { t.Fatal(err) } - dummyRecord := models.NewRecord(collection) - dummyRecord.Set("fieldA", "a") - dummyRecord.Set("fieldB", "b") - dummyRecord.Set("fieldC", "c") - if err := app.Dao().SaveRecord(dummyRecord); err != nil { + demo2Rec, err := app.FindRecordById(demo2Col, "llvuca81nly1qls") + if err != nil { + t.Fatal(err) + } + + usersCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + userRec, err := app.FindRecordById(usersCol, "4q1xlclmfloku33") + if err != nil { t.Fatal(err) } scenarios := []struct { name string + record *core.Record data map[string]any + managerAccess bool expectedErrors []string }{ + // base { - "duplicated unique value", - map[string]any{ - "fieldA": "a", - }, - []string{"fieldA"}, + name: "new base collection record with empty data", + record: core.NewRecord(demo2Col), + data: map[string]any{}, + expectedErrors: []string{"title"}, }, { - "duplicated combined unique value", - map[string]any{ - "fieldB": "b", - "fieldC": "c", + name: "new base collection record with invalid data", + record: core.NewRecord(demo2Col), + data: map[string]any{ + "title": "", + // should be ignored + "custom": "abc", + "oldPassword": "123", + "password": "456", + "passwordConfirm": "789", }, - []string{"fieldB", "fieldC"}, + expectedErrors: []string{"title"}, }, { - "non-duplicated unique value", - map[string]any{ - "fieldA": "a2", + name: "new base collection record with valid data", + record: core.NewRecord(demo2Col), + data: map[string]any{ + "title": "abc", + // should be ignored + "custom": "abc", + "oldPassword": "123", + "password": "456", + "passwordConfirm": "789", }, - nil, + expectedErrors: []string{}, }, { - "non-duplicated combined unique value", - map[string]any{ - "fieldB": "b", - "fieldC": "d", + name: "existing base collection record with empty data", + record: demo2Rec, + data: map[string]any{}, + expectedErrors: []string{}, + }, + { + name: "existing base collection record with invalid data", + record: demo2Rec, + data: map[string]any{ + "title": "", }, - nil, + expectedErrors: []string{"title"}, + }, + { + name: "existing base collection record with valid data", + record: demo2Rec, + data: map[string]any{ + "title": "abc", + }, + expectedErrors: []string{}, + }, + + // auth + { + name: "new auth collection record with empty data", + record: core.NewRecord(usersCol), + data: map[string]any{}, + expectedErrors: []string{"password", "passwordConfirm"}, + }, + { + name: "new auth collection record with invalid record and invalid form data (without manager acess)", + record: core.NewRecord(usersCol), + data: map[string]any{ + "verified": true, + "emailVisibility": true, + "email": "test@example.com", + "password": "456", + "passwordConfirm": "789", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + // fail the form validator + expectedErrors: []string{"verified", "passwordConfirm"}, + }, + { + name: "new auth collection record with invalid record and valid form data (without manager acess)", + record: core.NewRecord(usersCol), + data: map[string]any{ + "verified": false, + "emailVisibility": true, + "email": "test@example.com", + "password": "456", + "passwordConfirm": "456", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + // fail the record fields validator + expectedErrors: []string{"password", "username"}, + }, + { + name: "new auth collection record with invalid record and invalid form data (with manager acess)", + record: core.NewRecord(usersCol), + managerAccess: true, + data: map[string]any{ + "verified": true, + "emailVisibility": true, + "email": "test@example.com", + "password": "456", + "passwordConfirm": "789", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + // fail the form validator + expectedErrors: []string{"passwordConfirm"}, + }, + { + name: "new auth collection record with invalid record and valid form data (with manager acess)", + record: core.NewRecord(usersCol), + managerAccess: true, + data: map[string]any{ + "verified": true, + "emailVisibility": true, + "email": "test@example.com", + "password": "456", + "passwordConfirm": "456", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + // fail the record fields validator + expectedErrors: []string{"password", "username"}, + }, + { + name: "new auth collection record with valid data", + record: core.NewRecord(usersCol), + data: map[string]any{ + "emailVisibility": true, + "email": "test_new@example.com", + "password": "1234567890", + "passwordConfirm": "1234567890", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + expectedErrors: []string{}, + }, + { + name: "new auth collection record with valid data and duplicated email", + record: core.NewRecord(usersCol), + data: map[string]any{ + "email": "test@example.com", + "password": "1234567890", + "passwordConfirm": "1234567890", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + "oldPassword": "123", + }, + // fail the unique db validator + expectedErrors: []string{"email"}, + }, + { + name: "existing auth collection record with empty data", + record: userRec, + data: map[string]any{}, + expectedErrors: []string{}, + }, + { + name: "existing auth collection record with invalid record data and invalid form data (without manager access)", + record: userRec, + data: map[string]any{ + "verified": true, + "email": "test_new@example.com", // not allowed to change + "oldPassword": "123", + "password": "456", + "passwordConfirm": "789", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + }, + // fail form validator + expectedErrors: []string{"verified", "email", "oldPassword", "passwordConfirm"}, + }, + { + name: "existing auth collection record with invalid record data and valid form data (without manager access)", + record: userRec, + data: map[string]any{ + "oldPassword": "1234567890", + "password": "12345678901", + "passwordConfirm": "12345678901", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + }, + // fail record fields validator + expectedErrors: []string{"username"}, + }, + { + name: "existing auth collection record with invalid record data and invalid form data (with manager access)", + record: userRec, + managerAccess: true, + data: map[string]any{ + "verified": true, + "email": "test_new@example.com", + "oldPassword": "123", // should be ignored + "password": "456", + "passwordConfirm": "789", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + }, + // fail form validator + expectedErrors: []string{"passwordConfirm"}, + }, + { + name: "existing auth collection record with invalid record data and valid form data (with manager access)", + record: userRec, + managerAccess: true, + data: map[string]any{ + "verified": true, + "email": "test_new@example.com", + "oldPassword": "1234567890", + "password": "12345678901", + "passwordConfirm": "12345678901", + "username": "!invalid", + // should be ignored (custom or hidden fields) + "tokenKey": strings.Repeat("a", 2), + "custom": "abc", + }, + // fail record fields validator + expectedErrors: []string{"username"}, + }, + { + name: "existing auth collection record with base valid data", + record: userRec, + data: map[string]any{ + "name": "test", + }, + expectedErrors: []string{}, + }, + { + name: "existing auth collection record with valid password and invalid oldPassword data", + record: userRec, + data: map[string]any{ + "name": "test", + "oldPassword": "invalid", + "password": "1234567890", + "passwordConfirm": "1234567890", + }, + expectedErrors: []string{"oldPassword"}, + }, + { + name: "existing auth collection record with valid password data", + record: userRec, + data: map[string]any{ + "name": "test", + "oldPassword": "1234567890", + "password": "0987654321", + "passwordConfirm": "0987654321", + }, + expectedErrors: []string{}, }, } for _, s := range scenarios { - record := models.NewRecord(collection) + t.Run(s.name, func(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() - form := forms.NewRecordUpsert(app, record) - if err := form.LoadData(s.data); err != nil { - t.Errorf("[%s] Failed to load form data", s.name) - continue - } - - result := form.Submit() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("[%s] Failed to parse errors %v", s.name, result) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs) - continue - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs) - continue + form := forms.NewRecordUpsert(testApp, s.record.Original()) + if s.managerAccess { + form.GrantManagerAccess() } - } + form.Load(s.data) + + result := form.Submit() + + tests.TestValidationErrors(t, result, s.expectedErrors) + }) } } -func TestRecordUpsertAddAndRemoveFiles(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() +func TestRecordUpsertSubmitFailure(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() - recordBefore, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t") + col, err := testApp.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - // create test temp files - tempDir := filepath.Join(app.DataDir(), "temp") - if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - tmpFile, _ := os.CreateTemp(os.TempDir(), "tmpfile1-*.txt") - tmpFile.Close() - - form := forms.NewRecordUpsert(app, recordBefore) - - f1, err := filesystem.NewFileFromPath(tmpFile.Name()) + record, err := testApp.FindRecordById(col, "imy661ixudk5izi") if err != nil { t.Fatal(err) } - f2, err := filesystem.NewFileFromPath(tmpFile.Name()) + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") if err != nil { t.Fatal(err) } - f3, err := filesystem.NewFileFromPath(tmpFile.Name()) + form := forms.NewRecordUpsert(testApp, record) + form.Load(map[string]any{ + "text": "test_update", + "file_one": file, + "select_one": "invalid", + }) + + validateCalls := 0 + testApp.OnRecordValidate(col.Name).BindFunc(func(e *core.RecordEvent) error { + validateCalls++ + return e.Next() + }) + + result := form.Submit() + + if result == nil { + t.Fatal("Expected Submit error, got nil") + } + + if validateCalls != 1 { + t.Fatalf("Expected validateCalls %d, got %d", 1, validateCalls) + } + + // refresh the record to ensure that the changes weren't persisted + record, err = testApp.FindRecordById(col, record.Id) if err != nil { t.Fatal(err) } - removed0 := "test_d61b33QdDU.txt" // replaced - removed1 := "300_WlbFWSGmW9.png" - removed2 := "logo_vcfJJG5TAh.svg" - - form.AddFiles("file_one", f1) // should replace the existin file - - form.AddFiles("file_many", f2, f3) // should append - - form.RemoveFiles("file_many", removed1, removed2) // should remove - - filesToUpload := form.FilesToUpload() - if v, ok := filesToUpload["file_one"]; !ok || len(v) != 1 { - t.Fatalf("Expected filesToUpload[file_one] to have exactly 1 file, got %v", v) - } - if v, ok := filesToUpload["file_many"]; !ok || len(v) != 2 { - t.Fatalf("Expected filesToUpload[file_many] to have exactly 2 file, got %v", v) + if v := record.GetString("text"); v == "test_update" { + t.Fatalf("Expected record.text to remain the same, got %q", v) } - filesToDelete := form.FilesToDelete() - if len(filesToDelete) != 3 { - t.Fatalf("Expected exactly 2 file to delete, got %v", filesToDelete) - } - for _, f := range []string{removed0, removed1, removed2} { - if !list.ExistInSlice(f, filesToDelete) { - t.Fatalf("Missing file %q from filesToDelete %v", f, filesToDelete) - } + if v := record.GetString("select_one"); v != "" { + t.Fatalf("Expected record.select_one to remain the same, got %q", v) } - if err := form.Submit(); err != nil { - t.Fatalf("Failed to submit the RecordUpsert form, got %v", err) + if v := record.GetString("file_one"); v != "" { + t.Fatalf("Expected record.file_one to remain the same, got %q", v) } - recordAfter, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t") - if err != nil { - t.Fatal(err) - } - - // ensure files deletion - if hasRecordFile(app, recordAfter, removed0) { - t.Fatalf("Expected the old file_one file to be deleted") - } - if hasRecordFile(app, recordAfter, removed1) { - t.Fatalf("Expected %s to be deleted", removed1) - } - if hasRecordFile(app, recordAfter, removed2) { - t.Fatalf("Expected %s to be deleted", removed2) - } - - fileOne := recordAfter.GetStringSlice("file_one") - if len(fileOne) == 0 { - t.Fatalf("Expected new file_one file to be uploaded") - } - - fileMany := recordAfter.GetStringSlice("file_many") - if len(fileMany) != 5 { - t.Fatalf("Expected file_many to be 5, got %v", fileMany) - } + testFilesCount(t, testApp, record, 0) } -func TestRecordUpsertUploadFailure(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() +func TestRecordUpsertSubmitSuccess(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() - collection, err := app.Dao().FindCollectionByNameOrId("demo3") + col, err := testApp.FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } - testDaos := []*daos.Dao{ - app.Dao(), // with hooks - daos.New(app.Dao().DB()), // without hooks + record, err := testApp.FindRecordById(col, "imy661ixudk5izi") + if err != nil { + t.Fatal(err) } - for i, dao := range testDaos { - // create with invalid file - { - prefix := fmt.Sprintf("%d-create", i) + file, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") + if err != nil { + t.Fatal(err) + } - new := models.NewRecord(collection) - new.Id = "123456789012341" + form := forms.NewRecordUpsert(testApp, record) + form.Load(map[string]any{ + "text": "test_update", + "file_one": file, + "select_one": "optionC", + }) - form := forms.NewRecordUpsert(app, new) - form.SetDao(dao) - form.LoadData(map[string]any{"title": "new_test"}) - form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}}) + validateCalls := 0 + testApp.OnRecordValidate(col.Name).BindFunc(func(e *core.RecordEvent) error { + validateCalls++ + return e.Next() + }) - if err := form.Submit(); err == nil { - t.Fatalf("[%s] Expected error, got nil", prefix) - } + result := form.Submit() - if r, err := app.Dao().FindRecordById(collection.Id, new.Id); err == nil { - t.Fatalf("[%s] Expected the inserted record to be deleted, found \n%v", prefix, r.PublicExport()) - } - } + if result != nil { + t.Fatalf("Expected Submit success, got error: %v", result) + } - // update with invalid file - { - prefix := fmt.Sprintf("%d-update", i) + if validateCalls != 1 { + t.Fatalf("Expected validateCalls %d, got %d", 1, validateCalls) + } - record, err := app.Dao().FindRecordById(collection.Id, "1tmknxy2868d869") - if err != nil { - t.Fatal(err) - } + // refresh the record to ensure that the changes were persisted + record, err = testApp.FindRecordById(col, record.Id) + if err != nil { + t.Fatal(err) + } - form := forms.NewRecordUpsert(app, record) - form.SetDao(dao) - form.LoadData(map[string]any{"title": "update_test"}) - form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}}) + if v := record.GetString("text"); v != "test_update" { + t.Fatalf("Expected record.text %q, got %q", "test_update", v) + } - if err := form.Submit(); err == nil { - t.Fatalf("[%s] Expected error, got nil", prefix) - } + if v := record.GetString("select_one"); v != "optionC" { + t.Fatalf("Expected record.select_one %q, got %q", "optionC", v) + } - if r, _ := app.Dao().FindRecordById(collection.Id, record.Id); r == nil || r.GetString("title") == "update_test" { - t.Fatalf("[%s] Expected the record changes to be reverted, got \n%v", prefix, r.PublicExport()) - } - } + if v := record.GetString("file_one"); v != file.Name { + t.Fatalf("Expected record.file_one %q, got %q", file.Name, v) + } + + testFilesCount(t, testApp, record, 2) // the file + attrs +} + +// ------------------------------------------------------------------- + +func testFilesCount(t *testing.T, app core.App, record *core.Record, count int) { + storageDir := filepath.Join(app.DataDir(), "storage", record.Collection().Id, record.Id) + + entries, _ := os.ReadDir(storageDir) + if len(entries) != count { + t.Errorf("Expected %d entries, got %d\n%v", count, len(entries), entries) } } diff --git a/forms/record_verification_confirm.go b/forms/record_verification_confirm.go deleted file mode 100644 index 2d0f7ad5..00000000 --- a/forms/record_verification_confirm.go +++ /dev/null @@ -1,116 +0,0 @@ -package forms - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/spf13/cast" -) - -// RecordVerificationConfirm is an auth record email verification confirmation form. -type RecordVerificationConfirm struct { - app core.App - collection *models.Collection - dao *daos.Dao - - Token string `form:"token" json:"token"` -} - -// NewRecordVerificationConfirm creates a new [RecordVerificationConfirm] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordVerificationConfirm(app core.App, collection *models.Collection) *RecordVerificationConfirm { - return &RecordVerificationConfirm{ - app: app, - dao: app.Dao(), - collection: collection, - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordVerificationConfirm) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *RecordVerificationConfirm) Validate() error { - return validation.ValidateStruct(form, - validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), - ) -} - -func (form *RecordVerificationConfirm) checkToken(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - claims, _ := security.ParseUnverifiedJWT(v) - email := cast.ToString(claims["email"]) - if email == "" { - return validation.NewError("validation_invalid_token_claims", "Missing email token claim.") - } - - record, err := form.dao.FindAuthRecordByToken( - v, - form.app.Settings().RecordVerificationToken.Secret, - ) - if err != nil || record == nil { - return validation.NewError("validation_invalid_token", "Invalid or expired token.") - } - - if record.Collection().Id != form.collection.Id { - return validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.") - } - - if record.Email() != email { - return validation.NewError("validation_token_email_mismatch", "The record email doesn't match with the requested token claims.") - } - - return nil -} - -// Submit validates and submits the form. -// On success returns the verified auth record associated to `form.Token`. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *RecordVerificationConfirm) Submit(interceptors ...InterceptorFunc[*models.Record]) (*models.Record, error) { - if err := form.Validate(); err != nil { - return nil, err - } - - record, err := form.dao.FindAuthRecordByToken( - form.Token, - form.app.Settings().RecordVerificationToken.Secret, - ) - if err != nil { - return nil, err - } - - wasVerified := record.Verified() - - if !wasVerified { - record.SetVerified(true) - } - - interceptorsErr := runInterceptors(record, func(m *models.Record) error { - record = m - - if wasVerified { - return nil // already verified - } - - return form.dao.SaveRecord(m) - }, interceptors...) - - if interceptorsErr != nil { - return nil, interceptorsErr - } - - return record, nil -} diff --git a/forms/record_verification_confirm_test.go b/forms/record_verification_confirm_test.go deleted file mode 100644 index ba4dcf38..00000000 --- a/forms/record_verification_confirm_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestRecordVerificationConfirmValidateAndSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectError bool - }{ - // empty data (Validate call check) - { - `{}`, - true, - }, - // expired token (Validate call check) - { - `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoxNjQwOTkxNjYxfQ.Avbt9IP8sBisVz_2AGrlxLDvangVq4PhL2zqQVYLKlE"}`, - true, - }, - // valid token (already verified record) - { - `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsImVtYWlsIjoidGVzdDJAZXhhbXBsZS5jb20iLCJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJ0eXBlIjoiYXV0aFJlY29yZCIsImV4cCI6MjIwODk4NTI2MX0.PsOABmYUzGbd088g8iIBL4-pf7DUZm0W5Ju6lL5JVRg"}`, - false, - }, - // valid token (unverified record) - { - `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.hL16TVmStHFdHLc4a860bRqJ3sFfzjv0_NRNzwsvsrc"}`, - false, - }, - } - - for i, s := range scenarios { - form := forms.NewRecordVerificationConfirm(testApp, authCollection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - record, err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if s.expectError { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - if hasErr { - continue - } - - claims, _ := security.ParseUnverifiedJWT(form.Token) - tokenRecordId := claims["id"] - - if record.Id != tokenRecordId { - t.Errorf("(%d) Expected record.Id %q, got %q", i, tokenRecordId, record.Id) - } - - if !record.Verified() { - t.Errorf("(%d) Expected record.Verified() to be true, got false", i) - } - } -} - -func TestRecordVerificationConfirmInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordVerificationConfirm(testApp, authCollection) - form.Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImNvbGxlY3Rpb25JZCI6Il9wYl91c2Vyc19hdXRoXyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiZXhwIjoyMjA4OTg1MjYxfQ.hL16TVmStHFdHLc4a860bRqJ3sFfzjv0_NRNzwsvsrc" - interceptorVerified := authRecord.Verified() - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorVerified = record.Verified() - interceptor2Called = true - return testErr - } - } - - _, submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorVerified == authRecord.Verified() { - t.Fatalf("Expected the form model to be filled before calling the interceptors") - } -} diff --git a/forms/record_verification_request.go b/forms/record_verification_request.go deleted file mode 100644 index 09b46799..00000000 --- a/forms/record_verification_request.go +++ /dev/null @@ -1,101 +0,0 @@ -package forms - -import ( - "errors" - "time" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" -) - -// RecordVerificationRequest is an auth record email verification request form. -type RecordVerificationRequest struct { - app core.App - collection *models.Collection - dao *daos.Dao - resendThreshold float64 // in seconds - - Email string `form:"email" json:"email"` -} - -// NewRecordVerificationRequest creates a new [RecordVerificationRequest] -// form initialized with from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewRecordVerificationRequest(app core.App, collection *models.Collection) *RecordVerificationRequest { - return &RecordVerificationRequest{ - app: app, - dao: app.Dao(), - collection: collection, - resendThreshold: 120, // 2 min - } -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *RecordVerificationRequest) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -// -// // This method doesn't verify that auth record with `form.Email` exists (this is done on Submit). -func (form *RecordVerificationRequest) Validate() error { - return validation.ValidateStruct(form, - validation.Field( - &form.Email, - validation.Required, - validation.Length(1, 255), - is.EmailFormat, - ), - ) -} - -// Submit validates and sends a verification request email -// to the `form.Email` auth record. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *RecordVerificationRequest) Submit(interceptors ...InterceptorFunc[*models.Record]) error { - if err := form.Validate(); err != nil { - return err - } - - record, err := form.dao.FindFirstRecordByData( - form.collection.Id, - schema.FieldNameEmail, - form.Email, - ) - if err != nil { - return err - } - - if !record.Verified() { - now := time.Now().UTC() - lastVerificationSentAt := record.LastVerificationSentAt().Time() - if (now.Sub(lastVerificationSentAt)).Seconds() < form.resendThreshold { - return errors.New("A verification email was already sent.") - } - } - - return runInterceptors(record, func(m *models.Record) error { - if m.Verified() { - return nil // already verified - } - - if err := mails.SendRecordVerification(form.app, m); err != nil { - return err - } - - // update last sent timestamp - m.SetLastVerificationSentAt(types.NowDateTime()) - - return form.dao.SaveRecord(m) - }, interceptors...) -} diff --git a/forms/record_verification_request_test.go b/forms/record_verification_request_test.go deleted file mode 100644 index 03b48372..00000000 --- a/forms/record_verification_request_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "testing" - "time" - - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestRecordVerificationRequestSubmit(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("clients") - if err != nil { - t.Fatal(err) - } - - scenarios := []struct { - jsonData string - expectError bool - expectMail bool - }{ - // empty field (Validate call check) - { - `{"email":""}`, - true, - false, - }, - // invalid email field (Validate call check) - { - `{"email":"invalid"}`, - true, - false, - }, - // nonexisting user - { - `{"email":"missing@example.com"}`, - true, - false, - }, - // existing user (already verified) - { - `{"email":"test@example.com"}`, - false, - false, - }, - // existing user (already verified) - repeating request to test threshod skip - { - `{"email":"test@example.com"}`, - false, - false, - }, - // existing user (unverified) - { - `{"email":"test2@example.com"}`, - false, - true, - }, - // existing user (inverified) - reached send threshod - { - `{"email":"test2@example.com"}`, - true, - false, - }, - } - - now := types.NowDateTime() - time.Sleep(1 * time.Millisecond) - - for i, s := range scenarios { - testApp.TestMailer.TotalSend = 0 // reset - form := forms.NewRecordVerificationRequest(testApp, authCollection) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("[%d] Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(r *models.Record) error { - interceptorCalls++ - return next(r) - } - } - - err := form.Submit(interceptor) - - // check interceptor calls - expectInterceptorCalls := 1 - if s.expectError { - expectInterceptorCalls = 0 - } - if interceptorCalls != expectInterceptorCalls { - t.Errorf("[%d] Expected interceptor to be called %d, got %d", i, expectInterceptorCalls, interceptorCalls) - } - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%d] Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - expectedMails := 0 - if s.expectMail { - expectedMails = 1 - } - if testApp.TestMailer.TotalSend != expectedMails { - t.Errorf("[%d] Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) - } - - if s.expectError { - continue - } - - user, err := testApp.Dao().FindAuthRecordByEmail(authCollection.Id, form.Email) - if err != nil { - t.Errorf("[%d] Expected user with email %q to exist, got nil", i, form.Email) - continue - } - - // check whether LastVerificationSentAt was updated - if !user.Verified() && user.LastVerificationSentAt().Time().Sub(now.Time()) < 0 { - t.Errorf("[%d] Expected LastVerificationSentAt to be after %v, got %v", i, now, user.LastVerificationSentAt()) - } - } -} - -func TestRecordVerificationRequestInterceptors(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - authCollection, err := testApp.Dao().FindCollectionByNameOrId("users") - if err != nil { - t.Fatal(err) - } - - authRecord, err := testApp.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - form := forms.NewRecordVerificationRequest(testApp, authCollection) - form.Email = authRecord.Email() - interceptorLastVerificationSentAt := authRecord.LastVerificationSentAt() - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptor1Called = true - return next(record) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] { - return func(record *models.Record) error { - interceptorLastVerificationSentAt = record.LastVerificationSentAt() - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } - - if interceptorLastVerificationSentAt.String() != authRecord.LastVerificationSentAt().String() { - t.Fatalf("Expected the form model to NOT be filled before calling the interceptors") - } -} diff --git a/forms/settings_upsert.go b/forms/settings_upsert.go deleted file mode 100644 index 9d14705d..00000000 --- a/forms/settings_upsert.go +++ /dev/null @@ -1,90 +0,0 @@ -package forms - -import ( - "os" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tools/types" -) - -// SettingsUpsert is a [settings.Settings] upsert (create/update) form. -type SettingsUpsert struct { - *settings.Settings - - app core.App - dao *daos.Dao -} - -// NewSettingsUpsert creates a new [SettingsUpsert] form with initializer -// config created from the provided [core.App] instance. -// -// If you want to submit the form as part of a transaction, -// you can change the default Dao via [SetDao()]. -func NewSettingsUpsert(app core.App) *SettingsUpsert { - form := &SettingsUpsert{ - app: app, - dao: app.Dao(), - } - - // load the application settings into the form - form.Settings, _ = app.Settings().Clone() - - return form -} - -// SetDao replaces the default form Dao instance with the provided one. -func (form *SettingsUpsert) SetDao(dao *daos.Dao) { - form.dao = dao -} - -// Validate makes the form validatable by implementing [validation.Validatable] interface. -func (form *SettingsUpsert) Validate() error { - return form.Settings.Validate() -} - -// Submit validates the form and upserts the loaded settings. -// -// On success the app settings will be refreshed with the form ones. -// -// You can optionally provide a list of InterceptorFunc to further -// modify the form behavior before persisting it. -func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc[*settings.Settings]) error { - if err := form.Validate(); err != nil { - return err - } - - return runInterceptors(form.Settings, func(s *settings.Settings) error { - form.Settings = s - - // persists settings change - encryptionKey := os.Getenv(form.app.EncryptionEnv()) - if err := form.dao.SaveSettings(form.Settings, encryptionKey); err != nil { - return err - } - - // reload app settings - if err := form.app.RefreshSettings(); err != nil { - return err - } - - // try to clear old logs not matching the new settings - createdBefore := time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays).UTC().Format(types.DefaultDateLayout) - expr := dbx.NewExp("[[created]] <= {:date} OR [[level]] < {:level}", dbx.Params{ - "date": createdBefore, - "level": form.Settings.Logs.MinLevel, - }) - form.app.LogsDao().NonconcurrentDB().Delete((&models.Log{}).TableName(), expr).Execute() - - // no logs are allowed -> try to reclaim preserved disk space after the previous delete operation - if form.Settings.Logs.MaxDays == 0 { - form.app.LogsDao().Vacuum() - } - - return nil - }, interceptors...) -} diff --git a/forms/settings_upsert_test.go b/forms/settings_upsert_test.go deleted file mode 100644 index fee6fbcf..00000000 --- a/forms/settings_upsert_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package forms_test - -import ( - "encoding/json" - "errors" - "os" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/forms" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/security" -) - -func TestNewSettingsUpsert(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - app.Settings().Meta.AppName = "name_update" - - form := forms.NewSettingsUpsert(app) - - formSettings, _ := json.Marshal(form.Settings) - appSettings, _ := json.Marshal(app.Settings()) - - if string(formSettings) != string(appSettings) { - t.Errorf("Expected settings \n%s, got \n%s", string(appSettings), string(formSettings)) - } -} - -func TestSettingsUpsertValidateAndSubmit(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - jsonData string - encryption bool - expectedErrors []string - }{ - // empty (plain) - {"{}", false, nil}, - // empty (encrypt) - {"{}", true, nil}, - // failure - invalid data - { - `{"meta": {"appName": ""}, "logs": {"maxDays": -1}}`, - false, - []string{"meta", "logs"}, - }, - // success - valid data (plain) - { - `{"meta": {"appName": "test"}, "logs": {"maxDays": 0}}`, - false, - nil, - }, - // success - valid data (encrypt) - { - `{"meta": {"appName": "test"}, "logs": {"maxDays": 7}}`, - true, - nil, - }, - } - - for i, s := range scenarios { - if s.encryption { - os.Setenv(app.EncryptionEnv(), security.RandomString(32)) - } else { - os.Unsetenv(app.EncryptionEnv()) - } - - form := forms.NewSettingsUpsert(app) - - // load data - loadErr := json.Unmarshal([]byte(s.jsonData), form) - if loadErr != nil { - t.Errorf("(%d) Failed to load form data: %v", i, loadErr) - continue - } - - interceptorCalls := 0 - interceptor := func(next forms.InterceptorNextFunc[*settings.Settings]) forms.InterceptorNextFunc[*settings.Settings] { - return func(s *settings.Settings) error { - interceptorCalls++ - return next(s) - } - } - - // parse errors - result := form.Submit(interceptor) - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("(%d) Failed to parse errors %v", i, result) - continue - } - - // check interceptor calls - expectInterceptorCall := 1 - if len(s.expectedErrors) > 0 { - expectInterceptorCall = 0 - } - if interceptorCalls != expectInterceptorCall { - t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - } - } - - if len(s.expectedErrors) > 0 { - continue - } - - formSettings, _ := json.Marshal(form.Settings) - appSettings, _ := json.Marshal(app.Settings()) - - if string(formSettings) != string(appSettings) { - t.Errorf("Expected app settings \n%s, got \n%s", string(appSettings), string(formSettings)) - } - } -} - -func TestSettingsUpsertSubmitInterceptors(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - form := forms.NewSettingsUpsert(app) - form.Meta.AppName = "test_new" - - testErr := errors.New("test_error") - - interceptor1Called := false - interceptor1 := func(next forms.InterceptorNextFunc[*settings.Settings]) forms.InterceptorNextFunc[*settings.Settings] { - return func(s *settings.Settings) error { - interceptor1Called = true - return next(s) - } - } - - interceptor2Called := false - interceptor2 := func(next forms.InterceptorNextFunc[*settings.Settings]) forms.InterceptorNextFunc[*settings.Settings] { - return func(s *settings.Settings) error { - interceptor2Called = true - return testErr - } - } - - submitErr := form.Submit(interceptor1, interceptor2) - if submitErr != testErr { - t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) - } - - if !interceptor1Called { - t.Fatalf("Expected interceptor1 to be called") - } - - if !interceptor2Called { - t.Fatalf("Expected interceptor2 to be called") - } -} diff --git a/forms/test_email_send.go b/forms/test_email_send.go index 5dd902e4..5c1d51e7 100644 --- a/forms/test_email_send.go +++ b/forms/test_email_send.go @@ -1,26 +1,29 @@ package forms import ( + "errors" + validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" ) const ( - templateVerification = "verification" - templatePasswordReset = "password-reset" - templateEmailChange = "email-change" + TestTemplateVerification = "verification" + TestTemplatePasswordReset = "password-reset" + TestTemplateEmailChange = "email-change" + TestTemplateOTP = "otp" + TestTemplateAuthAlert = "login-alert" ) // TestEmailSend is a email template test request form. type TestEmailSend struct { app core.App - Template string `form:"template" json:"template"` - Email string `form:"email" json:"email"` + Email string `form:"email" json:"email"` + Template string `form:"template" json:"template"` + Collection string `form:"collection" json:"collection"` // optional, fallbacks to _superusers } // NewTestEmailSend creates and initializes new TestEmailSend form. @@ -31,6 +34,11 @@ func NewTestEmailSend(app core.App) *TestEmailSend { // Validate makes the form validatable by implementing [validation.Validatable] interface. func (form *TestEmailSend) Validate() error { return validation.ValidateStruct(form, + validation.Field( + &form.Collection, + validation.Length(1, 255), + validation.By(form.checkAuthCollection), + ), validation.Field( &form.Email, validation.Required, @@ -40,38 +48,69 @@ func (form *TestEmailSend) Validate() error { validation.Field( &form.Template, validation.Required, - validation.In(templateVerification, templatePasswordReset, templateEmailChange), + validation.In( + TestTemplateVerification, + TestTemplatePasswordReset, + TestTemplateEmailChange, + TestTemplateOTP, + TestTemplateAuthAlert, + ), ), ) } +func (form *TestEmailSend) checkAuthCollection(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + c, _ := form.app.FindCollectionByNameOrId(v) + if c == nil || !c.IsAuth() { + return validation.NewError("validation_invalid_auth_collection", "Must be a valid auth collection id or name.") + } + + return nil +} + // Submit validates and sends a test email to the form.Email address. func (form *TestEmailSend) Submit() error { if err := form.Validate(); err != nil { return err } - // create a test auth record - collection := &models.Collection{ - BaseModel: models.BaseModel{Id: "__pb_test_collection_id__"}, - Name: "__pb_test_collection_name__", - Type: models.CollectionTypeAuth, + collectionIdOrName := form.Collection + if collectionIdOrName == "" { + collectionIdOrName = core.CollectionNameSuperusers } - record := models.NewRecord(collection) - record.Id = "__pb_test_id__" - record.Set(schema.FieldNameUsername, "pb_test") - record.Set(schema.FieldNameEmail, form.Email) + collection, err := form.app.FindCollectionByNameOrId(collectionIdOrName) + if err != nil { + return err + } + + record := core.NewRecord(collection) + for _, field := range collection.Fields { + if field.GetHidden() { + continue + } + record.Set(field.GetName(), "__pb_test_"+field.GetName()+"__") + } record.RefreshTokenKey() + record.SetEmail(form.Email) switch form.Template { - case templateVerification: + case TestTemplateVerification: return mails.SendRecordVerification(form.app, record) - case templatePasswordReset: + case TestTemplatePasswordReset: return mails.SendRecordPasswordReset(form.app, record) - case templateEmailChange: + case TestTemplateEmailChange: return mails.SendRecordChangeEmail(form.app, record, form.Email) + case TestTemplateOTP: + return mails.SendRecordOTP(form.app, record, "OTP_ID", "123456") + case TestTemplateAuthAlert: + return mails.SendRecordAuthAlert(form.app, record) + default: + return errors.New("unknown template " + form.Template) } - - return nil } diff --git a/forms/test_email_send_test.go b/forms/test_email_send_test.go index 4bae5ee3..0d58595b 100644 --- a/forms/test_email_send_test.go +++ b/forms/test_email_send_test.go @@ -1,6 +1,7 @@ package forms_test import ( + "fmt" "strings" "testing" @@ -15,43 +16,46 @@ func TestEmailSendValidateAndSubmit(t *testing.T) { scenarios := []struct { template string email string + collection string expectedErrors []string }{ - {"", "", []string{"template", "email"}}, - {"invalid", "test@example.com", []string{"template"}}, - {"verification", "invalid", []string{"email"}}, - {"verification", "test@example.com", nil}, - {"password-reset", "test@example.com", nil}, - {"email-change", "test@example.com", nil}, + {"", "", "", []string{"template", "email"}}, + {"invalid", "test@example.com", "", []string{"template"}}, + {forms.TestTemplateVerification, "invalid", "", []string{"email"}}, + {forms.TestTemplateVerification, "test@example.com", "invalid", []string{"collection"}}, + {forms.TestTemplateVerification, "test@example.com", "demo1", []string{"collection"}}, + {forms.TestTemplateVerification, "test@example.com", "users", nil}, + {forms.TestTemplatePasswordReset, "test@example.com", "", nil}, + {forms.TestTemplateEmailChange, "test@example.com", "", nil}, + {forms.TestTemplateOTP, "test@example.com", "", nil}, + {forms.TestTemplateAuthAlert, "test@example.com", "", nil}, } for i, s := range scenarios { - func() { + t.Run(fmt.Sprintf("%d_%s", i, s.template), func(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() form := forms.NewTestEmailSend(app) form.Email = s.email form.Template = s.template + form.Collection = s.collection result := form.Submit() // parse errors errs, ok := result.(validation.Errors) if !ok && result != nil { - t.Errorf("(%d) Failed to parse errors %v", i, result) - return + t.Fatalf("Failed to parse errors %v", result) } // check errors if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - return + t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) } for _, k := range s.expectedErrors { if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - return + t.Fatalf("Missing expected error key %q in %v", k, errs) } } @@ -60,24 +64,33 @@ func TestEmailSendValidateAndSubmit(t *testing.T) { expectedEmails = 0 } - if app.TestMailer.TotalSend != expectedEmails { - t.Errorf("(%d) Expected %d email(s) to be sent, got %d", i, expectedEmails, app.TestMailer.TotalSend) + if app.TestMailer.TotalSend() != expectedEmails { + t.Fatalf("Expected %d email(s) to be sent, got %d", expectedEmails, app.TestMailer.TotalSend()) } if len(s.expectedErrors) > 0 { return } - expectedContent := "Verify" - if s.template == "password-reset" { + var expectedContent string + switch s.template { + case forms.TestTemplatePasswordReset: expectedContent = "Reset password" - } else if s.template == "email-change" { + case forms.TestTemplateEmailChange: expectedContent = "Confirm new email" + case forms.TestTemplateVerification: + expectedContent = "Verify" + case forms.TestTemplateOTP: + expectedContent = "one-time password" + case forms.TestTemplateAuthAlert: + expectedContent = "from a new location" + default: + expectedContent = "__UNKNOWN_TEMPLATE__" } - if !strings.Contains(app.TestMailer.LastMessage.HTML, expectedContent) { - t.Errorf("(%d) Expected the email to contains %s, got \n%v", i, expectedContent, app.TestMailer.LastMessage.HTML) + if !strings.Contains(app.TestMailer.LastMessage().HTML, expectedContent) { + t.Errorf("Expected the email to contains %q, got\n%v", expectedContent, app.TestMailer.LastMessage().HTML) } - }() + }) } } diff --git a/forms/test_s3_filesystem.go b/forms/test_s3_filesystem.go index c2e26e59..c39c59ed 100644 --- a/forms/test_s3_filesystem.go +++ b/forms/test_s3_filesystem.go @@ -6,7 +6,6 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models/settings" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/security" ) @@ -46,7 +45,7 @@ func (form *TestS3Filesystem) Submit() error { return err } - var s3Config settings.S3Config + var s3Config core.S3Config if form.Filesystem == s3FilesystemBackups { s3Config = form.app.Settings().Backups.S3 diff --git a/forms/test_s3_filesystem_test.go b/forms/test_s3_filesystem_test.go index 71453705..391cef7d 100644 --- a/forms/test_s3_filesystem_test.go +++ b/forms/test_s3_filesystem_test.go @@ -11,9 +11,6 @@ import ( func TestS3FilesystemValidate(t *testing.T) { t.Parallel() - app, _ := tests.NewTestApp() - defer app.Cleanup() - scenarios := []struct { name string filesystem string @@ -42,28 +39,31 @@ func TestS3FilesystemValidate(t *testing.T) { } for _, s := range scenarios { - form := forms.NewTestS3Filesystem(app) - form.Filesystem = s.filesystem + t.Run(s.name, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() - result := form.Validate() + form := forms.NewTestS3Filesystem(app) + form.Filesystem = s.filesystem - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("[%s] Failed to parse errors %v", s.name, result) - continue - } + result := form.Validate() - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs) - continue - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs) + // parse errors + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Fatalf("Failed to parse errors %v", result) } - } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Fatalf("Missing expected error key %q in %v", k, errs) + } + } + }) } } diff --git a/forms/validators/file_test.go b/forms/validators/file_test.go deleted file mode 100644 index 07b5ec98..00000000 --- a/forms/validators/file_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package validators_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/rest" -) - -func TestUploadedFileSize(t *testing.T) { - t.Parallel() - - data, mp, err := tests.MockMultipartData(nil, "test") - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest(http.MethodPost, "/", data) - req.Header.Add("Content-Type", mp.FormDataContentType()) - - files, err := rest.FindUploadedFiles(req, "test") - if err != nil { - t.Fatal(err) - } - - if len(files) != 1 { - t.Fatalf("Expected one test file, got %d", len(files)) - } - - scenarios := []struct { - maxBytes int - file *filesystem.File - expectError bool - }{ - {0, nil, false}, - {4, nil, false}, - {3, files[0], true}, // all test files have "test" as content - {4, files[0], false}, - {5, files[0], false}, - } - - for i, s := range scenarios { - err := validators.UploadedFileSize(s.maxBytes)(s.file) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} - -func TestUploadedFileMimeType(t *testing.T) { - t.Parallel() - - data, mp, err := tests.MockMultipartData(nil, "test") - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest(http.MethodPost, "/", data) - req.Header.Add("Content-Type", mp.FormDataContentType()) - - files, err := rest.FindUploadedFiles(req, "test") - if err != nil { - t.Fatal(err) - } - - if len(files) != 1 { - t.Fatalf("Expected one test file, got %d", len(files)) - } - - scenarios := []struct { - types []string - file *filesystem.File - expectError bool - }{ - {nil, nil, false}, - {[]string{"image/jpeg"}, nil, false}, - {[]string{}, files[0], true}, - {[]string{"image/jpeg"}, files[0], true}, - // test files are detected as "text/plain; charset=utf-8" content type - {[]string{"image/jpeg", "text/plain; charset=utf-8"}, files[0], false}, - } - - for i, s := range scenarios { - err := validators.UploadedFileMimeType(s.types)(s.file) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} diff --git a/forms/validators/model.go b/forms/validators/model.go deleted file mode 100644 index 035c1239..00000000 --- a/forms/validators/model.go +++ /dev/null @@ -1,39 +0,0 @@ -package validators - -import ( - "database/sql" - "errors" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" -) - -// UniqueId checks whether the provided model id already exists. -// -// Example: -// -// validation.Field(&form.Id, validation.By(validators.UniqueId(form.dao, tableName))) -func UniqueId(dao *daos.Dao, tableName string) validation.RuleFunc { - return func(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - var foundId string - - err := dao.DB(). - Select("id"). - From(tableName). - Where(dbx.HashExp{"id": v}). - Limit(1). - Row(&foundId) - - if (err != nil && !errors.Is(err, sql.ErrNoRows)) || foundId != "" { - return validation.NewError("validation_invalid_id", "The model id is invalid or already exists.") - } - - return nil - } -} diff --git a/forms/validators/model_test.go b/forms/validators/model_test.go deleted file mode 100644 index f2759ebe..00000000 --- a/forms/validators/model_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package validators_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/tests" -) - -func TestUniqueId(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - scenarios := []struct { - id string - tableName string - expectError bool - }{ - {"", "", false}, - {"test", "", true}, - {"wsmn24bux7wo113", "_collections", true}, - {"test_unique_id", "unknown_table", true}, - {"test_unique_id", "_collections", false}, - } - - for i, s := range scenarios { - err := validators.UniqueId(app.Dao(), s.tableName)(s.id) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} diff --git a/forms/validators/record_data.go b/forms/validators/record_data.go deleted file mode 100644 index 9c4232f8..00000000 --- a/forms/validators/record_data.go +++ /dev/null @@ -1,393 +0,0 @@ -package validators - -import ( - "fmt" - "net/url" - "regexp" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" -) - -var requiredErr = validation.NewError("validation_required", "Missing required value") - -// NewRecordDataValidator creates new [models.Record] data validator -// using the provided record constraints and schema. -// -// Example: -// -// validator := NewRecordDataValidator(app.Dao(), record, nil) -// err := validator.Validate(map[string]any{"test":123}) -func NewRecordDataValidator( - dao *daos.Dao, - record *models.Record, - uploadedFiles map[string][]*filesystem.File, -) *RecordDataValidator { - return &RecordDataValidator{ - dao: dao, - record: record, - uploadedFiles: uploadedFiles, - } -} - -// RecordDataValidator defines a model.Record data validator -// using the provided record constraints and schema. -type RecordDataValidator struct { - dao *daos.Dao - record *models.Record - uploadedFiles map[string][]*filesystem.File -} - -// Validate validates the provided `data` by checking it against -// the validator record constraints and schema. -func (validator *RecordDataValidator) Validate(data map[string]any) error { - keyedSchema := validator.record.Collection().Schema.AsMap() - if len(keyedSchema) == 0 { - return nil // no fields to check - } - - if len(data) == 0 { - return validation.NewError("validation_empty_data", "No data to validate") - } - - errs := validation.Errors{} - - // check for unknown fields - for key := range data { - if _, ok := keyedSchema[key]; !ok { - errs[key] = validation.NewError("validation_unknown_field", "Unknown field") - } - } - if len(errs) > 0 { - return errs - } - - for key, field := range keyedSchema { - // normalize value to emulate the same behavior - // when fetching or persisting the record model - value := field.PrepareValue(data[key]) - - // check required constraint - if field.Required && validation.Required.Validate(value) != nil { - errs[key] = requiredErr - continue - } - - // validate field value by its field type - if err := validator.checkFieldValue(field, value); err != nil { - errs[key] = err - continue - } - } - - if len(errs) == 0 { - return nil - } - - return errs -} - -func (validator *RecordDataValidator) checkFieldValue(field *schema.SchemaField, value any) error { - switch field.Type { - case schema.FieldTypeText: - return validator.checkTextValue(field, value) - case schema.FieldTypeNumber: - return validator.checkNumberValue(field, value) - case schema.FieldTypeBool: - return validator.checkBoolValue(field, value) - case schema.FieldTypeEmail: - return validator.checkEmailValue(field, value) - case schema.FieldTypeUrl: - return validator.checkUrlValue(field, value) - case schema.FieldTypeEditor: - return validator.checkEditorValue(field, value) - case schema.FieldTypeDate: - return validator.checkDateValue(field, value) - case schema.FieldTypeSelect: - return validator.checkSelectValue(field, value) - case schema.FieldTypeJson: - return validator.checkJsonValue(field, value) - case schema.FieldTypeFile: - return validator.checkFileValue(field, value) - case schema.FieldTypeRelation: - return validator.checkRelationValue(field, value) - } - - return nil -} - -func (validator *RecordDataValidator) checkTextValue(field *schema.SchemaField, value any) error { - val, _ := value.(string) - if val == "" { - return nil // nothing to check (skip zero-defaults) - } - - options, _ := field.Options.(*schema.TextOptions) - - // note: casted to []rune to count multi-byte chars as one - length := len([]rune(val)) - - if options.Min != nil && length < *options.Min { - return validation.NewError("validation_min_text_constraint", fmt.Sprintf("Must be at least %d character(s)", *options.Min)) - } - - if options.Max != nil && length > *options.Max { - return validation.NewError("validation_max_text_constraint", fmt.Sprintf("Must be less than %d character(s)", *options.Max)) - } - - if options.Pattern != "" { - match, _ := regexp.MatchString(options.Pattern, val) - if !match { - return validation.NewError("validation_invalid_format", "Invalid value format") - } - } - - return nil -} - -func (validator *RecordDataValidator) checkNumberValue(field *schema.SchemaField, value any) error { - val, _ := value.(float64) - if val == 0 { - return nil // nothing to check (skip zero-defaults) - } - - options, _ := field.Options.(*schema.NumberOptions) - - if options.NoDecimal && val != float64(int64(val)) { - return validation.NewError("validation_no_decimal_constraint", "Decimal numbers are not allowed") - } - - if options.Min != nil && val < *options.Min { - return validation.NewError("validation_min_number_constraint", fmt.Sprintf("Must be larger than %f", *options.Min)) - } - - if options.Max != nil && val > *options.Max { - return validation.NewError("validation_max_number_constraint", fmt.Sprintf("Must be less than %f", *options.Max)) - } - - return nil -} - -func (validator *RecordDataValidator) checkBoolValue(field *schema.SchemaField, value any) error { - return nil -} - -func (validator *RecordDataValidator) checkEmailValue(field *schema.SchemaField, value any) error { - val, _ := value.(string) - if val == "" { - return nil // nothing to check - } - - if is.EmailFormat.Validate(val) != nil { - return validation.NewError("validation_invalid_email", "Must be a valid email") - } - - options, _ := field.Options.(*schema.EmailOptions) - domain := val[strings.LastIndex(val, "@")+1:] - - // only domains check - if len(options.OnlyDomains) > 0 && !list.ExistInSlice(domain, options.OnlyDomains) { - return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") - } - - // except domains check - if len(options.ExceptDomains) > 0 && list.ExistInSlice(domain, options.ExceptDomains) { - return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") - } - - return nil -} - -func (validator *RecordDataValidator) checkUrlValue(field *schema.SchemaField, value any) error { - val, _ := value.(string) - if val == "" { - return nil // nothing to check - } - - if is.URL.Validate(val) != nil { - return validation.NewError("validation_invalid_url", "Must be a valid url") - } - - options, _ := field.Options.(*schema.UrlOptions) - - // extract host/domain - u, _ := url.Parse(val) - host := u.Host - - // only domains check - if len(options.OnlyDomains) > 0 && !list.ExistInSlice(host, options.OnlyDomains) { - return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") - } - - // except domains check - if len(options.ExceptDomains) > 0 && list.ExistInSlice(host, options.ExceptDomains) { - return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") - } - - return nil -} - -func (validator *RecordDataValidator) checkEditorValue(field *schema.SchemaField, value any) error { - return nil -} - -func (validator *RecordDataValidator) checkDateValue(field *schema.SchemaField, value any) error { - val, _ := value.(types.DateTime) - if val.IsZero() { - if field.Required { - return requiredErr - } - return nil // nothing to check - } - - options, _ := field.Options.(*schema.DateOptions) - - if !options.Min.IsZero() { - if err := validation.Min(options.Min.Time()).Validate(val.Time()); err != nil { - return err - } - } - - if !options.Max.IsZero() { - if err := validation.Max(options.Max.Time()).Validate(val.Time()); err != nil { - return err - } - } - - return nil -} - -func (validator *RecordDataValidator) checkSelectValue(field *schema.SchemaField, value any) error { - normalizedVal := list.ToUniqueStringSlice(value) - if len(normalizedVal) == 0 { - if field.Required { - return requiredErr - } - return nil // nothing to check - } - - options, _ := field.Options.(*schema.SelectOptions) - - // check max selected items - if len(normalizedVal) > options.MaxSelect { - return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) - } - - // check against the allowed values - for _, val := range normalizedVal { - if !list.ExistInSlice(val, options.Values) { - return validation.NewError("validation_invalid_value", "Invalid value "+val) - } - } - - return nil -} - -var emptyJsonValues = []string{ - "null", `""`, "[]", "{}", -} - -func (validator *RecordDataValidator) checkJsonValue(field *schema.SchemaField, value any) error { - if is.JSON.Validate(value) != nil { - return validation.NewError("validation_invalid_json", "Must be a valid json value") - } - - raw, _ := types.ParseJsonRaw(value) - - options, _ := field.Options.(*schema.JsonOptions) - - if len(raw) > options.MaxSize { - return validation.NewError("validation_json_size_limit", fmt.Sprintf("The maximum allowed JSON size is %v bytes", options.MaxSize)) - } - - rawStr := strings.TrimSpace(raw.String()) - if field.Required && list.ExistInSlice(rawStr, emptyJsonValues) { - return requiredErr - } - - return nil -} - -func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField, value any) error { - names := list.ToUniqueStringSlice(value) - if len(names) == 0 && field.Required { - return requiredErr - } - - options, _ := field.Options.(*schema.FileOptions) - - if len(names) > options.MaxSelect { - return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) - } - - // extract the uploaded files - files := make([]*filesystem.File, 0, len(validator.uploadedFiles[field.Name])) - for _, file := range validator.uploadedFiles[field.Name] { - if list.ExistInSlice(file.Name, names) { - files = append(files, file) - } - } - - for _, file := range files { - // check size - if err := UploadedFileSize(options.MaxSize)(file); err != nil { - return err - } - - // check type - if len(options.MimeTypes) > 0 { - if err := UploadedFileMimeType(options.MimeTypes)(file); err != nil { - return err - } - } - } - - return nil -} - -func (validator *RecordDataValidator) checkRelationValue(field *schema.SchemaField, value any) error { - ids := list.ToUniqueStringSlice(value) - if len(ids) == 0 { - if field.Required { - return requiredErr - } - return nil // nothing to check - } - - options, _ := field.Options.(*schema.RelationOptions) - - if options.MinSelect != nil && len(ids) < *options.MinSelect { - return validation.NewError("validation_not_enough_values", fmt.Sprintf("Select at least %d", *options.MinSelect)) - } - - if options.MaxSelect != nil && len(ids) > *options.MaxSelect { - return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", *options.MaxSelect)) - } - - // check if the related records exist - // --- - relCollection, err := validator.dao.FindCollectionByNameOrId(options.CollectionId) - if err != nil { - return validation.NewError("validation_missing_rel_collection", "Relation connection is missing or cannot be accessed") - } - - var total int - validator.dao.RecordQuery(relCollection). - Select("count(*)"). - AndWhere(dbx.In("id", list.ToInterfaceSlice(ids)...)). - Row(&total) - if total != len(ids) { - return validation.NewError("validation_missing_rel_records", "Failed to find all relation records with the provided ids") - } - // --- - - return nil -} diff --git a/forms/validators/record_data_test.go b/forms/validators/record_data_test.go deleted file mode 100644 index 9dc58692..00000000 --- a/forms/validators/record_data_test.go +++ /dev/null @@ -1,1322 +0,0 @@ -package validators_test - -import ( - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/forms/validators" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/rest" - "github.com/pocketbase/pocketbase/tools/types" -) - -type testDataFieldScenario struct { - name string - data map[string]any - files map[string][]*filesystem.File - expectedErrors []string -} - -func TestRecordDataValidatorEmptyAndUnknown(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - collection, _ := app.Dao().FindCollectionByNameOrId("demo2") - record := models.NewRecord(collection) - validator := validators.NewRecordDataValidator(app.Dao(), record, nil) - - emptyErr := validator.Validate(map[string]any{}) - if emptyErr == nil { - t.Fatal("Expected error for empty data, got nil") - } - - unknownErr := validator.Validate(map[string]any{"unknown": 123}) - if unknownErr == nil { - t.Fatal("Expected error for unknown data, got nil") - } -} - -func TestRecordDataValidatorValidateText(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - min := 3 - max := 10 - pattern := `^\w+$` - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeText, - Options: &schema.TextOptions{ - Pattern: pattern, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeText, - Options: &schema.TextOptions{ - Min: &min, - Max: &max, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", "test") - dummy.Set("field2", "test") - dummy.Set("field3", "test") - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(text) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(text) check min constraint", - map[string]any{ - "field1": "test", - "field2": "test", - "field3": strings.Repeat("a", min-1), - }, - nil, - []string{"field3"}, - }, - { - "(text) check min constraint with multi-bytes char", - map[string]any{ - "field1": "test", - "field2": "test", - "field3": "𝌆", // 4 bytes should be counted as 1 char - }, - nil, - []string{"field3"}, - }, - { - "(text) check max constraint", - map[string]any{ - "field1": "test", - "field2": "test", - "field3": strings.Repeat("a", max+1), - }, - nil, - []string{"field3"}, - }, - { - "(text) check max constraint with multi-bytes chars", - map[string]any{ - "field1": "test", - "field2": "test", - "field3": strings.Repeat("𝌆", max), // shouldn't exceed the max limit even though max*4bytes chars are used - }, - nil, - []string{}, - }, - { - "(text) check pattern constraint", - map[string]any{ - "field1": nil, - "field2": "test!", - "field3": "test", - }, - nil, - []string{"field2"}, - }, - { - "(text) valid data (only required)", - map[string]any{ - "field2": "test", - }, - nil, - []string{}, - }, - { - "(text) valid data (all)", - map[string]any{ - "field1": "test", - "field2": 12345, // test value cast - "field3": "test2", - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateNumber(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - min := 2.0 - max := 150.0 - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeNumber, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeNumber, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeNumber, - Options: &schema.NumberOptions{ - Min: &min, - Max: &max, - }, - }, - &schema.SchemaField{ - Name: "field4", - Type: schema.FieldTypeNumber, - Options: &schema.NumberOptions{ - NoDecimal: true, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", 123) - dummy.Set("field2", 123) - dummy.Set("field3", 123) - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(number) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - "field4": nil, - }, - nil, - []string{"field2"}, - }, - { - "(number) check required constraint + casting", - map[string]any{ - "field1": "invalid", - "field2": "invalid", - "field3": "invalid", - "field4": "invalid", - }, - nil, - []string{"field2"}, - }, - { - "(number) check min constraint", - map[string]any{ - "field1": 0.5, - "field2": 1, - "field3": min - 0.5, - }, - nil, - []string{"field3"}, - }, - { - "(number) check min with zero-default", - map[string]any{ - "field2": 1, - "field3": 0, - }, - nil, - []string{}, - }, - { - "(number) check max constraint", - map[string]any{ - "field1": nil, - "field2": max, - "field3": max + 0.5, - }, - nil, - []string{"field3"}, - }, - { - "(number) check NoDecimal", - map[string]any{ - "field2": 1, - "field4": 456.789, - }, - nil, - []string{"field4"}, - }, - { - "(number) valid data (only required)", - map[string]any{ - "field2": 1, - }, - nil, - []string{}, - }, - { - "(number) valid data (all)", - map[string]any{ - "field1": nil, - "field2": 123, // test value cast - "field3": max, - "field4": 456, - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateBool(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeBool, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeBool, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeBool, - Options: &schema.BoolOptions{}, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", false) - dummy.Set("field2", true) - dummy.Set("field3", true) - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(bool) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(bool) check required constraint + casting", - map[string]any{ - "field1": "invalid", - "field2": "invalid", - "field3": "invalid", - }, - nil, - []string{"field2"}, - }, - { - "(bool) valid data (only required)", - map[string]any{ - "field2": 1, - }, - nil, - []string{}, - }, - { - "(bool) valid data (all)", - map[string]any{ - "field1": false, - "field2": true, - "field3": false, - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateEmail(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeEmail, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeEmail, - Options: &schema.EmailOptions{ - ExceptDomains: []string{"example.com"}, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeEmail, - Options: &schema.EmailOptions{ - OnlyDomains: []string{"example.com"}, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", "test@demo.com") - dummy.Set("field2", "test@test.com") - dummy.Set("field3", "test@example.com") - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(email) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(email) check email format validator", - map[string]any{ - "field1": "test", - "field2": "test.com", - "field3": 123, - }, - nil, - []string{"field1", "field2", "field3"}, - }, - { - "(email) check ExceptDomains constraint", - map[string]any{ - "field1": "test@example.com", - "field2": "test@example.com", - "field3": "test2@example.com", - }, - nil, - []string{"field2"}, - }, - { - "(email) check OnlyDomains constraint", - map[string]any{ - "field1": "test@test.com", - "field2": "test@test.com", - "field3": "test@test.com", - }, - nil, - []string{"field3"}, - }, - { - "(email) valid data (only required)", - map[string]any{ - "field2": "test@test.com", - }, - nil, - []string{}, - }, - { - "(email) valid data (all)", - map[string]any{ - "field1": "123@example.com", - "field2": "test@test.com", - "field3": "test2@example.com", - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateUrl(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeUrl, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeUrl, - Options: &schema.UrlOptions{ - ExceptDomains: []string{"example.com"}, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeUrl, - Options: &schema.UrlOptions{ - OnlyDomains: []string{"example.com"}, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", "http://demo.com") - dummy.Set("field2", "http://test.com") - dummy.Set("field3", "http://example.com") - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(url) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(url) check url format validator", - map[string]any{ - "field1": "/abc", - "field2": "test.com", // valid - "field3": "test@example.com", - }, - nil, - []string{"field1", "field3"}, - }, - { - "(url) check ExceptDomains constraint", - map[string]any{ - "field1": "http://example.com", - "field2": "http://example.com", - "field3": "https://example.com", - }, - nil, - []string{"field2"}, - }, - { - "(url) check OnlyDomains constraint", - map[string]any{ - "field1": "http://test.com/abc", - "field2": "http://test.com/abc", - "field3": "http://test.com/abc", - }, - nil, - []string{"field3"}, - }, - { - "(url) check subdomains constraint", - map[string]any{ - "field1": "http://test.test.com", - "field2": "http://test.example.com", - "field3": "http://test.example.com", - }, - nil, - []string{"field3"}, - }, - { - "(url) valid data (only required)", - map[string]any{ - "field2": "http://sub.test.com/abc", - }, - nil, - []string{}, - }, - { - "(url) valid data (all)", - map[string]any{ - "field1": "http://example.com/123", - "field2": "http://test.com/", - "field3": "http://example.com/test2", - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateDate(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - min, _ := types.ParseDateTime("2022-01-01 01:01:01.123") - max, _ := types.ParseDateTime("2030-01-01 01:01:01") - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeDate, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeDate, - Options: &schema.DateOptions{ - Min: min, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeDate, - Options: &schema.DateOptions{ - Max: max, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", "2022-01-01 01:01:01") - dummy.Set("field2", "2029-01-01 01:01:01.123") - dummy.Set("field3", "2029-01-01 01:01:01.123") - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(date) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(date) check required constraint + cast", - map[string]any{ - "field1": "invalid", - "field2": "invalid", - "field3": "invalid", - }, - nil, - []string{"field2"}, - }, - { - "(date) check required constraint + zero datetime", - map[string]any{ - "field1": "January 1, year 1, 00:00:00 UTC", - "field2": "0001-01-01 00:00:00", - "field3": "0001-01-01 00:00:00 +0000 UTC", - }, - nil, - []string{"field2"}, - }, - { - "(date) check min date constraint", - map[string]any{ - "field1": "2021-01-01 01:01:01", - "field2": "2021-01-01 01:01:01", - "field3": "2021-01-01 01:01:01", - }, - nil, - []string{"field2"}, - }, - { - "(date) check max date constraint", - map[string]any{ - "field1": "2030-02-01 01:01:01", - "field2": "2030-02-01 01:01:01", - "field3": "2030-02-01 01:01:01", - }, - nil, - []string{"field3"}, - }, - { - "(date) valid data (only required)", - map[string]any{ - "field2": "2029-01-01 01:01:01", - }, - nil, - []string{}, - }, - { - "(date) valid data (all)", - map[string]any{ - "field1": "2029-01-01 01:01:01.000", - "field2": "2029-01-01 01:01:01", - "field3": "2029-01-01 01:01:01.456", - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateSelect(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"1", "a", "b", "c"}, - MaxSelect: 1, - }, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"a", "b", "c"}, - MaxSelect: 2, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"a", "b", "c"}, - MaxSelect: 99, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", "a") - dummy.Set("field2", []string{"a", "b"}) - dummy.Set("field3", []string{"a", "b", "c"}) - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(select) check required constraint", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(select) check required constraint - empty values", - map[string]any{ - "field1": "", - "field2": "", - "field3": "", - }, - nil, - []string{"field2"}, - }, - { - "(select) check required constraint - multiple select cast", - map[string]any{ - "field1": "a", - "field2": "a", - "field3": "a", - }, - nil, - []string{}, - }, - { - "(select) check Values constraint", - map[string]any{ - "field1": 1, - "field2": "d", - "field3": 123, - }, - nil, - []string{"field2", "field3"}, - }, - { - "(select) check MaxSelect constraint", - map[string]any{ - "field1": []string{"a", "b"}, // this will be normalized to a single string value - "field2": []string{"a", "b", "c"}, - "field3": []string{"a", "b", "b", "b"}, // repeating values will be merged - }, - nil, - []string{"field2"}, - }, - { - "(select) valid data - only required fields", - map[string]any{ - "field2": []string{"a", "b"}, - }, - nil, - []string{}, - }, - { - "(select) valid data - all fields with normalizations", - map[string]any{ - "field1": "a", - "field2": []string{"a", "b", "b"}, // will be collapsed - "field3": "b", // will be normalzied to slice - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateJson(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeJson, - Options: &schema.JsonOptions{ - MaxSize: 10, - }, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeJson, - Options: &schema.JsonOptions{ - MaxSize: 9999, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeJson, - Options: &schema.JsonOptions{ - MaxSize: 9999, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", `{"test":123}`) - dummy.Set("field2", `{"test":123}`) - dummy.Set("field3", `{"test":123}`) - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "(json) check required constraint - nil", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "(json) check required constraint - zero string", - map[string]any{ - "field1": "", - "field2": "", - "field3": "", - }, - nil, - []string{"field2"}, - }, - { - "(json) check required constraint - zero number", - map[string]any{ - "field1": 0, - "field2": 0, - "field3": 0, - }, - nil, - []string{}, - }, - { - "(json) check required constraint - zero slice", - map[string]any{ - "field1": []string{}, - "field2": []string{}, - "field3": []string{}, - }, - nil, - []string{"field2"}, - }, - { - "(json) check required constraint - zero map", - map[string]any{ - "field1": map[string]string{}, - "field2": map[string]string{}, - "field3": map[string]string{}, - }, - nil, - []string{"field2"}, - }, - { - "(json) check MaxSize constraint", - map[string]any{ - "field1": `"123456789"`, // max 10bytes - "field2": 123, - }, - nil, - []string{"field1"}, - }, - { - "(json) check json text invalid obj, array and number normalizations", - map[string]any{ - "field1": `[1 2 3]`, - "field2": `{a: 123}`, - "field3": `123.456 abc`, - }, - nil, - []string{}, - }, - { - "(json) check json text reserved literals normalizations", - map[string]any{ - "field1": `true`, - "field2": `false`, - "field3": `null`, - }, - nil, - []string{}, - }, - { - "(json) valid data - only required fields", - map[string]any{ - "field2": `{"test":123}`, - }, - nil, - []string{}, - }, - { - "(json) valid data - all fields with normalizations", - map[string]any{ - "field1": `"12345678"`, - "field2": 123, - "field3": []string{"a", "b", "c"}, - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateFile(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 1, - MaxSize: 3, - }, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 2, - MaxSize: 10, - MimeTypes: []string{"image/jpeg", "text/plain; charset=utf-8"}, - }, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 3, - MaxSize: 10, - MimeTypes: []string{"image/jpeg"}, - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // stub uploaded files - data, mp, err := tests.MockMultipartData(nil, "test", "test", "test", "test", "test") - if err != nil { - t.Fatal(err) - } - req := httptest.NewRequest(http.MethodPost, "/", data) - req.Header.Add("Content-Type", mp.FormDataContentType()) - testFiles, err := rest.FindUploadedFiles(req, "test") - if err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "check required constraint - nil", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "check MaxSelect constraint", - map[string]any{ - "field1": "test1", - "field2": []string{"test1", testFiles[0].Name, testFiles[3].Name}, - "field3": []string{"test1", "test2", "test3", "test4"}, - }, - map[string][]*filesystem.File{ - "field2": {testFiles[0], testFiles[3]}, - }, - []string{"field2", "field3"}, - }, - { - "check MaxSize constraint", - map[string]any{ - "field1": testFiles[0].Name, - "field2": []string{"test1", testFiles[0].Name}, - "field3": []string{"test1", "test2", "test3"}, - }, - map[string][]*filesystem.File{ - "field1": {testFiles[0]}, - "field2": {testFiles[0]}, - }, - []string{"field1"}, - }, - { - "check MimeTypes constraint", - map[string]any{ - "field1": "test1", - "field2": []string{"test1", testFiles[0].Name}, - "field3": []string{testFiles[1].Name, testFiles[2].Name}, - }, - map[string][]*filesystem.File{ - "field2": {testFiles[0], testFiles[1], testFiles[2]}, - "field3": {testFiles[1], testFiles[2]}, - }, - []string{"field3"}, - }, - { - "valid data - no new files (just file ids)", - map[string]any{ - "field1": "test1", - "field2": []string{"test1", "test2"}, - "field3": []string{"test1", "test2", "test3"}, - }, - nil, - []string{}, - }, - { - "valid data - just new files", - map[string]any{ - "field1": nil, - "field2": []string{testFiles[0].Name, testFiles[1].Name}, - "field3": nil, - }, - map[string][]*filesystem.File{ - "field2": {testFiles[0], testFiles[1]}, - }, - []string{}, - }, - { - "valid data - mixed existing and new files", - map[string]any{ - "field1": "test1", - "field2": []string{"test1", testFiles[0].Name}, - "field3": "test1", // will be casted - }, - map[string][]*filesystem.File{ - "field2": {testFiles[0], testFiles[1], testFiles[2]}, - }, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func TestRecordDataValidatorValidateRelation(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - demo, _ := app.Dao().FindCollectionByNameOrId("demo3") - - // demo3 rel ids - relId1 := "mk5fmymtx4wsprk" - relId2 := "7nwo8tuiatetxdm" - relId3 := "lcl9d87w22ml6jy" - relId4 := "1tmknxy2868d869" - - // record rel ids from different collections - diffRelId1 := "0yxhwia2amd8gec" - diffRelId2 := "llvuca81nly1qls" - - // create new test collection - collection := &models.Collection{} - collection.Name = "validate_test" - collection.Schema = schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(1), - CollectionId: demo.Id, - }, - }, - &schema.SchemaField{ - Name: "field2", - Required: true, - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(2), - CollectionId: demo.Id, - }, - }, - &schema.SchemaField{ - Name: "field3", - Unique: true, - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MinSelect: types.Pointer(2), - CollectionId: demo.Id, - }, - }, - &schema.SchemaField{ - Name: "field4", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(3), - CollectionId: "", // missing or non-existing collection id - }, - }, - ) - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatal(err) - } - - // create dummy record (used for the unique check) - dummy := models.NewRecord(collection) - dummy.Set("field1", relId1) - dummy.Set("field2", []string{relId1, relId2}) - dummy.Set("field3", []string{relId1, relId2, relId3}) - if err := app.Dao().SaveRecord(dummy); err != nil { - t.Fatal(err) - } - - scenarios := []testDataFieldScenario{ - { - "check required constraint - nil", - map[string]any{ - "field1": nil, - "field2": nil, - "field3": nil, - }, - nil, - []string{"field2"}, - }, - { - "check required constraint - zero id", - map[string]any{ - "field1": "", - "field2": "", - "field3": "", - }, - nil, - []string{"field2"}, - }, - { - "check min constraint", - map[string]any{ - "field2": relId2, - "field3": []string{relId1}, - }, - nil, - []string{"field3"}, - }, - { - "check nonexisting collection id", - map[string]any{ - "field2": relId1, - "field4": relId1, - }, - nil, - []string{"field4"}, - }, - { - "check MaxSelect constraint", - map[string]any{ - "field1": []string{relId1, relId2}, // will be normalized to relId1 only - "field2": []string{relId1, relId2, relId3}, - "field3": []string{relId1, relId2, relId3, relId4}, - }, - nil, - []string{"field2"}, - }, - { - "check with ids from different collections", - map[string]any{ - "field1": diffRelId1, - "field2": []string{relId2, diffRelId1}, - "field3": []string{diffRelId1, diffRelId2}, - }, - nil, - []string{"field1", "field2", "field3"}, - }, - { - "valid data - only required fields", - map[string]any{ - "field2": []string{relId1, relId2}, - }, - nil, - []string{}, - }, - { - "valid data - all fields with normalization", - map[string]any{ - "field1": []string{relId1, relId2}, - "field2": relId2, - "field3": []string{relId3, relId2, relId1}, // unique is not triggered because the order is different - }, - nil, - []string{}, - }, - } - - checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) -} - -func checkValidatorErrors(t *testing.T, dao *daos.Dao, record *models.Record, scenarios []testDataFieldScenario) { - for i, s := range scenarios { - prefix := s.name - if prefix == "" { - prefix = fmt.Sprintf("%d", i) - } - - t.Run(prefix, func(t *testing.T) { - validator := validators.NewRecordDataValidator(dao, record, s.files) - result := validator.Validate(s.data) - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Fatalf("Missing expected error key %q in %v", k, errs) - } - } - }) - } -} diff --git a/forms/validators/string.go b/forms/validators/string.go deleted file mode 100644 index 00eb997d..00000000 --- a/forms/validators/string.go +++ /dev/null @@ -1,22 +0,0 @@ -package validators - -import ( - validation "github.com/go-ozzo/ozzo-validation/v4" -) - -// Compare checks whether the validated value matches another string. -// -// Example: -// -// validation.Field(&form.PasswordConfirm, validation.By(validators.Compare(form.Password))) -func Compare(valueToCompare string) validation.RuleFunc { - return func(value any) error { - v, _ := value.(string) - - if v != valueToCompare { - return validation.NewError("validation_values_mismatch", "Values don't match.") - } - - return nil - } -} diff --git a/forms/validators/string_test.go b/forms/validators/string_test.go deleted file mode 100644 index b7ea2ff3..00000000 --- a/forms/validators/string_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package validators_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/forms/validators" -) - -func TestCompare(t *testing.T) { - t.Parallel() - - scenarios := []struct { - valA string - valB string - expectError bool - }{ - {"", "", false}, - {"", "456", true}, - {"123", "", true}, - {"123", "456", true}, - {"123", "123", false}, - } - - for i, s := range scenarios { - err := validators.Compare(s.valA)(s.valB) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) - } - } -} diff --git a/forms/validators/validators.go b/forms/validators/validators.go deleted file mode 100644 index ec8c2177..00000000 --- a/forms/validators/validators.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package validators implements custom shared PocketBase validators. -package validators diff --git a/go.mod b/go.mod index 6e8f1883..03c45208 100644 --- a/go.mod +++ b/go.mod @@ -1,60 +1,58 @@ module github.com/pocketbase/pocketbase -go 1.22 +go 1.23 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/aws/aws-sdk-go-v2 v1.30.4 - github.com/aws/aws-sdk-go-v2/config v1.27.31 - github.com/aws/aws-sdk-go-v2/credentials v1.17.30 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.15 - github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1 + github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2/config v1.27.33 + github.com/aws/aws-sdk-go-v2/credentials v1.17.32 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.18 + github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 github.com/aws/smithy-go v1.20.4 github.com/disintegration/imaging v1.6.2 github.com/domodwyer/mailyak/v3 v3.6.2 - github.com/dop251/goja v0.0.0-20240822155948-fa6d1ed5e4b6 - github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc + github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d + github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c github.com/fatih/color v1.17.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gabriel-vasile/mimetype v1.4.5 github.com/ganigeorgiev/fexpr v0.4.1 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 - github.com/goccy/go-json v0.10.3 github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/mattn/go-sqlite3 v1.14.23 github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/tygoja v0.0.0-20240113091827-17918475d342 github.com/spf13/cast v1.7.0 github.com/spf13/cobra v1.8.1 gocloud.dev v0.39.0 - golang.org/x/crypto v0.26.0 - golang.org/x/net v0.28.0 - golang.org/x/oauth2 v0.22.0 + golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 + golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 - modernc.org/sqlite v1.32.0 + modernc.org/sqlite v1.33.1 ) require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // 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.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -66,23 +64,21 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/stretchr/testify v1.8.2 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/image v0.19.0 // indirect + golang.org/x/image v0.20.0 // indirect golang.org/x/mod v0.19.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/time v0.6.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.23.0 // indirect - golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect - google.golang.org/api v0.194.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect - google.golang.org/grpc v1.65.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/api v0.197.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect - modernc.org/libc v1.55.3 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/strutil v1.2.0 // indirect diff --git a/go.sum b/go.sum index 3e6c803c..0aebb678 100644 --- a/go.sum +++ b/go.sum @@ -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.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w= -cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk= +cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= 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.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= @@ -16,8 +16,6 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -25,48 +23,52 @@ 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.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= -github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= -github.com/aws/aws-sdk-go-v2/config v1.27.31 h1:kxBoRsjhT3pq0cKthgj6RU6bXTm/2SgdoUMyrVw0rAI= -github.com/aws/aws-sdk-go-v2/config v1.27.31/go.mod h1:z04nZdSWFPaDwK3DdJOG2r+scLQzMYuJeW0CujEm9FM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q= -github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.15 h1:ijB7hr56MngOiELJe0C5aQRaBQ11LveNgWFyG02AUto= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.15/go.mod h1:0QEmQSSWMVfiAk93l1/ayR9DQ9+jwni7gHS2NARZXB0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= +github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= +github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.18 h1:9DIp7vhmOPmueCDwpXa45bEbLHHTt1kcxChdTJWWxvI= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.18/go.mod h1:aJv/Fwz8r56ozwYFRC4bzoeL1L17GYQYemfblOBux1M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= 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.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1 h1:mx2ucgtv+MWzJesJY9Ig/8AFHgoE5FwLXwUVgW/FGdI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 h1:Kp6PWAlXwP1UvIflkIP6MFZYBNDCa4mFCGtxrpICVOg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -74,14 +76,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/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-20240822155948-fa6d1ed5e4b6 h1:0x8Sh2rKCTVUQnRTJFIwtRWAp91VMsnATQEsMAg14kM= -github.com/dop251/goja v0.0.0-20240822155948-fa6d1ed5e4b6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= -github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co= -github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c h1:hLoodLRD4KLWIH8eyAQCLcH8EqIrjac7fCkp/fHnvuQ= +github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -106,13 +113,11 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= -github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= -github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -140,8 +145,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -149,26 +155,30 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4= -github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -176,8 +186,8 @@ 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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/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= @@ -192,6 +202,7 @@ github.com/pocketbase/tygoja v0.0.0-20240113091827-17918475d342/go.mod h1:dOJ+pC 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= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -211,34 +222,30 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +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= 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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= -golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -255,11 +262,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -273,23 +280,26 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -303,29 +313,29 @@ golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= -golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s= -google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0= +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.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ= +google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= 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-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0= -google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4= +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/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk= google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 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= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -338,6 +348,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -348,16 +361,16 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= -modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M= modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -366,8 +379,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= -modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/golangci.yml b/golangci.yml index 6ba99fff..9b8db42e 100644 --- a/golangci.yml +++ b/golangci.yml @@ -1,14 +1,13 @@ run: - go: 1.21 + go: 1.23 concurrency: 4 timeout: 10m linters: disable-all: true enable: + - asasalint - asciicheck - - depguard - - exportloopref - gofmt - goimports - gomodguard @@ -20,6 +19,8 @@ linters: - nakedret - nolintlint - prealloc + - prealloc + - reassign - staticcheck - typecheck - unconvert diff --git a/mails/admin.go b/mails/admin.go deleted file mode 100644 index e4f484a2..00000000 --- a/mails/admin.go +++ /dev/null @@ -1,76 +0,0 @@ -package mails - -import ( - "fmt" - "net/mail" - - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/mails/templates" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tokens" - "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/rest" -) - -// SendAdminPasswordReset sends a password reset request email to the specified admin. -func SendAdminPasswordReset(app core.App, admin *models.Admin) error { - token, tokenErr := tokens.NewAdminResetPasswordToken(app, admin) - if tokenErr != nil { - return tokenErr - } - - actionUrl, urlErr := rest.NormalizeUrl(fmt.Sprintf( - "%s/_/#/confirm-password-reset/%s", - app.Settings().Meta.AppUrl, - token, - )) - if urlErr != nil { - return urlErr - } - - params := struct { - AppName string - AppUrl string - Admin *models.Admin - Token string - ActionUrl string - }{ - AppName: app.Settings().Meta.AppName, - AppUrl: app.Settings().Meta.AppUrl, - Admin: admin, - Token: token, - ActionUrl: actionUrl, - } - - mailClient := app.NewMailClient() - - // resolve body template - body, renderErr := resolveTemplateContent(params, templates.Layout, templates.AdminPasswordResetBody) - if renderErr != nil { - return renderErr - } - - message := &mailer.Message{ - From: mail.Address{ - Name: app.Settings().Meta.SenderName, - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{{Address: admin.Email}}, - Subject: "Reset admin password", - HTML: body, - } - - event := new(core.MailerAdminEvent) - event.MailClient = mailClient - event.Message = message - event.Admin = admin - event.Meta = map[string]any{"token": token} - - return app.OnMailerBeforeAdminResetPasswordSend().Trigger(event, func(e *core.MailerAdminEvent) error { - if err := e.MailClient.Send(e.Message); err != nil { - return err - } - - return app.OnMailerAfterAdminResetPasswordSend().Trigger(e) - }) -} diff --git a/mails/admin_test.go b/mails/admin_test.go deleted file mode 100644 index 1bfdb3de..00000000 --- a/mails/admin_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package mails_test - -import ( - "strings" - "testing" - - "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/tests" -) - -func TestSendAdminPasswordReset(t *testing.T) { - t.Parallel() - - testApp, _ := tests.NewTestApp() - defer testApp.Cleanup() - - // ensure that action url normalization will be applied - testApp.Settings().Meta.AppUrl = "http://localhost:8090////" - - admin, _ := testApp.Dao().FindAdminByEmail("test@example.com") - - err := mails.SendAdminPasswordReset(testApp, admin) - if err != nil { - t.Fatal(err) - } - - if testApp.TestMailer.TotalSend != 1 { - t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) - } - - expectedParts := []string{ - "http://localhost:8090/_/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", - } - for _, part := range expectedParts { - if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { - t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage.HTML) - } - } -} diff --git a/mails/record.go b/mails/record.go index dfeca5c9..8a59cb67 100644 --- a/mails/record.go +++ b/mails/record.go @@ -6,59 +6,14 @@ import ( "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/mails/templates" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tools/mailer" ) -// @todo remove after the refactoring -// -// SendRecordPasswordLoginAlert sends a OAuth2 password login alert to the specified auth record. -func SendRecordPasswordLoginAlert(app core.App, authRecord *models.Record, providerNames ...string) error { - params := struct { - AppName string - AppUrl string - Record *models.Record - ProviderNames []string - }{ - AppName: app.Settings().Meta.AppName, - AppUrl: app.Settings().Meta.AppUrl, - Record: authRecord, - ProviderNames: providerNames, - } - +// SendRecordAuthAlert sends a new device login alert to the specified auth record. +func SendRecordAuthAlert(app core.App, authRecord *core.Record) error { mailClient := app.NewMailClient() - // resolve body template - body, renderErr := resolveTemplateContent(params, templates.Layout, templates.PasswordLoginAlertBody) - if renderErr != nil { - return renderErr - } - - message := &mailer.Message{ - From: mail.Address{ - Name: app.Settings().Meta.SenderName, - Address: app.Settings().Meta.SenderAddress, - }, - To: []mail.Address{{Address: authRecord.Email()}}, - Subject: "Password login alert", - HTML: body, - } - - return mailClient.Send(message) -} - -// SendRecordPasswordReset sends a password reset request email to the specified user. -func SendRecordPasswordReset(app core.App, authRecord *models.Record) error { - token, tokenErr := tokens.NewRecordResetPasswordToken(app, authRecord) - if tokenErr != nil { - return tokenErr - } - - mailClient := app.NewMailClient() - - subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.ResetPasswordTemplate) + subject, body, err := resolveEmailTemplate(app, authRecord, authRecord.Collection().AuthAlert.EmailTemplate, nil) if err != nil { return err } @@ -74,31 +29,24 @@ func SendRecordPasswordReset(app core.App, authRecord *models.Record) error { } event := new(core.MailerRecordEvent) - event.MailClient = mailClient + event.App = app + event.Mailer = mailClient event.Message = message - event.Collection = authRecord.Collection() event.Record = authRecord - event.Meta = map[string]any{"token": token} - return app.OnMailerBeforeRecordResetPasswordSend().Trigger(event, func(e *core.MailerRecordEvent) error { - if err := e.MailClient.Send(e.Message); err != nil { - return err - } - - return app.OnMailerAfterRecordResetPasswordSend().Trigger(e) + return app.OnMailerRecordAuthAlertSend().Trigger(event, func(e *core.MailerRecordEvent) error { + return e.Mailer.Send(e.Message) }) } -// SendRecordVerification sends a verification request email to the specified user. -func SendRecordVerification(app core.App, authRecord *models.Record) error { - token, tokenErr := tokens.NewRecordVerifyToken(app, authRecord) - if tokenErr != nil { - return tokenErr - } - +// SendRecordOTP sends OTP email to the specified auth record. +func SendRecordOTP(app core.App, authRecord *core.Record, otpId string, pass string) error { mailClient := app.NewMailClient() - subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.VerificationTemplate) + subject, body, err := resolveEmailTemplate(app, authRecord, authRecord.Collection().OTP.EmailTemplate, map[string]any{ + core.EmailPlaceholderOTPId: otpId, + core.EmailPlaceholderOTP: pass, + }) if err != nil { return err } @@ -114,31 +62,108 @@ func SendRecordVerification(app core.App, authRecord *models.Record) error { } event := new(core.MailerRecordEvent) - event.MailClient = mailClient + event.App = app + event.Mailer = mailClient event.Message = message - event.Collection = authRecord.Collection() event.Record = authRecord - event.Meta = map[string]any{"token": token} + event.Meta = map[string]any{ + "otpId": otpId, + "password": pass, + } - return app.OnMailerBeforeRecordVerificationSend().Trigger(event, func(e *core.MailerRecordEvent) error { - if err := e.MailClient.Send(e.Message); err != nil { - return err - } - - return app.OnMailerAfterRecordVerificationSend().Trigger(e) + return app.OnMailerRecordOTPSend().Trigger(event, func(e *core.MailerRecordEvent) error { + return e.Mailer.Send(e.Message) }) } -// SendRecordChangeEmail sends a change email confirmation email to the specified user. -func SendRecordChangeEmail(app core.App, record *models.Record, newEmail string) error { - token, tokenErr := tokens.NewRecordChangeEmailToken(app, record, newEmail) +// SendRecordPasswordReset sends a password reset request email to the specified auth record. +func SendRecordPasswordReset(app core.App, authRecord *core.Record) error { + token, tokenErr := authRecord.NewPasswordResetToken() if tokenErr != nil { return tokenErr } mailClient := app.NewMailClient() - subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.ConfirmEmailChangeTemplate) + subject, body, err := resolveEmailTemplate(app, authRecord, authRecord.Collection().ResetPasswordTemplate, map[string]any{ + core.EmailPlaceholderToken: token, + }) + if err != nil { + return err + } + + message := &mailer.Message{ + From: mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{{Address: authRecord.Email()}}, + Subject: subject, + HTML: body, + } + + event := new(core.MailerRecordEvent) + event.App = app + event.Mailer = mailClient + event.Message = message + event.Record = authRecord + event.Meta = map[string]any{"token": token} + + return app.OnMailerRecordPasswordResetSend().Trigger(event, func(e *core.MailerRecordEvent) error { + return e.Mailer.Send(e.Message) + }) +} + +// SendRecordVerification sends a verification request email to the specified auth record. +func SendRecordVerification(app core.App, authRecord *core.Record) error { + token, tokenErr := authRecord.NewVerificationToken() + if tokenErr != nil { + return tokenErr + } + + mailClient := app.NewMailClient() + + subject, body, err := resolveEmailTemplate(app, authRecord, authRecord.Collection().VerificationTemplate, map[string]any{ + core.EmailPlaceholderToken: token, + }) + if err != nil { + return err + } + + message := &mailer.Message{ + From: mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{{Address: authRecord.Email()}}, + Subject: subject, + HTML: body, + } + + event := new(core.MailerRecordEvent) + event.App = app + event.Mailer = mailClient + event.Message = message + event.Record = authRecord + event.Meta = map[string]any{"token": token} + + return app.OnMailerRecordVerificationSend().Trigger(event, func(e *core.MailerRecordEvent) error { + return e.Mailer.Send(e.Message) + }) +} + +// SendRecordChangeEmail sends a change email confirmation email to the specified auth record. +func SendRecordChangeEmail(app core.App, authRecord *core.Record, newEmail string) error { + token, tokenErr := authRecord.NewEmailChangeToken(newEmail) + if tokenErr != nil { + return tokenErr + } + + mailClient := app.NewMailClient() + + subject, body, err := resolveEmailTemplate(app, authRecord, authRecord.Collection().ConfirmEmailChangeTemplate, map[string]any{ + core.EmailPlaceholderToken: token, + }) if err != nil { return err } @@ -154,42 +179,59 @@ func SendRecordChangeEmail(app core.App, record *models.Record, newEmail string) } event := new(core.MailerRecordEvent) - event.MailClient = mailClient + event.App = app + event.Mailer = mailClient event.Message = message - event.Collection = record.Collection() - event.Record = record + event.Record = authRecord event.Meta = map[string]any{ "token": token, "newEmail": newEmail, } - return app.OnMailerBeforeRecordChangeEmailSend().Trigger(event, func(e *core.MailerRecordEvent) error { - if err := e.MailClient.Send(e.Message); err != nil { - return err - } - - return app.OnMailerAfterRecordChangeEmailSend().Trigger(e) + return app.OnMailerRecordEmailChangeSend().Trigger(event, func(e *core.MailerRecordEvent) error { + return e.Mailer.Send(e.Message) }) } func resolveEmailTemplate( app core.App, - token string, - emailTemplate settings.EmailTemplate, + authRecord *core.Record, + emailTemplate core.EmailTemplate, + placeholders map[string]any, ) (subject string, body string, err error) { - subject, rawBody, _ := emailTemplate.Resolve( - app.Settings().Meta.AppName, - app.Settings().Meta.AppUrl, - token, - ) - - params := struct { - HtmlContent template.HTML - }{ - HtmlContent: template.HTML(rawBody), + if placeholders == nil { + placeholders = map[string]any{} } - body, err = resolveTemplateContent(params, templates.Layout, templates.HtmlBody) + // register default system placeholders + if _, ok := placeholders[core.EmailPlaceholderAppName]; !ok { + placeholders[core.EmailPlaceholderAppName] = app.Settings().Meta.AppName + } + if _, ok := placeholders[core.EmailPlaceholderAppURL]; !ok { + placeholders[core.EmailPlaceholderAppURL] = app.Settings().Meta.AppURL + } + + // register default auth record placeholders + for _, field := range authRecord.Collection().Fields { + if field.GetHidden() { + continue + } + + fieldPlacehodler := "{RECORD:" + field.GetName() + "}" + if _, ok := placeholders[fieldPlacehodler]; !ok { + placeholders[fieldPlacehodler] = authRecord.Get(field.GetName()) + } + } + + subject, rawBody := emailTemplate.Resolve(placeholders) + + params := struct { + HTMLContent template.HTML + }{ + HTMLContent: template.HTML(rawBody), + } + + body, err = resolveTemplateContent(params, templates.Layout, templates.HTMLBody) if err != nil { return "", "", err } diff --git a/mails/record_test.go b/mails/record_test.go index fa8840e8..1917493f 100644 --- a/mails/record_test.go +++ b/mails/record_test.go @@ -8,31 +8,32 @@ import ( "github.com/pocketbase/pocketbase/tests" ) -func TestSendRecordPasswordLoginAlert(t *testing.T) { +func TestSendRecordAuthAlert(t *testing.T) { t.Parallel() testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - // ensure that action url normalization will be applied - testApp.Settings().Meta.AppUrl = "http://localhost:8090////" + user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") - user, _ := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") - - err := mails.SendRecordPasswordLoginAlert(testApp, user, "test1", "test2") + err := mails.SendRecordAuthAlert(testApp, user) if err != nil { t.Fatal(err) } - if testApp.TestMailer.TotalSend != 1 { - t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + if testApp.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend()) } - expectedParts := []string{"using a password", "OAuth2", "test1", "test2", "auth linked"} - + expectedParts := []string{ + user.GetString("name") + "{RECORD:tokenKey}", // public and private record placeholder checks + "login to your " + testApp.Settings().Meta.AppName + " account from a new location", + "If this was you", + "If this wasn't you", + } for _, part := range expectedParts { - if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { - t.Fatalf("Couldn't find %s\n in\n %s", part, testApp.TestMailer.LastMessage.HTML) + if !strings.Contains(testApp.TestMailer.LastMessage().HTML, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage().HTML) } } } @@ -43,26 +44,24 @@ func TestSendRecordPasswordReset(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - // ensure that action url normalization will be applied - testApp.Settings().Meta.AppUrl = "http://localhost:8090////" - - user, _ := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") err := mails.SendRecordPasswordReset(testApp, user) if err != nil { t.Fatal(err) } - if testApp.TestMailer.TotalSend != 1 { - t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + if testApp.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend()) } expectedParts := []string{ + user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { - if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { - t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage.HTML) + if !strings.Contains(testApp.TestMailer.LastMessage().HTML, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage().HTML) } } } @@ -73,23 +72,24 @@ func TestSendRecordVerification(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - user, _ := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") err := mails.SendRecordVerification(testApp, user) if err != nil { t.Fatal(err) } - if testApp.TestMailer.TotalSend != 1 { - t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + if testApp.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend()) } expectedParts := []string{ + user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { - if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { - t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage.HTML) + if !strings.Contains(testApp.TestMailer.LastMessage().HTML, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage().HTML) } } } @@ -100,23 +100,53 @@ func TestSendRecordChangeEmail(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() - user, _ := testApp.Dao().FindFirstRecordByData("users", "email", "test@example.com") + user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") err := mails.SendRecordChangeEmail(testApp, user, "new_test@example.com") if err != nil { t.Fatal(err) } - if testApp.TestMailer.TotalSend != 1 { - t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + if testApp.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend()) } expectedParts := []string{ + user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { - if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { - t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage.HTML) + if !strings.Contains(testApp.TestMailer.LastMessage().HTML, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage().HTML) + } + } +} + +func TestSendRecordOTP(t *testing.T) { + t.Parallel() + + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + + err := mails.SendRecordOTP(testApp, user, "test_otp_id", "test_otp_code") + if err != nil { + t.Fatal(err) + } + + if testApp.TestMailer.TotalSend() != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend()) + } + + expectedParts := []string{ + user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} + "one-time password", + "test_otp_code", + } + for _, part := range expectedParts { + if !strings.Contains(testApp.TestMailer.LastMessage().HTML, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage().HTML) } } } diff --git a/mails/templates/admin_password_reset.go b/mails/templates/admin_password_reset.go deleted file mode 100644 index f6207c3f..00000000 --- a/mails/templates/admin_password_reset.go +++ /dev/null @@ -1,21 +0,0 @@ -package templates - -// Available variables: -// -// ``` -// Admin *models.Admin -// AppName string -// AppUrl string -// Token string -// ActionUrl string -// ``` -const AdminPasswordResetBody = ` -{{define "content"}} -

Hello,

-

Follow this link to reset your admin password for {{.AppName}}.

-

- Reset password -

-

If you did not request to reset your password, please ignore this email and the link will expire on its own.

-{{end}} -` diff --git a/mails/templates/html_content.go b/mails/templates/html_content.go index cb412751..34d58457 100644 --- a/mails/templates/html_content.go +++ b/mails/templates/html_content.go @@ -3,6 +3,6 @@ package templates // Available variables: // // ``` -// HtmlContent template.HTML +// HTMLContent template.HTML // ``` -const HtmlBody = `{{define "content"}}{{.HtmlContent}}{{end}}` +const HTMLBody = `{{define "content"}}{{.HTMLContent}}{{end}}` diff --git a/mails/templates/layout.go b/mails/templates/layout.go index 21e0b939..7a43ad92 100644 --- a/mails/templates/layout.go +++ b/mails/templates/layout.go @@ -53,7 +53,7 @@ const Layout = ` .btn { display: inline-block; vertical-align: top; - border: 1px solid #e5e5e5; + border: 0; cursor: pointer; color: #fff !important; background: #16161a !important; diff --git a/mails/templates/password_login_alert.go b/mails/templates/password_login_alert.go deleted file mode 100644 index 8ffd1299..00000000 --- a/mails/templates/password_login_alert.go +++ /dev/null @@ -1,30 +0,0 @@ -package templates - -// Available variables: -// -// ``` -// Record *models.Record -// AppName string -// AppUrl string -// ProviderNames []string -// ``` -const PasswordLoginAlertBody = ` -{{define "content"}} -

Hello,

-

- Just to let you know that someone has logged in to your {{.AppName}} account using a password while you already have - OAuth2 - {{range $index, $provider := .ProviderNames }} - {{if $index}}|{{end}} - {{ $provider }} - {{ end }} - auth linked. -

-

If you have recently signed in with a password, you may disregard this email.

-

If you don't recognize the above action, you should immediately change your {{.AppName}} account password.

-

- Thanks,
- {{.AppName}} team -

-{{end}} -` diff --git a/migrations/1640988000_init.go b/migrations/1640988000_init.go index 48d71a77..5535a71b 100644 --- a/migrations/1640988000_init.go +++ b/migrations/1640988000_init.go @@ -1,26 +1,19 @@ -// Package migrations contains the system PocketBase DB migrations. package migrations import ( + "fmt" "path/filepath" "runtime" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tools/migrate" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/types" ) -var AppMigrations migrate.MigrationsList - // Register is a short alias for `AppMigrations.Register()` // that is usually used in external/user defined migrations. func Register( - up func(db dbx.Builder) error, - down func(db dbx.Builder) error, + up func(app core.App) error, + down func(app core.App) error, optFilename ...string, ) { var optFiles []string @@ -30,29 +23,28 @@ func Register( _, path, _, _ := runtime.Caller(1) optFiles = append(optFiles, filepath.Base(path)) } - AppMigrations.Register(up, down, optFiles...) + core.AppMigrations.Register(up, down, optFiles...) } func init() { - AppMigrations.Register(func(db dbx.Builder) error { - _, tablesErr := db.NewQuery(` - CREATE TABLE {{_admins}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, - [[avatar]] INTEGER DEFAULT 0 NOT NULL, - [[email]] TEXT UNIQUE NOT NULL, - [[tokenKey]] TEXT UNIQUE NOT NULL, - [[passwordHash]] TEXT NOT NULL, - [[lastResetSentAt]] TEXT DEFAULT "" NOT NULL, - [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, - [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL - ); + core.SystemMigrations.Register(func(txApp core.App) error { + if err := createLogsTable(txApp); err != nil { + return fmt.Errorf("_logs error: %w", err) + } + if err := createParamsTable(txApp); err != nil { + return fmt.Errorf("_params exec error: %w", err) + } + + // ----------------------------------------------------------- + + _, execerr := txApp.DB().NewQuery(` CREATE TABLE {{_collections}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, + [[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL, [[system]] BOOLEAN DEFAULT FALSE NOT NULL, [[type]] TEXT DEFAULT "base" NOT NULL, [[name]] TEXT UNIQUE NOT NULL, - [[schema]] JSON DEFAULT "[]" NOT NULL, + [[fields]] JSON DEFAULT "[]" NOT NULL, [[indexes]] JSON DEFAULT "[]" NOT NULL, [[listRule]] TEXT DEFAULT NULL, [[viewRule]] TEXT DEFAULT NULL, @@ -63,104 +55,54 @@ 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 TABLE {{_params}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, - [[key]] TEXT UNIQUE NOT NULL, - [[value]] JSON DEFAULT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL - ); - - CREATE TABLE {{_externalAuths}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, - [[collectionId]] TEXT NOT NULL, - [[recordId]] TEXT NOT NULL, - [[provider]] TEXT NOT NULL, - [[providerId]] TEXT NOT NULL, - [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, - [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, - --- - FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE - ); - - CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]); - CREATE UNIQUE INDEX _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]]); `).Execute() - if tablesErr != nil { - return tablesErr + if execerr != nil { + return fmt.Errorf("_collections exec error: %w", execerr) } - dao := daos.New(db) + if err := createMFAsCollection(txApp); err != nil { + return fmt.Errorf("_mfas error: %w", err) + } - // inserts default settings - // ----------------------------------------------------------- - defaultSettings := settings.New() - if err := dao.SaveSettings(defaultSettings); err != nil { + if err := createOTPsCollection(txApp); err != nil { + return fmt.Errorf("_otps error: %w", err) + } + + if err := createExternalAuthsCollection(txApp); err != nil { + return fmt.Errorf("_externalAuths error: %w", err) + } + + if err := createAuthOriginsCollection(txApp); err != nil { + return fmt.Errorf("_authOrigins error: %w", err) + } + + if err := createSuperusersCollection(txApp); err != nil { + return fmt.Errorf("_superusers error: %w", err) + } + + if err := createUsersCollection(txApp); err != nil { + return fmt.Errorf("users error: %w", err) + } + + return nil + }, func(txApp core.App) error { + _, err := txApp.AuxDB().DropTable("_logs").Execute() + if err != nil { return err } - // inserts the system users collection - // ----------------------------------------------------------- - usersCollection := &models.Collection{} - usersCollection.MarkAsNew() - usersCollection.Id = "_pb_users_auth_" - usersCollection.Name = "users" - usersCollection.Type = models.CollectionTypeAuth - usersCollection.ListRule = types.Pointer("id = @request.auth.id") - usersCollection.ViewRule = types.Pointer("id = @request.auth.id") - usersCollection.CreateRule = types.Pointer("") - usersCollection.UpdateRule = types.Pointer("id = @request.auth.id") - usersCollection.DeleteRule = types.Pointer("id = @request.auth.id") - - // set auth options - usersCollection.SetOptions(models.CollectionAuthOptions{ - ManageRule: nil, - AllowOAuth2Auth: true, - AllowUsernameAuth: true, - AllowEmailAuth: true, - MinPasswordLength: 8, - RequireEmail: false, - }) - - // set optional default fields - usersCollection.Schema = schema.NewSchema( - &schema.SchemaField{ - Id: "users_name", - Type: schema.FieldTypeText, - Name: "name", - Options: &schema.TextOptions{}, - }, - &schema.SchemaField{ - Id: "users_avatar", - Type: schema.FieldTypeFile, - Name: "avatar", - Options: &schema.FileOptions{ - MaxSelect: 1, - MaxSize: 5242880, - MimeTypes: []string{ - "image/jpeg", - "image/png", - "image/svg+xml", - "image/gif", - "image/webp", - }, - }, - }, - ) - - return dao.SaveCollection(usersCollection) - }, func(db dbx.Builder) error { tables := []string{ "users", - "_externalAuths", + core.CollectionNameSuperusers, + core.CollectionNameMFAs, + core.CollectionNameOTPs, + core.CollectionNameAuthOrigins, "_params", "_collections", - "_admins", } for _, name := range tables { - if _, err := db.DropTable(name).Execute(); err != nil { + if _, err := txApp.DB().DropTable(name).Execute(); err != nil { return err } } @@ -168,3 +110,252 @@ func init() { return nil }) } + +func createParamsTable(txApp core.App) error { + _, execErr := txApp.DB().NewQuery(` + CREATE TABLE {{_params}} ( + [[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL, + [[value]] JSON DEFAULT NULL, + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, + [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL + ); + `).Execute() + + return execErr +} + +func createLogsTable(txApp core.App) error { + _, execErr := txApp.AuxDB().NewQuery(` + CREATE TABLE {{_logs}} ( + [[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL, + [[level]] INTEGER DEFAULT 0 NOT NULL, + [[message]] TEXT DEFAULT "" NOT NULL, + [[data]] JSON DEFAULT "{}" NOT NULL, + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL + ); + + CREATE INDEX idx_logs_level on {{_logs}} ([[level]]); + CREATE INDEX idx_logs_message on {{_logs}} ([[message]]); + CREATE INDEX idx_logs_created_hour on {{_logs}} (strftime('%Y-%m-%d %H:00:00', [[created]])); + `).Execute() + + return execErr +} + +func createMFAsCollection(txApp core.App) error { + col := core.NewBaseCollection(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", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "recordRef", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "method", + System: true, + Required: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "created", + System: true, + OnCreate: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "updated", + System: true, + OnCreate: true, + OnUpdate: true, + }) + col.AddIndex("idx_mfas_collectionRef_recordRef", false, "collectionRef,recordRef", "") + + return txApp.Save(col) +} + +func createOTPsCollection(txApp core.App) error { + col := core.NewBaseCollection(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", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "recordRef", + System: true, + Required: true, + }) + col.Fields.Add(&core.PasswordField{ + Name: "password", + System: true, + Hidden: true, + Required: true, + Cost: 8, // low cost for better performce and because it is not critical + }) + col.Fields.Add(&core.AutodateField{ + Name: "created", + System: true, + OnCreate: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "updated", + System: true, + OnCreate: true, + OnUpdate: true, + }) + col.AddIndex("idx_otps_collectionRef_recordRef", false, "collectionRef, recordRef", "") + + return txApp.Save(col) +} + +func createAuthOriginsCollection(txApp core.App) error { + col := core.NewBaseCollection(core.CollectionNameAuthOrigins) + 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", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "recordRef", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "fingerprint", + System: true, + Required: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "created", + System: true, + OnCreate: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "updated", + System: true, + OnCreate: true, + OnUpdate: true, + }) + col.AddIndex("idx_authOrigins_unique_pairs", true, "collectionRef, recordRef, fingerprint", "") + + return txApp.Save(col) +} + +func createExternalAuthsCollection(txApp core.App) error { + col := core.NewBaseCollection(core.CollectionNameExternalAuths) + 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", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "recordRef", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "provider", + System: true, + Required: true, + }) + col.Fields.Add(&core.TextField{ + Name: "providerId", + System: true, + Required: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "created", + System: true, + OnCreate: true, + }) + col.Fields.Add(&core.AutodateField{ + Name: "updated", + System: true, + OnCreate: true, + OnUpdate: true, + }) + col.AddIndex("idx_externalAuths_record_provider", true, "collectionRef, recordRef, provider", "") + col.AddIndex("idx_externalAuths_collection_provider", true, "collectionRef, provider, providerId", "") + + return txApp.Save(col) +} + +func createSuperusersCollection(txApp core.App) error { + superusers := core.NewAuthCollection(core.CollectionNameSuperusers) + superusers.System = true + superusers.Fields.Add(&core.EmailField{ + Name: "email", + System: true, + Required: true, + }) + superusers.Fields.Add(&core.AutodateField{ + Name: "created", + System: true, + OnCreate: true, + }) + superusers.Fields.Add(&core.AutodateField{ + Name: "updated", + System: true, + OnCreate: true, + OnUpdate: true, + }) + superusers.AuthToken.Duration = 86400 // 1 day + + return txApp.Save(superusers) +} + +func createUsersCollection(txApp core.App) error { + users := core.NewAuthCollection("users") + users.Fields.Add(&core.TextField{ + Name: "name", + Max: 255, + }) + users.Fields.Add(&core.FileField{ + Name: "avatar", + MaxSelect: 1, + MimeTypes: []string{"image/jpeg", "image/png", "image/svg+xml", "image/gif", "image/webp"}, + }) + users.Fields.Add(&core.AutodateField{ + Name: "created", + OnCreate: true, + }) + users.Fields.Add(&core.AutodateField{ + Name: "updated", + OnCreate: true, + OnUpdate: true, + }) + users.OAuth2.MappedFields.Name = "name" + users.OAuth2.MappedFields.AvatarURL = "avatar" + + return txApp.Save(users) +} diff --git a/migrations/1673167670_multi_match_migrate.go b/migrations/1673167670_multi_match_migrate.go deleted file mode 100644 index 936df7c0..00000000 --- a/migrations/1673167670_multi_match_migrate.go +++ /dev/null @@ -1,215 +0,0 @@ -package migrations - -import ( - "regexp" - "strings" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// This migration replaces for backward compatibility the default operators -// (=, !=, >, etc.) with their any/opt equivalent (?=, ?=, ?>, etc.) -// in any muli-rel expression collection rule. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - exprRegex := regexp.MustCompile(`([\@\'\"\w\.]+)\s*(=|!=|~|!~|>|>=|<|<=)\s*([\@\'\"\w\.]+)`) - - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - findCollection := func(nameOrId string) *models.Collection { - for _, c := range collections { - if c.Id == nameOrId || c.Name == nameOrId { - return c - } - } - - return nil - } - - var isMultiRelLiteral func(mainCollection *models.Collection, literal string) bool - isMultiRelLiteral = func(mainCollection *models.Collection, literal string) bool { - if strings.HasPrefix(literal, "@collection.") { - return true - } - - if strings.HasPrefix(literal, `"`) || - strings.HasPrefix(literal, `'`) || - strings.HasPrefix(literal, "@request.method") || - strings.HasPrefix(literal, "@request.data") || - strings.HasPrefix(literal, "@request.query") { - return false - } - - parts := strings.Split(literal, ".") - if len(parts) <= 1 { - return false - } - - if strings.HasPrefix(literal, "@request.auth") && len(parts) >= 4 { - // check each auth collection - for _, c := range collections { - if c.IsAuth() && isMultiRelLiteral(c, strings.Join(parts[2:], ".")) { - return true - } - } - - return false - } - - activeCollection := mainCollection - - for i, p := range parts { - f := activeCollection.Schema.GetFieldByName(p) - if f == nil || f.Type != schema.FieldTypeRelation { - return false // not a relation field - } - - // is multi-relation and not the last prop - opt, ok := f.Options.(*schema.RelationOptions) - if ok && (opt.MaxSelect == nil || *opt.MaxSelect != 1) && i != len(parts)-1 { - return true - } - - activeCollection = findCollection(opt.CollectionId) - if activeCollection == nil { - return false - } - } - - return false - } - - // replace all multi-match operators to their any/opt equivalent, eg. "=" => "?=" - migrateRule := func(collection *models.Collection, rule *string) (*string, error) { - if rule == nil || *rule == "" { - return rule, nil - } - - newRule := *rule - parts := exprRegex.FindAllStringSubmatch(newRule, -1) - - for _, p := range parts { - if isMultiRelLiteral(collection, p[1]) || isMultiRelLiteral(collection, p[3]) { - newRule = strings.ReplaceAll(newRule, p[0], p[1]+" ?"+p[2]+" "+p[3]) - } - } - - return &newRule, nil - } - - var ruleErr error - for _, c := range collections { - c.ListRule, ruleErr = migrateRule(c, c.ListRule) - if ruleErr != nil { - return ruleErr - } - - c.ViewRule, ruleErr = migrateRule(c, c.ViewRule) - if ruleErr != nil { - return ruleErr - } - - c.CreateRule, ruleErr = migrateRule(c, c.CreateRule) - if ruleErr != nil { - return ruleErr - } - - c.UpdateRule, ruleErr = migrateRule(c, c.UpdateRule) - if ruleErr != nil { - return ruleErr - } - - c.DeleteRule, ruleErr = migrateRule(c, c.DeleteRule) - if ruleErr != nil { - return ruleErr - } - - if c.IsAuth() { - opt := c.AuthOptions() - opt.ManageRule, ruleErr = migrateRule(c, opt.ManageRule) - if ruleErr != nil { - return ruleErr - } - c.SetOptions(opt) - } - - if err := dao.Save(c); err != nil { - return err - } - } - - return nil - }, func(db dbx.Builder) error { - dao := daos.New(db) - - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - anyOpRegex := regexp.MustCompile(`\?(=|!=|~|!~|>|>=|<|<=)`) - - // replace any/opt operators to their old versions, eg. "?=" => "=" - revertRule := func(rule *string) (*string, error) { - if rule == nil || *rule == "" { - return rule, nil - } - - newRule := *rule - newRule = anyOpRegex.ReplaceAllString(newRule, "${1}") - - return &newRule, nil - } - - var ruleErr error - for _, c := range collections { - c.ListRule, ruleErr = revertRule(c.ListRule) - if ruleErr != nil { - return ruleErr - } - - c.ViewRule, ruleErr = revertRule(c.ViewRule) - if ruleErr != nil { - return ruleErr - } - - c.CreateRule, ruleErr = revertRule(c.CreateRule) - if ruleErr != nil { - return ruleErr - } - - c.UpdateRule, ruleErr = revertRule(c.UpdateRule) - if ruleErr != nil { - return ruleErr - } - - c.DeleteRule, ruleErr = revertRule(c.DeleteRule) - if ruleErr != nil { - return ruleErr - } - - if c.IsAuth() { - opt := c.AuthOptions() - opt.ManageRule, ruleErr = revertRule(opt.ManageRule) - if ruleErr != nil { - return ruleErr - } - c.SetOptions(opt) - } - - if err := dao.Save(c); err != nil { - return err - } - } - - return nil - }) -} diff --git a/migrations/1677152688_rename_authentik_to_oidc.go b/migrations/1677152688_rename_authentik_to_oidc.go deleted file mode 100644 index b0b0d748..00000000 --- a/migrations/1677152688_rename_authentik_to_oidc.go +++ /dev/null @@ -1,26 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" -) - -// This migration replaces the "authentikAuth" setting with "oidc". -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - _, err := db.NewQuery(` - UPDATE {{_params}} - SET [[value]] = replace([[value]], '"authentikAuth":', '"oidcAuth":') - WHERE [[key]] = 'settings' - `).Execute() - - return err - }, func(db dbx.Builder) error { - _, err := db.NewQuery(` - UPDATE {{_params}} - SET [[value]] = replace([[value]], '"oidcAuth":', '"authentikAuth":') - WHERE [[key]] = 'settings' - `).Execute() - - return err - }) -} diff --git a/migrations/1679943780_normalize_single_multiple_values.go b/migrations/1679943780_normalize_single_multiple_values.go deleted file mode 100644 index bc82bc4f..00000000 --- a/migrations/1679943780_normalize_single_multiple_values.go +++ /dev/null @@ -1,108 +0,0 @@ -package migrations - -import ( - "fmt" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// Normalizes old single and multiple values of MultiValuer fields (file, select, relation). -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - return normalizeMultivaluerFields(db) - }, func(db dbx.Builder) error { - return nil - }) -} - -func normalizeMultivaluerFields(db dbx.Builder) error { - dao := daos.New(db) - - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - for _, c := range collections { - if c.IsView() { - // skip view collections - continue - } - - for _, f := range c.Schema.Fields() { - opt, ok := f.Options.(schema.MultiValuer) - if !ok { - continue - } - - var updateQuery *dbx.Query - - if opt.IsMultiple() { - updateQuery = dao.DB().NewQuery(fmt.Sprintf( - `UPDATE {{%s}} set [[%s]] = ( - CASE - WHEN COALESCE([[%s]], '') = '' - THEN '[]' - ELSE ( - CASE - WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' - THEN [[%s]] - ELSE json_array([[%s]]) - END - ) - END - )`, - c.Name, - f.Name, - f.Name, - f.Name, - f.Name, - f.Name, - f.Name, - )) - } else { - updateQuery = dao.DB().NewQuery(fmt.Sprintf( - `UPDATE {{%s}} set [[%s]] = ( - CASE - WHEN COALESCE([[%s]], '[]') = '[]' - THEN '' - ELSE ( - CASE - WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array' - THEN COALESCE(json_extract([[%s]], '$[#-1]'), '') - ELSE [[%s]] - END - ) - END - )`, - c.Name, - f.Name, - f.Name, - f.Name, - f.Name, - f.Name, - f.Name, - )) - } - - if _, err := updateQuery.Execute(); err != nil { - return err - } - } - } - - // trigger view query update after the records normalization - // (ignore save error in case of invalid query to allow users to change it from the UI) - for _, c := range collections { - if !c.IsView() { - continue - } - - dao.SaveCollection(c) - } - - return nil -} diff --git a/migrations/1679943781_add_indexes_column.go b/migrations/1679943781_add_indexes_column.go deleted file mode 100644 index 96b1039b..00000000 --- a/migrations/1679943781_add_indexes_column.go +++ /dev/null @@ -1,141 +0,0 @@ -package migrations - -import ( - "fmt" - "strings" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/dbutils" - "github.com/pocketbase/pocketbase/tools/list" -) - -// Adds _collections indexes column (if not already). -// -// Note: This migration will be deleted once schema.SchemaField.Unuique is removed. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - // cleanup failed remaining/"dangling" temp views to prevent - // errors during the indexes upsert - // --- - tempViews := []string{} - viewsErr := db.Select("name"). - From("sqlite_schema"). - AndWhere(dbx.HashExp{"type": "view"}). - AndWhere(dbx.NewExp(`[[name]] LIKE '\_temp\_%' ESCAPE '\'`)). - Column(&tempViews) - if viewsErr != nil { - return viewsErr - } - for _, name := range tempViews { - if err := dao.DeleteView(name); err != nil { - return err - } - } - // --- - - cols, err := dao.TableColumns("_collections") - if err != nil { - return err - } - - var hasIndexesColumn bool - for _, col := range cols { - if col == "indexes" { - // already existing (probably via the init migration) - hasIndexesColumn = true - break - } - } - - if !hasIndexesColumn { - if _, err := db.AddColumn("_collections", "indexes", `JSON DEFAULT "[]" NOT NULL`).Execute(); err != nil { - return err - } - } - - collections := []*models.Collection{} - if err := dao.CollectionQuery().AndWhere(dbx.NewExp("type != 'view'")).All(&collections); err != nil { - return err - } - - type indexInfo struct { - Sql string `db:"sql"` - IndexName string `db:"name"` - TableName string `db:"tbl_name"` - } - - indexesQuery := db.NewQuery(`SELECT * FROM sqlite_master WHERE type = "index" and sql is not null`) - rawIndexes := []indexInfo{} - if err := indexesQuery.All(&rawIndexes); err != nil { - return err - } - - indexesByTableName := map[string][]indexInfo{} - for _, idx := range rawIndexes { - indexesByTableName[idx.TableName] = append(indexesByTableName[idx.TableName], idx) - } - - for _, c := range collections { - c.Indexes = nil // reset - - excludeIndexes := []string{ - "_" + c.Id + "_email_idx", - "_" + c.Id + "_username_idx", - "_" + c.Id + "_tokenKey_idx", - } - - // convert custom indexes into the related collections - for _, idx := range indexesByTableName[c.Name] { - if strings.Contains(idx.IndexName, "sqlite_autoindex_") || - list.ExistInSlice(idx.IndexName, excludeIndexes) { - continue - } - - // drop old index (it will be recreated with the collection) - if _, err := db.DropIndex(idx.TableName, idx.IndexName).Execute(); err != nil { - return err - } - - c.Indexes = append(c.Indexes, idx.Sql) - } - - // convert unique fields to indexes - FieldsLoop: - for _, f := range c.Schema.Fields() { - if !f.Unique { - continue - } - - for _, idx := range indexesByTableName[c.Name] { - parsed := dbutils.ParseIndex(idx.Sql) - if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, f.Name) { - continue FieldsLoop // already added - } - } - - c.Indexes = append(c.Indexes, fmt.Sprintf( - `CREATE UNIQUE INDEX "idx_unique_%s" on "%s" ("%s")`, - f.Id, - c.Name, - f.Name, - )) - } - - if len(c.Indexes) > 0 { - if err := dao.SaveCollection(c); err != nil { - return err - } - } - } - - return nil - }, func(db dbx.Builder) error { - _, err := db.DropColumn("_collections", "indexes").Execute() - - return err - }) -} diff --git a/migrations/1685164450_check_fk.go b/migrations/1685164450_check_fk.go deleted file mode 100644 index ecc0ff7d..00000000 --- a/migrations/1685164450_check_fk.go +++ /dev/null @@ -1,20 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" -) - -// Cleanup dangling deleted collections references -// (see https://github.com/pocketbase/pocketbase/discussions/2570). -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - _, err := db.NewQuery(` - DELETE FROM {{_externalAuths}} - WHERE [[collectionId]] NOT IN (SELECT [[id]] FROM {{_collections}}) - `).Execute() - - return err - }, func(db dbx.Builder) error { - return nil - }) -} diff --git a/migrations/1689579878_renormalize_single_multiple_values.go b/migrations/1689579878_renormalize_single_multiple_values.go deleted file mode 100644 index 51b62b94..00000000 --- a/migrations/1689579878_renormalize_single_multiple_values.go +++ /dev/null @@ -1,15 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" -) - -// Renormalizes old single and multiple values of MultiValuer fields (file, select, relation) -// (see https://github.com/pocketbase/pocketbase/issues/2930). -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - return normalizeMultivaluerFields(db) - }, func(db dbx.Builder) error { - return nil - }) -} diff --git a/migrations/1690319366_reset_null_values.go b/migrations/1690319366_reset_null_values.go deleted file mode 100644 index 306ab3a4..00000000 --- a/migrations/1690319366_reset_null_values.go +++ /dev/null @@ -1,58 +0,0 @@ -package migrations - -import ( - "fmt" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// Reset all previously inserted NULL values to the fields zero-default. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - for _, collection := range collections { - if collection.IsView() { - continue - } - - for _, f := range collection.Schema.Fields() { - defaultVal := "''" - - switch f.Type { - case schema.FieldTypeJson: - continue - case schema.FieldTypeBool: - defaultVal = "FALSE" - case schema.FieldTypeNumber: - defaultVal = "0" - default: - if opt, ok := f.Options.(schema.MultiValuer); ok && opt.IsMultiple() { - defaultVal = "'[]'" - } - } - - _, err := db.NewQuery(fmt.Sprintf( - "UPDATE {{%s}} SET [[%s]] = %s WHERE [[%s]] IS NULL", - collection.Name, - f.Name, - defaultVal, - f.Name, - )).Execute() - if err != nil { - return err - } - } - } - - return nil - }, nil) -} diff --git a/migrations/1690454337_transform_relations_to_views.go b/migrations/1690454337_transform_relations_to_views.go deleted file mode 100644 index e158dd3f..00000000 --- a/migrations/1690454337_transform_relations_to_views.go +++ /dev/null @@ -1,61 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// Transform the relation fields to views from non-view collections to json or text fields -// (see https://github.com/pocketbase/pocketbase/issues/3000). -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - views, err := dao.FindCollectionsByType(models.CollectionTypeView) - if err != nil { - return err - } - - for _, view := range views { - refs, err := dao.FindCollectionReferences(view) - if err != nil { - return nil - } - - for collection, fields := range refs { - if collection.IsView() { - continue // view-view relations are allowed - } - - for _, f := range fields { - opt, ok := f.Options.(schema.MultiValuer) - if !ok { - continue - } - - if opt.IsMultiple() { - f.Type = schema.FieldTypeJson - f.Options = &schema.JsonOptions{} - } else { - f.Type = schema.FieldTypeText - f.Options = &schema.TextOptions{} - } - - // replace the existing field - // (this usually is not necessary since it is a pointer, - // but it is better to be explicit in case FindCollectionReferences changes) - collection.Schema.AddField(f) - } - - // "raw" save without records table sync - if err := dao.Save(collection); err != nil { - return err - } - } - } - - return nil - }, nil) -} diff --git a/migrations/1691747913_resave_views.go b/migrations/1691747913_resave_views.go deleted file mode 100644 index 3d684609..00000000 --- a/migrations/1691747913_resave_views.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" -) - -// Resave all view collections to ensure that the proper id normalization is applied. -// (see https://github.com/pocketbase/pocketbase/issues/3110) -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - collections, err := dao.FindCollectionsByType(models.CollectionTypeView) - if err != nil { - return nil - } - - for _, collection := range collections { - // ignore errors to allow users to adjust - // the view queries after app start - dao.SaveCollection(collection) - } - - return nil - }, nil) -} diff --git a/migrations/1692609521_copy_display_fields.go b/migrations/1692609521_copy_display_fields.go deleted file mode 100644 index b2a20f59..00000000 --- a/migrations/1692609521_copy_display_fields.go +++ /dev/null @@ -1,62 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// Copy the now deprecated RelationOptions.DisplayFields values from -// all relation fields and register its value as Presentable under -// the specific field in the related collection. -// -// If there is more than one relation to a single collection with explicitly -// set DisplayFields only one of the configuration will be copied. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - indexedCollections := make(map[string]*models.Collection, len(collections)) - for _, collection := range collections { - indexedCollections[collection.Id] = collection - } - - for _, collection := range indexedCollections { - for _, f := range collection.Schema.Fields() { - if f.Type != schema.FieldTypeRelation { - continue - } - - options, ok := f.Options.(*schema.RelationOptions) - if !ok || len(options.DisplayFields) == 0 { - continue - } - - relCollection, ok := indexedCollections[options.CollectionId] - if !ok { - continue - } - - for _, name := range options.DisplayFields { - relField := relCollection.Schema.GetFieldByName(name) - if relField != nil { - relField.Presentable = true - } - } - - // only raw model save - if err := dao.Save(relCollection); err != nil { - return err - } - } - } - - return nil - }, nil) -} diff --git a/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go b/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go deleted file mode 100644 index 55265f97..00000000 --- a/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go +++ /dev/null @@ -1,23 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" -) - -// Fixes the unique _externalAuths constraint for old installations -// to allow a single OAuth2 provider to be registered for different auth collections. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - _, createErr := db.NewQuery("CREATE UNIQUE INDEX IF NOT EXISTS _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]])").Execute() - if createErr != nil { - return createErr - } - - _, dropErr := db.NewQuery("DROP INDEX IF EXISTS _externalAuths_provider_providerId_idx").Execute() - if dropErr != nil { - return dropErr - } - - return nil - }, nil) -} diff --git a/migrations/1702134272_set_default_json_max_size.go b/migrations/1702134272_set_default_json_max_size.go deleted file mode 100644 index c523e8ab..00000000 --- a/migrations/1702134272_set_default_json_max_size.go +++ /dev/null @@ -1,51 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" -) - -// Update all collections with json fields to have a default MaxSize json field option. -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - // note: update even the view collections to prevent - // unnecessary change detections during the automigrate - collections := []*models.Collection{} - if err := dao.CollectionQuery().All(&collections); err != nil { - return err - } - - for _, collection := range collections { - var needSave bool - - for _, f := range collection.Schema.Fields() { - if f.Type != schema.FieldTypeJson { - continue - } - - options, _ := f.Options.(*schema.JsonOptions) - if options == nil { - options = &schema.JsonOptions{} - } - options.MaxSize = 2000000 // 2mb - f.Options = options - needSave = true - } - - if !needSave { - continue - } - - // save only the collection model without updating its records table - if err := dao.Save(collection); err != nil { - return err - } - } - - return nil - }, nil) -} diff --git a/migrations/1717233556_v0.23_migrate.go b/migrations/1717233556_v0.23_migrate.go new file mode 100644 index 00000000..ea693056 --- /dev/null +++ b/migrations/1717233556_v0.23_migrate.go @@ -0,0 +1,912 @@ +package migrations + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" + "golang.org/x/crypto/bcrypt" +) + +// note: this migration will be deleted in future version + +func init() { + core.SystemMigrations.Register(func(txApp core.App) error { + // note: mfas and authOrigins tables are available only with v0.23 + hasUpgraded := txApp.HasTable(core.CollectionNameMFAs) && txApp.HasTable(core.CollectionNameAuthOrigins) + if hasUpgraded { + return nil + } + + oldSettings, err := loadOldSettings(txApp) + if err != nil { + return fmt.Errorf("failed to fetch old settings: %w", err) + } + + if err = migrateOldCollections(txApp, oldSettings); err != nil { + return err + } + + if err = migrateSuperusers(txApp, oldSettings); err != nil { + return fmt.Errorf("failed to migrate admins->superusers: %w", err) + } + + if err = migrateSettings(txApp, oldSettings); err != nil { + return fmt.Errorf("failed to migrate settings: %w", err) + } + + if err = migrateExternalAuths(txApp); err != nil { + return fmt.Errorf("failed to migrate externalAuths: %w", err) + } + + if err = createMFAsCollection(txApp); err != nil { + return fmt.Errorf("failed to create mfas collection: %w", err) + } + + if err = createOTPsCollection(txApp); err != nil { + return fmt.Errorf("failed to create otps collection: %w", err) + } + + if err = createAuthOriginsCollection(txApp); err != nil { + return fmt.Errorf("failed to create authOrigins collection: %w", err) + } + + if err = createLogsTable(txApp); err != nil { + return fmt.Errorf("failed tocreate logs table: %w", err) + } + + if err = os.Remove(filepath.Join(txApp.DataDir(), "logs.db")); err != nil { + txApp.Logger().Warn("Failed to delete old logs.db file") + } + + return nil + }, nil) +} + +// ------------------------------------------------------------------- + +func migrateSuperusers(txApp core.App, oldSettings *oldSettingsModel) error { + // create new superusers collection and table + err := createSuperusersCollection(txApp) + if err != nil { + return err + } + + // update with the token options from the old settings + superusersCollection, err := txApp.FindCollectionByNameOrId(core.CollectionNameSuperusers) + if err != nil { + return err + } + + superusersCollection.AuthToken.Secret = zeroFallback( + cast.ToString(getMapVal(oldSettings.Value, "adminAuthToken", "secret")), + superusersCollection.AuthToken.Secret, + ) + superusersCollection.AuthToken.Duration = zeroFallback( + cast.ToInt64(getMapVal(oldSettings.Value, "adminAuthToken", "duration")), + superusersCollection.AuthToken.Duration, + ) + superusersCollection.PasswordResetToken.Secret = zeroFallback( + cast.ToString(getMapVal(oldSettings.Value, "adminPasswordResetToken", "secret")), + superusersCollection.PasswordResetToken.Secret, + ) + superusersCollection.PasswordResetToken.Duration = zeroFallback( + cast.ToInt64(getMapVal(oldSettings.Value, "adminPasswordResetToken", "duration")), + superusersCollection.PasswordResetToken.Duration, + ) + superusersCollection.FileToken.Secret = zeroFallback( + cast.ToString(getMapVal(oldSettings.Value, "adminFileToken", "secret")), + superusersCollection.FileToken.Secret, + ) + superusersCollection.FileToken.Duration = zeroFallback( + cast.ToInt64(getMapVal(oldSettings.Value, "adminFileToken", "duration")), + superusersCollection.FileToken.Duration, + ) + if err = txApp.Save(superusersCollection); err != nil { + return fmt.Errorf("failed to migrate token configs: %w", err) + } + + // copy old admins records into the new one + _, err = txApp.DB().NewQuery(` + INSERT INTO {{` + core.CollectionNameSuperusers + `}} ([[id]], [[verified]], [[email]], [[password]], [[tokenKey]], [[created]], [[updated]]) + SELECT [[id]], true, [[email]], [[passwordHash]], [[tokenKey]], [[created]], [[updated]] FROM {{_admins}}; + `).Execute() + if err != nil { + return err + } + + // remove old admins table + _, err = txApp.DB().DropTable("_admins").Execute() + if err != nil { + return err + } + + return nil +} + +// ------------------------------------------------------------------- + +type oldSettingsModel struct { + Id string `db:"id" json:"id"` + Key string `db:"key" json:"key"` + RawValue types.JSONRaw `db:"value" json:"value"` + Value map[string]any `db:"-" json:"-"` +} + +func loadOldSettings(txApp core.App) (*oldSettingsModel, error) { + oldSettings := &oldSettingsModel{Value: map[string]any{}} + err := txApp.DB().Select().From("_params").Where(dbx.HashExp{"key": "settings"}).One(oldSettings) + if err != nil { + return nil, err + } + + // try without decrypt + plainDecodeErr := json.Unmarshal(oldSettings.RawValue, &oldSettings.Value) + + // failed, try to decrypt + if plainDecodeErr != nil { + encryptionKey := os.Getenv(txApp.EncryptionEnv()) + + // load without decryption has failed and there is no encryption key to use for decrypt + if encryptionKey == "" { + return nil, fmt.Errorf("invalid settings db data or missing encryption key %q", txApp.EncryptionEnv()) + } + + // decrypt + decrypted, decryptErr := security.Decrypt(string(oldSettings.RawValue), encryptionKey) + if decryptErr != nil { + return nil, decryptErr + } + + // decode again + decryptedDecodeErr := json.Unmarshal(decrypted, &oldSettings.Value) + if decryptedDecodeErr != nil { + return nil, decryptedDecodeErr + } + } + + return oldSettings, nil +} + +func migrateSettings(txApp core.App, oldSettings *oldSettingsModel) error { + // renamed old params collection + _, err := txApp.DB().RenameTable("_params", "_params_old").Execute() + if err != nil { + return err + } + + // create new params table + err = createParamsTable(txApp) + if err != nil { + return err + } + + // migrate old settings + newSettings := txApp.Settings() + // --- + newSettings.Meta.AppName = cast.ToString(getMapVal(oldSettings.Value, "meta", "appName")) + newSettings.Meta.AppURL = strings.TrimSuffix(cast.ToString(getMapVal(oldSettings.Value, "meta", "appUrl")), "/") + newSettings.Meta.HideControls = cast.ToBool(getMapVal(oldSettings.Value, "meta", "hideControls")) + newSettings.Meta.SenderName = cast.ToString(getMapVal(oldSettings.Value, "meta", "senderName")) + newSettings.Meta.SenderAddress = cast.ToString(getMapVal(oldSettings.Value, "meta", "senderAddress")) + // --- + newSettings.Logs.MaxDays = cast.ToInt(getMapVal(oldSettings.Value, "logs", "maxDays")) + newSettings.Logs.MinLevel = cast.ToInt(getMapVal(oldSettings.Value, "logs", "minLevel")) + newSettings.Logs.LogIP = cast.ToBool(getMapVal(oldSettings.Value, "logs", "logIp")) + // --- + newSettings.SMTP.Enabled = cast.ToBool(getMapVal(oldSettings.Value, "smtp", "enabled")) + newSettings.SMTP.Port = cast.ToInt(getMapVal(oldSettings.Value, "smtp", "port")) + newSettings.SMTP.Host = cast.ToString(getMapVal(oldSettings.Value, "smtp", "host")) + newSettings.SMTP.Username = cast.ToString(getMapVal(oldSettings.Value, "smtp", "username")) + newSettings.SMTP.Password = cast.ToString(getMapVal(oldSettings.Value, "smtp", "password")) + newSettings.SMTP.AuthMethod = cast.ToString(getMapVal(oldSettings.Value, "smtp", "authMethod")) + newSettings.SMTP.TLS = cast.ToBool(getMapVal(oldSettings.Value, "smtp", "tls")) + newSettings.SMTP.LocalName = cast.ToString(getMapVal(oldSettings.Value, "smtp", "localName")) + // --- + newSettings.Backups.Cron = cast.ToString(getMapVal(oldSettings.Value, "backups", "cron")) + newSettings.Backups.CronMaxKeep = cast.ToInt(getMapVal(oldSettings.Value, "backups", "cronMaxKeep")) + newSettings.Backups.S3 = core.S3Config{ + Enabled: cast.ToBool(getMapVal(oldSettings.Value, "backups", "s3", "enabled")), + Bucket: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "bucket")), + Region: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "region")), + Endpoint: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "endpoint")), + AccessKey: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "accessKey")), + Secret: cast.ToString(getMapVal(oldSettings.Value, "backups", "s3", "secret")), + ForcePathStyle: cast.ToBool(getMapVal(oldSettings.Value, "backups", "s3", "forcePathStyle")), + } + // --- + newSettings.S3 = core.S3Config{ + Enabled: cast.ToBool(getMapVal(oldSettings.Value, "s3", "enabled")), + Bucket: cast.ToString(getMapVal(oldSettings.Value, "s3", "bucket")), + Region: cast.ToString(getMapVal(oldSettings.Value, "s3", "region")), + Endpoint: cast.ToString(getMapVal(oldSettings.Value, "s3", "endpoint")), + AccessKey: cast.ToString(getMapVal(oldSettings.Value, "s3", "accessKey")), + Secret: cast.ToString(getMapVal(oldSettings.Value, "s3", "secret")), + ForcePathStyle: cast.ToBool(getMapVal(oldSettings.Value, "s3", "forcePathStyle")), + } + // --- + err = txApp.Save(newSettings) + if err != nil { + return err + } + + // remove old params table + _, err = txApp.DB().DropTable("_params_old").Execute() + if err != nil { + return err + } + + return nil +} + +// ------------------------------------------------------------------- + +func migrateExternalAuths(txApp core.App) error { + // renamed old externalAuths table + _, err := txApp.DB().RenameTable("_externalAuths", "_externalAuths_old").Execute() + if err != nil { + return err + } + + // create new externalAuths collection and table + err = createExternalAuthsCollection(txApp) + if err != nil { + return err + } + + // copy old externalAuths records into the new one + _, err = txApp.DB().NewQuery(` + INSERT INTO {{` + core.CollectionNameExternalAuths + `}} ([[id]], [[collectionRef]], [[recordRef]], [[provider]], [[providerId]], [[created]], [[updated]]) + SELECT [[id]], [[collectionId]], [[recordId]], [[provider]], [[providerId]], [[created]], [[updated]] FROM {{_externalAuths_old}}; + `).Execute() + if err != nil { + return err + } + + // remove old externalAuths table + _, err = txApp.DB().DropTable("_externalAuths_old").Execute() + if err != nil { + return err + } + + return nil +} + +// ------------------------------------------------------------------- + +func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error { + oldCollections := []*OldCollectionModel{} + err := txApp.DB().Select().From("_collections").All(&oldCollections) + if err != nil { + return err + } + + for _, c := range oldCollections { + dummyAuthCollection := core.NewAuthCollection("test") + + options := c.Options + c.Options = types.JSONMap[any]{} // reset + + // update rules + // --- + c.ListRule = migrateRule(c.ListRule) + c.ViewRule = migrateRule(c.ViewRule) + c.CreateRule = migrateRule(c.CreateRule) + c.UpdateRule = migrateRule(c.UpdateRule) + c.DeleteRule = migrateRule(c.DeleteRule) + + // migrate fields + // --- + for i, field := range c.Schema { + switch cast.ToString(field["type"]) { + case "bool": + field = toBoolField(field) + case "number": + field = toNumberField(field) + case "text": + field = toTextField(field) + case "url": + field = toURLField(field) + case "email": + field = toEmailField(field) + case "editor": + field = toEditorField(field) + case "date": + field = toDateField(field) + case "select": + field = toSelectField(field) + case "json": + field = toJSONField(field) + case "relation": + field = toRelationField(field) + case "file": + field = toFileField(field) + } + c.Schema[i] = field + } + + // type specific changes + switch c.Type { + case "auth": + // token configs + // --- + c.Options["authToken"] = map[string]any{ + "secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordAuthToken", "secret")), dummyAuthCollection.AuthToken.Secret), + "duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordAuthToken", "duration")), dummyAuthCollection.AuthToken.Duration), + } + c.Options["passwordResetToken"] = map[string]any{ + "secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordPasswordResetToken", "secret")), dummyAuthCollection.PasswordResetToken.Secret), + "duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordPasswordResetToken", "duration")), dummyAuthCollection.PasswordResetToken.Duration), + } + c.Options["emailChangeToken"] = map[string]any{ + "secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordEmailChangeToken", "secret")), dummyAuthCollection.EmailChangeToken.Secret), + "duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordEmailChangeToken", "duration")), dummyAuthCollection.EmailChangeToken.Duration), + } + c.Options["verificationToken"] = map[string]any{ + "secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordVerificationToken", "secret")), dummyAuthCollection.VerificationToken.Secret), + "duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordVerificationToken", "duration")), dummyAuthCollection.VerificationToken.Duration), + } + c.Options["fileToken"] = map[string]any{ + "secret": zeroFallback(cast.ToString(getMapVal(oldSettings.Value, "recordFileToken", "secret")), dummyAuthCollection.FileToken.Secret), + "duration": zeroFallback(cast.ToInt64(getMapVal(oldSettings.Value, "recordFileToken", "duration")), dummyAuthCollection.FileToken.Duration), + } + + onlyVerified := cast.ToBool(options["onlyVerified"]) + if onlyVerified { + c.Options["authRule"] = "verified=true" + } else { + c.Options["authRule"] = "" + } + + c.Options["manageRule"] = nil + if options["manageRule"] != nil { + manageRule := cast.ToString(options["manageRule"]) + c.Options["manageRule"] = &manageRule + } + + // passwordAuth + identityFields := []string{} + if cast.ToBool(options["allowEmailAuth"]) { + identityFields = append(identityFields, "email") + } + if cast.ToBool(options["allowUsernameAuth"]) { + identityFields = append(identityFields, "username") + } + c.Options["passwordAuth"] = map[string]any{ + "enabled": len(identityFields) > 0, + "identityFields": identityFields, + } + + // oauth2 + // --- + oauth2Providers := []map[string]any{} + providerNames := []string{ + "googleAuth", + "facebookAuth", + "githubAuth", + "gitlabAuth", + "discordAuth", + "twitterAuth", + "microsoftAuth", + "spotifyAuth", + "kakaoAuth", + "twitchAuth", + "stravaAuth", + "giteeAuth", + "livechatAuth", + "giteaAuth", + "oidcAuth", + "oidc2Auth", + "oidc3Auth", + "appleAuth", + "instagramAuth", + "vkAuth", + "yandexAuth", + "patreonAuth", + "mailcowAuth", + "bitbucketAuth", + "planningcenterAuth", + } + for _, name := range providerNames { + if !cast.ToBool(getMapVal(oldSettings.Value, name, "enabled")) { + continue + } + oauth2Providers = append(oauth2Providers, map[string]any{ + "name": strings.TrimSuffix(name, "Auth"), + "clientId": cast.ToString(getMapVal(oldSettings.Value, name, "clientId")), + "clientSecret": cast.ToString(getMapVal(oldSettings.Value, name, "clientSecret")), + "authURL": cast.ToString(getMapVal(oldSettings.Value, name, "authUrl")), + "tokenURL": cast.ToString(getMapVal(oldSettings.Value, name, "tokenUrl")), + "userInfoURL": cast.ToString(getMapVal(oldSettings.Value, name, "userApiUrl")), + "displayName": cast.ToString(getMapVal(oldSettings.Value, name, "displayName")), + "pkce": getMapVal(oldSettings.Value, name, "pkce"), + }) + } + + c.Options["oauth2"] = map[string]any{ + "enabled": cast.ToBool(options["allowOAuth2Auth"]) && len(oauth2Providers) > 0, + "providers": oauth2Providers, + "mappedFields": map[string]string{ + "username": "username", + }, + } + + // default email templates + // --- + emailTemplates := map[string]core.EmailTemplate{ + "verificationTemplate": dummyAuthCollection.VerificationTemplate, + "resetPasswordTemplate": dummyAuthCollection.ResetPasswordTemplate, + "confirmEmailChangeTemplate": dummyAuthCollection.ConfirmEmailChangeTemplate, + } + for name, fallback := range emailTemplates { + c.Options[name] = map[string]any{ + "subject": zeroFallback( + cast.ToString(getMapVal(oldSettings.Value, "meta", name, "subject")), + fallback.Subject, + ), + "body": zeroFallback( + strings.ReplaceAll( + cast.ToString(getMapVal(oldSettings.Value, "meta", name, "body")), + "{ACTION_URL}", + cast.ToString(getMapVal(oldSettings.Value, "meta", name, "actionUrl")), + ), + fallback.Body, + ), + } + } + + // mfa + // --- + c.Options["mfa"] = map[string]any{ + "enabled": dummyAuthCollection.MFA.Enabled, + "duration": dummyAuthCollection.MFA.Duration, + "rule": dummyAuthCollection.MFA.Rule, + } + + // otp + // --- + c.Options["otp"] = map[string]any{ + "enabled": dummyAuthCollection.OTP.Enabled, + "duration": dummyAuthCollection.OTP.Duration, + "length": dummyAuthCollection.OTP.Length, + "emailTemplate": map[string]any{ + "subject": dummyAuthCollection.OTP.EmailTemplate.Subject, + "body": dummyAuthCollection.OTP.EmailTemplate.Body, + }, + } + + // auth alerts + // --- + c.Options["authAlert"] = map[string]any{ + "enabled": dummyAuthCollection.AuthAlert.Enabled, + "emailTemplate": map[string]any{ + "subject": dummyAuthCollection.AuthAlert.EmailTemplate.Subject, + "body": dummyAuthCollection.AuthAlert.EmailTemplate.Body, + }, + } + + // add system field indexes + // --- + c.Indexes = append(types.JSONArray[string]{ + fmt.Sprintf("CREATE UNIQUE INDEX `_%s_username_idx` ON `%s` (username COLLATE NOCASE)", c.Id, c.Name), + fmt.Sprintf("CREATE UNIQUE INDEX `_%s_email_idx` ON `%s` (email) WHERE email != ''", c.Id, c.Name), + fmt.Sprintf("CREATE UNIQUE INDEX `_%s_tokenKey_idx` ON `%s` (tokenKey)", c.Id, c.Name), + }, c.Indexes...) + + // prepend the auth system fields + // --- + tokenKeyField := map[string]any{ + "type": "text", + "id": "_pbf_auth_tokenKey_", + "name": "tokenKey", + "system": true, + "hidden": true, + "required": true, + "presentable": false, + "primaryKey": false, + "min": 30, + "max": 60, + "pattern": "", + "autogeneratePattern": "[a-zA-Z0-9_]{50}", + } + passwordField := map[string]any{ + "type": "password", + "id": "_pbf_auth_password_", + "name": "password", + "presentable": false, + "system": true, + "hidden": true, + "required": true, + "pattern": "", + "min": cast.ToInt(options["minPasswordLength"]), + "cost": bcrypt.DefaultCost, // new default + } + emailField := map[string]any{ + "type": "email", + "id": "_pbf_auth_email_", + "name": "email", + "system": true, + "hidden": false, + "presentable": false, + "required": cast.ToBool(options["requireEmail"]), + "exceptDomains": cast.ToStringSlice(options["exceptEmailDomains"]), + "onlyDomains": cast.ToStringSlice(options["onlyEmailDomains"]), + } + emailVisibilityField := map[string]any{ + "type": "bool", + "id": "_pbf_auth_emailVisibility_", + "name": "emailVisibility", + "system": true, + "hidden": false, + "presentable": false, + "required": false, + } + verifiedField := map[string]any{ + "type": "bool", + "id": "_pbf_auth_verified_", + "name": "verified", + "system": true, + "hidden": false, + "presentable": false, + "required": false, + } + usernameField := map[string]any{ + "type": "text", + "id": "_pbf_auth_username_", + "name": "username", + "system": false, + "hidden": false, + "required": true, + "presentable": false, + "primaryKey": false, + "min": 3, + "max": 150, + "pattern": `^[\w][\w\.\-]*$`, + "autogeneratePattern": "users[0-9]{6}", + } + c.Schema = append(types.JSONArray[types.JSONMap[any]]{ + passwordField, + tokenKeyField, + emailField, + emailVisibilityField, + verifiedField, + usernameField, + }, c.Schema...) + + // rename passwordHash records rable column to password + // --- + _, err = txApp.DB().RenameColumn(c.Name, "passwordHash", "password").Execute() + if err != nil { + return err + } + + // delete unnecessary auth columns + dropColumns := []string{"lastResetSentAt", "lastVerificationSentAt", "lastAuthAlertSentAt"} + for _, drop := range dropColumns { + // ignore errors in case the columns don't exist + _, _ = txApp.DB().DropColumn(c.Name, drop).Execute() + } + case "view": + c.Options["viewQuery"] = cast.ToString(options["query"]) + } + + // prepend the id field + idField := map[string]any{ + "type": "text", + "id": "_pbf_text_id_", + "name": "id", + "system": true, + "required": true, + "presentable": false, + "hidden": false, + "primaryKey": true, + "min": 15, + "max": 15, + "pattern": "^[a-z0-9]+$", + "autogeneratePattern": "[a-z0-9]{15}", + } + c.Schema = append(types.JSONArray[types.JSONMap[any]]{idField}, c.Schema...) + + var addCreated, addUpdated bool + + if c.Type == "view" { + // manually check if the view has created/updated columns + columns, _ := txApp.TableColumns(c.Name) + for _, c := range columns { + if strings.EqualFold(c, "created") { + addCreated = true + } else if strings.EqualFold(c, "updated") { + addUpdated = true + } + } + } else { + addCreated = true + addUpdated = true + } + + if addCreated { + createdField := map[string]any{ + "type": "autodate", + "id": "_pbf_autodate_created_", + "name": "created", + "system": false, + "presentable": false, + "hidden": false, + "onCreate": true, + "onUpdate": false, + } + c.Schema = append(c.Schema, createdField) + } + + if addUpdated { + updatedField := map[string]any{ + "type": "autodate", + "id": "_pbf_autodate_updated_", + "name": "updated", + "system": false, + "presentable": false, + "hidden": false, + "onCreate": true, + "onUpdate": true, + } + c.Schema = append(c.Schema, updatedField) + } + + if err = txApp.DB().Model(c).Update(); err != nil { + return err + } + } + + _, err = txApp.DB().RenameColumn("_collections", "schema", "fields").Execute() + if err != nil { + return err + } + + // run collection validations + collections, err := txApp.FindAllCollections() + if err != nil { + return fmt.Errorf("failed to retrieve all collections: %w", err) + } + for _, c := range collections { + err = txApp.Validate(c) + if err != nil { + return fmt.Errorf("migrated collection %q validation failure: %w", c.Name, err) + } + } + + return nil +} + +type OldCollectionModel struct { + Id string `db:"id" json:"id"` + Created types.DateTime `db:"created" json:"created"` + Updated types.DateTime `db:"updated" json:"updated"` + Name string `db:"name" json:"name"` + Type string `db:"type" json:"type"` + System bool `db:"system" json:"system"` + Schema types.JSONArray[types.JSONMap[any]] `db:"schema" json:"schema"` + Indexes types.JSONArray[string] `db:"indexes" json:"indexes"` + ListRule *string `db:"listRule" json:"listRule"` + ViewRule *string `db:"viewRule" json:"viewRule"` + CreateRule *string `db:"createRule" json:"createRule"` + UpdateRule *string `db:"updateRule" json:"updateRule"` + DeleteRule *string `db:"deleteRule" json:"deleteRule"` + Options types.JSONMap[any] `db:"options" json:"options"` +} + +func (c OldCollectionModel) TableName() string { + return "_collections" +} + +func migrateRule(rule *string) *string { + if rule == nil { + return nil + } + + str := strings.ReplaceAll(*rule, "@request.data", "@request.body") + + return &str +} + +func toBoolField(data map[string]any) map[string]any { + return map[string]any{ + "type": "bool", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + } +} + +func toNumberField(data map[string]any) map[string]any { + return map[string]any{ + "type": "number", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "onlyInt": cast.ToBool(getMapVal(data, "options", "noDecimal")), + "min": getMapVal(data, "options", "min"), + "max": getMapVal(data, "options", "max"), + } +} + +func toTextField(data map[string]any) map[string]any { + return map[string]any{ + "type": "text", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "primaryKey": cast.ToBool(data["primaryKey"]), + "hidden": cast.ToBool(data["hidden"]), + "presentable": cast.ToBool(data["presentable"]), + "required": cast.ToBool(data["required"]), + "min": cast.ToInt(getMapVal(data, "options", "min")), + "max": cast.ToInt(getMapVal(data, "options", "max")), + "pattern": cast.ToString(getMapVal(data, "options", "pattern")), + "autogeneratePattern": cast.ToString(getMapVal(data, "options", "autogeneratePattern")), + } +} + +func toEmailField(data map[string]any) map[string]any { + return map[string]any{ + "type": "email", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "exceptDomains": cast.ToStringSlice(getMapVal(data, "options", "exceptDomains")), + "onlyDomains": cast.ToStringSlice(getMapVal(data, "options", "onlyDomains")), + } +} + +func toURLField(data map[string]any) map[string]any { + return map[string]any{ + "type": "url", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "exceptDomains": cast.ToStringSlice(getMapVal(data, "options", "exceptDomains")), + "onlyDomains": cast.ToStringSlice(getMapVal(data, "options", "onlyDomains")), + } +} + +func toEditorField(data map[string]any) map[string]any { + return map[string]any{ + "type": "editor", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "convertURLs": cast.ToBool(getMapVal(data, "options", "convertUrls")), + } +} + +func toDateField(data map[string]any) map[string]any { + return map[string]any{ + "type": "date", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "min": cast.ToString(getMapVal(data, "options", "min")), + "max": cast.ToString(getMapVal(data, "options", "max")), + } +} + +func toJSONField(data map[string]any) map[string]any { + return map[string]any{ + "type": "json", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "maxSize": cast.ToInt64(getMapVal(data, "options", "maxSize")), + } +} + +func toSelectField(data map[string]any) map[string]any { + return map[string]any{ + "type": "select", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "values": cast.ToStringSlice(getMapVal(data, "options", "values")), + "maxSelect": cast.ToInt(getMapVal(data, "options", "maxSelect")), + } +} + +func toRelationField(data map[string]any) map[string]any { + maxSelect := cast.ToInt(getMapVal(data, "options", "maxSelect")) + if maxSelect <= 0 { + maxSelect = 2147483647 + } + + return map[string]any{ + "type": "relation", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "collectionId": cast.ToString(getMapVal(data, "options", "collectionId")), + "cascadeDelete": cast.ToBool(getMapVal(data, "options", "cascadeDelete")), + "minSelect": cast.ToInt(getMapVal(data, "options", "minSelect")), + "maxSelect": maxSelect, + } +} + +func toFileField(data map[string]any) map[string]any { + return map[string]any{ + "type": "file", + "id": cast.ToString(data["id"]), + "name": cast.ToString(data["name"]), + "system": cast.ToBool(data["system"]), + "required": cast.ToBool(data["required"]), + "presentable": cast.ToBool(data["presentable"]), + "hidden": false, + "maxSelect": cast.ToInt(getMapVal(data, "options", "maxSelect")), + "maxSize": cast.ToInt64(getMapVal(data, "options", "maxSize")), + "thumbs": cast.ToStringSlice(getMapVal(data, "options", "thumbs")), + "mimeTypes": cast.ToStringSlice(getMapVal(data, "options", "mimeTypes")), + "protected": cast.ToBool(getMapVal(data, "options", "protected")), + } +} + +func getMapVal(m map[string]any, keys ...string) any { + if len(keys) == 0 { + return nil + } + + result, ok := m[keys[0]] + if !ok { + return nil + } + + // end key reached + if len(keys) == 1 { + return result + } + + if m, ok = result.(map[string]any); !ok { + return nil + } + + return getMapVal(m, keys[1:]...) +} + +func zeroFallback[T comparable](v T, fallback T) T { + var zero T + + if v == zero { + return fallback + } + + return v +} diff --git a/migrations/1718706525_add_login_alert_column.go b/migrations/1718706525_add_login_alert_column.go deleted file mode 100644 index 1ab71e69..00000000 --- a/migrations/1718706525_add_login_alert_column.go +++ /dev/null @@ -1,56 +0,0 @@ -package migrations - -import ( - "slices" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/security" -) - -// adds a "lastLoginAlertSentAt" column to all auth collection tables (if not already) -func init() { - AppMigrations.Register(func(db dbx.Builder) error { - dao := daos.New(db) - - collections := []*models.Collection{} - err := dao.CollectionQuery().AndWhere(dbx.HashExp{"type": models.CollectionTypeAuth}).All(&collections) - if err != nil { - return err - } - - var needToResetTokens bool - - for _, c := range collections { - columns, err := dao.TableColumns(c.Name) - if err != nil { - return err - } - if slices.Contains(columns, schema.FieldNameLastLoginAlertSentAt) { - continue // already inserted - } - - _, err = db.AddColumn(c.Name, schema.FieldNameLastLoginAlertSentAt, "TEXT DEFAULT '' NOT NULL").Execute() - if err != nil { - return err - } - - opts := c.AuthOptions() - if opts.AllowOAuth2Auth && (opts.AllowEmailAuth || opts.AllowUsernameAuth) { - needToResetTokens = true - } - } - - settings, _ := dao.FindSettings() - if needToResetTokens && settings != nil { - settings.RecordAuthToken.Secret = security.RandomString(50) - if err := dao.SaveSettings(settings); err != nil { - return err - } - } - - return nil - }, nil) -} diff --git a/migrations/logs/1640988000_init.go b/migrations/logs/1640988000_init.go deleted file mode 100644 index a8d8ef2e..00000000 --- a/migrations/logs/1640988000_init.go +++ /dev/null @@ -1,38 +0,0 @@ -package logs - -import ( - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/tools/migrate" -) - -var LogsMigrations migrate.MigrationsList - -func init() { - LogsMigrations.Register(func(db dbx.Builder) error { - _, err := db.NewQuery(` - CREATE TABLE {{_requests}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, - [[url]] TEXT DEFAULT "" NOT NULL, - [[method]] TEXT DEFAULT "get" NOT NULL, - [[status]] INTEGER DEFAULT 200 NOT NULL, - [[auth]] TEXT DEFAULT "guest" NOT NULL, - [[ip]] TEXT DEFAULT "127.0.0.1" NOT NULL, - [[referer]] TEXT DEFAULT "" NOT NULL, - [[userAgent]] TEXT DEFAULT "" NOT NULL, - [[meta]] JSON DEFAULT "{}" NOT NULL, - [[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 _request_status_idx on {{_requests}} ([[status]]); - CREATE INDEX _request_auth_idx on {{_requests}} ([[auth]]); - CREATE INDEX _request_ip_idx on {{_requests}} ([[ip]]); - CREATE INDEX _request_created_hour_idx on {{_requests}} (strftime('%Y-%m-%d %H:00:00', [[created]])); - `).Execute() - - return err - }, func(db dbx.Builder) error { - _, err := db.DropTable("_requests").Execute() - return err - }) -} diff --git a/migrations/logs/1660821103_add_user_ip_column.go b/migrations/logs/1660821103_add_user_ip_column.go deleted file mode 100644 index 1af099d5..00000000 --- a/migrations/logs/1660821103_add_user_ip_column.go +++ /dev/null @@ -1,57 +0,0 @@ -package logs - -import ( - "github.com/pocketbase/dbx" -) - -func init() { - LogsMigrations.Register(func(db dbx.Builder) error { - // delete old index (don't check for error because of backward compatibility with old installations) - db.DropIndex("_requests", "_request_ip_idx").Execute() - - // rename ip -> remoteIp - if _, err := db.RenameColumn("_requests", "ip", "remoteIp").Execute(); err != nil { - return err - } - - // add new userIp column - if _, err := db.AddColumn("_requests", "userIp", `TEXT DEFAULT "127.0.0.1" NOT NULL`).Execute(); err != nil { - return err - } - - // add new indexes - if _, err := db.CreateIndex("_requests", "_request_remote_ip_idx", "remoteIp").Execute(); err != nil { - return err - } - if _, err := db.CreateIndex("_requests", "_request_user_ip_idx", "userIp").Execute(); err != nil { - return err - } - - return nil - }, func(db dbx.Builder) error { - // delete new indexes - if _, err := db.DropIndex("_requests", "_request_remote_ip_idx").Execute(); err != nil { - return err - } - if _, err := db.DropIndex("_requests", "_request_user_ip_idx").Execute(); err != nil { - return err - } - - // drop userIp column - if _, err := db.DropColumn("_requests", "userIp").Execute(); err != nil { - return err - } - - // restore original remoteIp column name - if _, err := db.RenameColumn("_requests", "remoteIp", "ip").Execute(); err != nil { - return err - } - - // restore original index - if _, err := db.CreateIndex("_requests", "_request_ip_idx", "ip").Execute(); err != nil { - return err - } - - return nil - }) -} diff --git a/migrations/logs/1677760279_uppsercase_method.go b/migrations/logs/1677760279_uppsercase_method.go deleted file mode 100644 index cef96960..00000000 --- a/migrations/logs/1677760279_uppsercase_method.go +++ /dev/null @@ -1,18 +0,0 @@ -package logs - -import ( - "github.com/pocketbase/dbx" -) - -// This migration normalizes the request logs method to UPPERCASE (eg. "get" => "GET"). -func init() { - LogsMigrations.Register(func(db dbx.Builder) error { - _, err := db.NewQuery("UPDATE {{_requests}} SET method=UPPER(method)").Execute() - - return err - }, func(db dbx.Builder) error { - _, err := db.NewQuery("UPDATE {{_requests}} SET method=LOWER(method)").Execute() - - return err - }) -} diff --git a/migrations/logs/1699187560_logs_generalization.go b/migrations/logs/1699187560_logs_generalization.go deleted file mode 100644 index beb18333..00000000 --- a/migrations/logs/1699187560_logs_generalization.go +++ /dev/null @@ -1,57 +0,0 @@ -package logs - -import ( - "github.com/pocketbase/dbx" -) - -func init() { - LogsMigrations.Register(func(db dbx.Builder) error { - if _, err := db.DropTable("_requests").Execute(); err != nil { - return err - } - - _, err := db.NewQuery(` - CREATE TABLE {{_logs}} ( - [[id]] TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL, - [[level]] INTEGER DEFAULT 0 NOT NULL, - [[message]] TEXT DEFAULT "" NOT NULL, - [[data]] JSON DEFAULT "{}" NOT NULL, - [[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 _logs_level_idx on {{_logs}} ([[level]]); - CREATE INDEX _logs_message_idx on {{_logs}} ([[message]]); - CREATE INDEX _logs_created_hour_idx on {{_logs}} (strftime('%Y-%m-%d %H:00:00', [[created]])); - `).Execute() - - return err - }, func(db dbx.Builder) error { - if _, err := db.DropTable("_logs").Execute(); err != nil { - return err - } - - _, err := db.NewQuery(` - CREATE TABLE {{_requests}} ( - [[id]] TEXT PRIMARY KEY NOT NULL, - [[url]] TEXT DEFAULT "" NOT NULL, - [[method]] TEXT DEFAULT "get" NOT NULL, - [[status]] INTEGER DEFAULT 200 NOT NULL, - [[auth]] TEXT DEFAULT "guest" NOT NULL, - [[ip]] TEXT DEFAULT "127.0.0.1" NOT NULL, - [[referer]] TEXT DEFAULT "" NOT NULL, - [[userAgent]] TEXT DEFAULT "" NOT NULL, - [[meta]] JSON DEFAULT "{}" NOT NULL, - [[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 _request_status_idx on {{_requests}} ([[status]]); - CREATE INDEX _request_auth_idx on {{_requests}} ([[auth]]); - CREATE INDEX _request_ip_idx on {{_requests}} ([[ip]]); - CREATE INDEX _request_created_hour_idx on {{_requests}} (strftime('%Y-%m-%d %H:00:00', [[created]])); - `).Execute() - - return err - }) -} diff --git a/models/admin.go b/models/admin.go deleted file mode 100644 index 047aff76..00000000 --- a/models/admin.go +++ /dev/null @@ -1,67 +0,0 @@ -package models - -import ( - "errors" - - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" - "golang.org/x/crypto/bcrypt" -) - -var ( - _ Model = (*Admin)(nil) -) - -type Admin struct { - BaseModel - - Avatar int `db:"avatar" json:"avatar"` - Email string `db:"email" json:"email"` - TokenKey string `db:"tokenKey" json:"-"` - PasswordHash string `db:"passwordHash" json:"-"` - LastResetSentAt types.DateTime `db:"lastResetSentAt" json:"-"` -} - -// TableName returns the Admin model SQL table name. -func (m *Admin) TableName() string { - return "_admins" -} - -// ValidatePassword validates a plain password against the model's password. -func (m *Admin) ValidatePassword(password string) bool { - bytePassword := []byte(password) - bytePasswordHash := []byte(m.PasswordHash) - - // comparing the password with the hash - err := bcrypt.CompareHashAndPassword(bytePasswordHash, bytePassword) - - // nil means it is a match - return err == nil -} - -// SetPassword sets cryptographically secure string to `model.Password`. -// -// Additionally this method also resets the LastResetSentAt and the TokenKey fields. -func (m *Admin) SetPassword(password string) error { - if password == "" { - return errors.New("The provided plain password is empty") - } - - // hash the password - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) - if err != nil { - return err - } - - m.PasswordHash = string(hashedPassword) - m.LastResetSentAt = types.DateTime{} // reset - - // invalidate previously issued tokens - return m.RefreshTokenKey() -} - -// RefreshTokenKey generates and sets new random token key. -func (m *Admin) RefreshTokenKey() error { - m.TokenKey = security.RandomString(50) - return nil -} diff --git a/models/admin_test.go b/models/admin_test.go deleted file mode 100644 index 6730d229..00000000 --- a/models/admin_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestAdminTableName(t *testing.T) { - t.Parallel() - - m := models.Admin{} - if m.TableName() != "_admins" { - t.Fatalf("Unexpected table name, got %q", m.TableName()) - } -} - -func TestAdminValidatePassword(t *testing.T) { - t.Parallel() - - scenarios := []struct { - admin models.Admin - password string - expected bool - }{ - { - // empty passwordHash + empty pass - models.Admin{}, - "", - false, - }, - { - // empty passwordHash + nonempty pass - models.Admin{}, - "123456", - false, - }, - { - // nonempty passwordHash + empty pass - models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, - "", - false, - }, - { - // nonempty passwordHash + wrong pass - models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, - "654321", - false, - }, - { - // nonempty passwordHash + correct pass - models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, - "123456", - true, - }, - } - - for i, s := range scenarios { - result := s.admin.ValidatePassword(s.password) - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestAdminSetPassword(t *testing.T) { - t.Parallel() - - m := models.Admin{ - // 123456 - PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv.", - LastResetSentAt: types.NowDateTime(), - TokenKey: "test", - } - - // empty pass - err1 := m.SetPassword("") - if err1 == nil { - t.Fatal("Expected empty password error") - } - - err2 := m.SetPassword("654321") - if err2 != nil { - t.Fatalf("Expected nil, got error %v", err2) - } - - if !m.ValidatePassword("654321") { - t.Fatalf("Password is invalid") - } - - if m.TokenKey == "test" { - t.Fatalf("Expected TokenKey to change, got %v", m.TokenKey) - } - - if !m.LastResetSentAt.IsZero() { - t.Fatalf("Expected LastResetSentAt to be zero datetime, got %v", m.LastResetSentAt) - } -} - -func TestAdminRefreshTokenKey(t *testing.T) { - t.Parallel() - - m := models.Admin{TokenKey: "test"} - - m.RefreshTokenKey() - - // empty pass - if m.TokenKey == "" || m.TokenKey == "test" { - t.Fatalf("Expected TokenKey to change, got %q", m.TokenKey) - } -} diff --git a/models/backup_file_info.go b/models/backup_file_info.go deleted file mode 100644 index 794900f3..00000000 --- a/models/backup_file_info.go +++ /dev/null @@ -1,9 +0,0 @@ -package models - -import "github.com/pocketbase/pocketbase/tools/types" - -type BackupFileInfo struct { - Key string `json:"key"` - Size int64 `json:"size"` - Modified types.DateTime `json:"modified"` -} diff --git a/models/base.go b/models/base.go deleted file mode 100644 index 44f5a76d..00000000 --- a/models/base.go +++ /dev/null @@ -1,122 +0,0 @@ -// Package models implements all PocketBase DB models and DTOs. -package models - -import ( - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/types" -) - -const ( - // DefaultIdLength is the default length of the generated model id. - DefaultIdLength = 15 - - // DefaultIdAlphabet is the default characters set used for generating the model id. - DefaultIdAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" -) - -// ColumnValueMapper defines an interface for custom db model data serialization. -type ColumnValueMapper interface { - // ColumnValueMap returns the data to be used when persisting the model. - ColumnValueMap() map[string]any -} - -// FilesManager defines an interface with common methods that files manager models should implement. -type FilesManager interface { - // BaseFilesPath returns the storage dir path used by the interface instance. - BaseFilesPath() string -} - -// Model defines an interface with common methods that all db models should have. -type Model interface { - TableName() string - IsNew() bool - MarkAsNew() - MarkAsNotNew() - HasId() bool - GetId() string - SetId(id string) - GetCreated() types.DateTime - GetUpdated() types.DateTime - RefreshId() - RefreshCreated() - RefreshUpdated() -} - -// ------------------------------------------------------------------- -// BaseModel -// ------------------------------------------------------------------- - -// BaseModel defines common fields and methods used by all other models. -type BaseModel struct { - isNotNew bool - - Id string `db:"id" json:"id"` - Created types.DateTime `db:"created" json:"created"` - Updated types.DateTime `db:"updated" json:"updated"` -} - -// HasId returns whether the model has a nonzero id. -func (m *BaseModel) HasId() bool { - return m.GetId() != "" -} - -// GetId returns the model id. -func (m *BaseModel) GetId() string { - return m.Id -} - -// SetId sets the model id to the provided string value. -func (m *BaseModel) SetId(id string) { - m.Id = id -} - -// MarkAsNew marks the model as "new" (aka. enforces m.IsNew() to be true). -func (m *BaseModel) MarkAsNew() { - m.isNotNew = false -} - -// MarkAsNotNew marks the model as "not new" (aka. enforces m.IsNew() to be false) -func (m *BaseModel) MarkAsNotNew() { - m.isNotNew = true -} - -// IsNew indicates what type of db query (insert or update) -// should be used with the model instance. -func (m *BaseModel) IsNew() bool { - return !m.isNotNew -} - -// GetCreated returns the model Created datetime. -func (m *BaseModel) GetCreated() types.DateTime { - return m.Created -} - -// GetUpdated returns the model Updated datetime. -func (m *BaseModel) GetUpdated() types.DateTime { - return m.Updated -} - -// RefreshId generates and sets a new model id. -// -// The generated id is a cryptographically random 15 characters length string. -func (m *BaseModel) RefreshId() { - m.Id = security.RandomStringWithAlphabet(DefaultIdLength, DefaultIdAlphabet) -} - -// RefreshCreated updates the model Created field with the current datetime. -func (m *BaseModel) RefreshCreated() { - m.Created = types.NowDateTime() -} - -// RefreshUpdated updates the model Updated field with the current datetime. -func (m *BaseModel) RefreshUpdated() { - m.Updated = types.NowDateTime() -} - -// PostScan implements the [dbx.PostScanner] interface. -// -// It is executed right after the model was populated with the db row values. -func (m *BaseModel) PostScan() error { - m.MarkAsNotNew() - return nil -} diff --git a/models/base_test.go b/models/base_test.go deleted file mode 100644 index 434887f3..00000000 --- a/models/base_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" -) - -func TestBaseModelHasId(t *testing.T) { - t.Parallel() - - scenarios := []struct { - model models.BaseModel - expected bool - }{ - { - models.BaseModel{}, - false, - }, - { - models.BaseModel{Id: ""}, - false, - }, - { - models.BaseModel{Id: "abc"}, - true, - }, - } - - for i, s := range scenarios { - result := s.model.HasId() - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestBaseModelId(t *testing.T) { - t.Parallel() - - m := models.BaseModel{} - - if m.GetId() != "" { - t.Fatalf("Expected empty id value, got %v", m.GetId()) - } - - m.SetId("test") - - if m.GetId() != "test" { - t.Fatalf("Expected %q id, got %v", "test", m.GetId()) - } - - m.RefreshId() - - if len(m.GetId()) != 15 { - t.Fatalf("Expected 15 chars id, got %v", m.GetId()) - } -} - -func TestBaseModelIsNew(t *testing.T) { - t.Parallel() - - m0 := models.BaseModel{} - m1 := models.BaseModel{Id: ""} - m2 := models.BaseModel{Id: "test"} - m3 := models.BaseModel{} - m3.MarkAsNotNew() - m4 := models.BaseModel{Id: "test"} - m4.MarkAsNotNew() - m5 := models.BaseModel{Id: "test"} - m5.MarkAsNew() - m5.MarkAsNotNew() - m6 := models.BaseModel{} - m6.RefreshId() - m7 := models.BaseModel{} - m7.MarkAsNotNew() - m7.RefreshId() - m8 := models.BaseModel{} - m8.PostScan() - - scenarios := []struct { - model models.BaseModel - expected bool - }{ - {m0, true}, - {m1, true}, - {m2, true}, - {m3, false}, - {m4, false}, - {m5, false}, - {m6, true}, - {m7, false}, - {m8, false}, - } - - for i, s := range scenarios { - result := s.model.IsNew() - if result != s.expected { - t.Errorf("(%d) Expected IsNew %v, got %v", i, s.expected, result) - } - } -} - -func TestBaseModelCreated(t *testing.T) { - t.Parallel() - - m := models.BaseModel{} - - if !m.GetCreated().IsZero() { - t.Fatalf("Expected zero datetime, got %v", m.GetCreated()) - } - - m.RefreshCreated() - - if m.GetCreated().IsZero() { - t.Fatalf("Expected non-zero datetime, got %v", m.GetCreated()) - } -} - -func TestBaseModelUpdated(t *testing.T) { - t.Parallel() - - m := models.BaseModel{} - - if !m.GetUpdated().IsZero() { - t.Fatalf("Expected zero datetime, got %v", m.GetUpdated()) - } - - m.RefreshUpdated() - - if m.GetUpdated().IsZero() { - t.Fatalf("Expected non-zero datetime, got %v", m.GetUpdated()) - } -} diff --git a/models/collection.go b/models/collection.go deleted file mode 100644 index f9a3eaa5..00000000 --- a/models/collection.go +++ /dev/null @@ -1,220 +0,0 @@ -package models - -import ( - "encoding/json" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" -) - -var ( - _ Model = (*Collection)(nil) - _ FilesManager = (*Collection)(nil) -) - -const ( - CollectionTypeBase = "base" - CollectionTypeAuth = "auth" - CollectionTypeView = "view" -) - -type Collection struct { - BaseModel - - Name string `db:"name" json:"name"` - Type string `db:"type" json:"type"` - System bool `db:"system" json:"system"` - Schema schema.Schema `db:"schema" json:"schema"` - Indexes types.JsonArray[string] `db:"indexes" json:"indexes"` - - // rules - ListRule *string `db:"listRule" json:"listRule"` - ViewRule *string `db:"viewRule" json:"viewRule"` - CreateRule *string `db:"createRule" json:"createRule"` - UpdateRule *string `db:"updateRule" json:"updateRule"` - DeleteRule *string `db:"deleteRule" json:"deleteRule"` - - Options types.JsonMap `db:"options" json:"options"` -} - -// TableName returns the Collection model SQL table name. -func (m *Collection) TableName() string { - return "_collections" -} - -// BaseFilesPath returns the storage dir path used by the collection. -func (m *Collection) BaseFilesPath() string { - return m.Id -} - -// IsBase checks if the current collection has "base" type. -func (m *Collection) IsBase() bool { - return m.Type == CollectionTypeBase -} - -// IsAuth checks if the current collection has "auth" type. -func (m *Collection) IsAuth() bool { - return m.Type == CollectionTypeAuth -} - -// IsView checks if the current collection has "view" type. -func (m *Collection) IsView() bool { - return m.Type == CollectionTypeView -} - -// MarshalJSON implements the [json.Marshaler] interface. -func (m Collection) MarshalJSON() ([]byte, error) { - type alias Collection // prevent recursion - - m.NormalizeOptions() - - return json.Marshal(alias(m)) -} - -// BaseOptions decodes the current collection options and returns them -// as new [CollectionBaseOptions] instance. -func (m *Collection) BaseOptions() CollectionBaseOptions { - result := CollectionBaseOptions{} - m.DecodeOptions(&result) - return result -} - -// AuthOptions decodes the current collection options and returns them -// as new [CollectionAuthOptions] instance. -func (m *Collection) AuthOptions() CollectionAuthOptions { - result := CollectionAuthOptions{} - m.DecodeOptions(&result) - return result -} - -// ViewOptions decodes the current collection options and returns them -// as new [CollectionViewOptions] instance. -func (m *Collection) ViewOptions() CollectionViewOptions { - result := CollectionViewOptions{} - m.DecodeOptions(&result) - return result -} - -// NormalizeOptions updates the current collection options with a -// new normalized state based on the collection type. -func (m *Collection) NormalizeOptions() error { - var typedOptions any - switch m.Type { - case CollectionTypeAuth: - typedOptions = m.AuthOptions() - case CollectionTypeView: - typedOptions = m.ViewOptions() - default: - typedOptions = m.BaseOptions() - } - - // serialize - raw, err := json.Marshal(typedOptions) - if err != nil { - return err - } - - // load into a new JsonMap - m.Options = types.JsonMap{} - if err := json.Unmarshal(raw, &m.Options); err != nil { - return err - } - - return nil -} - -// DecodeOptions decodes the current collection options into the -// provided "result" (must be a pointer). -func (m *Collection) DecodeOptions(result any) error { - // raw serialize - raw, err := json.Marshal(m.Options) - if err != nil { - return err - } - - // decode into the provided result - if err := json.Unmarshal(raw, result); err != nil { - return err - } - - return nil -} - -// SetOptions normalizes and unmarshals the specified options into m.Options. -func (m *Collection) SetOptions(typedOptions any) error { - // serialize - raw, err := json.Marshal(typedOptions) - if err != nil { - return err - } - - m.Options = types.JsonMap{} - if err := json.Unmarshal(raw, &m.Options); err != nil { - return err - } - - return m.NormalizeOptions() -} - -// ------------------------------------------------------------------- - -// CollectionBaseOptions defines the "base" Collection.Options fields. -type CollectionBaseOptions struct { -} - -// Validate implements [validation.Validatable] interface. -func (o CollectionBaseOptions) Validate() error { - return nil -} - -// ------------------------------------------------------------------- - -// CollectionAuthOptions defines the "auth" Collection.Options fields. -type CollectionAuthOptions struct { - ManageRule *string `form:"manageRule" json:"manageRule"` - AllowOAuth2Auth bool `form:"allowOAuth2Auth" json:"allowOAuth2Auth"` - AllowUsernameAuth bool `form:"allowUsernameAuth" json:"allowUsernameAuth"` - AllowEmailAuth bool `form:"allowEmailAuth" json:"allowEmailAuth"` - RequireEmail bool `form:"requireEmail" json:"requireEmail"` - ExceptEmailDomains []string `form:"exceptEmailDomains" json:"exceptEmailDomains"` - OnlyVerified bool `form:"onlyVerified" json:"onlyVerified"` - OnlyEmailDomains []string `form:"onlyEmailDomains" json:"onlyEmailDomains"` - MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"` -} - -// Validate implements [validation.Validatable] interface. -func (o CollectionAuthOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field(&o.ManageRule, validation.NilOrNotEmpty), - validation.Field( - &o.ExceptEmailDomains, - validation.When(len(o.OnlyEmailDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - validation.Field( - &o.OnlyEmailDomains, - validation.When(len(o.ExceptEmailDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - validation.Field( - &o.MinPasswordLength, - validation.When(o.AllowUsernameAuth || o.AllowEmailAuth, validation.Required), - validation.Min(5), - validation.Max(72), - ), - ) -} - -// ------------------------------------------------------------------- - -// CollectionViewOptions defines the "view" Collection.Options fields. -type CollectionViewOptions struct { - Query string `form:"query" json:"query"` -} - -// Validate implements [validation.Validatable] interface. -func (o CollectionViewOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field(&o.Query, validation.Required), - ) -} diff --git a/models/collection_test.go b/models/collection_test.go deleted file mode 100644 index ab8c0525..00000000 --- a/models/collection_test.go +++ /dev/null @@ -1,522 +0,0 @@ -package models_test - -import ( - "encoding/json" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestCollectionTableName(t *testing.T) { - t.Parallel() - - m := models.Collection{} - if m.TableName() != "_collections" { - t.Fatalf("Unexpected table name, got %q", m.TableName()) - } -} - -func TestCollectionBaseFilesPath(t *testing.T) { - t.Parallel() - - m := models.Collection{} - - m.RefreshId() - - expected := m.Id - if m.BaseFilesPath() != expected { - t.Fatalf("Expected path %s, got %s", expected, m.BaseFilesPath()) - } -} - -func TestCollectionIsBase(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collection models.Collection - expected bool - }{ - {models.Collection{}, false}, - {models.Collection{Type: "unknown"}, false}, - {models.Collection{Type: models.CollectionTypeBase}, true}, - {models.Collection{Type: models.CollectionTypeAuth}, false}, - } - - for i, s := range scenarios { - result := s.collection.IsBase() - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestCollectionIsAuth(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collection models.Collection - expected bool - }{ - {models.Collection{}, false}, - {models.Collection{Type: "unknown"}, false}, - {models.Collection{Type: models.CollectionTypeBase}, false}, - {models.Collection{Type: models.CollectionTypeAuth}, true}, - } - - for i, s := range scenarios { - result := s.collection.IsAuth() - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestCollectionMarshalJSON(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - collection models.Collection - expected string - }{ - { - "no type", - models.Collection{Name: "test"}, - `{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, - }, - { - "unknown type + non empty options", - models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}, Indexes: types.JsonArray[string]{"idx_test"}}, - `{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"indexes":["idx_test"],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, - }, - { - "base type + non empty options", - models.Collection{Name: "test", Type: models.CollectionTypeBase, ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}}, - `{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"indexes":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, - }, - { - "auth type + non empty options", - models.Collection{BaseModel: models.BaseModel{Id: "test"}, Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "allowOAuth2Auth": true, "minPasswordLength": 4, "onlyVerified": true}}, - `{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"onlyVerified":true,"requireEmail":false}}`, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result, err := s.collection.MarshalJSON() - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - - if string(result) != s.expected { - t.Fatalf("Expected\n%v\ngot\n%v", s.expected, string(result)) - } - }) - } -} - -func TestCollectionBaseOptions(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - collection models.Collection - expected string - }{ - { - "no type", - models.Collection{Options: types.JsonMap{"test": 123}}, - "{}", - }, - { - "unknown type", - models.Collection{Type: "anything", Options: types.JsonMap{"test": 123}}, - "{}", - }, - { - "different type", - models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}}, - "{}", - }, - { - "base type", - models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123}}, - "{}", - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result := s.collection.BaseOptions() - - encoded, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - - if strEncoded := string(encoded); strEncoded != s.expected { - t.Fatalf("Expected \n%v \ngot \n%v", s.expected, strEncoded) - } - }) - } -} - -func TestCollectionAuthOptions(t *testing.T) { - t.Parallel() - - options := types.JsonMap{"test": 123, "minPasswordLength": 4} - expectedSerialization := `{"manageRule":null,"allowOAuth2Auth":false,"allowUsernameAuth":false,"allowEmailAuth":false,"requireEmail":false,"exceptEmailDomains":null,"onlyVerified":false,"onlyEmailDomains":null,"minPasswordLength":4}` - - scenarios := []struct { - name string - collection models.Collection - expected string - }{ - { - "no type", - models.Collection{Options: options}, - expectedSerialization, - }, - { - "unknown type", - models.Collection{Type: "anything", Options: options}, - expectedSerialization, - }, - { - "different type", - models.Collection{Type: models.CollectionTypeBase, Options: options}, - expectedSerialization, - }, - { - "auth type", - models.Collection{Type: models.CollectionTypeAuth, Options: options}, - expectedSerialization, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result := s.collection.AuthOptions() - - encoded, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - - if strEncoded := string(encoded); strEncoded != s.expected { - t.Fatalf("Expected \n%v \ngot \n%v", s.expected, strEncoded) - } - }) - } -} - -func TestCollectionViewOptions(t *testing.T) { - t.Parallel() - - options := types.JsonMap{"query": "select id from demo1", "minPasswordLength": 4} - expectedSerialization := `{"query":"select id from demo1"}` - - scenarios := []struct { - name string - collection models.Collection - expected string - }{ - { - "no type", - models.Collection{Options: options}, - expectedSerialization, - }, - { - "unknown type", - models.Collection{Type: "anything", Options: options}, - expectedSerialization, - }, - { - "different type", - models.Collection{Type: models.CollectionTypeBase, Options: options}, - expectedSerialization, - }, - { - "view type", - models.Collection{Type: models.CollectionTypeView, Options: options}, - expectedSerialization, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result := s.collection.ViewOptions() - - encoded, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - - if strEncoded := string(encoded); strEncoded != s.expected { - t.Fatalf("Expected \n%v \ngot \n%v", s.expected, strEncoded) - } - }) - } -} - -func TestNormalizeOptions(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - collection models.Collection - expected string // serialized options - }{ - { - "unknown type", - models.Collection{Type: "unknown", Options: types.JsonMap{"test": 123, "minPasswordLength": 4}}, - "{}", - }, - { - "base type", - models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}}, - "{}", - }, - { - "auth type", - models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}}, - `{"allowEmailAuth":false,"allowOAuth2Auth":false,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"onlyVerified":false,"requireEmail":false}`, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - if err := s.collection.NormalizeOptions(); err != nil { - t.Fatalf("Unexpected error %v", err) - } - - encoded, err := json.Marshal(s.collection.Options) - if err != nil { - t.Fatal(err) - } - - if strEncoded := string(encoded); strEncoded != s.expected { - t.Fatalf("Expected \n%v \ngot \n%v", s.expected, strEncoded) - } - }) - } -} - -func TestDecodeOptions(t *testing.T) { - t.Parallel() - - m := models.Collection{ - Options: types.JsonMap{"test": 123}, - } - - result := struct { - Test int - }{} - - if err := m.DecodeOptions(&result); err != nil { - t.Fatal(err) - } - - if result.Test != 123 { - t.Fatalf("Expected %v, got %v", 123, result.Test) - } -} - -func TestSetOptions(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - collection models.Collection - options any - expected string // serialized options - }{ - { - "no type", - models.Collection{}, - map[string]any{}, - "{}", - }, - { - "unknown type + non empty options", - models.Collection{Type: "unknown", Options: types.JsonMap{"test": 123}}, - map[string]any{"test": 456, "minPasswordLength": 4}, - "{}", - }, - { - "base type", - models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123}}, - map[string]any{"test": 456, "minPasswordLength": 4}, - "{}", - }, - { - "auth type", - models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123}}, - map[string]any{"test": 456, "minPasswordLength": 4}, - `{"allowEmailAuth":false,"allowOAuth2Auth":false,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"onlyVerified":false,"requireEmail":false}`, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - if err := s.collection.SetOptions(s.options); err != nil { - t.Fatalf("Unexpected error %v", err) - } - - encoded, err := json.Marshal(s.collection.Options) - if err != nil { - t.Fatal(err) - } - - if strEncoded := string(encoded); strEncoded != s.expected { - t.Fatalf("Expected\n%v\ngot\n%v", s.expected, strEncoded) - } - }) - } -} - -func TestCollectionBaseOptionsValidate(t *testing.T) { - t.Parallel() - - opt := models.CollectionBaseOptions{} - if err := opt.Validate(); err != nil { - t.Fatal(err) - } -} - -func TestCollectionAuthOptionsValidate(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - options models.CollectionAuthOptions - expectedErrors []string - }{ - { - "empty", - models.CollectionAuthOptions{}, - nil, - }, - { - "empty string ManageRule", - models.CollectionAuthOptions{ManageRule: types.Pointer("")}, - []string{"manageRule"}, - }, - { - "minPasswordLength < 5", - models.CollectionAuthOptions{MinPasswordLength: 3}, - []string{"minPasswordLength"}, - }, - { - "minPasswordLength > 72", - models.CollectionAuthOptions{MinPasswordLength: 73}, - []string{"minPasswordLength"}, - }, - { - "both OnlyDomains and ExceptDomains set", - models.CollectionAuthOptions{ - OnlyEmailDomains: []string{"example.com", "test.com"}, - ExceptEmailDomains: []string{"example.com", "test.com"}, - }, - []string{"onlyEmailDomains", "exceptEmailDomains"}, - }, - { - "only OnlyDomains set", - models.CollectionAuthOptions{ - OnlyEmailDomains: []string{"example.com", "test.com"}, - }, - []string{}, - }, - { - "only ExceptEmailDomains set", - models.CollectionAuthOptions{ - ExceptEmailDomains: []string{"example.com", "test.com"}, - }, - []string{}, - }, - { - "all fields with valid data", - models.CollectionAuthOptions{ - ManageRule: types.Pointer("test"), - AllowOAuth2Auth: true, - AllowUsernameAuth: true, - AllowEmailAuth: true, - RequireEmail: true, - ExceptEmailDomains: []string{"example.com", "test.com"}, - OnlyEmailDomains: nil, - MinPasswordLength: 5, - }, - []string{}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result := s.options.Validate() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - } - - if len(errs) != len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got errors \n%v", s.expectedErrors, result) - } - - for key := range errs { - if !list.ExistInSlice(key, s.expectedErrors) { - t.Fatalf("Unexpected error key %q in \n%v", key, errs) - } - } - }) - } -} - -func TestCollectionViewOptionsValidate(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - options models.CollectionViewOptions - expectedErrors []string - }{ - { - "empty", - models.CollectionViewOptions{}, - []string{"query"}, - }, - { - "valid data", - models.CollectionViewOptions{ - Query: "test123", - }, - []string{}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - result := s.options.Validate() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Fatalf("Failed to parse errors %v", result) - } - - if len(errs) != len(s.expectedErrors) { - t.Fatalf("Expected error keys %v, got errors \n%v", s.expectedErrors, result) - } - - for key := range errs { - if !list.ExistInSlice(key, s.expectedErrors) { - t.Fatalf("Unexpected error key %q in \n%v", key, errs) - } - } - }) - } -} diff --git a/models/external_auth.go b/models/external_auth.go deleted file mode 100644 index bf9e0314..00000000 --- a/models/external_auth.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -var _ Model = (*ExternalAuth)(nil) - -type ExternalAuth struct { - BaseModel - - CollectionId string `db:"collectionId" json:"collectionId"` - RecordId string `db:"recordId" json:"recordId"` - Provider string `db:"provider" json:"provider"` - ProviderId string `db:"providerId" json:"providerId"` -} - -func (m *ExternalAuth) TableName() string { - return "_externalAuths" -} diff --git a/models/external_auth_test.go b/models/external_auth_test.go deleted file mode 100644 index 7688daa2..00000000 --- a/models/external_auth_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" -) - -func TestExternalAuthTableName(t *testing.T) { - t.Parallel() - - m := models.ExternalAuth{} - if m.TableName() != "_externalAuths" { - t.Fatalf("Unexpected table name, got %q", m.TableName()) - } -} diff --git a/models/log.go b/models/log.go deleted file mode 100644 index b8153c35..00000000 --- a/models/log.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "github.com/pocketbase/pocketbase/tools/types" -) - -var _ Model = (*Log)(nil) - -type Log struct { - BaseModel - - Data types.JsonMap `db:"data" json:"data"` - Message string `db:"message" json:"message"` - Level int `db:"level" json:"level"` -} - -func (m *Log) TableName() string { - return "_logs" -} diff --git a/models/param.go b/models/param.go deleted file mode 100644 index cf5ef053..00000000 --- a/models/param.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -import ( - "github.com/pocketbase/pocketbase/tools/types" -) - -var _ Model = (*Param)(nil) - -const ( - ParamAppSettings = "settings" -) - -type Param struct { - BaseModel - - Key string `db:"key" json:"key"` - Value types.JsonRaw `db:"value" json:"value"` -} - -func (m *Param) TableName() string { - return "_params" -} diff --git a/models/param_test.go b/models/param_test.go deleted file mode 100644 index 0ea03c64..00000000 --- a/models/param_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" -) - -func TestParamTableName(t *testing.T) { - t.Parallel() - - m := models.Param{} - if m.TableName() != "_params" { - t.Fatalf("Unexpected table name, got %q", m.TableName()) - } -} diff --git a/models/record.go b/models/record.go deleted file mode 100644 index f15e8b86..00000000 --- a/models/record.go +++ /dev/null @@ -1,962 +0,0 @@ -package models - -import ( - "encoding/json" - "errors" - "fmt" - "regexp" - "strconv" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/security" - "github.com/pocketbase/pocketbase/tools/store" - "github.com/pocketbase/pocketbase/tools/types" - "github.com/spf13/cast" - "golang.org/x/crypto/bcrypt" -) - -var ( - _ Model = (*Record)(nil) - _ ColumnValueMapper = (*Record)(nil) - _ FilesManager = (*Record)(nil) -) - -type Record struct { - BaseModel - - collection *Collection - - exportUnknown bool // whether to export unknown fields - ignoreEmailVisibility bool // whether to ignore the emailVisibility flag for auth collections - loaded bool - originalData map[string]any // the original (aka. first loaded) model data - expand *store.Store[any] // expanded relations - data *store.Store[any] // any custom data in addition to the base model fields -} - -// NewRecord initializes a new empty Record model. -func NewRecord(collection *Collection) *Record { - return &Record{ - collection: collection, - data: store.New[any](nil), - } -} - -// nullStringMapValue returns the raw string value if it exist and -// its not NULL, otherwise - nil. -func nullStringMapValue(data dbx.NullStringMap, key string) any { - nullString, ok := data[key] - - if ok && nullString.Valid { - return nullString.String - } - - return nil -} - -// NewRecordFromNullStringMap initializes a single new Record model -// with data loaded from the provided NullStringMap. -// -// Note that this method is intended to load and Scan data from a database -// result and calls PostScan() which marks the record as "not new". -func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record { - resultMap := make(map[string]any, len(data)) - - // load schema fields - for _, field := range collection.Schema.Fields() { - resultMap[field.Name] = nullStringMapValue(data, field.Name) - } - - // load base model fields - for _, name := range schema.BaseModelFieldNames() { - resultMap[name] = nullStringMapValue(data, name) - } - - // load auth fields - if collection.IsAuth() { - for _, name := range schema.AuthFieldNames() { - resultMap[name] = nullStringMapValue(data, name) - } - } - - record := NewRecord(collection) - - record.Load(resultMap) - record.PostScan() - - return record -} - -// NewRecordsFromNullStringMaps initializes a new Record model for -// each row in the provided NullStringMap slice. -// -// Note that this method is intended to load and Scan data from a database -// result and calls PostScan() for each record marking them as "not new". -func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record { - result := make([]*Record, len(rows)) - - for i, row := range rows { - result[i] = NewRecordFromNullStringMap(collection, row) - } - - return result -} - -// TableName returns the table name associated to the current Record model. -func (m *Record) TableName() string { - return m.collection.Name -} - -// Collection returns the Collection model associated to the current Record model. -func (m *Record) Collection() *Collection { - return m.collection -} - -// OriginalCopy returns a copy of the current record model populated -// with its ORIGINAL data state (aka. the initially loaded) and -// everything else reset to the defaults. -func (m *Record) OriginalCopy() *Record { - newRecord := NewRecord(m.collection) - newRecord.Load(m.originalData) - - if m.IsNew() { - newRecord.MarkAsNew() - } else { - newRecord.MarkAsNotNew() - } - - return newRecord -} - -// CleanCopy returns a copy of the current record model populated only -// with its LATEST data state and everything else reset to the defaults. -func (m *Record) CleanCopy() *Record { - newRecord := NewRecord(m.collection) - newRecord.Load(m.data.GetAll()) - newRecord.Id = m.Id - newRecord.Created = m.Created - newRecord.Updated = m.Updated - - if m.IsNew() { - newRecord.MarkAsNew() - } else { - newRecord.MarkAsNotNew() - } - - return newRecord -} - -// Expand returns a shallow copy of the current Record model expand data. -func (m *Record) Expand() map[string]any { - if m.expand == nil { - m.expand = store.New[any](nil) - } - - return m.expand.GetAll() -} - -// SetExpand shallow copies the provided data to the current Record model's expand. -func (m *Record) SetExpand(expand map[string]any) { - if m.expand == nil { - m.expand = store.New[any](nil) - } - - m.expand.Reset(expand) -} - -// MergeExpand merges recursively the provided expand data into -// the current model's expand (if any). -// -// Note that if an expanded prop with the same key is a slice (old or new expand) -// then both old and new records will be merged into a new slice (aka. a :merge: [b,c] => [a,b,c]). -// Otherwise the "old" expanded record will be replace with the "new" one (aka. a :merge: aNew => aNew). -func (m *Record) MergeExpand(expand map[string]any) { - // nothing to merge - if len(expand) == 0 { - return - } - - // no old expand - if m.expand == nil { - m.expand = store.New(expand) - return - } - - oldExpand := m.expand.GetAll() - - for key, new := range expand { - old, ok := oldExpand[key] - if !ok { - oldExpand[key] = new - continue - } - - var wasOldSlice bool - var oldSlice []*Record - switch v := old.(type) { - case *Record: - oldSlice = []*Record{v} - case []*Record: - wasOldSlice = true - oldSlice = v - default: - // invalid old expand data -> assign directly the new - // (no matter whether new is valid or not) - oldExpand[key] = new - continue - } - - var wasNewSlice bool - var newSlice []*Record - switch v := new.(type) { - case *Record: - newSlice = []*Record{v} - case []*Record: - wasNewSlice = true - newSlice = v - default: - // invalid new expand data -> skip - continue - } - - oldIndexed := make(map[string]*Record, len(oldSlice)) - for _, oldRecord := range oldSlice { - oldIndexed[oldRecord.Id] = oldRecord - } - - for _, newRecord := range newSlice { - oldRecord := oldIndexed[newRecord.Id] - if oldRecord != nil { - // note: there is no need to update oldSlice since oldRecord is a reference - oldRecord.MergeExpand(newRecord.Expand()) - } else { - // missing new entry - oldSlice = append(oldSlice, newRecord) - } - } - - if wasOldSlice || wasNewSlice || len(oldSlice) == 0 { - oldExpand[key] = oldSlice - } else { - oldExpand[key] = oldSlice[0] - } - } - - m.expand.Reset(oldExpand) -} - -// SchemaData returns a shallow copy ONLY of the defined record schema fields data. -func (m *Record) SchemaData() map[string]any { - result := make(map[string]any, len(m.collection.Schema.Fields())) - - data := m.data.GetAll() - - for _, field := range m.collection.Schema.Fields() { - if v, ok := data[field.Name]; ok { - result[field.Name] = v - } - } - - return result -} - -// UnknownData returns a shallow copy ONLY of the unknown record fields data, -// aka. fields that are neither one of the base and special system ones, -// nor defined by the collection schema. -func (m *Record) UnknownData() map[string]any { - if m.data == nil { - return nil - } - - return m.extractUnknownData(m.data.GetAll()) -} - -// IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check. -func (m *Record) IgnoreEmailVisibility(state bool) { - m.ignoreEmailVisibility = state -} - -// WithUnknownData toggles the export/serialization of unknown data fields -// (false by default). -func (m *Record) WithUnknownData(state bool) { - m.exportUnknown = state -} - -// Set sets the provided key-value data pair for the current Record model. -// -// If the record collection has field with name matching the provided "key", -// the value will be further normalized according to the field rules. -func (m *Record) Set(key string, value any) { - switch key { - case schema.FieldNameId: - m.Id = cast.ToString(value) - case schema.FieldNameCreated: - m.Created, _ = types.ParseDateTime(value) - case schema.FieldNameUpdated: - m.Updated, _ = types.ParseDateTime(value) - case schema.FieldNameExpand: - m.SetExpand(cast.ToStringMap(value)) - default: - var v = value - - if field := m.Collection().Schema.GetFieldByName(key); field != nil { - v = field.PrepareValue(value) - } else if m.collection.IsAuth() { - // normalize auth fields - switch key { - case schema.FieldNameEmailVisibility, schema.FieldNameVerified: - v = cast.ToBool(value) - case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt, schema.FieldNameLastLoginAlertSentAt: - v, _ = types.ParseDateTime(value) - case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash: - v = cast.ToString(value) - } - } - - if m.data == nil { - m.data = store.New[any](nil) - } - - m.data.Set(key, v) - } -} - -// Get returns a normalized single record model data value for "key". -func (m *Record) Get(key string) any { - switch key { - case schema.FieldNameId: - return m.Id - case schema.FieldNameCreated: - return m.Created - case schema.FieldNameUpdated: - return m.Updated - default: - var v any - if m.data != nil { - v = m.data.Get(key) - } - - // normalize the field value in case it is missing or an incorrect type was set - // to ensure that the DB will always have normalized columns value. - if field := m.Collection().Schema.GetFieldByName(key); field != nil { - v = field.PrepareValue(v) - } else if m.collection.IsAuth() { - switch key { - case schema.FieldNameEmailVisibility, schema.FieldNameVerified: - v = cast.ToBool(v) - case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt, schema.FieldNameLastLoginAlertSentAt: - v, _ = types.ParseDateTime(v) - case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash: - v = cast.ToString(v) - } - } - - return v - } -} - -// GetBool returns the data value for "key" as a bool. -func (m *Record) GetBool(key string) bool { - return cast.ToBool(m.Get(key)) -} - -// GetString returns the data value for "key" as a string. -func (m *Record) GetString(key string) string { - return cast.ToString(m.Get(key)) -} - -// GetInt returns the data value for "key" as an int. -func (m *Record) GetInt(key string) int { - return cast.ToInt(m.Get(key)) -} - -// GetFloat returns the data value for "key" as a float64. -func (m *Record) GetFloat(key string) float64 { - return cast.ToFloat64(m.Get(key)) -} - -// GetTime returns the data value for "key" as a [time.Time] instance. -func (m *Record) GetTime(key string) time.Time { - return cast.ToTime(m.Get(key)) -} - -// GetDateTime returns the data value for "key" as a DateTime instance. -func (m *Record) GetDateTime(key string) types.DateTime { - d, _ := types.ParseDateTime(m.Get(key)) - return d -} - -// GetStringSlice returns the data value for "key" as a slice of unique strings. -func (m *Record) GetStringSlice(key string) []string { - return list.ToUniqueStringSlice(m.Get(key)) -} - -// ExpandedOne retrieves a single relation Record from the already -// loaded expand data of the current model. -// -// If the requested expand relation is multiple, this method returns -// only first available Record from the expanded relation. -// -// Returns nil if there is no such expand relation loaded. -func (m *Record) ExpandedOne(relField string) *Record { - if m.expand == nil { - return nil - } - - rel := m.expand.Get(relField) - - switch v := rel.(type) { - case *Record: - return v - case []*Record: - if len(v) > 0 { - return v[0] - } - } - - return nil -} - -// ExpandedAll retrieves a slice of relation Records from the already -// loaded expand data of the current model. -// -// If the requested expand relation is single, this method normalizes -// the return result and will wrap the single model as a slice. -// -// Returns nil slice if there is no such expand relation loaded. -func (m *Record) ExpandedAll(relField string) []*Record { - if m.expand == nil { - return nil - } - - rel := m.expand.Get(relField) - - switch v := rel.(type) { - case *Record: - return []*Record{v} - case []*Record: - return v - } - - return nil -} - -// Retrieves the "key" json field value and unmarshals it into "result". -// -// Example -// -// result := struct { -// FirstName string `json:"first_name"` -// }{} -// err := m.UnmarshalJSONField("my_field_name", &result) -func (m *Record) UnmarshalJSONField(key string, result any) error { - return json.Unmarshal([]byte(m.GetString(key)), &result) -} - -// BaseFilesPath returns the storage dir path used by the record. -func (m *Record) BaseFilesPath() string { - return fmt.Sprintf("%s/%s", m.Collection().BaseFilesPath(), m.Id) -} - -// FindFileFieldByFile returns the first file type field for which -// any of the record's data contains the provided filename. -func (m *Record) FindFileFieldByFile(filename string) *schema.SchemaField { - for _, field := range m.Collection().Schema.Fields() { - if field.Type == schema.FieldTypeFile { - names := m.GetStringSlice(field.Name) - if list.ExistInSlice(filename, names) { - return field - } - } - } - return nil -} - -// Load bulk loads the provided data into the current Record model. -func (m *Record) Load(data map[string]any) { - if !m.loaded { - m.loaded = true - m.originalData = data - } - - for k, v := range data { - m.Set(k, v) - } -} - -// ColumnValueMap implements [ColumnValueMapper] interface. -func (m *Record) ColumnValueMap() map[string]any { - result := make(map[string]any, len(m.collection.Schema.Fields())+3) - - // export schema field values - for _, field := range m.collection.Schema.Fields() { - result[field.Name] = m.getNormalizeDataValueForDB(field.Name) - } - - // export auth collection fields - if m.collection.IsAuth() { - for _, name := range schema.AuthFieldNames() { - result[name] = m.getNormalizeDataValueForDB(name) - } - } - - // export base model fields - result[schema.FieldNameId] = m.getNormalizeDataValueForDB(schema.FieldNameId) - result[schema.FieldNameCreated] = m.getNormalizeDataValueForDB(schema.FieldNameCreated) - result[schema.FieldNameUpdated] = m.getNormalizeDataValueForDB(schema.FieldNameUpdated) - - return result -} - -// PublicExport exports only the record fields that are safe to be public. -// -// For auth records, to force the export of the email field you need to set -// `m.IgnoreEmailVisibility(true)`. -func (m *Record) PublicExport() map[string]any { - result := make(map[string]any, len(m.collection.Schema.Fields())+5) - - // export unknown data fields if allowed - if m.exportUnknown { - for k, v := range m.UnknownData() { - result[k] = v - } - } - - // export schema field values - for _, field := range m.collection.Schema.Fields() { - result[field.Name] = m.Get(field.Name) - } - - // export some of the safe auth collection fields - if m.collection.IsAuth() { - result[schema.FieldNameVerified] = m.Verified() - result[schema.FieldNameUsername] = m.Username() - result[schema.FieldNameEmailVisibility] = m.EmailVisibility() - if m.ignoreEmailVisibility || m.EmailVisibility() { - result[schema.FieldNameEmail] = m.Email() - } - } - - // export base model fields - result[schema.FieldNameId] = m.GetId() - if created := m.GetCreated(); !m.Collection().IsView() || !created.IsZero() { - result[schema.FieldNameCreated] = created - } - if updated := m.GetUpdated(); !m.Collection().IsView() || !updated.IsZero() { - result[schema.FieldNameUpdated] = updated - } - - // add helper collection reference fields - result[schema.FieldNameCollectionId] = m.collection.Id - result[schema.FieldNameCollectionName] = m.collection.Name - - // add expand (if set) - if m.expand != nil && m.expand.Length() > 0 { - result[schema.FieldNameExpand] = m.expand.GetAll() - } - - return result -} - -// MarshalJSON implements the [json.Marshaler] interface. -// -// Only the data exported by `PublicExport()` will be serialized. -func (m Record) MarshalJSON() ([]byte, error) { - return json.Marshal(m.PublicExport()) -} - -// UnmarshalJSON implements the [json.Unmarshaler] interface. -func (m *Record) UnmarshalJSON(data []byte) error { - result := map[string]any{} - - if err := json.Unmarshal(data, &result); err != nil { - return err - } - - m.Load(result) - - return nil -} - -// ReplaceModifers returns a new map with applied modifier -// values based on the current record and the specified data. -// -// The resolved modifier keys will be removed. -// -// Multiple modifiers will be applied one after another, -// while reusing the previous base key value result (eg. 1; -5; +2 => -2). -// -// Example usage: -// -// newData := record.ReplaceModifers(data) -// // record: {"field": 10} -// // data: {"field+": 5} -// // newData: {"field": 15} -func (m *Record) ReplaceModifers(data map[string]any) map[string]any { - var clone = shallowCopy(data) - if len(clone) == 0 { - return clone - } - - var recordDataCache map[string]any - - // export recordData lazily - recordData := func() map[string]any { - if recordDataCache == nil { - recordDataCache = m.SchemaData() - } - return recordDataCache - } - - modifiers := schema.FieldValueModifiers() - - for _, field := range m.Collection().Schema.Fields() { - key := field.Name - - for _, m := range modifiers { - if mv, mOk := clone[key+m]; mOk { - if _, ok := clone[key]; !ok { - // get base value from the merged data - clone[key] = recordData()[key] - } - - clone[key] = field.PrepareValueWithModifier(clone[key], m, mv) - delete(clone, key+m) - } - } - - if field.Type != schema.FieldTypeFile { - continue - } - - // ----------------------------------------------------------- - // legacy file field modifiers (kept for backward compatibility) - // ----------------------------------------------------------- - - var oldNames []string - var toDelete []string - if _, ok := clone[key]; ok { - oldNames = list.ToUniqueStringSlice(clone[key]) - } else { - // get oldNames from the model - oldNames = list.ToUniqueStringSlice(recordData()[key]) - } - - // search for individual file name to delete (eg. "file.test.png = null") - for _, name := range oldNames { - suffixedKey := key + "." + name - if v, ok := clone[suffixedKey]; ok && cast.ToString(v) == "" { - toDelete = append(toDelete, name) - delete(clone, suffixedKey) - continue - } - } - - // search for individual file index to delete (eg. "file.0 = null") - keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`) - for indexedKey := range clone { - if keyExp.MatchString(indexedKey) && cast.ToString(clone[indexedKey]) == "" { - index, indexErr := strconv.Atoi(indexedKey[len(key)+1:]) - if indexErr != nil || index < 0 || index >= len(oldNames) { - continue - } - toDelete = append(toDelete, oldNames[index]) - delete(clone, indexedKey) - } - } - - if toDelete != nil { - clone[key] = field.PrepareValue(list.SubtractSlice(oldNames, toDelete)) - } - } - - return clone -} - -// getNormalizeDataValueForDB returns the "key" data value formatted for db storage. -func (m *Record) getNormalizeDataValueForDB(key string) any { - var val any - - // normalize auth fields - if m.collection.IsAuth() { - switch key { - case schema.FieldNameEmailVisibility, schema.FieldNameVerified: - return m.GetBool(key) - case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt, schema.FieldNameLastLoginAlertSentAt: - return m.GetDateTime(key) - case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash: - return m.GetString(key) - } - } - - val = m.Get(key) - - switch ids := val.(type) { - case []string: - // encode string slice - return append(types.JsonArray[string]{}, ids...) - case []int: - // encode int slice - return append(types.JsonArray[int]{}, ids...) - case []float64: - // encode float64 slice - return append(types.JsonArray[float64]{}, ids...) - case []any: - // encode interface slice - return append(types.JsonArray[any]{}, ids...) - default: - // no changes - return val - } -} - -// shallowCopy shallow copy data into a new map. -func shallowCopy(data map[string]any) map[string]any { - result := make(map[string]any, len(data)) - - for k, v := range data { - result[k] = v - } - - return result -} - -func (m *Record) extractUnknownData(data map[string]any) map[string]any { - knownFields := map[string]struct{}{} - - for _, name := range schema.SystemFieldNames() { - knownFields[name] = struct{}{} - } - for _, name := range schema.BaseModelFieldNames() { - knownFields[name] = struct{}{} - } - - for _, f := range m.collection.Schema.Fields() { - knownFields[f.Name] = struct{}{} - } - - if m.collection.IsAuth() { - for _, name := range schema.AuthFieldNames() { - knownFields[name] = struct{}{} - } - } - - result := map[string]any{} - - for k, v := range data { - if _, ok := knownFields[k]; !ok { - result[k] = v - } - } - - return result -} - -// ------------------------------------------------------------------- -// Auth helpers -// ------------------------------------------------------------------- - -var notAuthRecordErr = errors.New("Not an auth collection record.") - -// Username returns the "username" auth record data value. -func (m *Record) Username() string { - return m.GetString(schema.FieldNameUsername) -} - -// SetUsername sets the "username" auth record data value. -// -// This method doesn't check whether the provided value is a valid username. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetUsername(username string) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameUsername, username) - - return nil -} - -// Email returns the "email" auth record data value. -func (m *Record) Email() string { - return m.GetString(schema.FieldNameEmail) -} - -// SetEmail sets the "email" auth record data value. -// -// This method doesn't check whether the provided value is a valid email. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetEmail(email string) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameEmail, email) - - return nil -} - -// Verified returns the "emailVisibility" auth record data value. -func (m *Record) EmailVisibility() bool { - return m.GetBool(schema.FieldNameEmailVisibility) -} - -// SetEmailVisibility sets the "emailVisibility" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetEmailVisibility(visible bool) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameEmailVisibility, visible) - - return nil -} - -// Verified returns the "verified" auth record data value. -func (m *Record) Verified() bool { - return m.GetBool(schema.FieldNameVerified) -} - -// SetVerified sets the "verified" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetVerified(verified bool) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameVerified, verified) - - return nil -} - -// TokenKey returns the "tokenKey" auth record data value. -func (m *Record) TokenKey() string { - return m.GetString(schema.FieldNameTokenKey) -} - -// SetTokenKey sets the "tokenKey" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetTokenKey(key string) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameTokenKey, key) - - return nil -} - -// RefreshTokenKey generates and sets new random auth record "tokenKey". -// -// Returns an error if the record is not from an auth collection. -func (m *Record) RefreshTokenKey() error { - return m.SetTokenKey(security.RandomString(50)) -} - -// LastResetSentAt returns the "lastResentSentAt" auth record data value. -func (m *Record) LastResetSentAt() types.DateTime { - return m.GetDateTime(schema.FieldNameLastResetSentAt) -} - -// SetLastResetSentAt sets the "lastResentSentAt" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetLastResetSentAt(dateTime types.DateTime) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameLastResetSentAt, dateTime) - - return nil -} - -// LastVerificationSentAt returns the "lastVerificationSentAt" auth record data value. -func (m *Record) LastVerificationSentAt() types.DateTime { - return m.GetDateTime(schema.FieldNameLastVerificationSentAt) -} - -// SetLastVerificationSentAt sets an "lastVerificationSentAt" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetLastVerificationSentAt(dateTime types.DateTime) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameLastVerificationSentAt, dateTime) - - return nil -} - -// LastLoginAlertSentAt returns the "lastLoginAlertSentAt" auth record data value. -func (m *Record) LastLoginAlertSentAt() types.DateTime { - return m.GetDateTime(schema.FieldNameLastLoginAlertSentAt) -} - -// SetLastLoginAlertSentAt sets an "lastLoginAlertSentAt" auth record data value. -// -// Returns an error if the record is not from an auth collection. -func (m *Record) SetLastLoginAlertSentAt(dateTime types.DateTime) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - m.Set(schema.FieldNameLastLoginAlertSentAt, dateTime) - - return nil -} - -// PasswordHash returns the "passwordHash" auth record data value. -func (m *Record) PasswordHash() string { - return m.GetString(schema.FieldNamePasswordHash) -} - -// ValidatePassword validates a plain password against the auth record password. -// -// Returns false if the password is incorrect or record is not from an auth collection. -func (m *Record) ValidatePassword(password string) bool { - if !m.collection.IsAuth() { - return false - } - - err := bcrypt.CompareHashAndPassword([]byte(m.PasswordHash()), []byte(password)) - - return err == nil -} - -// SetPassword sets cryptographically secure string to the auth record "password" field. -// This method also resets the "lastResetSentAt" and the "tokenKey" fields. -// -// Returns an error if the record is not from an auth collection or -// an empty password is provided. -func (m *Record) SetPassword(password string) error { - if !m.collection.IsAuth() { - return notAuthRecordErr - } - - if password == "" { - return errors.New("The provided plain password is empty") - } - - // hash the password - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) - if err != nil { - return err - } - - m.Set(schema.FieldNamePasswordHash, string(hashedPassword)) - m.Set(schema.FieldNameLastResetSentAt, types.DateTime{}) - - // invalidate previously issued tokens - return m.RefreshTokenKey() -} diff --git a/models/record_test.go b/models/record_test.go deleted file mode 100644 index b43f9203..00000000 --- a/models/record_test.go +++ /dev/null @@ -1,2199 +0,0 @@ -package models_test - -import ( - "bytes" - "database/sql" - "encoding/json" - "testing" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestNewRecord(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Name: "test_collection", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }, - ), - } - - m := models.NewRecord(collection) - - if m.Collection().Name != collection.Name { - t.Fatalf("Expected collection with name %q, got %q", collection.Id, m.Collection().Id) - } - - if len(m.SchemaData()) != 0 { - t.Fatalf("Expected empty schema data, got %v", m.SchemaData()) - } -} - -func TestNewRecordFromNullStringMap(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Name: "test", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeBool, - }, - &schema.SchemaField{ - Name: "field4", - Type: schema.FieldTypeNumber, - }, - &schema.SchemaField{ - Name: "field5", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"test1", "test2"}, - MaxSelect: 1, - }, - }, - &schema.SchemaField{ - Name: "field6", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 2, - MaxSize: 1, - }, - }, - ), - } - - data := dbx.NullStringMap{ - "id": sql.NullString{ - String: "test_id", - Valid: true, - }, - "created": sql.NullString{ - String: "2022-01-01 10:00:00.123Z", - Valid: true, - }, - "updated": sql.NullString{ - String: "2022-01-01 10:00:00.456Z", - Valid: true, - }, - // auth collection specific fields - "username": sql.NullString{ - String: "test_username", - Valid: true, - }, - "email": sql.NullString{ - String: "test_email", - Valid: true, - }, - "emailVisibility": sql.NullString{ - String: "true", - Valid: true, - }, - "verified": sql.NullString{ - String: "", - Valid: false, - }, - "tokenKey": sql.NullString{ - String: "test_tokenKey", - Valid: true, - }, - "passwordHash": sql.NullString{ - String: "test_passwordHash", - Valid: true, - }, - "lastResetSentAt": sql.NullString{ - String: "2022-01-02 10:00:00.123Z", - Valid: true, - }, - "lastVerificationSentAt": sql.NullString{ - String: "2022-02-03 10:00:00.456Z", - Valid: true, - }, - // custom schema fields - "field1": sql.NullString{ - String: "test", - Valid: true, - }, - "field2": sql.NullString{ - String: "test", - Valid: false, // test invalid db serialization - }, - "field3": sql.NullString{ - String: "true", - Valid: true, - }, - "field4": sql.NullString{ - String: "123.123", - Valid: true, - }, - "field5": sql.NullString{ - String: `["test1","test2"]`, // will select only the last elem - Valid: true, - }, - "field6": sql.NullString{ - String: "test", // will be converted to slice - Valid: true, - }, - "unknown": sql.NullString{ - String: "test", - Valid: true, - }, - } - - scenarios := []struct { - collectionType string - expectedJson string - }{ - { - models.CollectionTypeBase, - `{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test2","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z"}`, - }, - { - models.CollectionTypeAuth, - `{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","email":"test_email","emailVisibility":true,"field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test2","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z","username":"test_username","verified":false}`, - }, - } - - for i, s := range scenarios { - collection.Type = s.collectionType - m := models.NewRecordFromNullStringMap(collection, data) - m.IgnoreEmailVisibility(true) - - encoded, err := m.MarshalJSON() - if err != nil { - t.Errorf("(%d) Unexpected error: %v", i, err) - continue - } - - if string(encoded) != s.expectedJson { - t.Errorf("(%d) Expected \n%v \ngot \n%v", i, s.expectedJson, string(encoded)) - } - - // additional data checks - if collection.IsAuth() { - if v := m.GetString(schema.FieldNamePasswordHash); v != "test_passwordHash" { - t.Errorf("(%d) Expected %q, got %q", i, "test_passwordHash", v) - } - if v := m.GetString(schema.FieldNameTokenKey); v != "test_tokenKey" { - t.Errorf("(%d) Expected %q, got %q", i, "test_tokenKey", v) - } - if v := m.GetString(schema.FieldNameLastResetSentAt); v != "2022-01-02 10:00:00.123Z" { - t.Errorf("(%d) Expected %q, got %q", i, "2022-01-02 10:00:00.123Z", v) - } - if v := m.GetString(schema.FieldNameLastVerificationSentAt); v != "2022-02-03 10:00:00.456Z" { - t.Errorf("(%d) Expected %q, got %q", i, "2022-01-02 10:00:00.123Z", v) - } - } - } -} - -func TestNewRecordsFromNullStringMaps(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Name: "test", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeUrl, - }, - ), - } - - data := []dbx.NullStringMap{ - { - "id": sql.NullString{ - String: "test_id1", - Valid: true, - }, - "created": sql.NullString{ - String: "2022-01-01 10:00:00.123Z", - Valid: true, - }, - "updated": sql.NullString{ - String: "2022-01-01 10:00:00.456Z", - Valid: true, - }, - // partial auth fields - "email": sql.NullString{ - String: "test_email", - Valid: true, - }, - "tokenKey": sql.NullString{ - String: "test_tokenKey", - Valid: true, - }, - "emailVisibility": sql.NullString{ - String: "true", - Valid: true, - }, - // custom schema fields - "field1": sql.NullString{ - String: "test", - Valid: true, - }, - "field2": sql.NullString{ - String: "123.123", - Valid: true, - }, - "field3": sql.NullString{ - String: "test", - Valid: false, // should force resolving to empty string - }, - "unknown": sql.NullString{ - String: "test", - Valid: true, - }, - }, - { - "field3": sql.NullString{ - String: "test", - Valid: true, - }, - "email": sql.NullString{ - String: "test_email", - Valid: true, - }, - "emailVisibility": sql.NullString{ - String: "false", - Valid: true, - }, - }, - } - - scenarios := []struct { - collectionType string - expectedJson string - }{ - { - models.CollectionTypeBase, - `[{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","field1":"test","field2":123.123,"field3":"","id":"test_id1","updated":"2022-01-01 10:00:00.456Z"},{"collectionId":"","collectionName":"test","created":"","field1":"","field2":0,"field3":"test","id":"","updated":""}]`, - }, - { - models.CollectionTypeAuth, - `[{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","email":"test_email","emailVisibility":true,"field1":"test","field2":123.123,"field3":"","id":"test_id1","updated":"2022-01-01 10:00:00.456Z","username":"","verified":false},{"collectionId":"","collectionName":"test","created":"","emailVisibility":false,"field1":"","field2":0,"field3":"test","id":"","updated":"","username":"","verified":false}]`, - }, - } - - for i, s := range scenarios { - collection.Type = s.collectionType - result := models.NewRecordsFromNullStringMaps(collection, data) - - encoded, err := json.Marshal(result) - if err != nil { - t.Errorf("(%d) Unexpected error: %v", i, err) - continue - } - - if string(encoded) != s.expectedJson { - t.Errorf("(%d) Expected \n%v \ngot \n%v", i, s.expectedJson, string(encoded)) - } - } -} - -func TestRecordTableName(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - collection.Name = "test" - collection.RefreshId() - - m := models.NewRecord(collection) - - if m.TableName() != collection.Name { - t.Fatalf("Expected table %q, got %q", collection.Name, m.TableName()) - } -} - -func TestRecordCollection(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - collection.RefreshId() - - m := models.NewRecord(collection) - - if m.Collection().Id != collection.Id { - t.Fatalf("Expected collection with id %v, got %v", collection.Id, m.Collection().Id) - } -} - -func TestRecordOriginalCopy(t *testing.T) { - t.Parallel() - - m := models.NewRecord(&models.Collection{}) - m.Load(map[string]any{"f": "123"}) - - // change the field - m.Set("f", "456") - - if v := m.GetString("f"); v != "456" { - t.Fatalf("Expected f to be %q, got %q", "456", v) - } - - if v := m.OriginalCopy().GetString("f"); v != "123" { - t.Fatalf("Expected the initial/original f to be %q, got %q", "123", v) - } - - // loading new data shouldn't affect the original state - m.Load(map[string]any{"f": "789"}) - - if v := m.GetString("f"); v != "789" { - t.Fatalf("Expected f to be %q, got %q", "789", v) - } - - if v := m.OriginalCopy().GetString("f"); v != "123" { - t.Fatalf("Expected the initial/original f still to be %q, got %q", "123", v) - } -} - -func TestRecordCleanCopy(t *testing.T) { - t.Parallel() - - m := models.NewRecord(&models.Collection{ - Name: "cname", - Type: models.CollectionTypeAuth, - }) - m.Load(map[string]any{ - "id": "id1", - "created": "2023-01-01 00:00:00.000Z", - "updated": "2023-01-02 00:00:00.000Z", - "username": "test", - "verified": true, - "email": "test@example.com", - "unknown": "456", - }) - - // make a change to ensure that the latest data is targeted - m.Set("id", "id2") - - // allow the special flags and options to check whether they will be ignored - m.SetExpand(map[string]any{"test": 123}) - m.IgnoreEmailVisibility(true) - m.WithUnknownData(true) - - copy := m.CleanCopy() - copyExport, _ := copy.MarshalJSON() - - expectedExport := []byte(`{"collectionId":"","collectionName":"cname","created":"2023-01-01 00:00:00.000Z","emailVisibility":false,"id":"id2","updated":"2023-01-02 00:00:00.000Z","username":"test","verified":true}`) - if !bytes.Equal(copyExport, expectedExport) { - t.Fatalf("Expected clean export \n%s, \ngot \n%s", expectedExport, copyExport) - } -} - -func TestRecordSetAndGetExpand(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - m := models.NewRecord(collection) - - data := map[string]any{"test": 123} - - m.SetExpand(data) - - // change the original data to check if it was shallow copied - data["test"] = 456 - - expand := m.Expand() - if v, ok := expand["test"]; !ok || v != 123 { - t.Fatalf("Expected expand.test to be %v, got %v", 123, v) - } -} - -func TestRecordMergeExpand(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - m := models.NewRecord(collection) - m.Id = "m" - - // a - a := models.NewRecord(collection) - a.Id = "a" - a1 := models.NewRecord(collection) - a1.Id = "a1" - a2 := models.NewRecord(collection) - a2.Id = "a2" - a3 := models.NewRecord(collection) - a3.Id = "a3" - a31 := models.NewRecord(collection) - a31.Id = "a31" - a32 := models.NewRecord(collection) - a32.Id = "a32" - a.SetExpand(map[string]any{ - "a1": a1, - "a23": []*models.Record{a2, a3}, - }) - a3.SetExpand(map[string]any{ - "a31": a31, - "a32": []*models.Record{a32}, - }) - - // b - b := models.NewRecord(collection) - b.Id = "b" - b1 := models.NewRecord(collection) - b1.Id = "b1" - b.SetExpand(map[string]any{ - "b1": b1, - }) - - // c - c := models.NewRecord(collection) - c.Id = "c" - - // load initial expand - m.SetExpand(map[string]any{ - "a": a, - "b": b, - "c": []*models.Record{c}, - }) - - // a (new) - aNew := models.NewRecord(collection) - aNew.Id = a.Id - a3New := models.NewRecord(collection) - a3New.Id = a3.Id - a32New := models.NewRecord(collection) - a32New.Id = "a32New" - a33New := models.NewRecord(collection) - a33New.Id = "a33New" - a3New.SetExpand(map[string]any{ - "a32": []*models.Record{a32New}, - "a33New": a33New, - }) - aNew.SetExpand(map[string]any{ - "a23": []*models.Record{a2, a3New}, - }) - - // b (new) - bNew := models.NewRecord(collection) - bNew.Id = "bNew" - dNew := models.NewRecord(collection) - dNew.Id = "dNew" - - // merge expands - m.MergeExpand(map[string]any{ - "a": aNew, - "b": []*models.Record{bNew}, - "dNew": dNew, - }) - - result := m.Expand() - - raw, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - rawStr := string(raw) - - expected := `{"a":{"collectionId":"","collectionName":"","created":"","expand":{"a1":{"collectionId":"","collectionName":"","created":"","id":"a1","updated":""},"a23":[{"collectionId":"","collectionName":"","created":"","id":"a2","updated":""},{"collectionId":"","collectionName":"","created":"","expand":{"a31":{"collectionId":"","collectionName":"","created":"","id":"a31","updated":""},"a32":[{"collectionId":"","collectionName":"","created":"","id":"a32","updated":""},{"collectionId":"","collectionName":"","created":"","id":"a32New","updated":""}],"a33New":{"collectionId":"","collectionName":"","created":"","id":"a33New","updated":""}},"id":"a3","updated":""}]},"id":"a","updated":""},"b":[{"collectionId":"","collectionName":"","created":"","expand":{"b1":{"collectionId":"","collectionName":"","created":"","id":"b1","updated":""}},"id":"b","updated":""},{"collectionId":"","collectionName":"","created":"","id":"bNew","updated":""}],"c":[{"collectionId":"","collectionName":"","created":"","id":"c","updated":""}],"dNew":{"collectionId":"","collectionName":"","created":"","id":"dNew","updated":""}}` - - if expected != rawStr { - t.Fatalf("Expected \n%v, \ngot \n%v", expected, rawStr) - } -} - -func TestRecordMergeExpandNilCheck(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - - scenarios := []struct { - name string - expand map[string]any - expected string - }{ - { - "nil expand", - nil, - `{"collectionId":"","collectionName":"","created":"","id":"","updated":""}`, - }, - { - "empty expand", - map[string]any{}, - `{"collectionId":"","collectionName":"","created":"","id":"","updated":""}`, - }, - { - "non-empty expand", - map[string]any{"test": models.NewRecord(collection)}, - `{"collectionId":"","collectionName":"","created":"","expand":{"test":{"collectionId":"","collectionName":"","created":"","id":"","updated":""}},"id":"","updated":""}`, - }, - } - - for _, s := range scenarios { - m := models.NewRecord(collection) - m.MergeExpand(s.expand) - - raw, err := json.Marshal(m) - if err != nil { - t.Fatal(err) - } - rawStr := string(raw) - - if rawStr != s.expected { - t.Fatalf("[%s] Expected \n%v, \ngot \n%v", s.name, s.expected, rawStr) - } - } -} - -func TestRecordExpandedRel(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - - main := models.NewRecord(collection) - - single := models.NewRecord(collection) - single.Id = "single" - - multiple1 := models.NewRecord(collection) - multiple1.Id = "multiple1" - - multiple2 := models.NewRecord(collection) - multiple2.Id = "multiple2" - - main.SetExpand(map[string]any{ - "single": single, - "multiple": []*models.Record{multiple1, multiple2}, - }) - - if v := main.ExpandedOne("missing"); v != nil { - t.Fatalf("Expected nil, got %v", v) - } - - if v := main.ExpandedOne("single"); v == nil || v.Id != "single" { - t.Fatalf("Expected record with id %q, got %v", "single", v) - } - - if v := main.ExpandedOne("multiple"); v == nil || v.Id != "multiple1" { - t.Fatalf("Expected record with id %q, got %v", "multiple1", v) - } -} - -func TestRecordExpandedAll(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - - main := models.NewRecord(collection) - - single := models.NewRecord(collection) - single.Id = "single" - - multiple1 := models.NewRecord(collection) - multiple1.Id = "multiple1" - - multiple2 := models.NewRecord(collection) - multiple2.Id = "multiple2" - - main.SetExpand(map[string]any{ - "single": single, - "multiple": []*models.Record{multiple1, multiple2}, - }) - - if v := main.ExpandedAll("missing"); v != nil { - t.Fatalf("Expected nil, got %v", v) - } - - if v := main.ExpandedAll("single"); len(v) != 1 || v[0].Id != "single" { - t.Fatalf("Expected [single] slice, got %v", v) - } - - if v := main.ExpandedAll("multiple"); len(v) != 2 || v[0].Id != "multiple1" || v[1].Id != "multiple2" { - t.Fatalf("Expected [multiple1, multiple2] slice, got %v", v) - } -} - -func TestRecordSchemaData(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Type: models.CollectionTypeAuth, - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - ), - } - - m := models.NewRecord(collection) - m.Set("email", "test@example.com") - m.Set("field1", 123) - m.Set("field2", 456) - m.Set("unknown", 789) - - encoded, err := json.Marshal(m.SchemaData()) - if err != nil { - t.Fatal(err) - } - - expected := `{"field1":"123","field2":456}` - - if v := string(encoded); v != expected { - t.Fatalf("Expected \n%v \ngot \n%v", v, expected) - } -} - -func TestRecordUnknownData(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - ), - } - - data := map[string]any{ - "id": "test_id", - "created": "2022-01-01 00:00:00.000", - "updated": "2022-01-01 00:00:00.000", - "collectionId": "test_collectionId", - "collectionName": "test_collectionName", - "expand": "test_expand", - "field1": "test_field1", - "field2": "test_field1", - "unknown1": "test_unknown1", - "unknown2": "test_unknown2", - "passwordHash": "test_passwordHash", - "username": "test_username", - "emailVisibility": true, - "email": "test_email", - "verified": true, - "tokenKey": "test_tokenKey", - "lastResetSentAt": "2022-01-01 00:00:00.000", - "lastVerificationSentAt": "2022-01-01 00:00:00.000", - } - - scenarios := []struct { - collectionType string - expectedKeys []string - }{ - { - models.CollectionTypeBase, - []string{ - "unknown1", - "unknown2", - "passwordHash", - "username", - "emailVisibility", - "email", - "verified", - "tokenKey", - "lastResetSentAt", - "lastVerificationSentAt", - }, - }, - { - models.CollectionTypeAuth, - []string{"unknown1", "unknown2"}, - }, - } - - for i, s := range scenarios { - collection.Type = s.collectionType - m := models.NewRecord(collection) - m.Load(data) - - result := m.UnknownData() - - if len(result) != len(s.expectedKeys) { - t.Errorf("(%d) Expected data \n%v \ngot \n%v", i, s.expectedKeys, result) - continue - } - - for _, key := range s.expectedKeys { - if _, ok := result[key]; !ok { - t.Errorf("(%d) Missing expected key %q in \n%v", i, key, result) - } - } - } -} - -func TestRecordSetAndGet(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - // fields that are not explicitly set to check - // the default retrieval value (single and multiple) - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeBool, - }, - &schema.SchemaField{ - Name: "field4", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - &schema.SchemaField{ - Name: "field5", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - ), - } - - m := models.NewRecord(collection) - m.Set("id", "test_id") - m.Set("created", "2022-09-15 00:00:00.123Z") - m.Set("updated", "invalid") - m.Set("field1", 123) // should be casted to string - m.Set("field2", "invlaid") // should be casted to zero-number - m.Set("unknown", 456) // undefined fields are allowed but not exported by default - m.Set("expand", map[string]any{"test": 123}) // should store the value in m.expand - - if v := m.Get("id"); v != "test_id" { - t.Fatalf("Expected id %q, got %q", "test_id", v) - } - - if v := m.GetString("created"); v != "2022-09-15 00:00:00.123Z" { - t.Fatalf("Expected created %q, got %q", "2022-09-15 00:00:00.123Z", v) - } - - if v := m.GetString("updated"); v != "" { - t.Fatalf("Expected updated to be empty, got %q", v) - } - - if v, ok := m.Get("field1").(string); !ok || v != "123" { - t.Fatalf("Expected field1 %#v, got %#v", "123", m.Get("field1")) - } - - if v, ok := m.Get("field2").(float64); !ok || v != 0.0 { - t.Fatalf("Expected field2 %#v, got %#v", 0.0, m.Get("field2")) - } - - if v, ok := m.Get("field3").(bool); !ok || v != false { - t.Fatalf("Expected field3 %#v, got %#v", false, m.Get("field3")) - } - - if v, ok := m.Get("field4").([]string); !ok || len(v) != 0 { - t.Fatalf("Expected field4 %#v, got %#v", "[]", m.Get("field4")) - } - - if v, ok := m.Get("field5").(string); !ok || len(v) != 0 { - t.Fatalf("Expected field5 %#v, got %#v", "", m.Get("field5")) - } - - if v := m.Get("unknown"); v != 456 { - t.Fatalf("Expected unknown %v, got %v", 456, v) - } - - if m.Expand()["test"] != 123 { - t.Fatalf("Expected expand to be %v, got %v", map[string]any{"test": 123}, m.Expand()) - } -} - -func TestRecordGetBool(t *testing.T) { - t.Parallel() - - scenarios := []struct { - value any - expected bool - }{ - {nil, false}, - {"", false}, - {0, false}, - {1, true}, - {[]string{"true"}, false}, - {time.Now(), false}, - {"test", false}, - {"false", false}, - {"true", true}, - {false, false}, - {true, true}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetBool("test") - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetString(t *testing.T) { - t.Parallel() - - scenarios := []struct { - value any - expected string - }{ - {nil, ""}, - {"", ""}, - {0, "0"}, - {1.4, "1.4"}, - {[]string{"true"}, ""}, - {map[string]int{"test": 1}, ""}, - {[]byte("abc"), "abc"}, - {"test", "test"}, - {false, "false"}, - {true, "true"}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetString("test") - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetInt(t *testing.T) { - t.Parallel() - - scenarios := []struct { - value any - expected int - }{ - {nil, 0}, - {"", 0}, - {[]string{"true"}, 0}, - {map[string]int{"test": 1}, 0}, - {time.Now(), 0}, - {"test", 0}, - {123, 123}, - {2.4, 2}, - {"123", 123}, - {"123.5", 0}, - {false, 0}, - {true, 1}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetInt("test") - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetFloat(t *testing.T) { - t.Parallel() - - scenarios := []struct { - value any - expected float64 - }{ - {nil, 0}, - {"", 0}, - {[]string{"true"}, 0}, - {map[string]int{"test": 1}, 0}, - {time.Now(), 0}, - {"test", 0}, - {123, 123}, - {2.4, 2.4}, - {"123", 123}, - {"123.5", 123.5}, - {false, 0}, - {true, 1}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetFloat("test") - if result != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetTime(t *testing.T) { - t.Parallel() - - nowTime := time.Now() - testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000Z") - - scenarios := []struct { - value any - expected time.Time - }{ - {nil, time.Time{}}, - {"", time.Time{}}, - {false, time.Time{}}, - {true, time.Time{}}, - {"test", time.Time{}}, - {[]string{"true"}, time.Time{}}, - {map[string]int{"test": 1}, time.Time{}}, - {1641024040, testTime}, - {"2022-01-01 08:00:40.000", testTime}, - {nowTime, nowTime}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetTime("test") - if !result.Equal(s.expected) { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetDateTime(t *testing.T) { - t.Parallel() - - nowTime := time.Now() - testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000Z") - - scenarios := []struct { - value any - expected time.Time - }{ - {nil, time.Time{}}, - {"", time.Time{}}, - {false, time.Time{}}, - {true, time.Time{}}, - {"test", time.Time{}}, - {[]string{"true"}, time.Time{}}, - {map[string]int{"test": 1}, time.Time{}}, - {1641024040, testTime}, - {"2022-01-01 08:00:40.000", testTime}, - {nowTime, nowTime}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetDateTime("test") - if !result.Time().Equal(s.expected) { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) - } - } -} - -func TestRecordGetStringSlice(t *testing.T) { - t.Parallel() - - nowTime := time.Now() - - scenarios := []struct { - value any - expected []string - }{ - {nil, []string{}}, - {"", []string{}}, - {false, []string{"false"}}, - {true, []string{"true"}}, - {nowTime, []string{}}, - {123, []string{"123"}}, - {"test", []string{"test"}}, - {map[string]int{"test": 1}, []string{}}, - {`["test1", "test2"]`, []string{"test1", "test2"}}, - {[]int{123, 123, 456}, []string{"123", "456"}}, - {[]string{"test", "test", "123"}, []string{"test", "123"}}, - } - - collection := &models.Collection{} - - for i, s := range scenarios { - m := models.NewRecord(collection) - m.Set("test", s.value) - - result := m.GetStringSlice("test") - - if len(result) != len(s.expected) { - t.Errorf("(%d) Expected %d elements, got %d: %v", i, len(s.expected), len(result), result) - continue - } - - for _, v := range result { - if !list.ExistInSlice(v, s.expected) { - t.Errorf("(%d) Cannot find %v in %v", i, v, s.expected) - } - } - } -} - -func TestRecordUnmarshalJSONField(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema(&schema.SchemaField{ - Name: "field", - Type: schema.FieldTypeJson, - }), - } - m := models.NewRecord(collection) - - var testPointer *string - var testStr string - var testInt int - var testBool bool - var testSlice []int - var testMap map[string]any - - scenarios := []struct { - value any - destination any - expectError bool - expectedJson string - }{ - {nil, testStr, true, `""`}, - {"", testStr, false, `""`}, - {1, testInt, false, `1`}, - {true, testBool, false, `true`}, - {[]int{1, 2, 3}, testSlice, false, `[1,2,3]`}, - {map[string]any{"test": 123}, testMap, false, `{"test":123}`}, - // json encoded values - {`null`, testPointer, false, `null`}, - {`true`, testBool, false, `true`}, - {`456`, testInt, false, `456`}, - {`"test"`, testStr, false, `"test"`}, - {`[4,5,6]`, testSlice, false, `[4,5,6]`}, - {`{"test":456}`, testMap, false, `{"test":456}`}, - } - - for i, s := range scenarios { - m.Set("field", s.value) - - err := m.UnmarshalJSONField("field", &s.destination) - hasErr := err != nil - - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v", i, s.expectError, hasErr) - continue - } - - raw, _ := json.Marshal(s.destination) - if v := string(raw); v != s.expectedJson { - t.Errorf("(%d) Expected %q, got %q", i, s.expectedJson, v) - } - } -} - -func TestRecordBaseFilesPath(t *testing.T) { - t.Parallel() - - collection := &models.Collection{} - collection.RefreshId() - collection.Name = "test" - - m := models.NewRecord(collection) - m.RefreshId() - - expected := collection.BaseFilesPath() + "/" + m.Id - result := m.BaseFilesPath() - - if result != expected { - t.Fatalf("Expected %q, got %q", expected, result) - } -} - -func TestRecordFindFileFieldByFile(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 1, - MaxSize: 1, - }, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 2, - MaxSize: 1, - }, - }, - ), - } - - m := models.NewRecord(collection) - m.Set("field1", "test") - m.Set("field2", "test.png") - m.Set("field3", []string{"test1.png", "test2.png"}) - - scenarios := []struct { - filename string - expectField string - }{ - {"", ""}, - {"test", ""}, - {"test2", ""}, - {"test.png", "field2"}, - {"test2.png", "field3"}, - } - - for i, s := range scenarios { - result := m.FindFileFieldByFile(s.filename) - - var fieldName string - if result != nil { - fieldName = result.Name - } - - if s.expectField != fieldName { - t.Errorf("(%d) Expected field %v, got %v", i, s.expectField, result) - continue - } - } -} - -func TestRecordLoadAndData(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - ), - } - - data := map[string]any{ - "id": "test_id", - "created": "2022-01-01 10:00:00.123Z", - "updated": "2022-01-01 10:00:00.456Z", - "field1": "test_field", - "field2": "123", // should be casted to float - "unknown": "test_unknown", - // auth collection sepcific casting test - "passwordHash": "test_passwordHash", - "emailVisibility": "12345", // should be casted to bool only for auth collections - "username": 123, // should be casted to string only for auth collections - "email": "test_email", - "verified": true, - "tokenKey": "test_tokenKey", - "lastResetSentAt": "2022-01-01 11:00:00.000", // should be casted to DateTime only for auth collections - "lastVerificationSentAt": "2022-01-01 12:00:00.000", // should be casted to DateTime only for auth collections - } - - scenarios := []struct { - collectionType string - }{ - {models.CollectionTypeBase}, - {models.CollectionTypeAuth}, - } - - for i, s := range scenarios { - collection.Type = s.collectionType - m := models.NewRecord(collection) - - m.Load(data) - - expectations := map[string]any{} - for k, v := range data { - expectations[k] = v - } - - expectations["created"], _ = types.ParseDateTime("2022-01-01 10:00:00.123Z") - expectations["updated"], _ = types.ParseDateTime("2022-01-01 10:00:00.456Z") - expectations["field2"] = 123.0 - - // extra casting test - if collection.IsAuth() { - lastResetSentAt, _ := types.ParseDateTime(expectations["lastResetSentAt"]) - lastVerificationSentAt, _ := types.ParseDateTime(expectations["lastVerificationSentAt"]) - expectations["emailVisibility"] = false - expectations["username"] = "123" - expectations["verified"] = true - expectations["lastResetSentAt"] = lastResetSentAt - expectations["lastVerificationSentAt"] = lastVerificationSentAt - } - - for k, v := range expectations { - if m.Get(k) != v { - t.Errorf("(%d) Expected field %s to be %v, got %v", i, k, v, m.Get(k)) - } - } - } -} - -func TestRecordColumnValueMap(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 1, - MaxSize: 1, - }, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - MaxSelect: 2, - Values: []string{"test1", "test2", "test3"}, - }, - }, - &schema.SchemaField{ - Name: "field4", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{ - MaxSelect: types.Pointer(2), - }, - }, - ), - } - - scenarios := []struct { - collectionType string - expectedJson string - }{ - { - models.CollectionTypeBase, - `{"created":"2022-01-01 10:00:30.123Z","field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id","updated":""}`, - }, - { - models.CollectionTypeAuth, - `{"created":"2022-01-01 10:00:30.123Z","email":"test_email","emailVisibility":true,"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id","lastLoginAlertSentAt":"","lastResetSentAt":"2022-01-02 10:00:30.123Z","lastVerificationSentAt":"","passwordHash":"test_passwordHash","tokenKey":"test_tokenKey","updated":"","username":"test_username","verified":false}`, - }, - } - - created, _ := types.ParseDateTime("2022-01-01 10:00:30.123Z") - lastResetSentAt, _ := types.ParseDateTime("2022-01-02 10:00:30.123Z") - data := map[string]any{ - "id": "test_id", - "created": created, - "field1": "test", - "field2": "test.png", - "field3": []string{"test1", "test2"}, - "field4": []string{"test11", "test12", "test11"}, // strip duplicate, - "unknown": "test_unknown", - "passwordHash": "test_passwordHash", - "username": "test_username", - "emailVisibility": true, - "email": "test_email", - "verified": "invalid", // should be casted - "tokenKey": "test_tokenKey", - "lastResetSentAt": lastResetSentAt, - } - - m := models.NewRecord(collection) - - for i, s := range scenarios { - collection.Type = s.collectionType - - m.Load(data) - - result := m.ColumnValueMap() - - encoded, err := json.Marshal(result) - if err != nil { - t.Errorf("(%d) Unexpected error %v", i, err) - continue - } - - if str := string(encoded); str != s.expectedJson { - t.Errorf("(%d) Expected \n%v \ngot \n%v", i, s.expectedJson, str) - } - } -} - -func TestRecordPublicExportAndMarshalJSON(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Name: "c_name", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{ - MaxSelect: 1, - MaxSize: 1, - }, - }, - &schema.SchemaField{ - Name: "field3", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - MaxSelect: 2, - Values: []string{"test1", "test2", "test3"}, - }, - }, - ), - } - collection.Id = "c_id" - - scenarios := []struct { - collectionType string - exportHidden bool - exportUnknown bool - expectedJson string - }{ - // base - { - models.CollectionTypeBase, - false, - false, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","updated":""}`, - }, - { - models.CollectionTypeBase, - true, - false, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","updated":""}`, - }, - { - models.CollectionTypeBase, - false, - true, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","lastResetSentAt":"2022-01-02 10:00:30.123Z","lastVerificationSentAt":"test_lastVerificationSentAt","passwordHash":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","updated":"","username":123,"verified":true}`, - }, - { - models.CollectionTypeBase, - true, - true, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","lastResetSentAt":"2022-01-02 10:00:30.123Z","lastVerificationSentAt":"test_lastVerificationSentAt","passwordHash":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","updated":"","username":123,"verified":true}`, - }, - - // auth - { - models.CollectionTypeAuth, - false, - false, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","emailVisibility":false,"expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","updated":"","username":"123","verified":true}`, - }, - { - models.CollectionTypeAuth, - true, - false, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","updated":"","username":"123","verified":true}`, - }, - { - models.CollectionTypeAuth, - false, - true, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","emailVisibility":false,"expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","updated":"","username":"123","verified":true}`, - }, - { - models.CollectionTypeAuth, - true, - true, - `{"collectionId":"c_id","collectionName":"c_name","created":"2022-01-01 10:00:30.123Z","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"test","field2":"test.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","updated":"","username":"123","verified":true}`, - }, - } - - created, _ := types.ParseDateTime("2022-01-01 10:00:30.123Z") - lastResetSentAt, _ := types.ParseDateTime("2022-01-02 10:00:30.123Z") - - data := map[string]any{ - "id": "test_id", - "created": created, - "field1": "test", - "field2": "test.png", - "field3": []string{"test1", "test2"}, - "expand": map[string]any{"test": 123}, - "collectionId": "m_id", // should be always ignored - "collectionName": "m_name", // should be always ignored - "unknown": "test_unknown", - "passwordHash": "test_passwordHash", - "username": 123, // for auth collections should be casted to string - "emailVisibility": "test_invalid", // for auth collections should be casted to bool - "email": "test_email", - "verified": true, - "tokenKey": "test_tokenKey", - "lastResetSentAt": lastResetSentAt, - "lastVerificationSentAt": "test_lastVerificationSentAt", - } - - m := models.NewRecord(collection) - - for i, s := range scenarios { - collection.Type = s.collectionType - - m.Load(data) - m.IgnoreEmailVisibility(s.exportHidden) - m.WithUnknownData(s.exportUnknown) - - exportResult, err := json.Marshal(m.PublicExport()) - if err != nil { - t.Errorf("(%d) Unexpected error %v", i, err) - continue - } - exportResultStr := string(exportResult) - - // MarshalJSON and PublicExport should return the same - marshalResult, err := m.MarshalJSON() - if err != nil { - t.Errorf("(%d) Unexpected error %v", i, err) - continue - } - marshalResultStr := string(marshalResult) - - if exportResultStr != marshalResultStr { - t.Errorf("(%d) Expected the PublicExport to be the same as MarshalJSON, but got \n%v \nvs \n%v", i, exportResultStr, marshalResultStr) - } - - if exportResultStr != s.expectedJson { - t.Errorf("(%d) Expected json \n%v \ngot \n%v", i, s.expectedJson, exportResultStr) - } - } -} - -func TestRecordUnmarshalJSON(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "field1", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "field2", - Type: schema.FieldTypeNumber, - }, - ), - } - - data := map[string]any{ - "id": "test_id", - "created": "2022-01-01 10:00:00.123Z", - "updated": "2022-01-01 10:00:00.456Z", - "field1": "test_field", - "field2": "123", // should be casted to float - "unknown": "test_unknown", - // auth collection sepcific casting test - "passwordHash": "test_passwordHash", - "emailVisibility": "12345", // should be casted to bool only for auth collections - "username": 123.123, // should be casted to string only for auth collections - "email": "test_email", - "verified": true, - "tokenKey": "test_tokenKey", - "lastResetSentAt": "2022-01-01 11:00:00.000", // should be casted to DateTime only for auth collections - "lastVerificationSentAt": "2022-01-01 12:00:00.000", // should be casted to DateTime only for auth collections - } - dataRaw, err := json.Marshal(data) - if err != nil { - t.Fatalf("Unexpected data marshal error %v", err) - } - - scenarios := []struct { - collectionType string - }{ - {models.CollectionTypeBase}, - {models.CollectionTypeAuth}, - } - - // with invalid data - m0 := models.NewRecord(collection) - if err := m0.UnmarshalJSON([]byte("test")); err == nil { - t.Fatal("Expected error, got nil") - } - - // with valid data (it should be pretty much the same as load) - for i, s := range scenarios { - collection.Type = s.collectionType - m := models.NewRecord(collection) - - err := m.UnmarshalJSON(dataRaw) - if err != nil { - t.Errorf("(%d) Unexpected error %v", i, err) - continue - } - - expectations := map[string]any{} - for k, v := range data { - expectations[k] = v - } - - expectations["created"], _ = types.ParseDateTime("2022-01-01 10:00:00.123Z") - expectations["updated"], _ = types.ParseDateTime("2022-01-01 10:00:00.456Z") - expectations["field2"] = 123.0 - - // extra casting test - if collection.IsAuth() { - lastResetSentAt, _ := types.ParseDateTime(expectations["lastResetSentAt"]) - lastVerificationSentAt, _ := types.ParseDateTime(expectations["lastVerificationSentAt"]) - expectations["emailVisibility"] = false - expectations["username"] = "123.123" - expectations["verified"] = true - expectations["lastResetSentAt"] = lastResetSentAt - expectations["lastVerificationSentAt"] = lastVerificationSentAt - } - - for k, v := range expectations { - if m.Get(k) != v { - t.Errorf("(%d) Expected field %s to be %v, got %v", i, k, v, m.Get(k)) - } - } - } -} - -func TestRecordReplaceModifers(t *testing.T) { - t.Parallel() - - collection := &models.Collection{ - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "text", - Type: schema.FieldTypeText, - }, - &schema.SchemaField{ - Name: "number", - Type: schema.FieldTypeNumber, - }, - &schema.SchemaField{ - Name: "rel_one", - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - &schema.SchemaField{ - Name: "rel_many", - Type: schema.FieldTypeRelation, - }, - &schema.SchemaField{ - Name: "select_one", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 1}, - }, - &schema.SchemaField{ - Name: "select_many", - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 10}, - }, - &schema.SchemaField{ - Name: "file_one", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 1}, - }, - &schema.SchemaField{ - Name: "file_one_index", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 1}, - }, - &schema.SchemaField{ - Name: "file_one_name", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 1}, - }, - &schema.SchemaField{ - Name: "file_many", - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 10}, - }, - ), - } - - record := models.NewRecord(collection) - - record.Load(map[string]any{ - "text": "test", - "number": 10, - "rel_one": "a", - "rel_many": []string{"a", "b"}, - "select_one": "a", - "select_many": []string{"a", "b", "c"}, - "file_one": "a", - "file_one_index": "b", - "file_one_name": "c", - "file_many": []string{"a", "b", "c", "d", "e", "f"}, - }) - - result := record.ReplaceModifers(map[string]any{ - "text-": "m-", - "text+": "m+", - "number-": 3, - "number+": 5, - "rel_one-": "a", - "rel_one+": "b", - "rel_many-": []string{"a"}, - "rel_many+": []string{"c", "d", "e"}, - "select_one-": "a", - "select_one+": "c", - "select_many-": []string{"b", "c"}, - "select_many+": []string{"d", "e"}, - "file_one+": "skip", // should be ignored - "file_one-": "a", - "file_one_index.0": "", - "file_one_name.c": "", - "file_many+": []string{"e", "f"}, // should be ignored - "file_many-": []string{"c", "d"}, - "file_many.f": nil, - "file_many.0": nil, - }) - - raw, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - - expected := `{"file_many":["b","e"],"file_one":"","file_one_index":"","file_one_name":"","number":12,"rel_many":["b","c","d","e"],"rel_one":"b","select_many":["a","d","e"],"select_one":"c","text":"test"}` - - if v := string(raw); v != expected { - t.Fatalf("Expected \n%s, \ngot \n%s", expected, v) - } -} - -// ------------------------------------------------------------------- -// Auth helpers: -// ------------------------------------------------------------------- - -func TestRecordUsername(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue := "test 1232 !@#%" // formatting isn't checked - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetUsername(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.Username(); v != "" { - t.Fatalf("(%d) Expected empty string, got %q", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameUsername); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameUsername, v) - } - } else { - if err := m.SetUsername(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.Username(); v != testValue { - t.Fatalf("(%d) Expected %q, got %q", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameUsername); v != testValue { - t.Fatalf("(%d) Expected data field value %q, got %q", i, testValue, v) - } - } - } -} - -func TestRecordEmail(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue := "test 1232 !@#%" // formatting isn't checked - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetEmail(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.Email(); v != "" { - t.Fatalf("(%d) Expected empty string, got %q", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameEmail); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameEmail, v) - } - } else { - if err := m.SetEmail(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.Email(); v != testValue { - t.Fatalf("(%d) Expected %q, got %q", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameEmail); v != testValue { - t.Fatalf("(%d) Expected data field value %q, got %q", i, testValue, v) - } - } - } -} - -func TestRecordEmailVisibility(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - value bool - expectError bool - }{ - {models.CollectionTypeBase, true, true}, - {models.CollectionTypeBase, true, true}, - {models.CollectionTypeAuth, false, false}, - {models.CollectionTypeAuth, true, false}, - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetEmailVisibility(s.value); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.EmailVisibility(); v != false { - t.Fatalf("(%d) Expected empty string, got %v", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameEmailVisibility); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameEmailVisibility, v) - } - } else { - if err := m.SetEmailVisibility(s.value); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.EmailVisibility(); v != s.value { - t.Fatalf("(%d) Expected %v, got %v", i, s.value, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameEmailVisibility); v != s.value { - t.Fatalf("(%d) Expected data field value %v, got %v", i, s.value, v) - } - } - } -} - -func TestRecordEmailVerified(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - value bool - expectError bool - }{ - {models.CollectionTypeBase, true, true}, - {models.CollectionTypeBase, true, true}, - {models.CollectionTypeAuth, false, false}, - {models.CollectionTypeAuth, true, false}, - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetVerified(s.value); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.Verified(); v != false { - t.Fatalf("(%d) Expected empty string, got %v", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameVerified); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameVerified, v) - } - } else { - if err := m.SetVerified(s.value); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.Verified(); v != s.value { - t.Fatalf("(%d) Expected %v, got %v", i, s.value, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameVerified); v != s.value { - t.Fatalf("(%d) Expected data field value %v, got %v", i, s.value, v) - } - } - } -} - -func TestRecordTokenKey(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue := "test 1232 !@#%" // formatting isn't checked - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetTokenKey(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.TokenKey(); v != "" { - t.Fatalf("(%d) Expected empty string, got %q", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameTokenKey); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameTokenKey, v) - } - } else { - if err := m.SetTokenKey(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.TokenKey(); v != testValue { - t.Fatalf("(%d) Expected %q, got %q", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameTokenKey); v != testValue { - t.Fatalf("(%d) Expected data field value %q, got %q", i, testValue, v) - } - } - } -} - -func TestRecordRefreshTokenKey(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.RefreshTokenKey(); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.TokenKey(); v != "" { - t.Fatalf("(%d) Expected empty string, got %q", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameTokenKey); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameTokenKey, v) - } - } else { - if err := m.RefreshTokenKey(); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.TokenKey(); len(v) != 50 { - t.Fatalf("(%d) Expected 50 chars, got %d", i, len(v)) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameTokenKey); v != m.TokenKey() { - t.Fatalf("(%d) Expected data field value %q, got %q", i, m.TokenKey(), v) - } - } - } -} - -func TestRecordLastPasswordLoginAlertSentAt(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue, err := types.ParseDateTime("2022-01-01 00:00:00.123Z") - if err != nil { - t.Fatal(err) - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetLastLoginAlertSentAt(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.LastLoginAlertSentAt(); !v.IsZero() { - t.Fatalf("(%d) Expected empty value, got %v", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameLastLoginAlertSentAt); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameLastLoginAlertSentAt, v) - } - } else { - if err := m.SetLastLoginAlertSentAt(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.LastLoginAlertSentAt(); v != testValue { - t.Fatalf("(%d) Expected %v, got %v", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameLastLoginAlertSentAt); v != testValue { - t.Fatalf("(%d) Expected data field value %v, got %v", i, testValue, v) - } - } - } -} - -func TestRecordLastResetSentAt(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue, err := types.ParseDateTime("2022-01-01 00:00:00.123Z") - if err != nil { - t.Fatal(err) - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetLastResetSentAt(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.LastResetSentAt(); !v.IsZero() { - t.Fatalf("(%d) Expected empty value, got %v", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameLastResetSentAt); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameLastResetSentAt, v) - } - } else { - if err := m.SetLastResetSentAt(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.LastResetSentAt(); v != testValue { - t.Fatalf("(%d) Expected %v, got %v", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameLastResetSentAt); v != testValue { - t.Fatalf("(%d) Expected data field value %v, got %v", i, testValue, v) - } - } - } -} - -func TestRecordLastVerificationSentAt(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - expectError bool - }{ - {models.CollectionTypeBase, true}, - {models.CollectionTypeAuth, false}, - } - - testValue, err := types.ParseDateTime("2022-01-01 00:00:00.123Z") - if err != nil { - t.Fatal(err) - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetLastVerificationSentAt(testValue); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.LastVerificationSentAt(); !v.IsZero() { - t.Fatalf("(%d) Expected empty value, got %v", i, v) - } - // verify that nothing is stored in the record data slice - if v := m.Get(schema.FieldNameLastVerificationSentAt); v != nil { - t.Fatalf("(%d) Didn't expect data field %q: %v", i, schema.FieldNameLastVerificationSentAt, v) - } - } else { - if err := m.SetLastVerificationSentAt(testValue); err != nil { - t.Fatalf("(%d) Expected nil, got error %v", i, err) - } - if v := m.LastVerificationSentAt(); v != testValue { - t.Fatalf("(%d) Expected %v, got %v", i, testValue, v) - } - // verify that the field is stored in the record data slice - if v := m.Get(schema.FieldNameLastVerificationSentAt); v != testValue { - t.Fatalf("(%d) Expected data field value %v, got %v", i, testValue, v) - } - } - } -} - -func TestRecordPasswordHash(t *testing.T) { - t.Parallel() - - m := models.NewRecord(&models.Collection{}) - - if v := m.PasswordHash(); v != "" { - t.Errorf("Expected PasswordHash() to be empty, got %v", v) - } - - m.Set(schema.FieldNamePasswordHash, "test") - - if v := m.PasswordHash(); v != "test" { - t.Errorf("Expected PasswordHash() to be 'test', got %v", v) - } -} - -func TestRecordValidatePassword(t *testing.T) { - t.Parallel() - - // 123456 - hash := "$2a$10$YKU8mPP8sTE3xZrpuM.xQuq27KJ7aIJB2oUeKPsDDqZshbl5g5cDK" - - scenarios := []struct { - collectionType string - password string - hash string - expected bool - }{ - {models.CollectionTypeBase, "123456", hash, false}, - {models.CollectionTypeAuth, "", "", false}, - {models.CollectionTypeAuth, "", hash, false}, - {models.CollectionTypeAuth, "123456", hash, true}, - {models.CollectionTypeAuth, "654321", hash, false}, - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - m.Set(schema.FieldNamePasswordHash, hash) - - if v := m.ValidatePassword(s.password); v != s.expected { - t.Errorf("(%d) Expected %v, got %v", i, s.expected, v) - } - } -} - -func TestRecordSetPassword(t *testing.T) { - t.Parallel() - - scenarios := []struct { - collectionType string - password string - expectError bool - }{ - {models.CollectionTypeBase, "", true}, - {models.CollectionTypeBase, "123456", true}, - {models.CollectionTypeAuth, "", true}, - {models.CollectionTypeAuth, "123456", false}, - } - - for i, s := range scenarios { - collection := &models.Collection{Type: s.collectionType} - m := models.NewRecord(collection) - - if s.expectError { - if err := m.SetPassword(s.password); err == nil { - t.Errorf("(%d) Expected error, got nil", i) - } - if v := m.GetString(schema.FieldNamePasswordHash); v != "" { - t.Errorf("(%d) Expected empty hash, got %q", i, v) - } - } else { - if err := m.SetPassword(s.password); err != nil { - t.Errorf("(%d) Expected nil, got err", i) - } - if v := m.GetString(schema.FieldNamePasswordHash); v == "" { - t.Errorf("(%d) Expected non empty hash", i) - } - if !m.ValidatePassword(s.password) { - t.Errorf("(%d) Expected true, got false", i) - } - } - } -} diff --git a/models/request.go b/models/request.go deleted file mode 100644 index 0b1784c0..00000000 --- a/models/request.go +++ /dev/null @@ -1,31 +0,0 @@ -package models - -import "github.com/pocketbase/pocketbase/tools/types" - -var _ Model = (*Request)(nil) - -// list with the supported values for `Request.Auth` -const ( - RequestAuthGuest = "guest" - RequestAuthAdmin = "admin" - RequestAuthRecord = "authRecord" -) - -// Deprecated: Replaced by the Log model and will be removed in a future version. -type Request struct { - BaseModel - - Url string `db:"url" json:"url"` - Method string `db:"method" json:"method"` - Status int `db:"status" json:"status"` - Auth string `db:"auth" json:"auth"` - UserIp string `db:"userIp" json:"userIp"` - RemoteIp string `db:"remoteIp" json:"remoteIp"` - Referer string `db:"referer" json:"referer"` - UserAgent string `db:"userAgent" json:"userAgent"` - Meta types.JsonMap `db:"meta" json:"meta"` -} - -func (m *Request) TableName() string { - return "_requests" -} diff --git a/models/request_info.go b/models/request_info.go deleted file mode 100644 index 216dd32f..00000000 --- a/models/request_info.go +++ /dev/null @@ -1,41 +0,0 @@ -package models - -import ( - "strings" - - "github.com/pocketbase/pocketbase/models/schema" -) - -const ( - RequestInfoContextDefault = "default" - RequestInfoContextRealtime = "realtime" - RequestInfoContextProtectedFile = "protectedFile" - RequestInfoContextOAuth2 = "oauth2" -) - -// RequestInfo defines a HTTP request data struct, usually used -// as part of the `@request.*` filter resolver. -type RequestInfo struct { - Context string `json:"context"` - Query map[string]any `json:"query"` - Data map[string]any `json:"data"` - Headers map[string]any `json:"headers"` - AuthRecord *Record `json:"authRecord"` - Admin *Admin `json:"admin"` - Method string `json:"method"` -} - -// HasModifierDataKeys loosely checks if the current struct has any modifier Data keys. -func (r *RequestInfo) HasModifierDataKeys() bool { - allModifiers := schema.FieldValueModifiers() - - for key := range r.Data { - for _, m := range allModifiers { - if strings.HasSuffix(key, m) { - return true - } - } - } - - return false -} diff --git a/models/request_info_test.go b/models/request_info_test.go deleted file mode 100644 index 157ddac1..00000000 --- a/models/request_info_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" -) - -func TestRequestInfoHasModifierDataKeys(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - requestInfo *models.RequestInfo - expected bool - }{ - { - "empty", - &models.RequestInfo{}, - false, - }, - { - "Data with regular fields", - &models.RequestInfo{ - Query: map[string]any{"data+": "demo"}, // should be ignored - Data: map[string]any{"a": 123, "b": "test", "c.d": false}, - }, - false, - }, - { - "Data with +modifier fields", - &models.RequestInfo{ - Data: map[string]any{"a+": 123, "b": "test", "c.d": false}, - }, - true, - }, - { - "Data with -modifier fields", - &models.RequestInfo{ - Data: map[string]any{"a": 123, "b-": "test", "c.d": false}, - }, - true, - }, - { - "Data with mixed modifier fields", - &models.RequestInfo{ - Data: map[string]any{"a": 123, "b-": "test", "c.d+": false}, - }, - true, - }, - } - - for _, s := range scenarios { - result := s.requestInfo.HasModifierDataKeys() - - if result != s.expected { - t.Fatalf("[%s] Expected %v, got %v", s.name, s.expected, result) - } - } -} diff --git a/models/request_test.go b/models/request_test.go deleted file mode 100644 index 0f1f99e5..00000000 --- a/models/request_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models" -) - -func TestRequestTableName(t *testing.T) { - m := models.Request{} - if m.TableName() != "_requests" { - t.Fatalf("Unexpected table name, got %q", m.TableName()) - } -} diff --git a/models/schema/schema.go b/models/schema/schema.go deleted file mode 100644 index d48b0550..00000000 --- a/models/schema/schema.go +++ /dev/null @@ -1,240 +0,0 @@ -// Package schema implements custom Schema and SchemaField datatypes -// for handling the Collection schema definitions. -package schema - -import ( - "database/sql/driver" - "encoding/json" - "fmt" - "strconv" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/security" -) - -// NewSchema creates a new Schema instance with the provided fields. -func NewSchema(fields ...*SchemaField) Schema { - s := Schema{} - - for _, f := range fields { - s.AddField(f) - } - - return s -} - -// Schema defines a dynamic db schema as a slice of `SchemaField`s. -type Schema struct { - fields []*SchemaField -} - -// Fields returns the registered schema fields. -func (s *Schema) Fields() []*SchemaField { - return s.fields -} - -// InitFieldsOptions calls `InitOptions()` for all schema fields. -func (s *Schema) InitFieldsOptions() error { - for _, field := range s.Fields() { - if err := field.InitOptions(); err != nil { - return err - } - } - return nil -} - -// Clone creates a deep clone of the current schema. -func (s *Schema) Clone() (*Schema, error) { - copyRaw, err := json.Marshal(s) - if err != nil { - return nil, err - } - - result := &Schema{} - if err := json.Unmarshal(copyRaw, result); err != nil { - return nil, err - } - - return result, nil -} - -// AsMap returns a map with all registered schema field. -// The returned map is indexed with each field name. -func (s *Schema) AsMap() map[string]*SchemaField { - result := map[string]*SchemaField{} - - for _, field := range s.fields { - result[field.Name] = field - } - - return result -} - -// GetFieldById returns a single field by its id. -func (s *Schema) GetFieldById(id string) *SchemaField { - for _, field := range s.fields { - if field.Id == id { - return field - } - } - return nil -} - -// GetFieldByName returns a single field by its name. -func (s *Schema) GetFieldByName(name string) *SchemaField { - for _, field := range s.fields { - if field.Name == name { - return field - } - } - return nil -} - -// RemoveField removes a single schema field by its id. -// -// This method does nothing if field with `id` doesn't exist. -func (s *Schema) RemoveField(id string) { - for i, field := range s.fields { - if field.Id == id { - s.fields = append(s.fields[:i], s.fields[i+1:]...) - return - } - } -} - -// AddField registers the provided newField to the current schema. -// -// If field with `newField.Id` already exist, the existing field is -// replaced with the new one. -// -// Otherwise the new field is appended to the other schema fields. -func (s *Schema) AddField(newField *SchemaField) { - if newField.Id == "" { - // set default id - newField.Id = strings.ToLower(security.PseudorandomString(8)) - } - - for i, field := range s.fields { - // replace existing - if field.Id == newField.Id { - s.fields[i] = newField - return - } - } - - // add new field - s.fields = append(s.fields, newField) -} - -// Validate makes Schema validatable by implementing [validation.Validatable] interface. -// -// Internally calls each individual field's validator and additionally -// checks for invalid renamed fields and field name duplications. -func (s Schema) Validate() error { - return validation.Validate(&s.fields, validation.By(func(value any) error { - fields := s.fields // use directly the schema value to avoid unnecessary interface casting - - ids := []string{} - names := []string{} - for i, field := range fields { - if list.ExistInSlice(field.Id, ids) { - return validation.Errors{ - strconv.Itoa(i): validation.Errors{ - "id": validation.NewError( - "validation_duplicated_field_id", - "Duplicated or invalid schema field id", - ), - }, - } - } - - // field names are used as db columns and should be case insensitive - nameLower := strings.ToLower(field.Name) - - if list.ExistInSlice(nameLower, names) { - return validation.Errors{ - strconv.Itoa(i): validation.Errors{ - "name": validation.NewError( - "validation_duplicated_field_name", - "Duplicated or invalid schema field name", - ), - }, - } - } - - ids = append(ids, field.Id) - names = append(names, nameLower) - } - - return nil - })) -} - -// MarshalJSON implements the [json.Marshaler] interface. -func (s Schema) MarshalJSON() ([]byte, error) { - if s.fields == nil { - s.fields = []*SchemaField{} - } - return json.Marshal(s.fields) -} - -// UnmarshalJSON implements the [json.Unmarshaler] interface. -// -// On success, all schema field options are auto initialized. -func (s *Schema) UnmarshalJSON(data []byte) error { - fields := []*SchemaField{} - if err := json.Unmarshal(data, &fields); err != nil { - return err - } - - s.fields = []*SchemaField{} - - for _, f := range fields { - s.AddField(f) - } - - for _, field := range s.fields { - if err := field.InitOptions(); err != nil { - // ignore the error and remove the invalid field - s.RemoveField(field.Id) - } - } - - return nil -} - -// Value implements the [driver.Valuer] interface. -func (s Schema) Value() (driver.Value, error) { - if s.fields == nil { - // initialize an empty slice to ensure that `[]` is returned - s.fields = []*SchemaField{} - } - - data, err := json.Marshal(s.fields) - - return string(data), err -} - -// Scan implements [sql.Scanner] interface to scan the provided value -// into the current Schema instance. -func (s *Schema) Scan(value any) error { - var data []byte - switch v := value.(type) { - case nil: - // no cast needed - case []byte: - data = v - case string: - data = []byte(v) - default: - return fmt.Errorf("Failed to unmarshal Schema value %q.", value) - } - - if len(data) == 0 { - data = []byte("[]") - } - - return s.UnmarshalJSON(data) -} diff --git a/models/schema/schema_field.go b/models/schema/schema_field.go deleted file mode 100644 index 38a735f1..00000000 --- a/models/schema/schema_field.go +++ /dev/null @@ -1,730 +0,0 @@ -package schema - -import ( - "encoding/json" - "errors" - "regexp" - "strconv" - "strings" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/tools/filesystem" - "github.com/pocketbase/pocketbase/tools/list" - "github.com/pocketbase/pocketbase/tools/types" - "github.com/spf13/cast" -) - -var schemaFieldNameRegex = regexp.MustCompile(`^\w+$`) - -// field value modifiers -const ( - FieldValueModifierAdd string = "+" - FieldValueModifierSubtract string = "-" -) - -// FieldValueModifiers returns a list with all available field modifier tokens. -func FieldValueModifiers() []string { - return []string{ - FieldValueModifierAdd, - FieldValueModifierSubtract, - } -} - -// commonly used field names -const ( - FieldNameId string = "id" - FieldNameCreated string = "created" - FieldNameUpdated string = "updated" - FieldNameCollectionId string = "collectionId" - FieldNameCollectionName string = "collectionName" - FieldNameExpand string = "expand" - FieldNameUsername string = "username" - FieldNameEmail string = "email" - FieldNameEmailVisibility string = "emailVisibility" - FieldNameVerified string = "verified" - FieldNameTokenKey string = "tokenKey" - FieldNamePasswordHash string = "passwordHash" - FieldNameLastResetSentAt string = "lastResetSentAt" - FieldNameLastVerificationSentAt string = "lastVerificationSentAt" - FieldNameLastLoginAlertSentAt string = "lastLoginAlertSentAt" -) - -// BaseModelFieldNames returns the field names that all models have (id, created, updated). -func BaseModelFieldNames() []string { - return []string{ - FieldNameId, - FieldNameCreated, - FieldNameUpdated, - } -} - -// SystemFields returns special internal field names that are usually readonly. -func SystemFieldNames() []string { - return []string{ - FieldNameCollectionId, - FieldNameCollectionName, - FieldNameExpand, - } -} - -// AuthFieldNames returns the reserved "auth" collection auth field names. -func AuthFieldNames() []string { - return []string{ - FieldNameUsername, - FieldNameEmail, - FieldNameEmailVisibility, - FieldNameVerified, - FieldNameTokenKey, - FieldNamePasswordHash, - FieldNameLastResetSentAt, - FieldNameLastVerificationSentAt, - FieldNameLastLoginAlertSentAt, - } -} - -// All valid field types -const ( - FieldTypeText string = "text" - FieldTypeNumber string = "number" - FieldTypeBool string = "bool" - FieldTypeEmail string = "email" - FieldTypeUrl string = "url" - FieldTypeEditor string = "editor" - FieldTypeDate string = "date" - FieldTypeSelect string = "select" - FieldTypeJson string = "json" - FieldTypeFile string = "file" - FieldTypeRelation string = "relation" - - // Deprecated: Will be removed in v0.9+ - FieldTypeUser string = "user" -) - -// FieldTypes returns slice with all supported field types. -func FieldTypes() []string { - return []string{ - FieldTypeText, - FieldTypeNumber, - FieldTypeBool, - FieldTypeEmail, - FieldTypeUrl, - FieldTypeEditor, - FieldTypeDate, - FieldTypeSelect, - FieldTypeJson, - FieldTypeFile, - FieldTypeRelation, - } -} - -// ArraybleFieldTypes returns slice with all array value supported field types. -func ArraybleFieldTypes() []string { - return []string{ - FieldTypeSelect, - FieldTypeFile, - FieldTypeRelation, - } -} - -// SchemaField defines a single schema field structure. -type SchemaField struct { - System bool `form:"system" json:"system"` - Id string `form:"id" json:"id"` - Name string `form:"name" json:"name"` - Type string `form:"type" json:"type"` - Required bool `form:"required" json:"required"` - - // Presentable indicates whether the field is suitable for - // visualization purposes (eg. in the Admin UI relation views). - Presentable bool `form:"presentable" json:"presentable"` - - // Deprecated: This field is no-op and will be removed in future versions. - // Please use the collection.Indexes field to define a unique constraint. - Unique bool `form:"unique" json:"unique"` - - Options any `form:"options" json:"options"` -} - -// ColDefinition returns the field db column type definition as string. -func (f *SchemaField) ColDefinition() string { - switch f.Type { - case FieldTypeNumber: - return "NUMERIC DEFAULT 0 NOT NULL" - case FieldTypeBool: - return "BOOLEAN DEFAULT FALSE NOT NULL" - case FieldTypeJson: - return "JSON DEFAULT NULL" - default: - if opt, ok := f.Options.(MultiValuer); ok && opt.IsMultiple() { - return "JSON DEFAULT '[]' NOT NULL" - } - - return "TEXT DEFAULT '' NOT NULL" - } -} - -// String serializes and returns the current field as string. -func (f SchemaField) String() string { - data, _ := f.MarshalJSON() - return string(data) -} - -// MarshalJSON implements the [json.Marshaler] interface. -func (f SchemaField) MarshalJSON() ([]byte, error) { - type alias SchemaField // alias to prevent recursion - - f.InitOptions() - - return json.Marshal(alias(f)) -} - -// UnmarshalJSON implements the [json.Unmarshaler] interface. -// -// The schema field options are auto initialized on success. -func (f *SchemaField) UnmarshalJSON(data []byte) error { - type alias *SchemaField // alias to prevent recursion - - a := alias(f) - - if err := json.Unmarshal(data, a); err != nil { - return err - } - - return f.InitOptions() -} - -// Validate makes `SchemaField` validatable by implementing [validation.Validatable] interface. -func (f SchemaField) Validate() error { - // init field options (if not already) - f.InitOptions() - - excludeNames := BaseModelFieldNames() - // exclude special filter literals - excludeNames = append(excludeNames, "null", "true", "false", "_rowid_") - // exclude system literals - excludeNames = append(excludeNames, SystemFieldNames()...) - - return validation.ValidateStruct(&f, - validation.Field(&f.Options, validation.Required, validation.By(f.checkOptions)), - validation.Field(&f.Id, validation.Required, validation.Length(5, 255)), - validation.Field( - &f.Name, - validation.Required, - validation.Length(1, 255), - validation.Match(schemaFieldNameRegex), - validation.NotIn(list.ToInterfaceSlice(excludeNames)...), - validation.By(f.checkForVia), - ), - validation.Field(&f.Type, validation.Required, validation.In(list.ToInterfaceSlice(FieldTypes())...)), - // currently file fields cannot be unique because a proper - // hash/content check could cause performance issues - validation.Field(&f.Unique, validation.When(f.Type == FieldTypeFile, validation.Empty)), - ) -} - -func (f *SchemaField) checkOptions(value any) error { - v, ok := value.(FieldOptions) - if !ok { - return validation.NewError("validation_invalid_options", "Failed to initialize field options") - } - - return v.Validate() -} - -// @todo merge with the collections during the refactoring -func (f *SchemaField) checkForVia(value any) error { - v, _ := value.(string) - if v == "" { - return nil - } - - if strings.Contains(strings.ToLower(v), "_via_") { - return validation.NewError("validation_invalid_name", "The name of the field cannot contain '_via_'.") - } - - return nil -} - -// InitOptions initializes the current field options based on its type. -// -// Returns error on unknown field type. -func (f *SchemaField) InitOptions() error { - if _, ok := f.Options.(FieldOptions); ok { - return nil // already inited - } - - serialized, err := json.Marshal(f.Options) - if err != nil { - return err - } - - var options any - switch f.Type { - case FieldTypeText: - options = &TextOptions{} - case FieldTypeNumber: - options = &NumberOptions{} - case FieldTypeBool: - options = &BoolOptions{} - case FieldTypeEmail: - options = &EmailOptions{} - case FieldTypeUrl: - options = &UrlOptions{} - case FieldTypeEditor: - options = &EditorOptions{} - case FieldTypeDate: - options = &DateOptions{} - case FieldTypeSelect: - options = &SelectOptions{} - case FieldTypeJson: - options = &JsonOptions{} - case FieldTypeFile: - options = &FileOptions{} - case FieldTypeRelation: - options = &RelationOptions{} - - // Deprecated: Will be removed in v0.9+ - case FieldTypeUser: - options = &UserOptions{} - - default: - return errors.New("Missing or unknown field field type.") - } - - if err := json.Unmarshal(serialized, options); err != nil { - return err - } - - f.Options = options - - return nil -} - -// PrepareValue returns normalized and properly formatted field value. -func (f *SchemaField) PrepareValue(value any) any { - // init field options (if not already) - f.InitOptions() - - switch f.Type { - case FieldTypeText, FieldTypeEmail, FieldTypeUrl, FieldTypeEditor: - return cast.ToString(value) - case FieldTypeJson: - val := value - - if str, ok := val.(string); ok { - // in order to support seamlessly both json and multipart/form-data requests, - // the following normalization rules are applied for plain string values: - // - "true" is converted to the json `true` - // - "false" is converted to the json `false` - // - "null" is converted to the json `null` - // - "[1,2,3]" is converted to the json `[1,2,3]` - // - "{\"a\":1,\"b\":2}" is converted to the json `{"a":1,"b":2}` - // - numeric strings are converted to json number - // - double quoted strings are left as they are (aka. without normalizations) - // - any other string (empty string too) is double quoted - if str == "" { - val = strconv.Quote(str) - } else if str == "null" || str == "true" || str == "false" { - val = str - } else if ((str[0] >= '0' && str[0] <= '9') || - str[0] == '-' || - str[0] == '"' || - str[0] == '[' || - str[0] == '{') && - is.JSON.Validate(str) == nil { - val = str - } else { - val = strconv.Quote(str) - } - } - - val, _ = types.ParseJsonRaw(val) - return val - case FieldTypeNumber: - return cast.ToFloat64(value) - case FieldTypeBool: - return cast.ToBool(value) - case FieldTypeDate: - val, _ := types.ParseDateTime(value) - return val - case FieldTypeSelect: - val := list.ToUniqueStringSlice(value) - - options, _ := f.Options.(*SelectOptions) - if !options.IsMultiple() { - if len(val) > 0 { - return val[len(val)-1] // the last selected - } - return "" - } - - return val - case FieldTypeFile: - val := list.ToUniqueStringSlice(value) - - options, _ := f.Options.(*FileOptions) - if !options.IsMultiple() { - if len(val) > 0 { - return val[len(val)-1] // the last selected - } - return "" - } - - return val - case FieldTypeRelation: - ids := list.ToUniqueStringSlice(value) - - options, _ := f.Options.(*RelationOptions) - if !options.IsMultiple() { - if len(ids) > 0 { - return ids[len(ids)-1] // the last selected - } - return "" - } - - return ids - default: - return value // unmodified - } -} - -// PrepareValueWithModifier returns normalized and properly formatted field value -// by "merging" baseValue with the modifierValue based on the specified modifier (+ or -). -func (f *SchemaField) PrepareValueWithModifier(baseValue any, modifier string, modifierValue any) any { - resolvedValue := baseValue - - switch f.Type { - case FieldTypeNumber: - switch modifier { - case FieldValueModifierAdd: - resolvedValue = cast.ToFloat64(baseValue) + cast.ToFloat64(modifierValue) - case FieldValueModifierSubtract: - resolvedValue = cast.ToFloat64(baseValue) - cast.ToFloat64(modifierValue) - } - case FieldTypeSelect, FieldTypeRelation: - switch modifier { - case FieldValueModifierAdd: - resolvedValue = append( - list.ToUniqueStringSlice(baseValue), - list.ToUniqueStringSlice(modifierValue)..., - ) - case FieldValueModifierSubtract: - resolvedValue = list.SubtractSlice( - list.ToUniqueStringSlice(baseValue), - list.ToUniqueStringSlice(modifierValue), - ) - } - case FieldTypeFile: - // note: file for now supports only the subtract modifier - if modifier == FieldValueModifierSubtract { - resolvedValue = list.SubtractSlice( - list.ToUniqueStringSlice(baseValue), - list.ToUniqueStringSlice(modifierValue), - ) - } - } - - return f.PrepareValue(resolvedValue) -} - -// ------------------------------------------------------------------- - -// MultiValuer defines common interface methods that every multi-valued (eg. with MaxSelect) field option struct has. -type MultiValuer interface { - IsMultiple() bool -} - -// FieldOptions defines common interface methods that every field option struct has. -type FieldOptions interface { - Validate() error -} - -type TextOptions struct { - Min *int `form:"min" json:"min"` - Max *int `form:"max" json:"max"` - Pattern string `form:"pattern" json:"pattern"` -} - -func (o TextOptions) Validate() error { - minVal := 0 - if o.Min != nil { - minVal = *o.Min - } - - return validation.ValidateStruct(&o, - validation.Field(&o.Min, validation.Min(0)), - validation.Field(&o.Max, validation.Min(minVal)), - validation.Field(&o.Pattern, validation.By(o.checkRegex)), - ) -} - -func (o *TextOptions) checkRegex(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - if _, err := regexp.Compile(v); err != nil { - return validation.NewError("validation_invalid_regex", err.Error()) - } - - return nil -} - -// ------------------------------------------------------------------- - -type NumberOptions struct { - Min *float64 `form:"min" json:"min"` - Max *float64 `form:"max" json:"max"` - NoDecimal bool `form:"noDecimal" json:"noDecimal"` -} - -func (o NumberOptions) Validate() error { - var maxRules []validation.Rule - if o.Min != nil && o.Max != nil { - maxRules = append(maxRules, validation.Min(*o.Min), validation.By(o.checkNoDecimal)) - } - - return validation.ValidateStruct(&o, - validation.Field(&o.Min, validation.By(o.checkNoDecimal)), - validation.Field(&o.Max, maxRules...), - ) -} - -func (o *NumberOptions) checkNoDecimal(value any) error { - v, _ := value.(*float64) - if v == nil || !o.NoDecimal { - return nil // nothing to check - } - - if *v != float64(int64(*v)) { - return validation.NewError("validation_no_decimal_constraint", "Decimal numbers are not allowed.") - } - - return nil -} - -// ------------------------------------------------------------------- - -type BoolOptions struct { -} - -func (o BoolOptions) Validate() error { - return nil -} - -// ------------------------------------------------------------------- - -type EmailOptions struct { - ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` - OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` -} - -func (o EmailOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field( - &o.ExceptDomains, - validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - validation.Field( - &o.OnlyDomains, - validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - ) -} - -// ------------------------------------------------------------------- - -type UrlOptions struct { - ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` - OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` -} - -func (o UrlOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field( - &o.ExceptDomains, - validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - validation.Field( - &o.OnlyDomains, - validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), - ), - ) -} - -// ------------------------------------------------------------------- - -type EditorOptions struct { - // ConvertUrls is usually used to instruct the editor whether to - // apply url conversion (eg. stripping the domain name in case the - // urls are using the same domain as the one where the editor is loaded). - // - // (see also https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls) - ConvertUrls bool `form:"convertUrls" json:"convertUrls"` -} - -func (o EditorOptions) Validate() error { - return nil -} - -// ------------------------------------------------------------------- - -type DateOptions struct { - Min types.DateTime `form:"min" json:"min"` - Max types.DateTime `form:"max" json:"max"` -} - -func (o DateOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field(&o.Max, validation.By(o.checkRange(o.Min, o.Max))), - ) -} - -func (o *DateOptions) checkRange(min types.DateTime, max types.DateTime) validation.RuleFunc { - return func(value any) error { - v, _ := value.(types.DateTime) - - if v.IsZero() || min.IsZero() || max.IsZero() { - return nil // nothing to check - } - - return validation.Date(types.DefaultDateLayout). - Min(min.Time()). - Max(max.Time()). - Validate(v.String()) - } -} - -// ------------------------------------------------------------------- - -type SelectOptions struct { - MaxSelect int `form:"maxSelect" json:"maxSelect"` - Values []string `form:"values" json:"values"` -} - -func (o SelectOptions) Validate() error { - max := len(o.Values) - if max == 0 { - max = 1 - } - - return validation.ValidateStruct(&o, - validation.Field(&o.Values, validation.Required), - validation.Field( - &o.MaxSelect, - validation.Required, - validation.Min(1), - validation.Max(max), - ), - ) -} - -// IsMultiple implements MultiValuer interface and checks whether the -// current field options support multiple values. -func (o SelectOptions) IsMultiple() bool { - return o.MaxSelect > 1 -} - -// ------------------------------------------------------------------- - -type JsonOptions struct { - MaxSize int `form:"maxSize" json:"maxSize"` -} - -func (o JsonOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field(&o.MaxSize, validation.Required, validation.Min(1)), - ) -} - -// ------------------------------------------------------------------- - -var _ MultiValuer = (*FileOptions)(nil) - -type FileOptions struct { - MimeTypes []string `form:"mimeTypes" json:"mimeTypes"` - Thumbs []string `form:"thumbs" json:"thumbs"` - MaxSelect int `form:"maxSelect" json:"maxSelect"` - MaxSize int `form:"maxSize" json:"maxSize"` - Protected bool `form:"protected" json:"protected"` -} - -func (o FileOptions) Validate() error { - return validation.ValidateStruct(&o, - validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)), - validation.Field(&o.MaxSize, validation.Required, validation.Min(1)), - validation.Field(&o.Thumbs, validation.Each( - validation.NotIn("0x0", "0x0t", "0x0b", "0x0f"), - validation.Match(filesystem.ThumbSizeRegex), - )), - ) -} - -// IsMultiple implements MultiValuer interface and checks whether the -// current field options support multiple values. -func (o FileOptions) IsMultiple() bool { - return o.MaxSelect > 1 -} - -// ------------------------------------------------------------------- - -var _ MultiValuer = (*RelationOptions)(nil) - -type RelationOptions struct { - // CollectionId is the id of the related collection. - CollectionId string `form:"collectionId" json:"collectionId"` - - // CascadeDelete indicates whether the root model should be deleted - // in case of delete of all linked relations. - CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"` - - // MinSelect indicates the min number of allowed relation records - // that could be linked to the main model. - // - // If nil no limits are applied. - MinSelect *int `form:"minSelect" json:"minSelect"` - - // MaxSelect indicates the max number of allowed relation records - // that could be linked to the main model. - // - // If nil no limits are applied. - MaxSelect *int `form:"maxSelect" json:"maxSelect"` - - // Deprecated: This field is no-op and will be removed in future versions. - // Instead use the individula SchemaField.Presentable option for each field in the relation collection. - DisplayFields []string `form:"displayFields" json:"displayFields"` -} - -func (o RelationOptions) Validate() error { - minVal := 0 - if o.MinSelect != nil { - minVal = *o.MinSelect - } - - return validation.ValidateStruct(&o, - validation.Field(&o.CollectionId, validation.Required), - validation.Field(&o.MinSelect, validation.Min(0)), - validation.Field(&o.MaxSelect, validation.NilOrNotEmpty, validation.Min(minVal)), - ) -} - -// IsMultiple implements MultiValuer interface and checks whether the -// current field options support multiple values. -func (o RelationOptions) IsMultiple() bool { - return o.MaxSelect == nil || *o.MaxSelect > 1 -} - -// ------------------------------------------------------------------- - -// Deprecated: Will be removed in v0.9+ -type UserOptions struct { - MaxSelect int `form:"maxSelect" json:"maxSelect"` - CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"` -} - -// Deprecated: Will be removed in v0.9+ -func (o UserOptions) Validate() error { - return nil -} diff --git a/models/schema/schema_field_test.go b/models/schema/schema_field_test.go deleted file mode 100644 index abc7c244..00000000 --- a/models/schema/schema_field_test.go +++ /dev/null @@ -1,2278 +0,0 @@ -package schema_test - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestBaseModelFieldNames(t *testing.T) { - result := schema.BaseModelFieldNames() - expected := 3 - - if len(result) != expected { - t.Fatalf("Expected %d field names, got %d (%v)", expected, len(result), result) - } -} - -func TestSystemFieldNames(t *testing.T) { - result := schema.SystemFieldNames() - expected := 3 - - if len(result) != expected { - t.Fatalf("Expected %d field names, got %d (%v)", expected, len(result), result) - } -} - -func TestAuthFieldNames(t *testing.T) { - result := schema.AuthFieldNames() - expected := 9 - - if len(result) != expected { - t.Fatalf("Expected %d auth field names, got %d (%v)", expected, len(result), result) - } -} - -func TestFieldTypes(t *testing.T) { - result := schema.FieldTypes() - expected := 11 - - if len(result) != expected { - t.Fatalf("Expected %d types, got %d (%v)", expected, len(result), result) - } -} - -func TestArraybleFieldTypes(t *testing.T) { - result := schema.ArraybleFieldTypes() - expected := 3 - - if len(result) != expected { - t.Fatalf("Expected %d arrayble types, got %d (%v)", expected, len(result), result) - } -} - -func TestSchemaFieldColDefinition(t *testing.T) { - scenarios := []struct { - field schema.SchemaField - expected string - }{ - { - schema.SchemaField{Type: schema.FieldTypeText, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"}, - "NUMERIC DEFAULT 0 NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"}, - "BOOLEAN DEFAULT FALSE NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeUrl, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeEditor, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeDate, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeJson, Name: "test"}, - "JSON DEFAULT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test_multiple", Options: &schema.SelectOptions{MaxSelect: 2}}, - "JSON DEFAULT '[]' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeFile, Name: "test"}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeFile, Name: "test_multiple", Options: &schema.FileOptions{MaxSelect: 2}}, - "JSON DEFAULT '[]' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test", Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "TEXT DEFAULT '' NOT NULL", - }, - { - schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test_multiple", Options: &schema.RelationOptions{MaxSelect: nil}}, - "JSON DEFAULT '[]' NOT NULL", - }, - } - - for i, s := range scenarios { - def := s.field.ColDefinition() - if def != s.expected { - t.Errorf("(%d) Expected definition %q, got %q", i, s.expected, def) - } - } -} - -func TestSchemaFieldString(t *testing.T) { - f := schema.SchemaField{ - Id: "abc", - Name: "test", - Type: schema.FieldTypeText, - Required: true, - Presentable: true, - System: true, - Options: &schema.TextOptions{ - Pattern: "test", - }, - } - - result := f.String() - expected := `{"system":true,"id":"abc","name":"test","type":"text","required":true,"presentable":true,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}` - - if result != expected { - t.Errorf("Expected \n%v, got \n%v", expected, result) - } -} - -func TestSchemaFieldMarshalJSON(t *testing.T) { - scenarios := []struct { - field schema.SchemaField - expected string - }{ - // empty - { - schema.SchemaField{}, - `{"system":false,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - // without defined options - { - schema.SchemaField{ - Id: "abc", - Name: "test", - Type: schema.FieldTypeText, - Required: true, - Presentable: true, - System: true, - }, - `{"system":true,"id":"abc","name":"test","type":"text","required":true,"presentable":true,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, - }, - // with defined options - { - schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - Required: true, - Unique: false, - System: true, - Options: &schema.TextOptions{ - Pattern: "test", - }, - }, - `{"system":true,"id":"","name":"test","type":"text","required":true,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, - }, - } - - for i, s := range scenarios { - result, err := s.field.MarshalJSON() - if err != nil { - t.Fatalf("(%d) %v", i, err) - } - - if string(result) != s.expected { - t.Errorf("(%d), Expected \n%v, got \n%v", i, s.expected, string(result)) - } - } -} - -func TestSchemaFieldUnmarshalJSON(t *testing.T) { - scenarios := []struct { - data []byte - expectError bool - expectJson string - }{ - { - nil, - true, - `{"system":false,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - []byte{}, - true, - `{"system":false,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - []byte(`{"system": true}`), - true, - `{"system":true,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - []byte(`{"invalid"`), - true, - `{"system":false,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - []byte(`{"type":"text","system":true}`), - false, - `{"system":true,"id":"","name":"","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, - }, - { - []byte(`{"type":"text","options":{"pattern":"test"}}`), - false, - `{"system":false,"id":"","name":"","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, - }, - } - - for i, s := range scenarios { - f := schema.SchemaField{} - err := f.UnmarshalJSON(s.data) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - } - - if f.String() != s.expectJson { - t.Errorf("(%d), Expected json \n%v, got \n%v", i, s.expectJson, f.String()) - } - } -} - -func TestSchemaFieldValidate(t *testing.T) { - scenarios := []struct { - name string - field schema.SchemaField - expectedErrors []string - }{ - { - "empty field", - schema.SchemaField{}, - []string{"id", "options", "name", "type"}, - }, - { - "missing id", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "", - Name: "test", - }, - []string{"id"}, - }, - { - "invalid id length check", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234", - Name: "test", - }, - []string{"id"}, - }, - { - "valid id length check", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "12345", - Name: "test", - }, - []string{}, - }, - { - "invalid name format", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "test!@#", - }, - []string{"name"}, - }, - { - "name with _via_", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "a_via_b", - }, - []string{"name"}, - }, - { - "reserved name (null)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "null", - }, - []string{"name"}, - }, - { - "reserved name (true)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "null", - }, - []string{"name"}, - }, - { - "reserved name (false)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "false", - }, - []string{"name"}, - }, - { - "reserved name (_rowid_)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "_rowid_", - }, - []string{"name"}, - }, - { - "reserved name (id)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameId, - }, - []string{"name"}, - }, - { - "reserved name (created)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameCreated, - }, - []string{"name"}, - }, - { - "reserved name (updated)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameUpdated, - }, - []string{"name"}, - }, - { - "reserved name (collectionId)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameCollectionId, - }, - []string{"name"}, - }, - { - "reserved name (collectionName)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameCollectionName, - }, - []string{"name"}, - }, - { - "reserved name (expand)", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: schema.FieldNameExpand, - }, - []string{"name"}, - }, - { - "valid name", - schema.SchemaField{ - Type: schema.FieldTypeText, - Id: "1234567890", - Name: "test", - }, - []string{}, - }, - { - "unique check for type file", - schema.SchemaField{ - Type: schema.FieldTypeFile, - Id: "1234567890", - Name: "test", - Unique: true, - Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}, - }, - []string{"unique"}, - }, - { - "trigger options validator (auto init)", - schema.SchemaField{ - Type: schema.FieldTypeFile, - Id: "1234567890", - Name: "test", - }, - []string{"options"}, - }, - { - "trigger options validator (invalid option field value)", - schema.SchemaField{ - Type: schema.FieldTypeFile, - Id: "1234567890", - Name: "test", - Options: &schema.FileOptions{MaxSelect: 0, MaxSize: 0}, - }, - []string{"options"}, - }, - { - "trigger options validator (valid option field value)", - schema.SchemaField{ - Type: schema.FieldTypeFile, - Id: "1234567890", - Name: "test", - Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}, - }, - []string{}, - }, - } - - for _, s := range scenarios { - result := s.field.Validate() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("[%s] Failed to parse errors %v", s.name, result) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs) - } - } - } -} - -func TestSchemaFieldInitOptions(t *testing.T) { - scenarios := []struct { - field schema.SchemaField - expectError bool - expectJson string - }{ - { - schema.SchemaField{}, - true, - `{"system":false,"id":"","name":"","type":"","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - schema.SchemaField{Type: "unknown"}, - true, - `{"system":false,"id":"","name":"","type":"unknown","required":false,"presentable":false,"unique":false,"options":null}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeText}, - false, - `{"system":false,"id":"","name":"","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeNumber}, - false, - `{"system":false,"id":"","name":"","type":"number","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"noDecimal":false}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeBool}, - false, - `{"system":false,"id":"","name":"","type":"bool","required":false,"presentable":false,"unique":false,"options":{}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeEmail}, - false, - `{"system":false,"id":"","name":"","type":"email","required":false,"presentable":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeUrl}, - false, - `{"system":false,"id":"","name":"","type":"url","required":false,"presentable":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeEditor}, - false, - `{"system":false,"id":"","name":"","type":"editor","required":false,"presentable":false,"unique":false,"options":{"convertUrls":false}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeDate}, - false, - `{"system":false,"id":"","name":"","type":"date","required":false,"presentable":false,"unique":false,"options":{"min":"","max":""}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeSelect}, - false, - `{"system":false,"id":"","name":"","type":"select","required":false,"presentable":false,"unique":false,"options":{"maxSelect":0,"values":null}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeJson}, - false, - `{"system":false,"id":"","name":"","type":"json","required":false,"presentable":false,"unique":false,"options":{"maxSize":0}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeFile}, - false, - `{"system":false,"id":"","name":"","type":"file","required":false,"presentable":false,"unique":false,"options":{"mimeTypes":null,"thumbs":null,"maxSelect":0,"maxSize":0,"protected":false}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeRelation}, - false, - `{"system":false,"id":"","name":"","type":"relation","required":false,"presentable":false,"unique":false,"options":{"collectionId":"","cascadeDelete":false,"minSelect":null,"maxSelect":null,"displayFields":null}}`, - }, - { - schema.SchemaField{Type: schema.FieldTypeUser}, - false, - `{"system":false,"id":"","name":"","type":"user","required":false,"presentable":false,"unique":false,"options":{"maxSelect":0,"cascadeDelete":false}}`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeText, - Options: &schema.TextOptions{Pattern: "test"}, - }, - false, - `{"system":false,"id":"","name":"","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, - }, - } - - for i, s := range scenarios { - t.Run(fmt.Sprintf("s%d_%s", i, s.field.Type), func(t *testing.T) { - err := s.field.InitOptions() - - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("Expected %v, got %v (%v)", s.expectError, hasErr, err) - } - - if s.field.String() != s.expectJson { - t.Fatalf(" Expected\n%v\ngot\n%v", s.expectJson, s.field.String()) - } - }) - } -} - -func TestSchemaFieldPrepareValue(t *testing.T) { - scenarios := []struct { - field schema.SchemaField - value any - expectJson string - }{ - {schema.SchemaField{Type: "unknown"}, "test", `"test"`}, - {schema.SchemaField{Type: "unknown"}, 123, "123"}, - {schema.SchemaField{Type: "unknown"}, []int{1, 2, 1}, "[1,2,1]"}, - - // text - {schema.SchemaField{Type: schema.FieldTypeText}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeText}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeText}, []int{1, 2}, `""`}, - {schema.SchemaField{Type: schema.FieldTypeText}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeText}, 123, `"123"`}, - - // email - {schema.SchemaField{Type: schema.FieldTypeEmail}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeEmail}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeEmail}, []int{1, 2}, `""`}, - {schema.SchemaField{Type: schema.FieldTypeEmail}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeEmail}, 123, `"123"`}, - - // url - {schema.SchemaField{Type: schema.FieldTypeUrl}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeUrl}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeUrl}, []int{1, 2}, `""`}, - {schema.SchemaField{Type: schema.FieldTypeUrl}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeUrl}, 123, `"123"`}, - - // editor - {schema.SchemaField{Type: schema.FieldTypeEditor}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeEditor}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeEditor}, []int{1, 2}, `""`}, - {schema.SchemaField{Type: schema.FieldTypeEditor}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeEditor}, 123, `"123"`}, - - // json - {schema.SchemaField{Type: schema.FieldTypeJson}, nil, "null"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "null", "null"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, 123, "123"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, -123, "-123"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "123", "123"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "-123", "-123"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, 123.456, "123.456"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, -123.456, "-123.456"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "123.456", "123.456"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "-123.456", "-123.456"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "123.456 abc", `"123.456 abc"`}, // invalid numeric string - {schema.SchemaField{Type: schema.FieldTypeJson}, "-a123", `"-a123"`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, true, "true"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "true", "true"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, false, "false"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "false", "false"}, - {schema.SchemaField{Type: schema.FieldTypeJson}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `test`, `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `"test"`, `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `{test":1}`, `"{test\":1}"`}, // invalid object string - {schema.SchemaField{Type: schema.FieldTypeJson}, `[1 2 3]`, `"[1 2 3]"`}, // invalid array string - {schema.SchemaField{Type: schema.FieldTypeJson}, map[string]int{}, `{}`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `{}`, `{}`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, map[string]int{"test": 123}, `{"test":123}`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `{"test":123}`, `{"test":123}`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, []int{}, `[]`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `[]`, `[]`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, []int{1, 2, 1}, `[1,2,1]`}, - {schema.SchemaField{Type: schema.FieldTypeJson}, `[1,2,1]`, `[1,2,1]`}, - - // number - {schema.SchemaField{Type: schema.FieldTypeNumber}, nil, "0"}, - {schema.SchemaField{Type: schema.FieldTypeNumber}, "", "0"}, - {schema.SchemaField{Type: schema.FieldTypeNumber}, "test", "0"}, - {schema.SchemaField{Type: schema.FieldTypeNumber}, 1, "1"}, - {schema.SchemaField{Type: schema.FieldTypeNumber}, 1.5, "1.5"}, - {schema.SchemaField{Type: schema.FieldTypeNumber}, "1.5", "1.5"}, - - // bool - {schema.SchemaField{Type: schema.FieldTypeBool}, nil, "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, 1, "true"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, 0, "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, "", "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, "test", "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, "false", "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, "true", "true"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, false, "false"}, - {schema.SchemaField{Type: schema.FieldTypeBool}, true, "true"}, - - // date - {schema.SchemaField{Type: schema.FieldTypeDate}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, "test", `""`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, 1641024040, `"2022-01-01 08:00:40.000Z"`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123Z"`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123Z", `"2022-01-01 11:27:10.123Z"`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, types.DateTime{}, `""`}, - {schema.SchemaField{Type: schema.FieldTypeDate}, time.Time{}, `""`}, - - // select (single) - {schema.SchemaField{Type: schema.FieldTypeSelect}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`}, - {schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test2"`}, - { - // no values validation/filtering - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{ - Values: []string{"test1", "test2"}, - }, - }, - "test", - `"test"`, - }, - // select (multiple) - { - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - nil, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - "", - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - []string{}, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - 123, - `["123"]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - "test", - `["test"]`, - }, - { - // no values validation - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - []string{"test1", "test2", "test3"}, - `["test1","test2","test3"]`, - }, - { - // duplicated values - schema.SchemaField{ - Type: schema.FieldTypeSelect, - Options: &schema.SelectOptions{MaxSelect: 2}, - }, - []string{"test1", "test2", "test1"}, - `["test1","test2"]`, - }, - - // file (single) - {schema.SchemaField{Type: schema.FieldTypeFile}, nil, `""`}, - {schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`}, - {schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`}, - {schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`}, - {schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test2"`}, - // file (multiple) - { - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - nil, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - "", - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - []string{}, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - 123, - `["123"]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - "test", - `["test"]`, - }, - { - // no values validation - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - []string{"test1", "test2", "test3"}, - `["test1","test2","test3"]`, - }, - { - // duplicated values - schema.SchemaField{ - Type: schema.FieldTypeFile, - Options: &schema.FileOptions{MaxSelect: 2}, - }, - []string{"test1", "test2", "test1"}, - `["test1","test2"]`, - }, - - // relation (single) - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - nil, - `""`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - "", - `""`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - 123, - `"123"`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - "abc", - `"abc"`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}, - }, - "1ba88b4f-e9da-42f0-9764-9a55c953e724", - `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`, - }, - { - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, - `"2ba88b4f-e9da-42f0-9764-9a55c953e724"`, - }, - // relation (multiple) - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - nil, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - "", - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - []string{}, - `[]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - 123, - `["123"]`, - }, - { - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - []string{"", "abc"}, - `["abc"]`, - }, - { - // no values validation - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, - `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, - }, - { - // duplicated values - schema.SchemaField{ - Type: schema.FieldTypeRelation, - Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)}, - }, - []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"}, - `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, - }, - } - - for i, s := range scenarios { - result := s.field.PrepareValue(s.value) - - encoded, err := json.Marshal(result) - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - - if string(encoded) != s.expectJson { - t.Errorf("(%d), Expected %v, got %v", i, s.expectJson, string(encoded)) - } - } -} - -func TestSchemaFieldPrepareValueWithModifier(t *testing.T) { - scenarios := []struct { - name string - field schema.SchemaField - baseValue any - modifier string - modifierValue any - expectJson string - }{ - // text - { - "text with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeText}, - "base", - "+", - "new", - `"base"`, - }, - { - "text with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeText}, - "base", - "-", - "new", - `"base"`, - }, - { - "text with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeText}, - "base", - "?", - "new", - `"base"`, - }, - { - "text cast check", - schema.SchemaField{Type: schema.FieldTypeText}, - 123, - "?", - "new", - `"123"`, - }, - - // number - { - "number with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeNumber}, - 1, - "+", - 4, - `5`, - }, - { - "number with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeNumber}, - 1, - "-", - 4, - `-3`, - }, - { - "number with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeNumber}, - "1", - "?", - 4, - `1`, - }, - { - "number cast check", - schema.SchemaField{Type: schema.FieldTypeNumber}, - "test", - "+", - "4", - `4`, - }, - - // bool - { - "bool with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeBool}, - true, - "+", - false, - `true`, - }, - { - "bool with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeBool}, - true, - "-", - false, - `true`, - }, - { - "bool with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeBool}, - true, - "?", - false, - `true`, - }, - { - "bool cast check", - schema.SchemaField{Type: schema.FieldTypeBool}, - "true", - "?", - false, - `true`, - }, - - // email - { - "email with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeEmail}, - "base", - "+", - "new", - `"base"`, - }, - { - "email with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeEmail}, - "base", - "-", - "new", - `"base"`, - }, - { - "email with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeEmail}, - "base", - "?", - "new", - `"base"`, - }, - { - "email cast check", - schema.SchemaField{Type: schema.FieldTypeEmail}, - 123, - "?", - "new", - `"123"`, - }, - - // url - { - "url with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeUrl}, - "base", - "+", - "new", - `"base"`, - }, - { - "url with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeUrl}, - "base", - "-", - "new", - `"base"`, - }, - { - "url with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeUrl}, - "base", - "?", - "new", - `"base"`, - }, - { - "url cast check", - schema.SchemaField{Type: schema.FieldTypeUrl}, - 123, - "-", - "new", - `"123"`, - }, - - // editor - { - "editor with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeEditor}, - "base", - "+", - "new", - `"base"`, - }, - { - "editor with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeEditor}, - "base", - "-", - "new", - `"base"`, - }, - { - "editor with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeEditor}, - "base", - "?", - "new", - `"base"`, - }, - { - "editor cast check", - schema.SchemaField{Type: schema.FieldTypeEditor}, - 123, - "-", - "new", - `"123"`, - }, - - // date - { - "date with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeDate}, - "2023-01-01 00:00:00.123", - "+", - "2023-02-01 00:00:00.456", - `"2023-01-01 00:00:00.123Z"`, - }, - { - "date with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeDate}, - "2023-01-01 00:00:00.123Z", - "-", - "2023-02-01 00:00:00.456Z", - `"2023-01-01 00:00:00.123Z"`, - }, - { - "date with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeDate}, - "2023-01-01 00:00:00.123", - "?", - "2023-01-01 00:00:00.456", - `"2023-01-01 00:00:00.123Z"`, - }, - { - "date cast check", - schema.SchemaField{Type: schema.FieldTypeDate}, - 1672524000, // 2022-12-31 22:00:00.000Z - "+", - 100, - `"2022-12-31 22:00:00.000Z"`, - }, - - // json - { - "json with '+' modifier", - schema.SchemaField{Type: schema.FieldTypeJson}, - 10, - "+", - 5, - `10`, - }, - { - "json with '+' modifier (slice)", - schema.SchemaField{Type: schema.FieldTypeJson}, - []string{"a", "b"}, - "+", - "c", - `["a","b"]`, - }, - { - "json with '-' modifier", - schema.SchemaField{Type: schema.FieldTypeJson}, - 10, - "-", - 5, - `10`, - }, - { - "json with '-' modifier (slice)", - schema.SchemaField{Type: schema.FieldTypeJson}, - `["a","b"]`, - "-", - "c", - `["a","b"]`, - }, - { - "json with unknown modifier", - schema.SchemaField{Type: schema.FieldTypeJson}, - `"base"`, - "?", - `"new"`, - `"base"`, - }, - - // single select - { - "single select with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "", - "+", - "b", - `"b"`, - }, - { - "single select with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "a", - "+", - "b", - `"b"`, - }, - { - "single select with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "", - "-", - "a", - `""`, - }, - { - "single select with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "a", - "-", - "", - `"a"`, - }, - { - "single select with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "a", - "-", - "b", - `"a"`, - }, - { - "single select with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "a", - "-", - "a", - `""`, - }, - { - "single select with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "a", - "-", - []string{"b", "a", "c", "123"}, - `""`, - }, - { - "single select with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}}, - "", - "?", - "a", - `""`, - }, - - // multi select - { - "multi select with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - nil, - "+", - "b", - `["b"]`, - }, - { - "multi select with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - []string{"a"}, - "+", - []string{"b", "c"}, - `["a","b","c"]`, - }, - { - "multi select with '+' modifier (nonempty base; already existing value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - []string{"a", "b"}, - "+", - "b", - `["a","b"]`, - }, - { - "multi select with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - nil, - "-", - []string{"a"}, - `[]`, - }, - { - "multi select with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - "a", - "-", - "", - `["a"]`, - }, - { - "multi select with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - "a", - "-", - "b", - `["a"]`, - }, - { - "multi select with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - []string{"a", "b", "c", "d"}, - "-", - "c", - `["a","b","d"]`, - }, - { - "multi select with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - []string{"a", "b", "c", "d"}, - "-", - []string{"b", "a", "123"}, - `["c","d"]`, - }, - { - "multi select with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}}, - []string{"a", "b"}, - "?", - "a", - `["a","b"]`, - }, - - // single relation - { - "single relation with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "", - "+", - "b", - `"b"`, - }, - { - "single relation with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "a", - "+", - "b", - `"b"`, - }, - { - "single relation with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "", - "-", - "a", - `""`, - }, - { - "single relation with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "a", - "-", - "", - `"a"`, - }, - { - "single relation with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "a", - "-", - "b", - `"a"`, - }, - { - "single relation with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "a", - "-", - "a", - `""`, - }, - { - "single relation with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "a", - "-", - []string{"b", "a", "c", "123"}, - `""`, - }, - { - "single relation with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, - "", - "?", - "a", - `""`, - }, - - // multi relation - { - "multi relation with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - nil, - "+", - "b", - `["b"]`, - }, - { - "multi relation with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - []string{"a"}, - "+", - []string{"b", "c"}, - `["a","b","c"]`, - }, - { - "multi relation with '+' modifier (nonempty base; already existing value)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - []string{"a", "b"}, - "+", - "b", - `["a","b"]`, - }, - { - "multi relation with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - nil, - "-", - []string{"a"}, - `[]`, - }, - { - "multi relation with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - "a", - "-", - "", - `["a"]`, - }, - { - "multi relation with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - "a", - "-", - "b", - `["a"]`, - }, - { - "multi relation with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - []string{"a", "b", "c", "d"}, - "-", - "c", - `["a","b","d"]`, - }, - { - "multi relation with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - []string{"a", "b", "c", "d"}, - "-", - []string{"b", "a", "123"}, - `["c","d"]`, - }, - { - "multi relation with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeRelation}, - []string{"a", "b"}, - "?", - "a", - `["a","b"]`, - }, - - // single file - { - "single file with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "", - "+", - "b", - `""`, - }, - { - "single file with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "a", - "+", - "b", - `"a"`, - }, - { - "single file with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "", - "-", - "a", - `""`, - }, - { - "single file with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "a", - "-", - "", - `"a"`, - }, - { - "single file with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "a", - "-", - "b", - `"a"`, - }, - { - "single file with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "a", - "-", - "a", - `""`, - }, - { - "single file with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "a", - "-", - []string{"b", "a", "c", "123"}, - `""`, - }, - { - "single file with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}}, - "", - "?", - "a", - `""`, - }, - - // multi file - { - "multi file with '+' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - nil, - "+", - "b", - `[]`, - }, - { - "multi file with '+' modifier (nonempty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - []string{"a"}, - "+", - []string{"b", "c"}, - `["a"]`, - }, - { - "multi file with '+' modifier (nonempty base; already existing value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - []string{"a", "b"}, - "+", - "b", - `["a","b"]`, - }, - { - "multi file with '-' modifier (empty base)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - nil, - "-", - []string{"a"}, - `[]`, - }, - { - "multi file with '-' modifier (nonempty base and empty modifier value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - "a", - "-", - "", - `["a"]`, - }, - { - "multi file with '-' modifier (nonempty base and different value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - "a", - "-", - "b", - `["a"]`, - }, - { - "multi file with '-' modifier (nonempty base and matching value)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - []string{"a", "b", "c", "d"}, - "-", - "c", - `["a","b","d"]`, - }, - { - "multi file with '-' modifier (nonempty base and matching value in a slice)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - []string{"a", "b", "c", "d"}, - "-", - []string{"b", "a", "123"}, - `["c","d"]`, - }, - { - "multi file with unknown modifier (nonempty)", - schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}}, - []string{"a", "b"}, - "?", - "a", - `["a","b"]`, - }, - } - - for _, s := range scenarios { - result := s.field.PrepareValueWithModifier(s.baseValue, s.modifier, s.modifierValue) - - encoded, err := json.Marshal(result) - if err != nil { - t.Fatalf("[%s] %v", s.name, err) - } - - if string(encoded) != s.expectJson { - t.Fatalf("[%s], Expected %v, got %v", s.name, s.expectJson, string(encoded)) - } - } -} - -// ------------------------------------------------------------------- - -type fieldOptionsScenario struct { - name string - options schema.FieldOptions - expectedErrors []string -} - -func checkFieldOptionsScenarios(t *testing.T, scenarios []fieldOptionsScenario) { - for i, s := range scenarios { - result := s.options.Validate() - - prefix := fmt.Sprintf("%d", i) - if s.name != "" { - prefix = s.name - } - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("[%s] Failed to parse errors %v", prefix, result) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", prefix, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", prefix, k, errs) - } - } - } -} - -func TestTextOptionsValidate(t *testing.T) { - minus := -1 - number0 := 0 - number1 := 10 - number2 := 20 - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.TextOptions{}, - []string{}, - }, - { - "min - failure", - schema.TextOptions{ - Min: &minus, - }, - []string{"min"}, - }, - { - "min - success", - schema.TextOptions{ - Min: &number0, - }, - []string{}, - }, - { - "max - failure without min", - schema.TextOptions{ - Max: &minus, - }, - []string{"max"}, - }, - { - "max - failure with min", - schema.TextOptions{ - Min: &number2, - Max: &number1, - }, - []string{"max"}, - }, - { - "max - success", - schema.TextOptions{ - Min: &number1, - Max: &number2, - }, - []string{}, - }, - { - "pattern - failure", - schema.TextOptions{Pattern: "(test"}, - []string{"pattern"}, - }, - { - "pattern - success", - schema.TextOptions{Pattern: `^\#?\w+$`}, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestNumberOptionsValidate(t *testing.T) { - int1 := 10.0 - int2 := 20.0 - - decimal1 := 10.5 - decimal2 := 20.5 - - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.NumberOptions{}, - []string{}, - }, - { - "max - without min", - schema.NumberOptions{ - Max: &int1, - }, - []string{}, - }, - { - "max - failure with min", - schema.NumberOptions{ - Min: &int2, - Max: &int1, - }, - []string{"max"}, - }, - { - "max - success with min", - schema.NumberOptions{ - Min: &int1, - Max: &int2, - }, - []string{}, - }, - { - "NoDecimal range failure", - schema.NumberOptions{ - Min: &decimal1, - Max: &decimal2, - NoDecimal: true, - }, - []string{"min", "max"}, - }, - { - "NoDecimal range success", - schema.NumberOptions{ - Min: &int1, - Max: &int2, - NoDecimal: true, - }, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestBoolOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.BoolOptions{}, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestEmailOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.EmailOptions{}, - []string{}, - }, - { - "ExceptDomains failure", - schema.EmailOptions{ - ExceptDomains: []string{"invalid"}, - }, - []string{"exceptDomains"}, - }, - { - "ExceptDomains success", - schema.EmailOptions{ - ExceptDomains: []string{"example.com", "sub.example.com"}, - }, - []string{}, - }, - { - "OnlyDomains check", - schema.EmailOptions{ - OnlyDomains: []string{"invalid"}, - }, - []string{"onlyDomains"}, - }, - { - "OnlyDomains success", - schema.EmailOptions{ - OnlyDomains: []string{"example.com", "sub.example.com"}, - }, - []string{}, - }, - { - "OnlyDomains + ExceptDomains at the same time", - schema.EmailOptions{ - ExceptDomains: []string{"test1.com"}, - OnlyDomains: []string{"test2.com"}, - }, - []string{"exceptDomains", "onlyDomains"}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestUrlOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.UrlOptions{}, - []string{}, - }, - { - "ExceptDomains failure", - schema.UrlOptions{ - ExceptDomains: []string{"invalid"}, - }, - []string{"exceptDomains"}, - }, - { - "ExceptDomains success", - schema.UrlOptions{ - ExceptDomains: []string{"example.com", "sub.example.com"}, - }, - []string{}, - }, - { - "OnlyDomains check", - schema.UrlOptions{ - OnlyDomains: []string{"invalid"}, - }, - []string{"onlyDomains"}, - }, - { - "OnlyDomains success", - schema.UrlOptions{ - OnlyDomains: []string{"example.com", "sub.example.com"}, - }, - []string{}, - }, - { - "OnlyDomains + ExceptDomains at the same time", - schema.UrlOptions{ - ExceptDomains: []string{"test1.com"}, - OnlyDomains: []string{"test2.com"}, - }, - []string{"exceptDomains", "onlyDomains"}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestEditorOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.EditorOptions{}, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestDateOptionsValidate(t *testing.T) { - date1 := types.NowDateTime() - date2, _ := types.ParseDateTime(date1.Time().AddDate(1, 0, 0)) - - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.DateOptions{}, - []string{}, - }, - { - "min only", - schema.DateOptions{ - Min: date1, - }, - []string{}, - }, - { - "max only", - schema.DateOptions{ - Min: date1, - }, - []string{}, - }, - { - "zero min + max", - schema.DateOptions{ - Min: types.DateTime{}, - Max: date1, - }, - []string{}, - }, - { - "min + zero max", - schema.DateOptions{ - Min: date1, - Max: types.DateTime{}, - }, - []string{}, - }, - { - "min > max", - schema.DateOptions{ - Min: date2, - Max: date1, - }, - []string{"max"}, - }, - { - "min == max", - schema.DateOptions{ - Min: date1, - Max: date1, - }, - []string{"max"}, - }, - { - "min < max", - schema.DateOptions{ - Min: date1, - Max: date2, - }, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestSelectOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.SelectOptions{}, - []string{"values", "maxSelect"}, - }, - { - "MaxSelect <= 0", - schema.SelectOptions{ - Values: []string{"test1", "test2"}, - MaxSelect: 0, - }, - []string{"maxSelect"}, - }, - { - "MaxSelect > Values", - schema.SelectOptions{ - Values: []string{"test1", "test2"}, - MaxSelect: 3, - }, - []string{"maxSelect"}, - }, - { - "MaxSelect <= Values", - schema.SelectOptions{ - Values: []string{"test1", "test2"}, - MaxSelect: 2, - }, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestSelectOptionsIsMultiple(t *testing.T) { - scenarios := []struct { - maxSelect int - expect bool - }{ - {-1, false}, - {0, false}, - {1, false}, - {2, true}, - } - - for i, s := range scenarios { - opt := schema.SelectOptions{ - MaxSelect: s.maxSelect, - } - - if v := opt.IsMultiple(); v != s.expect { - t.Errorf("[%d] Expected %v, got %v", i, s.expect, v) - } - } -} - -func TestJsonOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.JsonOptions{}, - []string{"maxSize"}, - }, - { - "MaxSize < 0", - schema.JsonOptions{MaxSize: -1}, - []string{"maxSize"}, - }, - { - "MaxSize > 0", - schema.JsonOptions{MaxSize: 1}, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestFileOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.FileOptions{}, - []string{"maxSelect", "maxSize"}, - }, - { - "MaxSelect <= 0 && maxSize <= 0", - schema.FileOptions{ - MaxSize: 0, - MaxSelect: 0, - }, - []string{"maxSelect", "maxSize"}, - }, - { - "MaxSelect > 0 && maxSize > 0", - schema.FileOptions{ - MaxSize: 2, - MaxSelect: 1, - }, - []string{}, - }, - { - "invalid thumbs format", - schema.FileOptions{ - MaxSize: 1, - MaxSelect: 2, - Thumbs: []string{"100", "200x100"}, - }, - []string{"thumbs"}, - }, - { - "invalid thumbs format - zero width and height", - schema.FileOptions{ - MaxSize: 1, - MaxSelect: 2, - Thumbs: []string{"0x0", "0x0t", "0x0b", "0x0f"}, - }, - []string{"thumbs"}, - }, - { - "valid thumbs format", - schema.FileOptions{ - MaxSize: 1, - MaxSelect: 2, - Thumbs: []string{ - "100x100", "200x100", "0x100", "100x0", - "10x10t", "10x10b", "10x10f", - }, - }, - []string{}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestFileOptionsIsMultiple(t *testing.T) { - scenarios := []struct { - maxSelect int - expect bool - }{ - {-1, false}, - {0, false}, - {1, false}, - {2, true}, - } - - for i, s := range scenarios { - opt := schema.FileOptions{ - MaxSelect: s.maxSelect, - } - - if v := opt.IsMultiple(); v != s.expect { - t.Errorf("[%d] Expected %v, got %v", i, s.expect, v) - } - } -} - -func TestRelationOptionsValidate(t *testing.T) { - scenarios := []fieldOptionsScenario{ - { - "empty", - schema.RelationOptions{}, - []string{"collectionId"}, - }, - { - "empty CollectionId", - schema.RelationOptions{ - CollectionId: "", - MaxSelect: types.Pointer(1), - }, - []string{"collectionId"}, - }, - { - "MinSelect < 0", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: types.Pointer(-1), - }, - []string{"minSelect"}, - }, - { - "MinSelect >= 0", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: types.Pointer(0), - }, - []string{}, - }, - { - "MaxSelect <= 0", - schema.RelationOptions{ - CollectionId: "abc", - MaxSelect: types.Pointer(0), - }, - []string{"maxSelect"}, - }, - { - "MaxSelect > 0 && nonempty CollectionId", - schema.RelationOptions{ - CollectionId: "abc", - MaxSelect: types.Pointer(1), - }, - []string{}, - }, - { - "MinSelect < MaxSelect", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: nil, - MaxSelect: types.Pointer(1), - }, - []string{}, - }, - { - "MinSelect = MaxSelect (non-zero)", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: types.Pointer(1), - MaxSelect: types.Pointer(1), - }, - []string{}, - }, - { - "MinSelect = MaxSelect (both zero)", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: types.Pointer(0), - MaxSelect: types.Pointer(0), - }, - []string{"maxSelect"}, - }, - { - "MinSelect > MaxSelect", - schema.RelationOptions{ - CollectionId: "abc", - MinSelect: types.Pointer(2), - MaxSelect: types.Pointer(1), - }, - []string{"maxSelect"}, - }, - } - - checkFieldOptionsScenarios(t, scenarios) -} - -func TestRelationOptionsIsMultiple(t *testing.T) { - scenarios := []struct { - maxSelect *int - expect bool - }{ - {nil, true}, - {types.Pointer(-1), false}, - {types.Pointer(0), false}, - {types.Pointer(1), false}, - {types.Pointer(2), true}, - } - - for i, s := range scenarios { - opt := schema.RelationOptions{ - MaxSelect: s.maxSelect, - } - - if v := opt.IsMultiple(); v != s.expect { - t.Errorf("[%d] Expected %v, got %v", i, s.expect, v) - } - } -} diff --git a/models/schema/schema_test.go b/models/schema/schema_test.go deleted file mode 100644 index 7ff4afac..00000000 --- a/models/schema/schema_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package schema_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/models/schema" -) - -func TestNewSchemaAndFields(t *testing.T) { - testSchema := schema.NewSchema( - &schema.SchemaField{Id: "id1", Name: "test1"}, - &schema.SchemaField{Name: "test2"}, - &schema.SchemaField{Id: "id1", Name: "test1_new"}, // should replace the original id1 field - ) - - fields := testSchema.Fields() - - if len(fields) != 2 { - t.Fatalf("Expected 2 fields, got %d (%v)", len(fields), fields) - } - - for _, f := range fields { - if f.Id == "" { - t.Fatalf("Expected field id to be set, found empty id for field %v", f) - } - } - - if fields[0].Name != "test1_new" { - t.Fatalf("Expected field with name test1_new, got %s", fields[0].Name) - } - - if fields[1].Name != "test2" { - t.Fatalf("Expected field with name test2, got %s", fields[1].Name) - } -} - -func TestSchemaInitFieldsOptions(t *testing.T) { - f0 := &schema.SchemaField{Name: "test1", Type: "unknown"} - schema0 := schema.NewSchema(f0) - - err0 := schema0.InitFieldsOptions() - if err0 == nil { - t.Fatalf("Expected unknown field schema to fail, got nil") - } - - // --- - - f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} - schema1 := schema.NewSchema(f1, f2) - - err1 := schema1.InitFieldsOptions() - if err1 != nil { - t.Fatal(err1) - } - - if _, ok := f1.Options.(*schema.TextOptions); !ok { - t.Fatalf("Failed to init f1 options") - } - - if _, ok := f2.Options.(*schema.EmailOptions); !ok { - t.Fatalf("Failed to init f2 options") - } -} - -func TestSchemaClone(t *testing.T) { - f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} - s1 := schema.NewSchema(f1, f2) - - s2, err := s1.Clone() - if err != nil { - t.Fatal(err) - } - - s1Encoded, _ := s1.MarshalJSON() - s2Encoded, _ := s2.MarshalJSON() - - if string(s1Encoded) != string(s2Encoded) { - t.Fatalf("Expected the cloned schema to be equal, got %v VS\n %v", s1, s2) - } - - // change in one schema shouldn't result to change in the other - // (aka. check if it is a deep clone) - s1.Fields()[0].Name = "test1_update" - if s2.Fields()[0].Name != "test1" { - t.Fatalf("Expected s2 field name to not change, got %q", s2.Fields()[0].Name) - } -} - -func TestSchemaAsMap(t *testing.T) { - f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} - testSchema := schema.NewSchema(f1, f2) - - result := testSchema.AsMap() - - if len(result) != 2 { - t.Fatalf("Expected 2 map elements, got %d (%v)", len(result), result) - } - - expectedIndexes := []string{f1.Name, f2.Name} - - for _, index := range expectedIndexes { - if _, ok := result[index]; !ok { - t.Fatalf("Missing index %q", index) - } - } -} - -func TestSchemaGetFieldByName(t *testing.T) { - f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeText} - testSchema := schema.NewSchema(f1, f2) - - // missing field - result1 := testSchema.GetFieldByName("missing") - if result1 != nil { - t.Fatalf("Found unexpected field %v", result1) - } - - // existing field - result2 := testSchema.GetFieldByName("test1") - if result2 == nil || result2.Name != "test1" { - t.Fatalf("Cannot find field with Name 'test1', got %v ", result2) - } -} - -func TestSchemaGetFieldById(t *testing.T) { - f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText} - testSchema := schema.NewSchema(f1, f2) - - // missing field id - result1 := testSchema.GetFieldById("test1") - if result1 != nil { - t.Fatalf("Found unexpected field %v", result1) - } - - // existing field id - result2 := testSchema.GetFieldById("id2") - if result2 == nil || result2.Id != "id2" { - t.Fatalf("Cannot find field with id 'id2', got %v ", result2) - } -} - -func TestSchemaRemoveField(t *testing.T) { - f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText} - f3 := &schema.SchemaField{Id: "id3", Name: "test3", Type: schema.FieldTypeText} - testSchema := schema.NewSchema(f1, f2, f3) - - testSchema.RemoveField("id2") - testSchema.RemoveField("test3") // should do nothing - - expected := []string{"test1", "test3"} - - if len(testSchema.Fields()) != len(expected) { - t.Fatalf("Expected %d, got %d (%v)", len(expected), len(testSchema.Fields()), testSchema) - } - - for _, name := range expected { - if f := testSchema.GetFieldByName(name); f == nil { - t.Fatalf("Missing field %q", name) - } - } -} - -func TestSchemaAddField(t *testing.T) { - f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{Id: "f2Id", Name: "test2", Type: schema.FieldTypeText} - f3 := &schema.SchemaField{Id: "f3Id", Name: "test3", Type: schema.FieldTypeText} - testSchema := schema.NewSchema(f1, f2, f3) - - f2New := &schema.SchemaField{Id: "f2Id", Name: "test2_new", Type: schema.FieldTypeEmail} - f4 := &schema.SchemaField{Name: "test4", Type: schema.FieldTypeUrl} - - testSchema.AddField(f2New) - testSchema.AddField(f4) - - if len(testSchema.Fields()) != 4 { - t.Fatalf("Expected %d, got %d (%v)", 4, len(testSchema.Fields()), testSchema) - } - - // check if each field has id - for _, f := range testSchema.Fields() { - if f.Id == "" { - t.Fatalf("Expected field id to be set, found empty id for field %v", f) - } - } - - // check if f2 field was replaced - if f := testSchema.GetFieldById("f2Id"); f == nil || f.Type != schema.FieldTypeEmail { - t.Fatalf("Expected f2 field to be replaced, found %v", f) - } - - // check if f4 was added - if f := testSchema.GetFieldByName("test4"); f == nil || f.Name != "test4" { - t.Fatalf("Expected f4 field to be added, found %v", f) - } -} - -func TestSchemaValidate(t *testing.T) { - // emulate duplicated field ids - duplicatedIdsSchema := schema.NewSchema( - &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}, - &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}, - ) - duplicatedIdsSchema.Fields()[1].Id = "id1" // manually set existing id - - scenarios := []struct { - schema schema.Schema - expectError bool - }{ - // no fields - { - schema.NewSchema(), - false, - }, - // duplicated field ids - { - duplicatedIdsSchema, - true, - }, - // duplicated field names (case insensitive) - { - schema.NewSchema( - &schema.SchemaField{Name: "test", Type: schema.FieldTypeText}, - &schema.SchemaField{Name: "TeSt", Type: schema.FieldTypeText}, - ), - true, - }, - // failure - base individual fields validation - { - schema.NewSchema( - &schema.SchemaField{Name: "", Type: schema.FieldTypeText}, - ), - true, - }, - // success - base individual fields validation - { - schema.NewSchema( - &schema.SchemaField{Name: "test", Type: schema.FieldTypeText}, - ), - false, - }, - // failure - individual field options validation - { - schema.NewSchema( - &schema.SchemaField{Name: "test", Type: schema.FieldTypeFile}, - ), - true, - }, - // success - individual field options validation - { - schema.NewSchema( - &schema.SchemaField{Name: "test", Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}}, - ), - false, - }, - } - - for i, s := range scenarios { - err := s.schema.Validate() - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } - } -} - -func TestSchemaMarshalJSON(t *testing.T) { - f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText} - f2 := &schema.SchemaField{ - Id: "f2id", - Name: "test2", - Type: schema.FieldTypeText, - Options: &schema.TextOptions{Pattern: "test"}, - } - testSchema := schema.NewSchema(f1, f2) - - result, err := testSchema.MarshalJSON() - if err != nil { - t.Fatal(err) - } - - expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"id":"f2id","name":"test2","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]` - - if string(result) != expected { - t.Fatalf("Expected %s, got %s", expected, string(result)) - } -} - -func TestSchemaUnmarshalJSON(t *testing.T) { - encoded := `[{"system":false,"id":"fid1", "name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]` - testSchema := schema.Schema{} - testSchema.AddField(&schema.SchemaField{Name: "tempField", Type: schema.FieldTypeUrl}) - err := testSchema.UnmarshalJSON([]byte(encoded)) - if err != nil { - t.Fatal(err) - } - - fields := testSchema.Fields() - if len(fields) != 2 { - t.Fatalf("Expected 2 fields, found %v", fields) - } - - f1 := testSchema.GetFieldByName("test1") - if f1 == nil { - t.Fatal("Expected to find field 'test1', got nil") - } - if f1.Id != "fid1" { - t.Fatalf("Expected fid1 id, got %s", f1.Id) - } - _, ok := f1.Options.(*schema.TextOptions) - if !ok { - t.Fatal("'test1' field options are not inited.") - } - - f2 := testSchema.GetFieldByName("test2") - if f2 == nil { - t.Fatal("Expected to find field 'test2', got nil") - } - if f2.Id == "" { - t.Fatal("Expected f2 id to be set, got empty string") - } - o2, ok := f2.Options.(*schema.TextOptions) - if !ok { - t.Fatal("'test2' field options are not inited.") - } - if o2.Pattern != "test" { - t.Fatalf("Expected pattern to be %q, got %q", "test", o2.Pattern) - } -} - -func TestSchemaValue(t *testing.T) { - // empty schema - s1 := schema.Schema{} - v1, err := s1.Value() - if err != nil { - t.Fatal(err) - } - if v1 != "[]" { - t.Fatalf("Expected nil, got %v", v1) - } - - // schema with fields - f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText} - s2 := schema.NewSchema(f1) - - v2, err := s2.Value() - if err != nil { - t.Fatal(err) - } - expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]` - - if v2 != expected { - t.Fatalf("Expected %v, got %v", expected, v2) - } -} - -func TestSchemaScan(t *testing.T) { - scenarios := []struct { - data any - expectError bool - expectJson string - }{ - {nil, false, "[]"}, - {"", false, "[]"}, - {[]byte{}, false, "[]"}, - {"[]", false, "[]"}, - {"invalid", true, "[]"}, - {123, true, "[]"}, - // no field type - {`[{}]`, true, `[]`}, - // unknown field type - { - `[{"system":false,"id":"123","name":"test1","type":"unknown","required":false,"presentable":false,"unique":false}]`, - true, - `[]`, - }, - // without options - { - `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false}]`, - false, - `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`, - }, - // with options - { - `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`, - false, - `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"presentable":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`, - }, - } - - for i, s := range scenarios { - testSchema := schema.Schema{} - - err := testSchema.Scan(s.data) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } - - json, _ := testSchema.MarshalJSON() - if string(json) != s.expectJson { - t.Errorf("(%d) Expected json %v, got %v", i, s.expectJson, string(json)) - } - } -} diff --git a/models/settings/settings.go b/models/settings/settings.go deleted file mode 100644 index 32f1ebeb..00000000 --- a/models/settings/settings.go +++ /dev/null @@ -1,703 +0,0 @@ -package settings - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/pocketbase/pocketbase/tools/auth" - "github.com/pocketbase/pocketbase/tools/cron" - "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/rest" - "github.com/pocketbase/pocketbase/tools/security" -) - -// SecretMask is the default settings secrets replacement value -// (see Settings.RedactClone()). -const SecretMask string = "******" - -// Settings defines common app configuration options. -type Settings struct { - mux sync.RWMutex - - Meta MetaConfig `form:"meta" json:"meta"` - Logs LogsConfig `form:"logs" json:"logs"` - Smtp SmtpConfig `form:"smtp" json:"smtp"` - S3 S3Config `form:"s3" json:"s3"` - Backups BackupsConfig `form:"backups" json:"backups"` - - AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"` - AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"` - AdminFileToken TokenConfig `form:"adminFileToken" json:"adminFileToken"` - RecordAuthToken TokenConfig `form:"recordAuthToken" json:"recordAuthToken"` - RecordPasswordResetToken TokenConfig `form:"recordPasswordResetToken" json:"recordPasswordResetToken"` - RecordEmailChangeToken TokenConfig `form:"recordEmailChangeToken" json:"recordEmailChangeToken"` - RecordVerificationToken TokenConfig `form:"recordVerificationToken" json:"recordVerificationToken"` - RecordFileToken TokenConfig `form:"recordFileToken" json:"recordFileToken"` - - // Deprecated: Will be removed in v0.9+ - EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"` - - GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"` - FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"` - GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"` - GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"` - DiscordAuth AuthProviderConfig `form:"discordAuth" json:"discordAuth"` - TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"` - MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"` - SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"` - KakaoAuth AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"` - TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"` - StravaAuth AuthProviderConfig `form:"stravaAuth" json:"stravaAuth"` - GiteeAuth AuthProviderConfig `form:"giteeAuth" json:"giteeAuth"` - LivechatAuth AuthProviderConfig `form:"livechatAuth" json:"livechatAuth"` - GiteaAuth AuthProviderConfig `form:"giteaAuth" json:"giteaAuth"` - OIDCAuth AuthProviderConfig `form:"oidcAuth" json:"oidcAuth"` - OIDC2Auth AuthProviderConfig `form:"oidc2Auth" json:"oidc2Auth"` - OIDC3Auth AuthProviderConfig `form:"oidc3Auth" json:"oidc3Auth"` - AppleAuth AuthProviderConfig `form:"appleAuth" json:"appleAuth"` - InstagramAuth AuthProviderConfig `form:"instagramAuth" json:"instagramAuth"` - VKAuth AuthProviderConfig `form:"vkAuth" json:"vkAuth"` - YandexAuth AuthProviderConfig `form:"yandexAuth" json:"yandexAuth"` - PatreonAuth AuthProviderConfig `form:"patreonAuth" json:"patreonAuth"` - MailcowAuth AuthProviderConfig `form:"mailcowAuth" json:"mailcowAuth"` - BitbucketAuth AuthProviderConfig `form:"bitbucketAuth" json:"bitbucketAuth"` - PlanningcenterAuth AuthProviderConfig `form:"planningcenterAuth" json:"planningcenterAuth"` -} - -// New creates and returns a new default Settings instance. -func New() *Settings { - return &Settings{ - Meta: MetaConfig{ - AppName: "Acme", - AppUrl: "http://localhost:8090", - HideControls: false, - SenderName: "Support", - SenderAddress: "support@example.com", - VerificationTemplate: defaultVerificationTemplate, - ResetPasswordTemplate: defaultResetPasswordTemplate, - ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate, - }, - Logs: LogsConfig{ - MaxDays: 5, - LogIp: true, - }, - Smtp: SmtpConfig{ - Enabled: false, - Host: "smtp.example.com", - Port: 587, - Username: "", - Password: "", - Tls: false, - }, - Backups: BackupsConfig{ - CronMaxKeep: 3, - }, - AdminAuthToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 1209600, // 14 days - }, - AdminPasswordResetToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 1800, // 30 minutes - }, - AdminFileToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 120, // 2 minutes - }, - RecordAuthToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 1209600, // 14 days - }, - RecordPasswordResetToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 1800, // 30 minutes - }, - RecordVerificationToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 604800, // 7 days - }, - RecordFileToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 120, // 2 minutes - }, - RecordEmailChangeToken: TokenConfig{ - Secret: security.RandomString(50), - Duration: 1800, // 30 minutes - }, - GoogleAuth: AuthProviderConfig{ - Enabled: false, - }, - FacebookAuth: AuthProviderConfig{ - Enabled: false, - }, - GithubAuth: AuthProviderConfig{ - Enabled: false, - }, - GitlabAuth: AuthProviderConfig{ - Enabled: false, - }, - DiscordAuth: AuthProviderConfig{ - Enabled: false, - }, - TwitterAuth: AuthProviderConfig{ - Enabled: false, - }, - MicrosoftAuth: AuthProviderConfig{ - Enabled: false, - }, - SpotifyAuth: AuthProviderConfig{ - Enabled: false, - }, - KakaoAuth: AuthProviderConfig{ - Enabled: false, - }, - TwitchAuth: AuthProviderConfig{ - Enabled: false, - }, - StravaAuth: AuthProviderConfig{ - Enabled: false, - }, - GiteeAuth: AuthProviderConfig{ - Enabled: false, - }, - LivechatAuth: AuthProviderConfig{ - Enabled: false, - }, - GiteaAuth: AuthProviderConfig{ - Enabled: false, - }, - OIDCAuth: AuthProviderConfig{ - Enabled: false, - }, - OIDC2Auth: AuthProviderConfig{ - Enabled: false, - }, - OIDC3Auth: AuthProviderConfig{ - Enabled: false, - }, - AppleAuth: AuthProviderConfig{ - Enabled: false, - }, - InstagramAuth: AuthProviderConfig{ - Enabled: false, - }, - VKAuth: AuthProviderConfig{ - Enabled: false, - }, - YandexAuth: AuthProviderConfig{ - Enabled: false, - }, - PatreonAuth: AuthProviderConfig{ - Enabled: false, - }, - MailcowAuth: AuthProviderConfig{ - Enabled: false, - }, - BitbucketAuth: AuthProviderConfig{ - Enabled: false, - }, - PlanningcenterAuth: AuthProviderConfig{ - Enabled: false, - }, - } -} - -// Validate makes Settings validatable by implementing [validation.Validatable] interface. -func (s *Settings) Validate() error { - s.mux.Lock() - defer s.mux.Unlock() - - return validation.ValidateStruct(s, - validation.Field(&s.Meta), - validation.Field(&s.Logs), - validation.Field(&s.AdminAuthToken), - validation.Field(&s.AdminPasswordResetToken), - validation.Field(&s.AdminFileToken), - validation.Field(&s.RecordAuthToken), - validation.Field(&s.RecordPasswordResetToken), - validation.Field(&s.RecordEmailChangeToken), - validation.Field(&s.RecordVerificationToken), - validation.Field(&s.RecordFileToken), - validation.Field(&s.Smtp), - validation.Field(&s.S3), - validation.Field(&s.Backups), - validation.Field(&s.GoogleAuth), - validation.Field(&s.FacebookAuth), - validation.Field(&s.GithubAuth), - validation.Field(&s.GitlabAuth), - validation.Field(&s.DiscordAuth), - validation.Field(&s.TwitterAuth), - validation.Field(&s.MicrosoftAuth), - validation.Field(&s.SpotifyAuth), - validation.Field(&s.KakaoAuth), - validation.Field(&s.TwitchAuth), - validation.Field(&s.StravaAuth), - validation.Field(&s.GiteeAuth), - validation.Field(&s.LivechatAuth), - validation.Field(&s.GiteaAuth), - validation.Field(&s.OIDCAuth), - validation.Field(&s.OIDC2Auth), - validation.Field(&s.OIDC3Auth), - validation.Field(&s.AppleAuth), - validation.Field(&s.InstagramAuth), - validation.Field(&s.VKAuth), - validation.Field(&s.YandexAuth), - validation.Field(&s.PatreonAuth), - validation.Field(&s.MailcowAuth), - validation.Field(&s.BitbucketAuth), - validation.Field(&s.PlanningcenterAuth), - ) -} - -// Merge merges `other` settings into the current one. -func (s *Settings) Merge(other *Settings) error { - s.mux.Lock() - defer s.mux.Unlock() - - bytes, err := json.Marshal(other) - if err != nil { - return err - } - - return json.Unmarshal(bytes, s) -} - -// Clone creates a new deep copy of the current settings. -func (s *Settings) Clone() (*Settings, error) { - clone := &Settings{} - if err := clone.Merge(s); err != nil { - return nil, err - } - return clone, nil -} - -// RedactClone creates a new deep copy of the current settings, -// while replacing the secret values with `******`. -func (s *Settings) RedactClone() (*Settings, error) { - clone, err := s.Clone() - if err != nil { - return nil, err - } - - sensitiveFields := []*string{ - &clone.Smtp.Password, - &clone.S3.Secret, - &clone.Backups.S3.Secret, - &clone.AdminAuthToken.Secret, - &clone.AdminPasswordResetToken.Secret, - &clone.AdminFileToken.Secret, - &clone.RecordAuthToken.Secret, - &clone.RecordPasswordResetToken.Secret, - &clone.RecordEmailChangeToken.Secret, - &clone.RecordVerificationToken.Secret, - &clone.RecordFileToken.Secret, - &clone.GoogleAuth.ClientSecret, - &clone.FacebookAuth.ClientSecret, - &clone.GithubAuth.ClientSecret, - &clone.GitlabAuth.ClientSecret, - &clone.DiscordAuth.ClientSecret, - &clone.TwitterAuth.ClientSecret, - &clone.MicrosoftAuth.ClientSecret, - &clone.SpotifyAuth.ClientSecret, - &clone.KakaoAuth.ClientSecret, - &clone.TwitchAuth.ClientSecret, - &clone.StravaAuth.ClientSecret, - &clone.GiteeAuth.ClientSecret, - &clone.LivechatAuth.ClientSecret, - &clone.GiteaAuth.ClientSecret, - &clone.OIDCAuth.ClientSecret, - &clone.OIDC2Auth.ClientSecret, - &clone.OIDC3Auth.ClientSecret, - &clone.AppleAuth.ClientSecret, - &clone.InstagramAuth.ClientSecret, - &clone.VKAuth.ClientSecret, - &clone.YandexAuth.ClientSecret, - &clone.PatreonAuth.ClientSecret, - &clone.MailcowAuth.ClientSecret, - &clone.BitbucketAuth.ClientSecret, - &clone.PlanningcenterAuth.ClientSecret, - } - - // mask all sensitive fields - for _, v := range sensitiveFields { - if v != nil && *v != "" { - *v = SecretMask - } - } - - return clone, nil -} - -// NamedAuthProviderConfigs returns a map with all registered OAuth2 -// provider configurations (indexed by their name identifier). -func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig { - s.mux.RLock() - defer s.mux.RUnlock() - - return map[string]AuthProviderConfig{ - auth.NameGoogle: s.GoogleAuth, - auth.NameFacebook: s.FacebookAuth, - auth.NameGithub: s.GithubAuth, - auth.NameGitlab: s.GitlabAuth, - auth.NameDiscord: s.DiscordAuth, - auth.NameTwitter: s.TwitterAuth, - auth.NameMicrosoft: s.MicrosoftAuth, - auth.NameSpotify: s.SpotifyAuth, - auth.NameKakao: s.KakaoAuth, - auth.NameTwitch: s.TwitchAuth, - auth.NameStrava: s.StravaAuth, - auth.NameGitee: s.GiteeAuth, - auth.NameLivechat: s.LivechatAuth, - auth.NameGitea: s.GiteaAuth, - auth.NameOIDC: s.OIDCAuth, - auth.NameOIDC + "2": s.OIDC2Auth, - auth.NameOIDC + "3": s.OIDC3Auth, - auth.NameApple: s.AppleAuth, - auth.NameInstagram: s.InstagramAuth, - auth.NameVK: s.VKAuth, - auth.NameYandex: s.YandexAuth, - auth.NamePatreon: s.PatreonAuth, - auth.NameMailcow: s.MailcowAuth, - auth.NameBitbucket: s.BitbucketAuth, - auth.NamePlanningcenter: s.PlanningcenterAuth, - } -} - -// ------------------------------------------------------------------- - -type TokenConfig struct { - Secret string `form:"secret" json:"secret"` - Duration int64 `form:"duration" json:"duration"` -} - -// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface. -func (c TokenConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)), - validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(63072000)), - ) -} - -// ------------------------------------------------------------------- - -type SmtpConfig struct { - Enabled bool `form:"enabled" json:"enabled"` - Host string `form:"host" json:"host"` - Port int `form:"port" json:"port"` - Username string `form:"username" json:"username"` - Password string `form:"password" json:"password"` - - // SMTP AUTH - PLAIN (default) or LOGIN - AuthMethod string `form:"authMethod" json:"authMethod"` - - // Whether to enforce TLS encryption for the mail server connection. - // - // When set to false StartTLS command is send, leaving the server - // to decide whether to upgrade the connection or not. - Tls bool `form:"tls" json:"tls"` - - // LocalName is optional domain name or IP address used for the - // EHLO/HELO exchange (if not explicitly set, defaults to "localhost"). - // - // This is required only by some SMTP servers, such as Gmail SMTP-relay. - LocalName string `form:"localName" json:"localName"` -} - -// Validate makes SmtpConfig validatable by implementing [validation.Validatable] interface. -func (c SmtpConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field( - &c.Host, - validation.When(c.Enabled, validation.Required), - is.Host, - ), - validation.Field( - &c.Port, - validation.When(c.Enabled, validation.Required), - validation.Min(0), - ), - validation.Field( - &c.AuthMethod, - // don't require it for backward compatibility - // (fallback internally to PLAIN) - // validation.When(c.Enabled, validation.Required), - validation.In(mailer.SmtpAuthLogin, mailer.SmtpAuthPlain), - ), - validation.Field(&c.LocalName, is.Host), - ) -} - -// ------------------------------------------------------------------- - -type S3Config struct { - Enabled bool `form:"enabled" json:"enabled"` - Bucket string `form:"bucket" json:"bucket"` - Region string `form:"region" json:"region"` - Endpoint string `form:"endpoint" json:"endpoint"` - AccessKey string `form:"accessKey" json:"accessKey"` - Secret string `form:"secret" json:"secret"` - ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"` -} - -// Validate makes S3Config validatable by implementing [validation.Validatable] interface. -func (c S3Config) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)), - ) -} - -// ------------------------------------------------------------------- - -type BackupsConfig struct { - // Cron is a cron expression to schedule auto backups, eg. "* * * * *". - // - // Leave it empty to disable the auto backups functionality. - Cron string `form:"cron" json:"cron"` - - // CronMaxKeep is the max number of cron generated backups to - // keep before removing older entries. - // - // This field works only when the cron config has valid cron expression. - CronMaxKeep int `form:"cronMaxKeep" json:"cronMaxKeep"` - - // S3 is an optional S3 storage config specifying where to store the app backups. - S3 S3Config `form:"s3" json:"s3"` -} - -// Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface. -func (c BackupsConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.S3), - validation.Field(&c.Cron, validation.By(checkCronExpression)), - validation.Field( - &c.CronMaxKeep, - validation.When(c.Cron != "", validation.Required), - validation.Min(1), - ), - ) -} - -func checkCronExpression(value any) error { - v, _ := value.(string) - if v == "" { - return nil // nothing to check - } - - _, err := cron.NewSchedule(v) - if err != nil { - return validation.NewError("validation_invalid_cron", err.Error()) - } - - return nil -} - -// ------------------------------------------------------------------- - -type MetaConfig struct { - AppName string `form:"appName" json:"appName"` - AppUrl string `form:"appUrl" json:"appUrl"` - HideControls bool `form:"hideControls" json:"hideControls"` - SenderName string `form:"senderName" json:"senderName"` - SenderAddress string `form:"senderAddress" json:"senderAddress"` - VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"` - ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"` - ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"` -} - -// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. -func (c MetaConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)), - validation.Field(&c.AppUrl, validation.Required, is.URL), - validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)), - validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required), - validation.Field(&c.VerificationTemplate, validation.Required), - validation.Field(&c.ResetPasswordTemplate, validation.Required), - validation.Field(&c.ConfirmEmailChangeTemplate, validation.Required), - ) -} - -type EmailTemplate struct { - Body string `form:"body" json:"body"` - Subject string `form:"subject" json:"subject"` - ActionUrl string `form:"actionUrl" json:"actionUrl"` - Hidden bool `form:"hidden" json:"hidden"` -} - -// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface. -func (t EmailTemplate) Validate() error { - return validation.ValidateStruct(&t, - validation.Field(&t.Subject, validation.Required), - validation.Field( - &t.Body, - validation.Required, - validation.By(checkPlaceholderParams(EmailPlaceholderActionUrl)), - ), - validation.Field( - &t.ActionUrl, - validation.Required, - validation.By(checkPlaceholderParams(EmailPlaceholderToken)), - ), - ) -} - -func checkPlaceholderParams(params ...string) validation.RuleFunc { - return func(value any) error { - v, _ := value.(string) - - for _, param := range params { - if !strings.Contains(v, param) { - return validation.NewError( - "validation_missing_required_param", - fmt.Sprintf("Missing required parameter %q", param), - ) - } - } - - return nil - } -} - -// Resolve replaces the placeholder parameters in the current email -// template and returns its components as ready-to-use strings. -func (t EmailTemplate) Resolve( - appName string, - appUrl, - token string, -) (subject, body, actionUrl string) { - // replace action url placeholder params (if any) - actionUrlParams := map[string]string{ - EmailPlaceholderAppName: appName, - EmailPlaceholderAppUrl: appUrl, - EmailPlaceholderToken: token, - } - actionUrl = t.ActionUrl - for k, v := range actionUrlParams { - actionUrl = strings.ReplaceAll(actionUrl, k, v) - } - actionUrl, _ = rest.NormalizeUrl(actionUrl) - - // replace body placeholder params (if any) - bodyParams := map[string]string{ - EmailPlaceholderAppName: appName, - EmailPlaceholderAppUrl: appUrl, - EmailPlaceholderToken: token, - EmailPlaceholderActionUrl: actionUrl, - } - body = t.Body - for k, v := range bodyParams { - body = strings.ReplaceAll(body, k, v) - } - - // replace subject placeholder params (if any) - subjectParams := map[string]string{ - EmailPlaceholderAppName: appName, - EmailPlaceholderAppUrl: appUrl, - } - subject = t.Subject - for k, v := range subjectParams { - subject = strings.ReplaceAll(subject, k, v) - } - - return subject, body, actionUrl -} - -// ------------------------------------------------------------------- - -type LogsConfig struct { - MaxDays int `form:"maxDays" json:"maxDays"` - MinLevel int `form:"minLevel" json:"minLevel"` - LogIp bool `form:"logIp" json:"logIp"` -} - -// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface. -func (c LogsConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.MaxDays, validation.Min(0)), - ) -} - -// ------------------------------------------------------------------- - -type AuthProviderConfig struct { - Enabled bool `form:"enabled" json:"enabled"` - ClientId string `form:"clientId" json:"clientId"` - ClientSecret string `form:"clientSecret" json:"clientSecret"` - AuthUrl string `form:"authUrl" json:"authUrl"` - TokenUrl string `form:"tokenUrl" json:"tokenUrl"` - UserApiUrl string `form:"userApiUrl" json:"userApiUrl"` - DisplayName string `form:"displayName" json:"displayName"` - PKCE *bool `form:"pkce" json:"pkce"` -} - -// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface. -func (c AuthProviderConfig) Validate() error { - return validation.ValidateStruct(&c, - validation.Field(&c.ClientId, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.ClientSecret, validation.When(c.Enabled, validation.Required)), - validation.Field(&c.AuthUrl, is.URL), - validation.Field(&c.TokenUrl, is.URL), - validation.Field(&c.UserApiUrl, is.URL), - ) -} - -// SetupProvider loads the current AuthProviderConfig into the specified provider. -func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error { - if !c.Enabled { - return errors.New("the provider is not enabled") - } - - if c.ClientId != "" { - provider.SetClientId(c.ClientId) - } - - if c.ClientSecret != "" { - provider.SetClientSecret(c.ClientSecret) - } - - if c.AuthUrl != "" { - provider.SetAuthUrl(c.AuthUrl) - } - - if c.UserApiUrl != "" { - provider.SetUserApiUrl(c.UserApiUrl) - } - - if c.TokenUrl != "" { - provider.SetTokenUrl(c.TokenUrl) - } - - if c.DisplayName != "" { - provider.SetDisplayName(c.DisplayName) - } - - if c.PKCE != nil { - provider.SetPKCE(*c.PKCE) - } - - return nil -} - -// ------------------------------------------------------------------- - -// Deprecated: Will be removed in v0.9+ -type EmailAuthConfig struct { - Enabled bool `form:"enabled" json:"enabled"` - ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` - OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` - MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"` -} - -// Deprecated: Will be removed in v0.9+ -func (c EmailAuthConfig) Validate() error { - return nil -} diff --git a/models/settings/settings_templates.go b/models/settings/settings_templates.go deleted file mode 100644 index a0f913a3..00000000 --- a/models/settings/settings_templates.go +++ /dev/null @@ -1,54 +0,0 @@ -package settings - -// Common settings placeholder tokens -const ( - EmailPlaceholderAppName string = "{APP_NAME}" - EmailPlaceholderAppUrl string = "{APP_URL}" - EmailPlaceholderToken string = "{TOKEN}" - EmailPlaceholderActionUrl string = "{ACTION_URL}" -) - -var defaultVerificationTemplate = EmailTemplate{ - Subject: "Verify your " + EmailPlaceholderAppName + " email", - Body: `

Hello,

-

Thank you for joining us at ` + EmailPlaceholderAppName + `.

-

Click on the button below to verify your email address.

-

- Verify -

-

- Thanks,
- ` + EmailPlaceholderAppName + ` team -

`, - ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-verification/" + EmailPlaceholderToken, -} - -var defaultResetPasswordTemplate = EmailTemplate{ - Subject: "Reset your " + EmailPlaceholderAppName + " password", - Body: `

Hello,

-

Click on the button below to reset your password.

-

- Reset password -

-

If you didn't ask to reset your password, you can ignore this email.

-

- Thanks,
- ` + EmailPlaceholderAppName + ` team -

`, - ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-password-reset/" + EmailPlaceholderToken, -} - -var defaultConfirmEmailChangeTemplate = EmailTemplate{ - Subject: "Confirm your " + EmailPlaceholderAppName + " new email address", - Body: `

Hello,

-

Click on the button below to confirm your new email address.

-

- Confirm new email -

-

If you didn't ask to change your email address, you can ignore this email.

-

- Thanks,
- ` + EmailPlaceholderAppName + ` team -

`, - ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-email-change/" + EmailPlaceholderToken, -} diff --git a/models/settings/settings_test.go b/models/settings/settings_test.go deleted file mode 100644 index e54ceb5e..00000000 --- a/models/settings/settings_test.go +++ /dev/null @@ -1,1034 +0,0 @@ -package settings_test - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "testing" - - validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/pocketbase/models/settings" - "github.com/pocketbase/pocketbase/tools/auth" - "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/types" -) - -func TestSettingsValidate(t *testing.T) { - s := settings.New() - - // set invalid settings data - s.Meta.AppName = "" - s.Logs.MaxDays = -10 - s.Smtp.Enabled = true - s.Smtp.Host = "" - s.S3.Enabled = true - s.S3.Endpoint = "invalid" - s.AdminAuthToken.Duration = -10 - s.AdminPasswordResetToken.Duration = -10 - s.AdminFileToken.Duration = -10 - s.RecordAuthToken.Duration = -10 - s.RecordPasswordResetToken.Duration = -10 - s.RecordEmailChangeToken.Duration = -10 - s.RecordVerificationToken.Duration = -10 - s.RecordFileToken.Duration = -10 - s.GoogleAuth.Enabled = true - s.GoogleAuth.ClientId = "" - s.FacebookAuth.Enabled = true - s.FacebookAuth.ClientId = "" - s.GithubAuth.Enabled = true - s.GithubAuth.ClientId = "" - s.GitlabAuth.Enabled = true - s.GitlabAuth.ClientId = "" - s.DiscordAuth.Enabled = true - s.DiscordAuth.ClientId = "" - s.TwitterAuth.Enabled = true - s.TwitterAuth.ClientId = "" - s.MicrosoftAuth.Enabled = true - s.MicrosoftAuth.ClientId = "" - s.SpotifyAuth.Enabled = true - s.SpotifyAuth.ClientId = "" - s.KakaoAuth.Enabled = true - s.KakaoAuth.ClientId = "" - s.TwitchAuth.Enabled = true - s.TwitchAuth.ClientId = "" - s.StravaAuth.Enabled = true - s.StravaAuth.ClientId = "" - s.GiteeAuth.Enabled = true - s.GiteeAuth.ClientId = "" - s.LivechatAuth.Enabled = true - s.LivechatAuth.ClientId = "" - s.GiteaAuth.Enabled = true - s.GiteaAuth.ClientId = "" - s.OIDCAuth.Enabled = true - s.OIDCAuth.ClientId = "" - s.OIDC2Auth.Enabled = true - s.OIDC2Auth.ClientId = "" - s.OIDC3Auth.Enabled = true - s.OIDC3Auth.ClientId = "" - s.AppleAuth.Enabled = true - s.AppleAuth.ClientId = "" - s.InstagramAuth.Enabled = true - s.InstagramAuth.ClientId = "" - s.VKAuth.Enabled = true - s.VKAuth.ClientId = "" - s.YandexAuth.Enabled = true - s.YandexAuth.ClientId = "" - s.PatreonAuth.Enabled = true - s.PatreonAuth.ClientId = "" - s.MailcowAuth.Enabled = true - s.MailcowAuth.ClientId = "" - s.BitbucketAuth.Enabled = true - s.BitbucketAuth.ClientId = "" - s.PlanningcenterAuth.Enabled = true - s.PlanningcenterAuth.ClientId = "" - - // check if Validate() is triggering the members validate methods. - err := s.Validate() - if err == nil { - t.Fatalf("Expected error, got nil") - } - - expectations := []string{ - `"meta":{`, - `"logs":{`, - `"smtp":{`, - `"s3":{`, - `"adminAuthToken":{`, - `"adminPasswordResetToken":{`, - `"adminFileToken":{`, - `"recordAuthToken":{`, - `"recordPasswordResetToken":{`, - `"recordEmailChangeToken":{`, - `"recordVerificationToken":{`, - `"recordFileToken":{`, - `"googleAuth":{`, - `"facebookAuth":{`, - `"githubAuth":{`, - `"gitlabAuth":{`, - `"discordAuth":{`, - `"twitterAuth":{`, - `"microsoftAuth":{`, - `"spotifyAuth":{`, - `"kakaoAuth":{`, - `"twitchAuth":{`, - `"stravaAuth":{`, - `"giteeAuth":{`, - `"livechatAuth":{`, - `"giteaAuth":{`, - `"oidcAuth":{`, - `"oidc2Auth":{`, - `"oidc3Auth":{`, - `"appleAuth":{`, - `"instagramAuth":{`, - `"vkAuth":{`, - `"yandexAuth":{`, - `"patreonAuth":{`, - `"mailcowAuth":{`, - `"bitbucketAuth":{`, - `"planningcenterAuth":{`, - } - - errBytes, _ := json.Marshal(err) - jsonErr := string(errBytes) - for _, expected := range expectations { - if !strings.Contains(jsonErr, expected) { - t.Errorf("Expected error key %s in %v", expected, jsonErr) - } - } -} - -func TestSettingsMerge(t *testing.T) { - s1 := settings.New() - s1.Meta.AppUrl = "old_app_url" - - s2 := settings.New() - s2.Meta.AppName = "test" - s2.Logs.MaxDays = 123 - s2.Smtp.Host = "test" - s2.Smtp.Enabled = true - s2.S3.Enabled = true - s2.S3.Endpoint = "test" - s2.Backups.Cron = "* * * * *" - s2.AdminAuthToken.Duration = 1 - s2.AdminPasswordResetToken.Duration = 2 - s2.AdminFileToken.Duration = 2 - s2.RecordAuthToken.Duration = 3 - s2.RecordPasswordResetToken.Duration = 4 - s2.RecordEmailChangeToken.Duration = 5 - s2.RecordVerificationToken.Duration = 6 - s2.RecordFileToken.Duration = 7 - s2.GoogleAuth.Enabled = true - s2.GoogleAuth.ClientId = "google_test" - s2.FacebookAuth.Enabled = true - s2.FacebookAuth.ClientId = "facebook_test" - s2.GithubAuth.Enabled = true - s2.GithubAuth.ClientId = "github_test" - s2.GitlabAuth.Enabled = true - s2.GitlabAuth.ClientId = "gitlab_test" - s2.DiscordAuth.Enabled = true - s2.DiscordAuth.ClientId = "discord_test" - s2.TwitterAuth.Enabled = true - s2.TwitterAuth.ClientId = "twitter_test" - s2.MicrosoftAuth.Enabled = true - s2.MicrosoftAuth.ClientId = "microsoft_test" - s2.SpotifyAuth.Enabled = true - s2.SpotifyAuth.ClientId = "spotify_test" - s2.KakaoAuth.Enabled = true - s2.KakaoAuth.ClientId = "kakao_test" - s2.TwitchAuth.Enabled = true - s2.TwitchAuth.ClientId = "twitch_test" - s2.StravaAuth.Enabled = true - s2.StravaAuth.ClientId = "strava_test" - s2.GiteeAuth.Enabled = true - s2.GiteeAuth.ClientId = "gitee_test" - s2.LivechatAuth.Enabled = true - s2.LivechatAuth.ClientId = "livechat_test" - s2.GiteaAuth.Enabled = true - s2.GiteaAuth.ClientId = "gitea_test" - s2.OIDCAuth.Enabled = true - s2.OIDCAuth.ClientId = "oidc_test" - s2.OIDC2Auth.Enabled = true - s2.OIDC2Auth.ClientId = "oidc2_test" - s2.OIDC3Auth.Enabled = true - s2.OIDC3Auth.ClientId = "oidc3_test" - s2.AppleAuth.Enabled = true - s2.AppleAuth.ClientId = "apple_test" - s2.InstagramAuth.Enabled = true - s2.InstagramAuth.ClientId = "instagram_test" - s2.VKAuth.Enabled = true - s2.VKAuth.ClientId = "vk_test" - s2.YandexAuth.Enabled = true - s2.YandexAuth.ClientId = "yandex_test" - s2.PatreonAuth.Enabled = true - s2.PatreonAuth.ClientId = "patreon_test" - s2.MailcowAuth.Enabled = true - s2.MailcowAuth.ClientId = "mailcow_test" - s2.BitbucketAuth.Enabled = true - s2.BitbucketAuth.ClientId = "bitbucket_test" - s2.PlanningcenterAuth.Enabled = true - s2.PlanningcenterAuth.ClientId = "planningcenter_test" - - if err := s1.Merge(s2); err != nil { - t.Fatal(err) - } - - s1Encoded, err := json.Marshal(s1) - if err != nil { - t.Fatal(err) - } - - s2Encoded, err := json.Marshal(s2) - if err != nil { - t.Fatal(err) - } - - if string(s1Encoded) != string(s2Encoded) { - t.Fatalf("Expected the same serialization, got %v VS %v", string(s1Encoded), string(s2Encoded)) - } -} - -func TestSettingsClone(t *testing.T) { - s1 := settings.New() - - s2, err := s1.Clone() - if err != nil { - t.Fatal(err) - } - - s1Bytes, err := json.Marshal(s1) - if err != nil { - t.Fatal(err) - } - - s2Bytes, err := json.Marshal(s2) - if err != nil { - t.Fatal(err) - } - - if string(s1Bytes) != string(s2Bytes) { - t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes)) - } - - // verify that it is a deep copy - s1.Meta.AppName = "new" - if s1.Meta.AppName == s2.Meta.AppName { - t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName) - } -} - -func TestSettingsRedactClone(t *testing.T) { - testSecret := "test_secret" - - s1 := settings.New() - - // control fields - s1.Meta.AppName = "test123" - - // secrets - s1.Smtp.Password = testSecret - s1.S3.Secret = testSecret - s1.Backups.S3.Secret = testSecret - s1.AdminAuthToken.Secret = testSecret - s1.AdminPasswordResetToken.Secret = testSecret - s1.AdminFileToken.Secret = testSecret - s1.RecordAuthToken.Secret = testSecret - s1.RecordPasswordResetToken.Secret = testSecret - s1.RecordEmailChangeToken.Secret = testSecret - s1.RecordVerificationToken.Secret = testSecret - s1.RecordFileToken.Secret = testSecret - s1.GoogleAuth.ClientSecret = testSecret - s1.FacebookAuth.ClientSecret = testSecret - s1.GithubAuth.ClientSecret = testSecret - s1.GitlabAuth.ClientSecret = testSecret - s1.DiscordAuth.ClientSecret = testSecret - s1.TwitterAuth.ClientSecret = testSecret - s1.MicrosoftAuth.ClientSecret = testSecret - s1.SpotifyAuth.ClientSecret = testSecret - s1.KakaoAuth.ClientSecret = testSecret - s1.TwitchAuth.ClientSecret = testSecret - s1.StravaAuth.ClientSecret = testSecret - s1.GiteeAuth.ClientSecret = testSecret - s1.LivechatAuth.ClientSecret = testSecret - s1.GiteaAuth.ClientSecret = testSecret - s1.OIDCAuth.ClientSecret = testSecret - s1.OIDC2Auth.ClientSecret = testSecret - s1.OIDC3Auth.ClientSecret = testSecret - s1.AppleAuth.ClientSecret = testSecret - s1.InstagramAuth.ClientSecret = testSecret - s1.VKAuth.ClientSecret = testSecret - s1.YandexAuth.ClientSecret = testSecret - s1.PatreonAuth.ClientSecret = testSecret - s1.MailcowAuth.ClientSecret = testSecret - s1.BitbucketAuth.ClientSecret = testSecret - s1.PlanningcenterAuth.ClientSecret = testSecret - - s1Bytes, err := json.Marshal(s1) - if err != nil { - t.Fatal(err) - } - - s2, err := s1.RedactClone() - if err != nil { - t.Fatal(err) - } - - s2Bytes, err := json.Marshal(s2) - if err != nil { - t.Fatal(err) - } - - if bytes.Equal(s1Bytes, s2Bytes) { - t.Fatalf("Expected the 2 settings to differ, got \n%s", s2Bytes) - } - - if strings.Contains(string(s2Bytes), testSecret) { - t.Fatalf("Expected %q secret to be replaced with mask, got \n%s", testSecret, s2Bytes) - } - - if !strings.Contains(string(s2Bytes), settings.SecretMask) { - t.Fatalf("Expected the secrets to be replaced with the secret mask, got \n%s", s2Bytes) - } - - if !strings.Contains(string(s2Bytes), `"appName":"test123"`) { - t.Fatalf("Missing control field in \n%s", s2Bytes) - } -} - -func TestNamedAuthProviderConfigs(t *testing.T) { - s := settings.New() - - s.GoogleAuth.ClientId = "google_test" - s.FacebookAuth.ClientId = "facebook_test" - s.GithubAuth.ClientId = "github_test" - s.GitlabAuth.ClientId = "gitlab_test" - s.GitlabAuth.Enabled = true // control - s.DiscordAuth.ClientId = "discord_test" - s.TwitterAuth.ClientId = "twitter_test" - s.MicrosoftAuth.ClientId = "microsoft_test" - s.SpotifyAuth.ClientId = "spotify_test" - s.KakaoAuth.ClientId = "kakao_test" - s.TwitchAuth.ClientId = "twitch_test" - s.StravaAuth.ClientId = "strava_test" - s.GiteeAuth.ClientId = "gitee_test" - s.LivechatAuth.ClientId = "livechat_test" - s.GiteaAuth.ClientId = "gitea_test" - s.OIDCAuth.ClientId = "oidc_test" - s.OIDC2Auth.ClientId = "oidc2_test" - s.OIDC3Auth.ClientId = "oidc3_test" - s.AppleAuth.ClientId = "apple_test" - s.InstagramAuth.ClientId = "instagram_test" - s.VKAuth.ClientId = "vk_test" - s.YandexAuth.ClientId = "yandex_test" - s.PatreonAuth.ClientId = "patreon_test" - s.MailcowAuth.ClientId = "mailcow_test" - s.BitbucketAuth.ClientId = "bitbucket_test" - s.PlanningcenterAuth.ClientId = "planningcenter_test" - - result := s.NamedAuthProviderConfigs() - - encoded, err := json.Marshal(result) - if err != nil { - t.Fatal(err) - } - encodedStr := string(encoded) - - expectedParts := []string{ - `"discord":{"enabled":false,"clientId":"discord_test"`, - `"facebook":{"enabled":false,"clientId":"facebook_test"`, - `"github":{"enabled":false,"clientId":"github_test"`, - `"gitlab":{"enabled":true,"clientId":"gitlab_test"`, - `"google":{"enabled":false,"clientId":"google_test"`, - `"microsoft":{"enabled":false,"clientId":"microsoft_test"`, - `"spotify":{"enabled":false,"clientId":"spotify_test"`, - `"twitter":{"enabled":false,"clientId":"twitter_test"`, - `"kakao":{"enabled":false,"clientId":"kakao_test"`, - `"twitch":{"enabled":false,"clientId":"twitch_test"`, - `"strava":{"enabled":false,"clientId":"strava_test"`, - `"gitee":{"enabled":false,"clientId":"gitee_test"`, - `"livechat":{"enabled":false,"clientId":"livechat_test"`, - `"gitea":{"enabled":false,"clientId":"gitea_test"`, - `"oidc":{"enabled":false,"clientId":"oidc_test"`, - `"oidc2":{"enabled":false,"clientId":"oidc2_test"`, - `"oidc3":{"enabled":false,"clientId":"oidc3_test"`, - `"apple":{"enabled":false,"clientId":"apple_test"`, - `"instagram":{"enabled":false,"clientId":"instagram_test"`, - `"vk":{"enabled":false,"clientId":"vk_test"`, - `"yandex":{"enabled":false,"clientId":"yandex_test"`, - `"patreon":{"enabled":false,"clientId":"patreon_test"`, - `"mailcow":{"enabled":false,"clientId":"mailcow_test"`, - `"bitbucket":{"enabled":false,"clientId":"bitbucket_test"`, - `"planningcenter":{"enabled":false,"clientId":"planningcenter_test"`, - } - for _, p := range expectedParts { - if !strings.Contains(encodedStr, p) { - t.Fatalf("Expected \n%s \nin \n%s", p, encodedStr) - } - } -} - -func TestTokenConfigValidate(t *testing.T) { - scenarios := []struct { - config settings.TokenConfig - expectError bool - }{ - // zero values - { - settings.TokenConfig{}, - true, - }, - // invalid data - { - settings.TokenConfig{ - Secret: strings.Repeat("a", 5), - Duration: 4, - }, - true, - }, - // valid secret but invalid duration - { - settings.TokenConfig{ - Secret: strings.Repeat("a", 30), - Duration: 63072000 + 1, - }, - true, - }, - // valid data - { - settings.TokenConfig{ - Secret: strings.Repeat("a", 30), - Duration: 100, - }, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestSmtpConfigValidate(t *testing.T) { - scenarios := []struct { - config settings.SmtpConfig - expectError bool - }{ - // zero values (disabled) - { - settings.SmtpConfig{}, - false, - }, - // zero values (enabled) - { - settings.SmtpConfig{Enabled: true}, - true, - }, - // invalid data - { - settings.SmtpConfig{ - Enabled: true, - Host: "test:test:test", - Port: -10, - }, - true, - }, - // invalid auth method - { - settings.SmtpConfig{ - Enabled: true, - Host: "example.com", - Port: 100, - AuthMethod: "example", - }, - true, - }, - // valid data (no explicit auth method) - { - settings.SmtpConfig{ - Enabled: true, - Host: "example.com", - Port: 100, - Tls: true, - }, - false, - }, - // valid data (explicit auth method - login) - { - settings.SmtpConfig{ - Enabled: true, - Host: "example.com", - Port: 100, - AuthMethod: mailer.SmtpAuthLogin, - }, - false, - }, - // invalid ehlo/helo name - { - settings.SmtpConfig{ - Enabled: true, - Host: "example.com", - Port: 100, - LocalName: "invalid!", - }, - true, - }, - // valid ehlo/helo name - { - settings.SmtpConfig{ - Enabled: true, - Host: "example.com", - Port: 100, - LocalName: "example.com", - }, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestS3ConfigValidate(t *testing.T) { - scenarios := []struct { - config settings.S3Config - expectError bool - }{ - // zero values (disabled) - { - settings.S3Config{}, - false, - }, - // zero values (enabled) - { - settings.S3Config{Enabled: true}, - true, - }, - // invalid data - { - settings.S3Config{ - Enabled: true, - Endpoint: "test:test:test", - }, - true, - }, - // valid data (url endpoint) - { - settings.S3Config{ - Enabled: true, - Endpoint: "https://localhost:8090", - Bucket: "test", - Region: "test", - AccessKey: "test", - Secret: "test", - }, - false, - }, - // valid data (hostname endpoint) - { - settings.S3Config{ - Enabled: true, - Endpoint: "example.com", - Bucket: "test", - Region: "test", - AccessKey: "test", - Secret: "test", - }, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestMetaConfigValidate(t *testing.T) { - invalidTemplate := settings.EmailTemplate{ - Subject: "test", - ActionUrl: "test", - Body: "test", - } - - noPlaceholdersTemplate := settings.EmailTemplate{ - Subject: "test", - ActionUrl: "http://example.com", - Body: "test", - } - - withPlaceholdersTemplate := settings.EmailTemplate{ - Subject: "test", - ActionUrl: "http://example.com" + settings.EmailPlaceholderToken, - Body: "test" + settings.EmailPlaceholderActionUrl, - } - - scenarios := []struct { - config settings.MetaConfig - expectError bool - }{ - // zero values - { - settings.MetaConfig{}, - true, - }, - // invalid data - { - settings.MetaConfig{ - AppName: strings.Repeat("a", 300), - AppUrl: "test", - SenderName: strings.Repeat("a", 300), - SenderAddress: "invalid_email", - VerificationTemplate: invalidTemplate, - ResetPasswordTemplate: invalidTemplate, - ConfirmEmailChangeTemplate: invalidTemplate, - }, - true, - }, - // invalid data (missing required placeholders) - { - settings.MetaConfig{ - AppName: "test", - AppUrl: "https://example.com", - SenderName: "test", - SenderAddress: "test@example.com", - VerificationTemplate: noPlaceholdersTemplate, - ResetPasswordTemplate: noPlaceholdersTemplate, - ConfirmEmailChangeTemplate: noPlaceholdersTemplate, - }, - true, - }, - // valid data - { - settings.MetaConfig{ - AppName: "test", - AppUrl: "https://example.com", - SenderName: "test", - SenderAddress: "test@example.com", - VerificationTemplate: withPlaceholdersTemplate, - ResetPasswordTemplate: withPlaceholdersTemplate, - ConfirmEmailChangeTemplate: withPlaceholdersTemplate, - }, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestBackupsConfigValidate(t *testing.T) { - scenarios := []struct { - name string - config settings.BackupsConfig - expectedErrors []string - }{ - { - "zero value", - settings.BackupsConfig{}, - []string{}, - }, - { - "invalid cron", - settings.BackupsConfig{ - Cron: "invalid", - CronMaxKeep: 0, - }, - []string{"cron", "cronMaxKeep"}, - }, - { - "invalid enabled S3", - settings.BackupsConfig{ - S3: settings.S3Config{ - Enabled: true, - }, - }, - []string{"s3"}, - }, - { - "valid data", - settings.BackupsConfig{ - S3: settings.S3Config{ - Enabled: true, - Endpoint: "example.com", - Bucket: "test", - Region: "test", - AccessKey: "test", - Secret: "test", - }, - Cron: "*/10 * * * *", - CronMaxKeep: 1, - }, - []string{}, - }, - } - - for _, s := range scenarios { - result := s.config.Validate() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("[%s] Failed to parse errors %v", s.name, result) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs) - } - } - } -} - -func TestEmailTemplateValidate(t *testing.T) { - scenarios := []struct { - emailTemplate settings.EmailTemplate - expectedErrors []string - }{ - // require values - { - settings.EmailTemplate{}, - []string{"subject", "actionUrl", "body"}, - }, - // missing placeholders - { - settings.EmailTemplate{ - Subject: "test", - ActionUrl: "test", - Body: "test", - }, - []string{"actionUrl", "body"}, - }, - // valid data - { - settings.EmailTemplate{ - Subject: "test", - ActionUrl: "test" + settings.EmailPlaceholderToken, - Body: "test" + settings.EmailPlaceholderActionUrl, - }, - []string{}, - }, - } - - for i, s := range scenarios { - result := s.emailTemplate.Validate() - - // parse errors - errs, ok := result.(validation.Errors) - if !ok && result != nil { - t.Errorf("(%d) Failed to parse errors %v", i, result) - continue - } - - // check errors - if len(errs) > len(s.expectedErrors) { - t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) - } - for _, k := range s.expectedErrors { - if _, ok := errs[k]; !ok { - t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) - } - } - } -} - -func TestEmailTemplateResolve(t *testing.T) { - allPlaceholders := settings.EmailPlaceholderActionUrl + settings.EmailPlaceholderToken + settings.EmailPlaceholderAppName + settings.EmailPlaceholderAppUrl - - scenarios := []struct { - emailTemplate settings.EmailTemplate - expectedSubject string - expectedBody string - expectedActionUrl string - }{ - // no placeholders - { - emailTemplate: settings.EmailTemplate{ - Subject: "subject:", - Body: "body:", - ActionUrl: "/actionUrl////", - }, - expectedSubject: "subject:", - expectedActionUrl: "/actionUrl/", - expectedBody: "body:", - }, - // with placeholders - { - emailTemplate: settings.EmailTemplate{ - ActionUrl: "/actionUrl////" + allPlaceholders, - Subject: "subject:" + allPlaceholders, - Body: "body:" + allPlaceholders, - }, - expectedActionUrl: fmt.Sprintf( - "/actionUrl/%%7BACTION_URL%%7D%s%s%s", - "token_test", - "name_test", - "url_test", - ), - expectedSubject: fmt.Sprintf( - "subject:%s%s%s%s", - settings.EmailPlaceholderActionUrl, - settings.EmailPlaceholderToken, - "name_test", - "url_test", - ), - expectedBody: fmt.Sprintf( - "body:%s%s%s%s", - fmt.Sprintf( - "/actionUrl/%%7BACTION_URL%%7D%s%s%s", - "token_test", - "name_test", - "url_test", - ), - "token_test", - "name_test", - "url_test", - ), - }, - } - - for i, s := range scenarios { - subject, body, actionUrl := s.emailTemplate.Resolve("name_test", "url_test", "token_test") - - if s.expectedSubject != subject { - t.Errorf("(%d) Expected subject %q got %q", i, s.expectedSubject, subject) - } - - if s.expectedBody != body { - t.Errorf("(%d) Expected body \n%v got \n%v", i, s.expectedBody, body) - } - - if s.expectedActionUrl != actionUrl { - t.Errorf("(%d) Expected actionUrl \n%v got \n%v", i, s.expectedActionUrl, actionUrl) - } - } -} - -func TestLogsConfigValidate(t *testing.T) { - scenarios := []struct { - config settings.LogsConfig - expectError bool - }{ - // zero values - { - settings.LogsConfig{}, - false, - }, - // invalid data - { - settings.LogsConfig{MaxDays: -10}, - true, - }, - // valid data - { - settings.LogsConfig{MaxDays: 1}, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestAuthProviderConfigValidate(t *testing.T) { - scenarios := []struct { - config settings.AuthProviderConfig - expectError bool - }{ - // zero values (disabled) - { - settings.AuthProviderConfig{}, - false, - }, - // zero values (enabled) - { - settings.AuthProviderConfig{Enabled: true}, - true, - }, - // invalid data - { - settings.AuthProviderConfig{ - Enabled: true, - ClientId: "", - ClientSecret: "", - AuthUrl: "test", - TokenUrl: "test", - UserApiUrl: "test", - }, - true, - }, - // valid data (only the required) - { - settings.AuthProviderConfig{ - Enabled: true, - ClientId: "test", - ClientSecret: "test", - }, - false, - }, - // valid data (fill all fields) - { - settings.AuthProviderConfig{ - Enabled: true, - ClientId: "test", - ClientSecret: "test", - DisplayName: "test", - PKCE: types.Pointer(true), - AuthUrl: "https://example.com", - TokenUrl: "https://example.com", - UserApiUrl: "https://example.com", - }, - false, - }, - } - - for i, scenario := range scenarios { - result := scenario.config.Validate() - - if result != nil && !scenario.expectError { - t.Errorf("(%d) Didn't expect error, got %v", i, result) - } - - if result == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } - } -} - -func TestAuthProviderConfigSetupProvider(t *testing.T) { - provider := auth.NewGithubProvider() - - // disabled config - c1 := settings.AuthProviderConfig{Enabled: false} - if err := c1.SetupProvider(provider); err == nil { - t.Errorf("Expected error, got nil") - } - - c2 := settings.AuthProviderConfig{ - Enabled: true, - ClientId: "test_ClientId", - ClientSecret: "test_ClientSecret", - AuthUrl: "test_AuthUrl", - UserApiUrl: "test_UserApiUrl", - TokenUrl: "test_TokenUrl", - DisplayName: "test_DisplayName", - PKCE: types.Pointer(true), - } - if err := c2.SetupProvider(provider); err != nil { - t.Error(err) - } - - if provider.ClientId() != c2.ClientId { - t.Fatalf("Expected ClientId %s, got %s", c2.ClientId, provider.ClientId()) - } - - if provider.ClientSecret() != c2.ClientSecret { - t.Fatalf("Expected ClientSecret %s, got %s", c2.ClientSecret, provider.ClientSecret()) - } - - if provider.AuthUrl() != c2.AuthUrl { - t.Fatalf("Expected AuthUrl %s, got %s", c2.AuthUrl, provider.AuthUrl()) - } - - if provider.UserApiUrl() != c2.UserApiUrl { - t.Fatalf("Expected UserApiUrl %s, got %s", c2.UserApiUrl, provider.UserApiUrl()) - } - - if provider.TokenUrl() != c2.TokenUrl { - t.Fatalf("Expected TokenUrl %s, got %s", c2.TokenUrl, provider.TokenUrl()) - } - - if provider.DisplayName() != c2.DisplayName { - t.Fatalf("Expected DisplayName %s, got %s", c2.DisplayName, provider.DisplayName()) - } - - if provider.PKCE() != *c2.PKCE { - t.Fatalf("Expected PKCE %v, got %v", *c2.PKCE, provider.PKCE()) - } -} diff --git a/models/table_info.go b/models/table_info.go deleted file mode 100644 index ef62bf88..00000000 --- a/models/table_info.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "github.com/pocketbase/pocketbase/tools/types" - -type TableInfoRow struct { - // the `db:"pk"` tag has special semantic so we cannot rename - // the original field without specifying a custom mapper - PK int - - Index int `db:"cid"` - Name string `db:"name"` - Type string `db:"type"` - NotNull bool `db:"notnull"` - DefaultValue types.JsonRaw `db:"dflt_value"` -} diff --git a/plugins/ghupdate/ghupdate.go b/plugins/ghupdate/ghupdate.go index f85a771c..6d98ac34 100644 --- a/plugins/ghupdate/ghupdate.go +++ b/plugins/ghupdate/ghupdate.go @@ -252,6 +252,7 @@ func (p *plugin) update(withBackup bool) error { fmt.Print("\n") color.Cyan("Here is a list with some of the %s changes:", latest.Tag) // remove the update command note to avoid "stuttering" + // (@todo consider moving to a config option) releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1)) color.Cyan(releaseNotes) fmt.Print("\n") diff --git a/plugins/ghupdate/ghupdate_test.go b/plugins/ghupdate/ghupdate_test.go index fc6bb97c..e692cc8e 100644 --- a/plugins/ghupdate/ghupdate_test.go +++ b/plugins/ghupdate/ghupdate_test.go @@ -26,11 +26,13 @@ func TestCompareVersions(t *testing.T) { {"3.2.4", "3.2.3", -1}, } - for i, s := range scenarios { - result := compareVersions(s.a, s.b) + for _, s := range scenarios { + t.Run(s.a+"VS"+s.b, func(t *testing.T) { + result := compareVersions(s.a, s.b) - if result != s.expected { - t.Fatalf("[%d] Expected %q vs %q to result in %d, got %d", i, s.a, s.b, s.expected, result) - } + if result != s.expected { + t.Fatalf("Expected %q vs %q to result in %d, got %d", s.a, s.b, s.expected, result) + } + }) } } diff --git a/plugins/jsvm/binds.go b/plugins/jsvm/binds.go index d85962a0..68b4799a 100644 --- a/plugins/jsvm/binds.go +++ b/plugins/jsvm/binds.go @@ -12,29 +12,23 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strings" "time" "github.com/dop251/goja" validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" + "github.com/golang-jwt/jwt/v4" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/mails" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tokens" - "github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/inflector" - "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/security" "github.com/pocketbase/pocketbase/tools/subscriptions" "github.com/pocketbase/pocketbase/tools/types" @@ -49,11 +43,11 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { appType := reflect.TypeOf(app) appValue := reflect.ValueOf(app) totalMethods := appType.NumMethod() - excludeHooks := []string{"OnBeforeServe"} + excludeHooks := []string{"OnServe"} for i := 0; i < totalMethods; i++ { method := appType.Method(i) - if !strings.HasPrefix(method.Name, "On") || list.ExistInSlice(method.Name, excludeHooks) { + if !strings.HasPrefix(method.Name, "On") || slices.Contains(excludeHooks, method.Name) { continue // not a hook or excluded } @@ -69,9 +63,9 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { } hookInstance := appValue.MethodByName(method.Name).Call(tagsAsValues)[0] - addFunc := hookInstance.MethodByName("Add") + hookBindFunc := hookInstance.MethodByName("BindFunc") - handlerType := addFunc.Type().In(0) + handlerType := hookBindFunc.Type().In(0) handler := reflect.MakeFunc(handlerType, func(args []reflect.Value) (results []reflect.Value) { handlerArgs := make([]any, len(args)) @@ -84,15 +78,10 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { res, err := executor.RunProgram(pr) executor.Set("__args", goja.Undefined()) - // check for returned error or false + // check for returned error value if res != nil { - switch v := res.Export().(type) { - case error: - return v - case bool: - if !v { - return hook.StopPropagation - } + if resErr, ok := res.Export().(error); ok { + return resErr } } @@ -103,20 +92,16 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { }) // register the wrapped hook handler - addFunc.Call([]reflect.Value{handler}) + hookBindFunc.Call([]reflect.Value{handler}) }) } } func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { - scheduler := cron.New() - - var wasServeTriggered bool - loader.Set("cronAdd", func(jobId, cronExpr, handler string) { pr := goja.MustCompile("", "{("+handler+").apply(undefined)}", true) - err := scheduler.Add(jobId, cronExpr, func() { + err := app.Cron().Add(jobId, cronExpr, func() { err := executors.run(func(executor *goja.Runtime) error { _, err := executor.RunProgram(pr) return err @@ -133,32 +118,29 @@ func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { if err != nil { panic("[cronAdd] failed to register cron job " + jobId + ": " + err.Error()) } - - // start the ticker (if not already) - if wasServeTriggered && scheduler.Total() > 0 && !scheduler.HasStarted() { - scheduler.Start() - } }) + // note: it is not necessary needed but it is here for consistency loader.Set("cronRemove", func(jobId string) { - scheduler.Remove(jobId) - - // stop the ticker if there are no other jobs - if scheduler.Total() == 0 { - scheduler.Stop() - } + app.Cron().Remove(jobId) }) - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - // start the ticker (if not already) - if scheduler.Total() > 0 && !scheduler.HasStarted() { - scheduler.Start() - } + // register the removal helper also in the executors to allow removing cron jobs from everywhere + oldFactory := executors.factory + executors.factory = func() *goja.Runtime { + vm := oldFactory() - wasServeTriggered = true + vm.Set("cronRemove", func(jobId string) { + app.Cron().Remove(jobId) + }) - return nil - }) + return vm + } + for _, item := range executors.items { + item.vm.Set("cronRemove", func(jobId string) { + app.Cron().Remove(jobId) + }) + } } func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { @@ -168,15 +150,15 @@ func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { panic("[routerAdd] failed to wrap middlewares: " + err.Error()) } - wrappedHandler, err := wrapHandler(executors, handler) + wrappedHandler, err := wrapHandlerFunc(executors, handler) if err != nil { panic("[routerAdd] failed to wrap handler: " + err.Error()) } - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.Add(strings.ToUpper(method), path, wrappedHandler, wrappedMiddlewares...) + app.OnServe().BindFunc(func(e *core.ServeEvent) error { + e.Router.Route(strings.ToUpper(method), path, wrappedHandler).Bind(wrappedMiddlewares...) - return nil + return e.Next() }) }) @@ -186,40 +168,28 @@ func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) { panic("[routerUse] failed to wrap middlewares: " + err.Error()) } - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.Use(wrappedMiddlewares...) - return nil - }) - }) - - loader.Set("routerPre", func(middlewares ...goja.Value) { - wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...) - if err != nil { - panic("[routerPre] failed to wrap middlewares: " + err.Error()) - } - - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.Pre(wrappedMiddlewares...) - return nil + app.OnServe().BindFunc(func(e *core.ServeEvent) error { + e.Router.Bind(wrappedMiddlewares...) + return e.Next() }) }) } -func wrapHandler(executors *vmsPool, handler goja.Value) (echo.HandlerFunc, error) { +func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (hook.HandlerFunc[*core.RequestEvent], error) { if handler == nil { return nil, errors.New("handler must be non-nil") } switch h := handler.Export().(type) { - case echo.HandlerFunc: - // "native" handler - no need to wrap + case hook.HandlerFunc[*core.RequestEvent]: + // "native" handler func - no need to wrap return h, nil case func(goja.FunctionCall) goja.Value, string: pr := goja.MustCompile("", "{("+handler.String()+").apply(undefined, __args)}", true) - wrappedHandler := func(c echo.Context) error { + wrappedHandler := func(e *core.RequestEvent) error { return executors.run(func(executor *goja.Runtime) error { - executor.Set("__args", []any{c}) + executor.Set("__args", []any{e}) res, err := executor.RunProgram(pr) executor.Set("__args", goja.Undefined()) @@ -240,29 +210,44 @@ func wrapHandler(executors *vmsPool, handler goja.Value) (echo.HandlerFunc, erro } } -func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.MiddlewareFunc, error) { - wrappedMiddlewares := make([]echo.MiddlewareFunc, len(rawMiddlewares)) +type gojaHookHandler struct { + priority int + id string + serializedFunc string +} + +func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.Handler[*core.RequestEvent], error) { + wrappedMiddlewares := make([]*hook.Handler[*core.RequestEvent], len(rawMiddlewares)) for i, m := range rawMiddlewares { if m == nil { - return nil, errors.New("middleware func must be non-nil") + return nil, errors.New("middleware must be non-nil") } switch v := m.Export().(type) { - case echo.MiddlewareFunc: - // "native" middleware - no need to wrap + case *hook.Handler[*core.RequestEvent]: + // "native" middleware handler - no need to wrap wrappedMiddlewares[i] = v - case func(goja.FunctionCall) goja.Value, string: - pr := goja.MustCompile("", "{(("+m.String()+").apply(undefined, __args)).apply(undefined, __args2)}", true) + case hook.HandlerFunc[*core.RequestEvent]: + // "native" middleware func - wrap as handler + wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{ + Func: v, + } + case *gojaHookHandler: + if v.serializedFunc == "" { + return nil, errors.New("missing or invalid Middleware function") + } - wrappedMiddlewares[i] = func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + pr := goja.MustCompile("", "{("+v.serializedFunc+").apply(undefined, __args)}", true) + + wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{ + Id: v.id, + Priority: v.priority, + Func: func(e *core.RequestEvent) error { return executors.run(func(executor *goja.Runtime) error { - executor.Set("__args", []any{next}) - executor.Set("__args2", []any{c}) + executor.Set("__args", []any{e}) res, err := executor.RunProgram(pr) executor.Set("__args", goja.Undefined()) - executor.Set("__args2", goja.Undefined()) // check for returned error if res != nil { @@ -273,7 +258,28 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.M return err }) - } + }, + } + case func(goja.FunctionCall) goja.Value, string: + pr := goja.MustCompile("", "{("+m.String()+").apply(undefined, __args)}", true) + + wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{ + Func: func(e *core.RequestEvent) error { + return executors.run(func(executor *goja.Runtime) error { + executor.Set("__args", []any{e}) + res, err := executor.RunProgram(pr) + executor.Set("__args", goja.Undefined()) + + // check for returned error + if res != nil { + if v, ok := res.Export().(error); ok { + return v + } + } + + return err + }) + }, } default: return nil, errors.New("unsupported goja middleware type") @@ -286,9 +292,10 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]echo.M func baseBinds(vm *goja.Runtime) { vm.SetFieldNameMapper(FieldMapper{}) + // deprecated: use toString vm.Set("readerToString", func(r io.Reader, maxBytes int) (string, error) { if maxBytes == 0 { - maxBytes = rest.DefaultMaxMemory + maxBytes = router.DefaultMaxMemory } limitReader := io.LimitReader(r, int64(maxBytes)) @@ -301,6 +308,34 @@ func baseBinds(vm *goja.Runtime) { return string(bodyBytes), nil }) + vm.Set("toString", func(raw any, maxReaderBytes int) (string, error) { + switch v := raw.(type) { + case io.Reader: + if maxReaderBytes == 0 { + maxReaderBytes = router.DefaultMaxMemory + } + + limitReader := io.LimitReader(v, int64(maxReaderBytes)) + + bodyBytes, readErr := io.ReadAll(limitReader) + if readErr != nil { + return "", readErr + } + + return string(bodyBytes), nil + default: + str, err := cast.ToStringE(v) + if err == nil { + return str, nil + } + + // as a last attempt try to json encode the value + rawBytes, _ := json.Marshal(raw) + + return string(rawBytes), nil + } + }) + vm.Set("sleep", func(milliseconds int64) { time.Sleep(time.Duration(milliseconds) * time.Millisecond) }) @@ -313,6 +348,15 @@ func baseBinds(vm *goja.Runtime) { return elem.Addr().Interface() }) + vm.Set("unmarshal", func(data, dst any) error { + raw, err := json.Marshal(data) + if err != nil { + return err + } + + return json.Unmarshal(raw, &dst) + }) + vm.Set("DynamicModel", func(call goja.ConstructorCall) *goja.Object { shape, ok := call.Argument(0).Export().(map[string]any) if !ok || len(shape) == 0 { @@ -327,17 +371,17 @@ func baseBinds(vm *goja.Runtime) { }) vm.Set("Record", func(call goja.ConstructorCall) *goja.Object { - var instance *models.Record + var instance *core.Record - collection, ok := call.Argument(0).Export().(*models.Collection) + collection, ok := call.Argument(0).Export().(*core.Collection) if ok { - instance = models.NewRecord(collection) + instance = core.NewRecord(collection) data, ok := call.Argument(1).Export().(map[string]any) if ok { instance.Load(data) } } else { - instance = &models.Record{} + instance = &core.Record{} } instanceValue := vm.ToValue(instance).(*goja.Object) @@ -347,24 +391,91 @@ func baseBinds(vm *goja.Runtime) { }) vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object { - instance := &models.Collection{} + 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{} return structConstructorUnmarshal(vm, call, instance) }) - vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object { - instance := &models.Admin{} - return structConstructorUnmarshal(vm, call, instance) - }) + // fields + // --- + vm.Set("Field", func(call goja.ConstructorCall) *goja.Object { + data, _ := call.Argument(0).Export().(map[string]any) + rawDataSlice, _ := json.Marshal([]any{data}) - vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object { - instance := &schema.Schema{} - return structConstructorUnmarshal(vm, call, instance) - }) + fieldsList := core.NewFieldsList() + _ = fieldsList.UnmarshalJSON(rawDataSlice) - vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object { - instance := &schema.SchemaField{} + if len(fieldsList) == 0 { + return nil + } + + field := fieldsList[0] + + fieldValue := vm.ToValue(field).(*goja.Object) + fieldValue.SetPrototype(call.This.Prototype()) + + return fieldValue + }) + vm.Set("NumberField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.NumberField{} return structConstructorUnmarshal(vm, call, instance) }) + vm.Set("BoolField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.BoolField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("TextField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.TextField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("URLField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.URLField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("EmailField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.EmailField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("EditorField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.EditorField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("PasswordField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.PasswordField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("DateField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.DateField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("AutodateField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.AutodateField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("JSONField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.JSONField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("RelationField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.RelationField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("SelectField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.SelectField{} + return structConstructorUnmarshal(vm, call, instance) + }) + vm.Set("FileField", func(call goja.ConstructorCall) *goja.Object { + instance := &core.FileField{} + return structConstructorUnmarshal(vm, call, instance) + }) + // --- vm.Set("MailerMessage", func(call goja.ConstructorCall) *goja.Object { instance := &mailer.Message{} @@ -377,10 +488,28 @@ func baseBinds(vm *goja.Runtime) { }) vm.Set("RequestInfo", func(call goja.ConstructorCall) *goja.Object { - instance := &models.RequestInfo{Context: models.RequestInfoContextDefault} + instance := &core.RequestInfo{Context: core.RequestInfoContextDefault} return structConstructor(vm, call, instance) }) + // ```js + // new Middleware((e) => { + // return e.next() + // }, 100, "example_middleware") + // ``` + vm.Set("Middleware", func(call goja.ConstructorCall) *goja.Object { + instance := &gojaHookHandler{} + + instance.serializedFunc = call.Argument(0).String() + instance.priority = cast.ToInt(call.Argument(1).Export()) + instance.id = cast.ToString(call.Argument(2).Export()) + + instanceValue := vm.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + + return instanceValue + }) + vm.Set("DateTime", func(call goja.ConstructorCall) *goja.Object { instance := types.NowDateTime() @@ -406,24 +535,6 @@ func baseBinds(vm *goja.Runtime) { return instanceValue }) - vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object { - concurrentDB, _ := call.Argument(0).Export().(dbx.Builder) - if concurrentDB == nil { - panic("[Dao] missing required Dao(concurrentDB, [nonconcurrentDB]) argument") - } - - nonConcurrentDB, _ := call.Argument(1).Export().(dbx.Builder) - if nonConcurrentDB == nil { - nonConcurrentDB = concurrentDB - } - - instance := daos.NewMultiDB(concurrentDB, nonConcurrentDB) - instanceValue := vm.ToValue(instance).(*goja.Object) - instanceValue.SetPrototype(call.This.Prototype()) - - return instanceValue - }) - vm.Set("Cookie", func(call goja.ConstructorCall) *goja.Object { instance := &http.Cookie{} return structConstructor(vm, call, instance) @@ -462,30 +573,10 @@ func mailsBinds(vm *goja.Runtime) { obj := vm.NewObject() vm.Set("$mails", obj) - // admin - obj.Set("sendAdminPasswordReset", mails.SendAdminPasswordReset) - - // record obj.Set("sendRecordPasswordReset", mails.SendRecordPasswordReset) obj.Set("sendRecordVerification", mails.SendRecordVerification) obj.Set("sendRecordChangeEmail", mails.SendRecordChangeEmail) -} - -func tokensBinds(vm *goja.Runtime) { - obj := vm.NewObject() - vm.Set("$tokens", obj) - - // admin - obj.Set("adminAuthToken", tokens.NewAdminAuthToken) - obj.Set("adminResetPasswordToken", tokens.NewAdminResetPasswordToken) - obj.Set("adminFileToken", tokens.NewAdminFileToken) - - // record - obj.Set("recordAuthToken", tokens.NewRecordAuthToken) - obj.Set("recordVerifyToken", tokens.NewRecordVerifyToken) - obj.Set("recordResetPasswordToken", tokens.NewRecordResetPasswordToken) - obj.Set("recordChangeEmailToken", tokens.NewRecordChangeEmailToken) - obj.Set("recordFileToken", tokens.NewRecordFileToken) + obj.Set("sendRecordOTP", mails.SendRecordOTP) } func securityBinds(vm *goja.Runtime) { @@ -502,6 +593,7 @@ func securityBinds(vm *goja.Runtime) { // random obj.Set("randomString", security.RandomString) + obj.Set("randomStringByRegex", security.RandomStringByRegex) obj.Set("randomStringWithAlphabet", security.RandomStringWithAlphabet) obj.Set("pseudorandomString", security.PseudorandomString) obj.Set("pseudorandomStringWithAlphabet", security.PseudorandomStringWithAlphabet) @@ -513,7 +605,9 @@ func securityBinds(vm *goja.Runtime) { obj.Set("parseJWT", func(token string, verificationKey string) (map[string]any, error) { return security.ParseJWT(token, verificationKey) }) - obj.Set("createJWT", security.NewJWT) + obj.Set("createJWT", func(payload jwt.MapClaims, signingKey string, secDuration int) (string, error) { + return security.NewJWT(payload, signingKey, time.Duration(secDuration)*time.Second) + }) // encryption obj.Set("encrypt", security.Encrypt) @@ -535,7 +629,7 @@ func filesystemBinds(vm *goja.Runtime) { obj.Set("fileFromPath", filesystem.NewFileFromPath) obj.Set("fileFromBytes", filesystem.NewFileFromBytes) obj.Set("fileFromMultipart", filesystem.NewFileFromMultipart) - obj.Set("fileFromUrl", func(url string, secTimeout int) (*filesystem.File, error) { + obj.Set("fileFromURL", func(url string, secTimeout int) (*filesystem.File, error) { if secTimeout == 0 { secTimeout = 120 } @@ -543,7 +637,7 @@ func filesystemBinds(vm *goja.Runtime) { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(secTimeout)*time.Second) defer cancel() - return filesystem.NewFileFromUrl(ctx, url) + return filesystem.NewFileFromURL(ctx, url) }) } @@ -592,24 +686,8 @@ func osBinds(vm *goja.Runtime) { } func formsBinds(vm *goja.Runtime) { - registerFactoryAsConstructor(vm, "AdminLoginForm", forms.NewAdminLogin) - registerFactoryAsConstructor(vm, "AdminPasswordResetConfirmForm", forms.NewAdminPasswordResetConfirm) - registerFactoryAsConstructor(vm, "AdminPasswordResetRequestForm", forms.NewAdminPasswordResetRequest) - registerFactoryAsConstructor(vm, "AdminUpsertForm", forms.NewAdminUpsert) registerFactoryAsConstructor(vm, "AppleClientSecretCreateForm", forms.NewAppleClientSecretCreate) - registerFactoryAsConstructor(vm, "CollectionUpsertForm", forms.NewCollectionUpsert) - registerFactoryAsConstructor(vm, "CollectionsImportForm", forms.NewCollectionsImport) - registerFactoryAsConstructor(vm, "RealtimeSubscribeForm", forms.NewRealtimeSubscribe) - registerFactoryAsConstructor(vm, "RecordEmailChangeConfirmForm", forms.NewRecordEmailChangeConfirm) - registerFactoryAsConstructor(vm, "RecordEmailChangeRequestForm", forms.NewRecordEmailChangeRequest) - registerFactoryAsConstructor(vm, "RecordOAuth2LoginForm", forms.NewRecordOAuth2Login) - registerFactoryAsConstructor(vm, "RecordPasswordLoginForm", forms.NewRecordPasswordLogin) - registerFactoryAsConstructor(vm, "RecordPasswordResetConfirmForm", forms.NewRecordPasswordResetConfirm) - registerFactoryAsConstructor(vm, "RecordPasswordResetRequestForm", forms.NewRecordPasswordResetRequest) registerFactoryAsConstructor(vm, "RecordUpsertForm", forms.NewRecordUpsert) - registerFactoryAsConstructor(vm, "RecordVerificationConfirmForm", forms.NewRecordVerificationConfirm) - registerFactoryAsConstructor(vm, "RecordVerificationRequestForm", forms.NewRecordVerificationRequest) - registerFactoryAsConstructor(vm, "SettingsUpsertForm", forms.NewSettingsUpsert) registerFactoryAsConstructor(vm, "TestEmailSendForm", forms.NewTestEmailSend) registerFactoryAsConstructor(vm, "TestS3FilesystemForm", forms.NewTestS3Filesystem) } @@ -618,33 +696,33 @@ func apisBinds(vm *goja.Runtime) { obj := vm.NewObject() vm.Set("$apis", obj) - obj.Set("staticDirectoryHandler", func(dir string, indexFallback bool) echo.HandlerFunc { - return apis.StaticDirectoryHandler(os.DirFS(dir), indexFallback) + obj.Set("static", func(dir string, indexFallback bool) hook.HandlerFunc[*core.RequestEvent] { + return apis.Static(os.DirFS(dir), indexFallback) }) // middlewares obj.Set("requireGuestOnly", apis.RequireGuestOnly) - obj.Set("requireRecordAuth", apis.RequireRecordAuth) - obj.Set("requireAdminAuth", apis.RequireAdminAuth) - obj.Set("requireAdminAuthOnlyIfAny", apis.RequireAdminAuthOnlyIfAny) - obj.Set("requireAdminOrRecordAuth", apis.RequireAdminOrRecordAuth) - obj.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth) - obj.Set("activityLogger", apis.ActivityLogger) - obj.Set("gzip", middleware.Gzip) - obj.Set("bodyLimit", middleware.BodyLimit) + obj.Set("requireAuth", apis.RequireAuth) + obj.Set("requireSuperuserAuth", apis.RequireSuperuserAuth) + obj.Set("requireSuperuserAuthOnlyIfAny", apis.RequireSuperuserAuthOnlyIfAny) + obj.Set("requireSuperuserOrOwnerAuth", apis.RequireSuperuserOrOwnerAuth) + obj.Set("skipSuccessActivityLog", apis.SkipSuccessActivityLog) + obj.Set("gzip", apis.Gzip) + obj.Set("bodyLimit", apis.BodyLimit) // record helpers - obj.Set("requestInfo", apis.RequestInfo) obj.Set("recordAuthResponse", apis.RecordAuthResponse) obj.Set("enrichRecord", apis.EnrichRecord) obj.Set("enrichRecords", apis.EnrichRecords) // api errors - registerFactoryAsConstructor(vm, "ApiError", apis.NewApiError) - registerFactoryAsConstructor(vm, "NotFoundError", apis.NewNotFoundError) - registerFactoryAsConstructor(vm, "BadRequestError", apis.NewBadRequestError) - registerFactoryAsConstructor(vm, "ForbiddenError", apis.NewForbiddenError) - registerFactoryAsConstructor(vm, "UnauthorizedError", apis.NewUnauthorizedError) + registerFactoryAsConstructor(vm, "ApiError", router.NewApiError) + registerFactoryAsConstructor(vm, "NotFoundError", router.NewNotFoundError) + registerFactoryAsConstructor(vm, "BadRequestError", router.NewBadRequestError) + registerFactoryAsConstructor(vm, "ForbiddenError", router.NewForbiddenError) + registerFactoryAsConstructor(vm, "UnauthorizedError", router.NewUnauthorizedError) + registerFactoryAsConstructor(vm, "TooManyRequestsError", router.NewTooManyRequestsError) + registerFactoryAsConstructor(vm, "InternalServerError", router.NewInternalServerError) } func httpClientBinds(vm *goja.Runtime) { @@ -661,7 +739,7 @@ func httpClientBinds(vm *goja.Runtime) { }) type sendResult struct { - Json any `json:"json"` + JSON any `json:"json"` Headers map[string][]string `json:"headers"` Cookies map[string]*http.Cookie `json:"cookies"` Raw string `json:"raw"` @@ -727,6 +805,8 @@ func httpClientBinds(vm *goja.Runtime) { reqBody = bytes.NewReader(encoded) } else { switch v := config.Body.(type) { + case io.Reader: + reqBody = v case FormData: body, mp, err := v.toMultipart() if err != nil { @@ -755,13 +835,6 @@ func httpClientBinds(vm *goja.Runtime) { req.Header.Set("content-type", contentType) } - // @todo consider removing during the refactoring - // - // fallback to json content-type - if req.Header.Get("content-type") == "" { - req.Header.Set("content-type", "application/json") - } - res, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -787,12 +860,12 @@ func httpClientBinds(vm *goja.Runtime) { if len(result.Raw) != 0 { // try as map - result.Json = map[string]any{} - if err := json.Unmarshal(bodyRaw, &result.Json); err != nil { + result.JSON = map[string]any{} + if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil { // try as slice - result.Json = []any{} - if err := json.Unmarshal(bodyRaw, &result.Json); err != nil { - result.Json = nil + result.JSON = []any{} + if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil { + result.JSON = nil } } } @@ -864,7 +937,7 @@ func structConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object { if data := call.Argument(0).Export(); data != nil { if raw, err := json.Marshal(data); err == nil { - json.Unmarshal(raw, instance) + _ = json.Unmarshal(raw, instance) } } @@ -893,13 +966,13 @@ func newDynamicModel(shape map[string]any) any { switch kind := vt.Kind(); kind { case reflect.Map: raw, _ := json.Marshal(v) - newV := types.JsonMap{} + newV := types.JSONMap[any]{} newV.Scan(raw) v = newV vt = reflect.TypeOf(v) case reflect.Slice, reflect.Array: raw, _ := json.Marshal(v) - newV := types.JsonArray[any]{} + newV := types.JSONArray[any]{} newV.Scan(raw) v = newV vt = reflect.TypeOf(newV) diff --git a/plugins/jsvm/binds_test.go b/plugins/jsvm/binds_test.go index d2568dc6..faeaf8ee 100644 --- a/plugins/jsvm/binds_test.go +++ b/plugins/jsvm/binds_test.go @@ -15,16 +15,12 @@ import ( "github.com/dop251/goja" validation "github.com/go-ozzo/ozzo-validation/v4" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/router" "github.com/spf13/cast" ) @@ -47,13 +43,10 @@ func TestBaseBindsCount(t *testing.T) { vm := goja.New() baseBinds(vm) - testBindsCount(vm, "this", 17, t) + testBindsCount(vm, "this", 34, t) } func TestBaseBindsSleep(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) vm.Set("reader", strings.NewReader("test")) @@ -73,9 +66,6 @@ func TestBaseBindsSleep(t *testing.T) { } func TestBaseBindsReaderToString(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) vm.Set("reader", strings.NewReader("test")) @@ -92,10 +82,63 @@ func TestBaseBindsReaderToString(t *testing.T) { } } -func TestBaseBindsCookie(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() +func TestBaseBindsToString(t *testing.T) { + vm := goja.New() + baseBinds(vm) + vm.Set("scenarios", []struct { + Name string + Value any + Expected string + }{ + {"null", nil, ""}, + {"string", "test", "test"}, + {"number", -12.4, "-12.4"}, + {"bool", true, "true"}, + {"arr", []int{1, 2, 3}, `[1,2,3]`}, + {"obj", map[string]any{"test": 123}, `{"test":123}`}, + {"reader", strings.NewReader("test"), "test"}, + {"struct", struct { + Name string + private string + }{Name: "123", private: "456"}, `{"Name":"123"}`}, + }) + _, err := vm.RunString(` + for (let s of scenarios) { + let result = toString(s.value) + + if (result != s.expected) { + throw new Error('[' + s.name + '] Expected string ' + s.expected + ', got ' + result); + } + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestBaseBindsUnmarshal(t *testing.T) { + vm := goja.New() + baseBinds(vm) + vm.Set("data", &map[string]any{"a": 123}) + + _, err := vm.RunString(` + unmarshal({"b": 456}, data) + + if (data.a != 123) { + throw new Error('Expected data.a 123, got ' + data.a); + } + + if (data.b != 456) { + throw new Error('Expected data.b 456, got ' + data.b); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestBaseBindsCookie(t *testing.T) { vm := goja.New() baseBinds(vm) @@ -125,9 +168,6 @@ func TestBaseBindsCookie(t *testing.T) { } func TestBaseBindsSubscriptionMessage(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) vm.Set("bytesToString", func(b []byte) string { @@ -159,7 +199,7 @@ func TestBaseBindsRecord(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - collection, err := app.Dao().FindCollectionByNameOrId("users") + collection, err := app.FindCachedCollectionByNameOrId("users") if err != nil { t.Fatal(err) } @@ -175,7 +215,7 @@ func TestBaseBindsRecord(t *testing.T) { t.Fatal(err) } - m1, ok := v1.Export().(*models.Record) + m1, ok := v1.Export().(*core.Record) if !ok { t.Fatalf("Expected m1 to be models.Record, got \n%v", m1) } @@ -187,9 +227,9 @@ func TestBaseBindsRecord(t *testing.T) { t.Fatal(err) } - m2, ok := v2.Export().(*models.Record) + m2, ok := v2.Export().(*core.Record) if !ok { - t.Fatalf("Expected m2 to be models.Record, got \n%v", m2) + t.Fatalf("Expected m2 to be core.Record, got \n%v", m2) } if m2.Collection().Name != "users" { @@ -205,14 +245,14 @@ func TestBaseBindsCollection(t *testing.T) { vm := goja.New() baseBinds(vm) - v, err := vm.RunString(`new Collection({ name: "test", createRule: "@request.auth.id != ''", schema: [{name: "title", "type": "text"}] })`) + v, err := vm.RunString(`new Collection({ name: "test", createRule: "@request.auth.id != ''", fields: [{name: "title", "type": "text"}] })`) if err != nil { t.Fatal(err) } - m, ok := v.Export().(*models.Collection) + m, ok := v.Export().(*core.Collection) if !ok { - t.Fatalf("Expected models.Collection, got %v", m) + t.Fatalf("Expected core.Collection, got %v", m) } if m.Name != "test" { @@ -224,61 +264,174 @@ func TestBaseBindsCollection(t *testing.T) { t.Fatalf("Expected create rule %q, got %v", "@request.auth.id != ''", m.CreateRule) } - if f := m.Schema.GetFieldByName("title"); f == nil { - t.Fatalf("Expected schema to be set, got %v", m.Schema) + if f := m.Fields.GetByName("title"); f == nil { + t.Fatalf("Expected fields to be set, got %v", m.Fields) } } -func TestBaseVMAdminBind(t *testing.T) { +func TestBaseBindsCollectionFactories(t *testing.T) { vm := goja.New() baseBinds(vm) - v, err := vm.RunString(`new Admin({ email: "test@example.com" })`) + 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) + + v, err := vm.RunString(`new FieldsList([{name: "title", "type": "text"}])`) if err != nil { t.Fatal(err) } - m, ok := v.Export().(*models.Admin) + m, ok := v.Export().(*core.FieldsList) if !ok { - t.Fatalf("Expected models.Admin, got %v", m) + t.Fatalf("Expected core.FieldsList, got %v", m) + } + + if f := m.GetByName("title"); f == nil { + t.Fatalf("Expected fields list to be loaded, got %v", m) } } -func TestBaseBindsSchema(t *testing.T) { +func TestBaseBindsField(t *testing.T) { vm := goja.New() baseBinds(vm) - v, err := vm.RunString(`new Schema([{name: "title", "type": "text"}])`) + v, err := vm.RunString(`new Field({name: "test", "type": "bool"})`) if err != nil { t.Fatal(err) } - m, ok := v.Export().(*schema.Schema) + f, ok := v.Export().(*core.BoolField) if !ok { - t.Fatalf("Expected schema.Schema, got %v", m) + t.Fatalf("Expected *core.BoolField, got %v", f) } - if f := m.GetFieldByName("title"); f == nil { - t.Fatalf("Expected schema fields to be loaded, got %v", m.Fields()) + if f.Name != "test" { + t.Fatalf("Expected field %q, got %v", "test", f) } } -func TestBaseBindsSchemaField(t *testing.T) { +func isType[T any](v any) bool { + _, ok := v.(T) + return ok +} + +func TestBaseBindsNamedFields(t *testing.T) { + t.Parallel() + vm := goja.New() baseBinds(vm) - v, err := vm.RunString(`new SchemaField({name: "title", "type": "text"})`) - if err != nil { - t.Fatal(err) + scenarios := []struct { + js string + typeFunc func(v any) bool + }{ + { + "new NumberField({name: 'test'})", + isType[*core.NumberField], + }, + { + "new BoolField({name: 'test'})", + isType[*core.BoolField], + }, + { + "new TextField({name: 'test'})", + isType[*core.TextField], + }, + { + "new URLField({name: 'test'})", + isType[*core.URLField], + }, + { + "new EmailField({name: 'test'})", + isType[*core.EmailField], + }, + { + "new EditorField({name: 'test'})", + isType[*core.EditorField], + }, + { + "new PasswordField({name: 'test'})", + isType[*core.PasswordField], + }, + { + "new DateField({name: 'test'})", + isType[*core.DateField], + }, + { + "new AutodateField({name: 'test'})", + isType[*core.AutodateField], + }, + { + "new JSONField({name: 'test'})", + isType[*core.JSONField], + }, + { + "new RelationField({name: 'test'})", + isType[*core.RelationField], + }, + { + "new SelectField({name: 'test'})", + isType[*core.SelectField], + }, + { + "new FileField({name: 'test'})", + isType[*core.FileField], + }, } - f, ok := v.Export().(*schema.SchemaField) - if !ok { - t.Fatalf("Expected schema.SchemaField, got %v", f) - } + for _, s := range scenarios { + t.Run(s.js, func(t *testing.T) { + v, err := vm.RunString(s.js) + if err != nil { + t.Fatal(err) + } - if f.Name != "title" { - t.Fatalf("Expected field %q, got %v", "title", f) + f, ok := v.Export().(core.Field) + if !ok { + t.Fatalf("Expected core.Field instance, got %T (%v)", f, f) + } + + if !s.typeFunc(f) { + t.Fatalf("Unexpected field type %T (%v)", f, f) + } + + if f.GetName() != "test" { + t.Fatalf("Expected field %q, got %v", "test", f) + } + }) } } @@ -363,17 +516,32 @@ func TestBaseBindsRequestInfo(t *testing.T) { baseBinds(vm) _, err := vm.RunString(` - let info = new RequestInfo({ - admin: new Admin({id: "test1"}), - data: {"name": "test2"} + const info = new RequestInfo({ + body: {"name": "test2"} }); - if (info.admin?.id != "test1") { - throw new Error('Expected info.admin.id to be test1, got: ' + info.admin?.id); + if (info.body?.name != "test2") { + throw new Error('Expected info.body.name to be test2, got: ' + info.body?.name); } + `) + if err != nil { + t.Fatal(err) + } +} - if (info.data?.name != "test2") { - throw new Error('Expected info.data.name to be test2, got: ' + info.data?.name); +func TestBaseBindsMiddleware(t *testing.T) { + vm := goja.New() + baseBinds(vm) + + _, err := vm.RunString(` + const m = new Middleware( + (e) => {}, + 10, + "test" + ); + + if (!m) { + throw new Error('Expected non-empty Middleware instance'); } `) if err != nil { @@ -449,59 +617,12 @@ func TestBaseBindsValidationError(t *testing.T) { } } -func TestBaseBindsDao(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - vm := goja.New() - baseBinds(vm) - vm.Set("db", app.Dao().ConcurrentDB()) - vm.Set("db2", app.Dao().NonconcurrentDB()) - - scenarios := []struct { - js string - concurrentDB dbx.Builder - nonconcurrentDB dbx.Builder - }{ - { - js: "new Dao(db)", - concurrentDB: app.Dao().ConcurrentDB(), - nonconcurrentDB: app.Dao().ConcurrentDB(), - }, - { - js: "new Dao(db, db2)", - concurrentDB: app.Dao().ConcurrentDB(), - nonconcurrentDB: app.Dao().NonconcurrentDB(), - }, - } - - for _, s := range scenarios { - v, err := vm.RunString(s.js) - if err != nil { - t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err) - } - - d, ok := v.Export().(*daos.Dao) - if !ok { - t.Fatalf("[%s] Expected daos.Dao, got %v", s.js, d) - } - - if d.ConcurrentDB() != s.concurrentDB { - t.Fatalf("[%s] The ConcurrentDB instances doesn't match", s.js) - } - - if d.NonconcurrentDB() != s.nonconcurrentDB { - t.Fatalf("[%s] The NonconcurrentDB instances doesn't match", s.js) - } - } -} - func TestDbxBinds(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() vm := goja.New() - vm.Set("db", app.Dao().DB()) + vm.Set("db", app.DB()) baseBinds(vm) dbxBinds(vm) @@ -602,12 +723,7 @@ func TestMailsBinds(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - record, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") + record, err := app.FindAuthRecordByEmail("users", "test@example.com") if err != nil { t.Fatal(err) } @@ -616,28 +732,27 @@ func TestMailsBinds(t *testing.T) { baseBinds(vm) mailsBinds(vm) vm.Set("$app", app) - vm.Set("admin", admin) vm.Set("record", record) _, vmErr := vm.RunString(` - $mails.sendAdminPasswordReset($app, admin); - if (!$app.testMailer.lastMessage.html.includes("/_/#/confirm-password-reset/")) { - throw new Error("Expected admin password reset email") - } - $mails.sendRecordPasswordReset($app, record); - if (!$app.testMailer.lastMessage.html.includes("/_/#/auth/confirm-password-reset/")) { - throw new Error("Expected record password reset email") + if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-password-reset/")) { + throw new Error("Expected record password reset email, got:" + JSON.stringify($app.testMailer.lastMessage())) } $mails.sendRecordVerification($app, record); - if (!$app.testMailer.lastMessage.html.includes("/_/#/auth/confirm-verification/")) { - throw new Error("Expected record verification email") + if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-verification/")) { + throw new Error("Expected record verification email, got:" + JSON.stringify($app.testMailer.lastMessage())) } $mails.sendRecordChangeEmail($app, record, "new@example.com"); - if (!$app.testMailer.lastMessage.html.includes("/_/#/auth/confirm-email-change/")) { - throw new Error("Expected record email change email") + if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-email-change/")) { + throw new Error("Expected record email change email, got:" + JSON.stringify($app.testMailer.lastMessage())) + } + + $mails.sendRecordOTP($app, record, "test_otp_id", "test_otp_pass"); + if (!$app.testMailer.lastMessage().html.includes("test_otp_pass")) { + throw new Error("Expected record OTP email, got:" + JSON.stringify($app.testMailer.lastMessage())) } `) if vmErr != nil { @@ -645,97 +760,14 @@ func TestMailsBinds(t *testing.T) { } } -func TestTokensBindsCount(t *testing.T) { - vm := goja.New() - tokensBinds(vm) - - testBindsCount(vm, "$tokens", 8, t) -} - -func TestTokensBinds(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - record, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - vm := goja.New() - baseBinds(vm) - tokensBinds(vm) - vm.Set("$app", app) - vm.Set("admin", admin) - vm.Set("record", record) - - sceneraios := []struct { - js string - key string - }{ - { - `$tokens.adminAuthToken($app, admin)`, - admin.TokenKey + app.Settings().AdminAuthToken.Secret, - }, - { - `$tokens.adminResetPasswordToken($app, admin)`, - admin.TokenKey + app.Settings().AdminPasswordResetToken.Secret, - }, - { - `$tokens.adminFileToken($app, admin)`, - admin.TokenKey + app.Settings().AdminFileToken.Secret, - }, - { - `$tokens.recordAuthToken($app, record)`, - record.TokenKey() + app.Settings().RecordAuthToken.Secret, - }, - { - `$tokens.recordVerifyToken($app, record)`, - record.TokenKey() + app.Settings().RecordVerificationToken.Secret, - }, - { - `$tokens.recordResetPasswordToken($app, record)`, - record.TokenKey() + app.Settings().RecordPasswordResetToken.Secret, - }, - { - `$tokens.recordChangeEmailToken($app, record)`, - record.TokenKey() + app.Settings().RecordEmailChangeToken.Secret, - }, - { - `$tokens.recordFileToken($app, record)`, - record.TokenKey() + app.Settings().RecordFileToken.Secret, - }, - } - - for _, s := range sceneraios { - result, err := vm.RunString(s.js) - if err != nil { - t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err) - } - - v, _ := result.Export().(string) - - if _, err := security.ParseJWT(v, s.key); err != nil { - t.Fatalf("[%s] Failed to parse JWT %v, got %v", s.js, v, err) - } - } -} - func TestSecurityBindsCount(t *testing.T) { vm := goja.New() securityBinds(vm) - testBindsCount(vm, "$security", 15, t) + testBindsCount(vm, "$security", 16, t) } func TestSecurityCryptoBinds(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) securityBinds(vm) @@ -770,9 +802,6 @@ func TestSecurityCryptoBinds(t *testing.T) { } func TestSecurityRandomStringBinds(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) securityBinds(vm) @@ -785,6 +814,7 @@ func TestSecurityRandomStringBinds(t *testing.T) { {`$security.randomStringWithAlphabet(7, "abc")`, 7}, {`$security.pseudorandomString(8)`, 8}, {`$security.pseudorandomStringWithAlphabet(9, "abc")`, 9}, + {`$security.randomStringByRegex("abc")`, 3}, } for _, s := range sceneraios { @@ -804,9 +834,6 @@ func TestSecurityRandomStringBinds(t *testing.T) { } func TestSecurityJWTBinds(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - sceneraios := []struct { name string js string @@ -864,9 +891,6 @@ func TestSecurityJWTBinds(t *testing.T) { } func TestSecurityEncryptAndDecryptBinds(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() baseBinds(vm) securityBinds(vm) @@ -903,7 +927,7 @@ func TestFilesystemBinds(t *testing.T) { vm := goja.New() vm.Set("mh", &multipart.FileHeader{Filename: "test"}) vm.Set("testFile", filepath.Join(app.DataDir(), "data.db")) - vm.Set("baseUrl", srv.URL) + vm.Set("baseURL", srv.URL) baseBinds(vm) filesystemBinds(vm) @@ -951,9 +975,9 @@ func TestFilesystemBinds(t *testing.T) { } } - // fileFromUrl (success) + // fileFromURL (success) { - v, err := vm.RunString(`$filesystem.fileFromUrl(baseUrl + "/test")`) + v, err := vm.RunString(`$filesystem.fileFromURL(baseURL + "/test")`) if err != nil { t.Fatal(err) } @@ -961,13 +985,13 @@ func TestFilesystemBinds(t *testing.T) { file, _ := v.Export().(*filesystem.File) if file == nil || file.OriginalName != "test" { - t.Fatalf("[fileFromUrl] Expected file with name %q, got %v", file.OriginalName, file) + t.Fatalf("[fileFromURL] Expected file with name %q, got %v", file.OriginalName, file) } } - // fileFromUrl (failure) + // fileFromURL (failure) { - _, err := vm.RunString(`$filesystem.fileFromUrl(baseUrl + "/error")`) + _, err := vm.RunString(`$filesystem.fileFromURL(baseURL + "/error")`) if err == nil { t.Fatal("Expected url fetch error") } @@ -978,30 +1002,24 @@ func TestFormsBinds(t *testing.T) { vm := goja.New() formsBinds(vm) - testBindsCount(vm, "this", 20, t) + testBindsCount(vm, "this", 4, t) } func TestApisBindsCount(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() apisBinds(vm) - testBindsCount(vm, "this", 6, t) - testBindsCount(vm, "$apis", 14, t) + testBindsCount(vm, "this", 8, t) + testBindsCount(vm, "$apis", 12, t) } func TestApisBindsApiError(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() apisBinds(vm) scenarios := []struct { js string - expectCode int + expectStatus int expectMessage string expectData string }{ @@ -1013,8 +1031,12 @@ func TestApisBindsApiError(t *testing.T) { {"new BadRequestError('test', {'test': 1})", 400, "Test.", `{"test":1}`}, {"new ForbiddenError()", 403, "You are not allowed to perform this request.", "null"}, {"new ForbiddenError('test', {'test': 1})", 403, "Test.", `{"test":1}`}, - {"new UnauthorizedError()", 401, "Missing or invalid authentication token.", "null"}, + {"new UnauthorizedError()", 401, "Missing or invalid authentication.", "null"}, {"new UnauthorizedError('test', {'test': 1})", 401, "Test.", `{"test":1}`}, + {"new TooManyRequestsError()", 429, "Too Many Requests.", "null"}, + {"new TooManyRequestsError('test', {'test': 1})", 429, "Test.", `{"test":1}`}, + {"new InternalServerError()", 500, "Something went wrong while processing your request.", "null"}, + {"new InternalServerError('test', {'test': 1})", 500, "Test.", `{"test":1}`}, } for _, s := range scenarios { @@ -1024,14 +1046,14 @@ func TestApisBindsApiError(t *testing.T) { continue } - apiErr, ok := v.Export().(*apis.ApiError) + apiErr, ok := v.Export().(*router.ApiError) if !ok { t.Errorf("[%s] Expected ApiError, got %v", s.js, v) continue } - if apiErr.Code != s.expectCode { - t.Errorf("[%s] Expected Code %d, got %d", s.js, s.expectCode, apiErr.Code) + if apiErr.Status != s.expectStatus { + t.Errorf("[%s] Expected Status %d, got %d", s.js, s.expectStatus, apiErr.Status) } if apiErr.Message != s.expectMessage { @@ -1065,7 +1087,7 @@ func TestLoadingDynamicModel(t *testing.T) { obj: {}, }) - $app.dao().db() + $app.db() .select("text", "bool", "number", "select_many", "json", "('{\"test\": 1}') as obj") .from("demo1") .where($dbx.hashExp({"id": "84nmscqy84lsi1t"})) @@ -1116,7 +1138,7 @@ func TestLoadingArrayOf(t *testing.T) { text: "", })) - $app.dao().db() + $app.db() .select("id", "text") .from("demo1") .where($dbx.exp("id='84nmscqy84lsi1t' OR id='al1h9ijdeojtsjy'")) @@ -1159,6 +1181,8 @@ func TestHttpClientBindsCount(t *testing.T) { } func TestHttpClientBindsSend(t *testing.T) { + t.Parallel() + // start a test server server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.URL.Query().Get("testError") != "" { @@ -1203,7 +1227,7 @@ func TestHttpClientBindsSend(t *testing.T) { vm := goja.New() baseBinds(vm) httpClientBinds(vm) - vm.Set("testUrl", server.URL) + vm.Set("testURL", server.URL) _, err := vm.RunString(` function getNestedVal(data, path) { @@ -1228,7 +1252,7 @@ func TestHttpClientBindsSend(t *testing.T) { let testTimeout; try { $http.send({ - url: testUrl + "?testTimeout=3", + url: testURL + "?testTimeout=3", timeout: 1 }) } catch (err) { @@ -1240,20 +1264,20 @@ func TestHttpClientBindsSend(t *testing.T) { // error response check const test0 = $http.send({ - url: testUrl + "?testError=1", + url: testURL + "?testError=1", }) // basic fields check const test1 = $http.send({ method: "post", - url: testUrl, + url: testURL, headers: {"header1": "123", "header2": "456"}, body: '789', }) // with custom content-type header const test2 = $http.send({ - url: testUrl, + url: testURL, headers: {"content-type": "text/plain"}, }) @@ -1261,7 +1285,7 @@ func TestHttpClientBindsSend(t *testing.T) { const formData = new FormData() formData.append("title", "123") const test3 = $http.send({ - url: testUrl, + url: testURL, body: formData, headers: {"content-type": "text/plain"}, // should be ignored }) @@ -1277,7 +1301,6 @@ func TestHttpClientBindsSend(t *testing.T) { "json.method": "POST", "json.headers.header1": "123", "json.headers.header2": "456", - "json.headers.content_type": "application/json", // default "json.body": "789", }], [test2, { @@ -1334,9 +1357,17 @@ func TestCronBindsCount(t *testing.T) { defer app.Cleanup() vm := goja.New() - cronBinds(app, vm, nil) + + pool := newPool(1, func() *goja.Runtime { return goja.New() }) + + cronBinds(app, vm, pool) testBindsCount(vm, "this", 2, t) + + pool.run(func(poolVM *goja.Runtime) error { + testBindsCount(poolVM, "this", 1, t) + return nil + }) } func TestHooksBindsCount(t *testing.T) { @@ -1346,7 +1377,7 @@ func TestHooksBindsCount(t *testing.T) { vm := goja.New() hooksBinds(app, vm, nil) - testBindsCount(vm, "this", 88, t) + testBindsCount(vm, "this", 82, t) } func TestHooksBinds(t *testing.T) { @@ -1371,35 +1402,41 @@ func TestHooksBinds(t *testing.T) { hooksBinds(app, vm, pool) _, err := vm.RunString(` - onModelBeforeUpdate((e) => { + onModelUpdate((e) => { result.called++; + e.next() }, "demo1") - onModelBeforeUpdate((e) => { + onModelUpdate((e) => { throw new Error("example"); }, "demo1") - onModelBeforeUpdate((e) => { + onModelUpdate((e) => { result.called++; + e.next(); }, "demo2") - onModelBeforeUpdate((e) => { + onModelUpdate((e) => { result.called++; + e.next() }, "demo2") - onModelBeforeUpdate((e) => { - return false + onModelUpdate((e) => { + // stop propagation }, "demo2") - onModelBeforeUpdate((e) => { + onModelUpdate((e) => { result.called++; + e.next(); }, "demo2") - onAfterBootstrap(() => { + onBootstrap((e) => { + e.next() + // check hooks propagation and tags filtering - const recordA = $app.dao().findFirstRecordByFilter("demo2", "1=1") + const recordA = $app.findFirstRecordByFilter("demo2", "1=1") recordA.set("title", "update") - $app.dao().saveRecord(recordA) + $app.save(recordA) if (result.called != 2) { throw new Error("Expected result.called to be 2, got " + result.called) } @@ -1410,9 +1447,9 @@ func TestHooksBinds(t *testing.T) { // check error handling let hasErr = false try { - const recordB = $app.dao().findFirstRecordByFilter("demo1", "1=1") + const recordB = $app.findFirstRecordByFilter("demo1", "1=1") recordB.set("text", "update") - $app.dao().saveRecord(recordB) + $app.save(recordB) } catch (err) { hasErr = true } @@ -1438,7 +1475,7 @@ func TestRouterBindsCount(t *testing.T) { vm := goja.New() routerBinds(app, vm, nil) - testBindsCount(vm, "this", 3, t) + testBindsCount(vm, "this", 2, t) } func TestRouterBinds(t *testing.T) { @@ -1446,9 +1483,8 @@ func TestRouterBinds(t *testing.T) { defer app.Cleanup() result := &struct { - AddCount int - UseCount int - PreCount int + AddCount int + WithCount int }{} vmFactory := func() *goja.Runtime { @@ -1467,68 +1503,52 @@ func TestRouterBinds(t *testing.T) { _, err := vm.RunString(` routerAdd("GET", "/test", (e) => { result.addCount++; - }, (next) => { - return (c) => { - result.addCount++; - - return next(c); - } + }, (e) => { + result.addCount++; + return e.next(); }) - routerUse((next) => { - return (c) => { - result.useCount++; + routerUse((e) => { + result.withCount++; - return next(c) - } - }) - - routerPre((next) => { - return (c) => { - result.preCount++; - - return next(c) - } + return e.next(); }) `) if err != nil { t.Fatal(err) } - e, err := apis.InitApi(app) + pbRouter, err := apis.NewRouter(app) if err != nil { t.Fatal(err) } - serveEvent := &core.ServeEvent{ - App: app, - Router: e, - } - if err := app.OnBeforeServe().Trigger(serveEvent); err != nil { + serveEvent := new(core.ServeEvent) + serveEvent.App = app + serveEvent.Router = pbRouter + if err = app.OnServe().Trigger(serveEvent); err != nil { t.Fatal(err) } rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) - e.ServeHTTP(rec, req) + + mux, err := serveEvent.Router.BuildMux() + if err != nil { + t.Fatalf("Failed to build router mux: %v", err) + } + mux.ServeHTTP(rec, req) if result.AddCount != 2 { t.Fatalf("Expected AddCount %d, got %d", 2, result.AddCount) } - if result.UseCount != 1 { - t.Fatalf("Expected UseCount %d, got %d", 1, result.UseCount) - } - - if result.PreCount != 1 { - t.Fatalf("Expected PreCount %d, got %d", 1, result.PreCount) + if result.WithCount != 1 { + t.Fatalf("Expected WithCount %d, got %d", 1, result.WithCount) } } func TestFilepathBindsCount(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() filepathBinds(vm) @@ -1536,9 +1556,6 @@ func TestFilepathBindsCount(t *testing.T) { } func TestOsBindsCount(t *testing.T) { - app, _ := tests.NewTestApp() - defer app.Cleanup() - vm := goja.New() osBinds(vm) diff --git a/plugins/jsvm/form_data.go b/plugins/jsvm/form_data.go index d4215761..ff413198 100644 --- a/plugins/jsvm/form_data.go +++ b/plugins/jsvm/form_data.go @@ -84,9 +84,7 @@ func (data FormData) Values() []any { result := make([]any, 0, len(data)) for _, values := range data { - for _, v := range values { - result = append(result, v) - } + result = append(result, values...) } return result diff --git a/plugins/jsvm/internal/types/generated/types.d.ts b/plugins/jsvm/internal/types/generated/types.d.ts index dac56c61..b02be9d0 100644 --- a/plugins/jsvm/internal/types/generated/types.d.ts +++ b/plugins/jsvm/internal/types/generated/types.d.ts @@ -1,4 +1,4 @@ -// 1710682789 +// 1727605671 // GENERATED CODE - DO NOT MODIFY BY HAND // ------------------------------------------------------------------- @@ -55,9 +55,9 @@ declare function cronRemove(jobId: string): void; * Example: * * ```js - * routerAdd("GET", "/hello", (c) => { - * return c.json(200, {"message": "Hello!"}) - * }, $apis.requireAdminOrRecordAuth()) + * routerAdd("GET", "/hello", (e) => { + * return e.json(200, {"message": "Hello!"}) + * }, $apis.requireAuth()) * ``` * * _Note that this method is available only in pb_hooks context._ @@ -67,8 +67,8 @@ declare function cronRemove(jobId: string): void; declare function routerAdd( method: string, path: string, - handler: echo.HandlerFunc, - ...middlewares: Array, + handler: (e: core.RequestEvent) => void, + ...middlewares: Array void)|Middleware>, ): void; /** @@ -78,11 +78,9 @@ declare function routerAdd( * Example: * * ```js - * routerUse((next) => { - * return (c) => { - * console.log(c.path()) - * return next(c) - * } + * routerUse((e) => { + * console.log(e.request.url.path) + * return e.next() * }) * ``` * @@ -90,34 +88,7 @@ declare function routerAdd( * * @group PocketBase */ -declare function routerUse(...middlewares: Array): void; - -/** - * RouterPre registers one or more global middlewares that are executed - * BEFORE the router processes the request. It is usually used for making - * changes to the request properties, for example, adding or removing - * a trailing slash or adding segments to a path so it matches a route. - * - * NB! Since the router will not have processed the request yet, - * middlewares registered at this level won't have access to any path - * related APIs from echo.Context. - * - * Example: - * - * ```js - * routerPre((next) => { - * return (c) => { - * console.log(c.request().url) - * return next(c) - * } - * }) - * ``` - * - * _Note that this method is available only in pb_hooks context._ - * - * @group PocketBase - */ -declare function routerPre(...middlewares: Array): void; +declare function routerUse(...middlewares: Array void)|Middleware): void; // ------------------------------------------------------------------- // baseBinds @@ -135,7 +106,7 @@ declare var __hooks: string // // See https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as type excludeHooks = { - [Property in keyof Type as Exclude]: Type[Property] + [Property in keyof Type as Exclude]: Type[Property] }; // CoreApp without the on* hook methods @@ -178,22 +149,34 @@ declare var $app: PocketBase declare var $template: template.Registry /** - * readerToString reads the content of the specified io.Reader until - * EOF or maxBytes are reached. + * This method is superseded by toString. * - * If maxBytes is not specified it will read up to 32MB. + * @deprecated + * @group PocketBase + */ +declare function readerToString(reader: any, maxBytes?: number): string; + +/** + * toString stringifies the specified value. * - * Note that after this call the reader can't be used anymore. + * Support optional second maxBytes argument to limit the max read bytes + * when the value is a io.Reader (default to 32MB). + * + * Types that don't have explicit string representation are json serialized. * * Example: * * ```js - * const rawBody = readerToString(c.request().body) + * // io.Reader + * const ex1 = toString(e.request.body) + * + * // slice of bytes ("hello") + * const ex2 = toString([104 101 108 108 111]) * ``` * * @group PocketBase */ -declare function readerToString(reader: any, maxBytes?: number): string; +declare function toString(val: any, maxBytes?: number): string; /** * sleep pauses the current goroutine for at least the specified user duration (in ms). @@ -228,15 +211,18 @@ declare function arrayOf(model: T): Array; /** * DynamicModel creates a new dynamic model with fields from the provided data shape. * + * Note that in order to use 0 as double/float initialization number you have to use negative zero (`-0`). + * * Example: * * ```js * const model = new DynamicModel({ - * name: "" - * age: 0, - * active: false, - * roles: [], - * meta: {} + * name: "" + * age: 0, // int64 + * totalSpent: -0, // float64 + * active: false, + * roles: [], + * meta: {} * }) * ``` * @@ -263,12 +249,12 @@ declare class DynamicModel { * @group PocketBase */ declare const Record: { - new(collection?: models.Collection, data?: { [key:string]: any }): models.Record + new(collection?: core.Collection, data?: { [key:string]: any }): core.Record // note: declare as "newable" const due to conflict with the Record TS utility type } -interface Collection extends models.Collection{} // merge +interface Collection extends core.Collection{} // merge /** * Collection model class. * @@ -279,12 +265,13 @@ interface Collection extends models.Collection{} // merge * listRule: "@request.auth.id != '' || status = 'public'", * viewRule: "@request.auth.id != '' || status = 'public'", * deleteRule: "@request.auth.id != ''", - * schema: [ + * fields: [ * { * name: "title", * type: "text", * required: true, - * options: { min: 6, max: 100 }, + * min: 6, + * max: 100, * }, * { * name: "description", @@ -296,44 +283,242 @@ interface Collection extends models.Collection{} // merge * * @group PocketBase */ -declare class Collection implements models.Collection { - constructor(data?: Partial) +declare class Collection implements core.Collection { + constructor(data?: Partial) } -interface Admin extends models.Admin{} // merge +interface BaseCollection extends core.Collection{} // merge /** - * Admin model class. + * Alias for a "base" collection class. * * ```js - * const admin = new Admin() - * admin.email = "test@example.com" - * admin.setPassword(1234567890) + * 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 Admin implements models.Admin { - constructor(data?: Partial) +declare class BaseCollection implements core.Collection { + constructor(data?: Partial) } -interface Schema extends schema.Schema{} // merge +interface AuthCollection extends core.Collection{} // merge /** - * Schema model class, usually used to define the Collection.schema field. + * 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 Schema implements schema.Schema { - constructor(data?: Partial) +declare class AuthCollection implements core.Collection { + constructor(data?: Partial) } -interface SchemaField extends schema.SchemaField{} // merge +interface ViewCollection extends core.Collection{} // merge /** - * SchemaField model class, usually used as part of the Schema model. + * 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 SchemaField implements schema.SchemaField { - constructor(data?: Partial) +declare class ViewCollection implements core.Collection { + constructor(data?: Partial) +} + +interface FieldsList extends core.FieldsList{} // merge +/** + * FieldsList model class, usually used to define the Collection.fields. + * + * @group PocketBase + */ +declare class FieldsList implements core.FieldsList { + constructor(data?: Partial) +} + +interface Field extends core.Field{} // merge +/** + * Field model class, usually used as part of the FieldsList model. + * + * @group PocketBase + */ +declare class Field implements core.Field { + constructor(data?: Partial) +} + +interface NumberField extends core.NumberField{} // merge +/** + * NumberField class defines a single "number" collection field. + * + * @group PocketBase + */ +declare class NumberField implements core.NumberField { + constructor(data?: Partial) +} + +interface BoolField extends core.BoolField{} // merge +/** + * BoolField class defines a single "bool" collection field. + * + * @group PocketBase + */ +declare class BoolField implements core.BoolField { + constructor(data?: Partial) +} + +interface TextField extends core.TextField{} // merge +/** + * TextField class defines a single "text" collection field. + * + * @group PocketBase + */ +declare class TextField implements core.TextField { + constructor(data?: Partial) +} + +interface URLField extends core.URLField{} // merge +/** + * URLField class defines a single "url" collection field. + * + * @group PocketBase + */ +declare class URLField implements core.URLField { + constructor(data?: Partial) +} + +interface EmailField extends core.EmailField{} // merge +/** + * EmailField class defines a single "email" collection field. + * + * @group PocketBase + */ +declare class EmailField implements core.EmailField { + constructor(data?: Partial) +} + +interface EditorField extends core.EditorField{} // merge +/** + * EditorField class defines a single "editor" collection field. + * + * @group PocketBase + */ +declare class EditorField implements core.EditorField { + constructor(data?: Partial) +} + +interface PasswordField extends core.PasswordField{} // merge +/** + * PasswordField class defines a single "password" collection field. + * + * @group PocketBase + */ +declare class PasswordField implements core.PasswordField { + constructor(data?: Partial) +} + +interface DateField extends core.DateField{} // merge +/** + * DateField class defines a single "date" collection field. + * + * @group PocketBase + */ +declare class DateField implements core.DateField { + constructor(data?: Partial) +} + +interface AutodateField extends core.AutodateField{} // merge +/** + * AutodateField class defines a single "autodate" collection field. + * + * @group PocketBase + */ +declare class AutodateField implements core.AutodateField { + constructor(data?: Partial) +} + +interface JSONField extends core.JSONField{} // merge +/** + * JSONField class defines a single "json" collection field. + * + * @group PocketBase + */ +declare class JSONField implements core.JSONField { + constructor(data?: Partial) +} + +interface RelationField extends core.RelationField{} // merge +/** + * RelationField class defines a single "relation" collection field. + * + * @group PocketBase + */ +declare class RelationField implements core.RelationField { + constructor(data?: Partial) +} + +interface SelectField extends core.SelectField{} // merge +/** + * SelectField class defines a single "select" collection field. + * + * @group PocketBase + */ +declare class SelectField implements core.SelectField { + constructor(data?: Partial) +} + +interface FileField extends core.FileField{} // merge +/** + * FileField class defines a single "file" collection field. + * + * @group PocketBase + */ +declare class FileField implements core.FileField { + constructor(data?: Partial) } interface MailerMessage extends mailer.Message{} // merge @@ -381,31 +566,55 @@ declare class Command implements cobra.Command { constructor(cmd?: Partial) } -interface RequestInfo extends models.RequestInfo{} // merge +interface RequestInfo extends core.RequestInfo{} // merge /** - * RequestInfo defines a single models.RequestInfo instance, usually used + * RequestInfo defines a single core.RequestInfo instance, usually used * as part of various filter checks. * * Example: * * ```js - * const authRecord = $app.dao().findAuthRecordByEmail("users", "test@example.com") + * const authRecord = $app.findAuthRecordByEmail("users", "test@example.com") * * const info = new RequestInfo({ - * authRecord: authRecord, - * data: {"name": 123}, - * headers: {"x-token": "..."}, + * auth: authRecord, + * body: {"name": 123}, + * headers: {"x-token": "..."}, * }) * - * const record = $app.dao().findFirstRecordByData("articles", "slug", "hello") + * const record = $app.findFirstRecordByData("articles", "slug", "hello") * - * const canAccess = $app.dao().canAccessRecord(record, info, "@request.auth.id != '' && @request.data.name = 123") + * const canAccess = $app.canAccessRecord(record, info, "@request.auth.id != '' && @request.body.name = 123") * ``` * * @group PocketBase */ -declare class RequestInfo implements models.RequestInfo { - constructor(date?: Partial) +declare class RequestInfo implements core.RequestInfo { + constructor(info?: Partial) +} + +/** + * Middleware defines a single request middleware handler. + * + * This class is usually used when you want to explicitly specify a priority to your custom route middleware. + * + * Example: + * + * ```js + * routerUse(new Middleware((e) => { + * console.log(e.request.url.path) + * return e.next() + * }, -10)) + * ``` + * + * @group PocketBase + */ +declare class Middleware { + constructor( + func: string|((e: core.RequestEvent) => void), + priority?: number, + id?: string, + ) } interface DateTime extends types.DateTime{} // merge @@ -441,15 +650,6 @@ declare class ValidationError implements ozzo_validation.Error { constructor(code?: string, message?: string) } -interface Dao extends daos.Dao{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class Dao implements daos.Dao { - constructor(concurrentDB?: dbx.Builder, nonconcurrentDB?: dbx.Builder) -} - interface Cookie extends http.Cookie{} // merge /** * A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an @@ -535,44 +735,21 @@ declare namespace $dbx { export let notBetween: dbx.notBetween } -// ------------------------------------------------------------------- -// tokensBinds -// ------------------------------------------------------------------- - -/** - * `$tokens` defines high level helpers to generate - * various admins and auth records tokens (auth, forgotten password, etc.). - * - * For more control over the generated token, you can check `$security`. - * - * @group PocketBase - */ -declare namespace $tokens { - let adminAuthToken: tokens.newAdminAuthToken - let adminResetPasswordToken: tokens.newAdminResetPasswordToken - let adminFileToken: tokens.newAdminFileToken - let recordAuthToken: tokens.newRecordAuthToken - let recordVerifyToken: tokens.newRecordVerifyToken - let recordResetPasswordToken: tokens.newRecordResetPasswordToken - let recordChangeEmailToken: tokens.newRecordChangeEmailToken - let recordFileToken: tokens.newRecordFileToken -} - // ------------------------------------------------------------------- // mailsBinds // ------------------------------------------------------------------- /** * `$mails` defines helpers to send common - * admins and auth records emails like verification, password reset, etc. + * auth records emails like verification, password reset, etc. * * @group PocketBase */ declare namespace $mails { - let sendAdminPasswordReset: mails.sendAdminPasswordReset let sendRecordPasswordReset: mails.sendRecordPasswordReset let sendRecordVerification: mails.sendRecordVerification let sendRecordChangeEmail: mails.sendRecordChangeEmail + let sendRecordOTP: mails.sendRecordOTP } // ------------------------------------------------------------------- @@ -588,6 +765,7 @@ declare namespace $mails { declare namespace $security { let randomString: security.randomString let randomStringWithAlphabet: security.randomStringWithAlphabet + let randomStringByRegex: security.randomStringByRegex let pseudorandomString: security.pseudorandomString let pseudorandomStringWithAlphabet: security.pseudorandomStringWithAlphabet let encrypt: security.encrypt @@ -598,7 +776,11 @@ declare namespace $security { let md5: security.md5 let sha256: security.sha256 let sha512: security.sha512 - let createJWT: security.newJWT + + /** + * {@inheritDoc security.newJWT} + */ + export function createJWT(payload: { [key:string]: any }, signingKey: string, secDuration: number): string /** * {@inheritDoc security.parseUnverifiedJWT} @@ -723,42 +905,6 @@ declare namespace $os { // formsBinds // ------------------------------------------------------------------- -interface AdminLoginForm extends forms.AdminLogin{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminLoginForm implements forms.AdminLogin { - constructor(app: CoreApp) -} - -interface AdminPasswordResetConfirmForm extends forms.AdminPasswordResetConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminPasswordResetConfirmForm implements forms.AdminPasswordResetConfirm { - constructor(app: CoreApp) -} - -interface AdminPasswordResetRequestForm extends forms.AdminPasswordResetRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminPasswordResetRequestForm implements forms.AdminPasswordResetRequest { - constructor(app: CoreApp) -} - -interface AdminUpsertForm extends forms.AdminUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminUpsertForm implements forms.AdminUpsert { - constructor(app: CoreApp, admin: models.Admin) -} - interface AppleClientSecretCreateForm extends forms.AppleClientSecretCreate{} // merge /** * @inheritDoc @@ -768,119 +914,13 @@ declare class AppleClientSecretCreateForm implements forms.AppleClientSecretCrea constructor(app: CoreApp) } -interface CollectionUpsertForm extends forms.CollectionUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class CollectionUpsertForm implements forms.CollectionUpsert { - constructor(app: CoreApp, collection: models.Collection) -} - -interface CollectionsImportForm extends forms.CollectionsImport{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class CollectionsImportForm implements forms.CollectionsImport { - constructor(app: CoreApp) -} - -interface RealtimeSubscribeForm extends forms.RealtimeSubscribe{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RealtimeSubscribeForm implements forms.RealtimeSubscribe {} - -interface RecordEmailChangeConfirmForm extends forms.RecordEmailChangeConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordEmailChangeConfirmForm implements forms.RecordEmailChangeConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordEmailChangeRequestForm extends forms.RecordEmailChangeRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordEmailChangeRequestForm implements forms.RecordEmailChangeRequest { - constructor(app: CoreApp, record: models.Record) -} - -interface RecordOAuth2LoginForm extends forms.RecordOAuth2Login{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordOAuth2LoginForm implements forms.RecordOAuth2Login { - constructor(app: CoreApp, collection: models.Collection, optAuthRecord?: models.Record) -} - -interface RecordPasswordLoginForm extends forms.RecordPasswordLogin{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordLoginForm implements forms.RecordPasswordLogin { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordPasswordResetConfirmForm extends forms.RecordPasswordResetConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordResetConfirmForm implements forms.RecordPasswordResetConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordPasswordResetRequestForm extends forms.RecordPasswordResetRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordResetRequestForm implements forms.RecordPasswordResetRequest { - constructor(app: CoreApp, collection: models.Collection) -} - interface RecordUpsertForm extends forms.RecordUpsert{} // merge /** * @inheritDoc * @group PocketBase */ declare class RecordUpsertForm implements forms.RecordUpsert { - constructor(app: CoreApp, record: models.Record) -} - -interface RecordVerificationConfirmForm extends forms.RecordVerificationConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordVerificationConfirmForm implements forms.RecordVerificationConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordVerificationRequestForm extends forms.RecordVerificationRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordVerificationRequestForm implements forms.RecordVerificationRequest { - constructor(app: CoreApp, collection: models.Collection) -} - -interface SettingsUpsertForm extends forms.SettingsUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class SettingsUpsertForm implements forms.SettingsUpsert { - constructor(app: CoreApp) + constructor(app: CoreApp, record: core.Record) } interface TestEmailSendForm extends forms.TestEmailSend{} // merge @@ -955,6 +995,26 @@ declare class UnauthorizedError implements apis.ApiError { constructor(message?: string, data?: any) } +interface TooManyRequestsError extends apis.ApiError{} // merge +/** + * TooManyRequestsError returns 429 ApiError. + * + * @group PocketBase + */ +declare class TooManyRequestsError implements apis.ApiError { + constructor(message?: string, data?: any) +} + +interface InternalServerError extends apis.ApiError{} // merge +/** + * InternalServerError returns 429 ApiError. + * + * @group PocketBase + */ +declare class InternalServerError implements apis.ApiError { + constructor(message?: string, data?: any) +} + /** * `$apis` defines commonly used PocketBase api helpers and middlewares. * @@ -967,21 +1027,19 @@ declare namespace $apis { * If a file resource is missing and indexFallback is set, the request * will be forwarded to the base index.html (useful for SPA). */ - export function staticDirectoryHandler(dir: string, indexFallback: boolean): echo.HandlerFunc + export function static(dir: string, indexFallback: boolean): (e: core.RequestEvent) => void - let requireGuestOnly: apis.requireGuestOnly - let requireRecordAuth: apis.requireRecordAuth - let requireAdminAuth: apis.requireAdminAuth - let requireAdminAuthOnlyIfAny: apis.requireAdminAuthOnlyIfAny - let requireAdminOrRecordAuth: apis.requireAdminOrRecordAuth - let requireAdminOrOwnerAuth: apis.requireAdminOrOwnerAuth - let activityLogger: apis.activityLogger - let requestInfo: apis.requestInfo - let recordAuthResponse: apis.recordAuthResponse - let gzip: middleware.gzip - let bodyLimit: middleware.bodyLimit - let enrichRecord: apis.enrichRecord - let enrichRecords: apis.enrichRecords + let requireGuestOnly: apis.requireGuestOnly + let requireAuth: apis.requireAuth + let requireSuperuserAuth: apis.requireSuperuserAuth + let requireSuperuserAuthOnlyIfAny: apis.requireSuperuserAuthOnlyIfAny + let requireSuperuserOrOwnerAuth: apis.requireSuperuserOrOwnerAuth + let skipSuccessActivityLog: apis.skipSuccessActivityLog + let gzip: apis.gzip + let bodyLimit: apis.bodyLimit + let recordAuthResponse: apis.recordAuthResponse + let enrichRecord: apis.enrichRecord + let enrichRecords: apis.enrichRecords } // ------------------------------------------------------------------- @@ -1007,9 +1065,10 @@ declare namespace $http { * * ```js * const res = $http.send({ - * url: "https://example.com", - * body: JSON.stringify({"title": "test"}) - * method: "post", + * method: "POST", + * url: "https://example.com", + * body: JSON.stringify({"title": "test"}), + * headers: { 'Content-Type': 'application/json' } * }) * * console.log(res.statusCode) // the response HTTP status code @@ -1026,7 +1085,7 @@ declare namespace $http { headers?: { [key:string]: string }, timeout?: number, // default to 120 - // deprecated, please use body instead + // @deprecated please use body instead data?: { [key:string]: any }, }): { statusCode: number, @@ -1049,96 +1108,90 @@ declare namespace $http { * @group PocketBase */ declare function migrate( - up: (db: dbx.Builder) => void, - down?: (db: dbx.Builder) => void + up: (txApp: CoreApp) => void, + down?: (txApp: CoreApp) => void ): void; -/** @group PocketBase */declare function onAdminAfterAuthRefreshRequest(handler: (e: core.AdminAuthRefreshEvent) => void): void -/** @group PocketBase */declare function onAdminAfterAuthWithPasswordRequest(handler: (e: core.AdminAuthWithPasswordEvent) => void): void -/** @group PocketBase */declare function onAdminAfterConfirmPasswordResetRequest(handler: (e: core.AdminConfirmPasswordResetEvent) => void): void -/** @group PocketBase */declare function onAdminAfterCreateRequest(handler: (e: core.AdminCreateEvent) => void): void -/** @group PocketBase */declare function onAdminAfterDeleteRequest(handler: (e: core.AdminDeleteEvent) => void): void -/** @group PocketBase */declare function onAdminAfterRequestPasswordResetRequest(handler: (e: core.AdminRequestPasswordResetEvent) => void): void -/** @group PocketBase */declare function onAdminAfterUpdateRequest(handler: (e: core.AdminUpdateEvent) => void): void -/** @group PocketBase */declare function onAdminAuthRequest(handler: (e: core.AdminAuthEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeAuthRefreshRequest(handler: (e: core.AdminAuthRefreshEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeAuthWithPasswordRequest(handler: (e: core.AdminAuthWithPasswordEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeConfirmPasswordResetRequest(handler: (e: core.AdminConfirmPasswordResetEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeCreateRequest(handler: (e: core.AdminCreateEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeDeleteRequest(handler: (e: core.AdminDeleteEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeRequestPasswordResetRequest(handler: (e: core.AdminRequestPasswordResetEvent) => void): void -/** @group PocketBase */declare function onAdminBeforeUpdateRequest(handler: (e: core.AdminUpdateEvent) => void): void -/** @group PocketBase */declare function onAdminViewRequest(handler: (e: core.AdminViewEvent) => void): void -/** @group PocketBase */declare function onAdminsListRequest(handler: (e: core.AdminsListEvent) => void): void -/** @group PocketBase */declare function onAfterApiError(handler: (e: core.ApiErrorEvent) => void): void -/** @group PocketBase */declare function onAfterBootstrap(handler: (e: core.BootstrapEvent) => void): void -/** @group PocketBase */declare function onBeforeApiError(handler: (e: core.ApiErrorEvent) => void): void -/** @group PocketBase */declare function onBeforeBootstrap(handler: (e: core.BootstrapEvent) => void): void -/** @group PocketBase */declare function onCollectionAfterCreateRequest(handler: (e: core.CollectionCreateEvent) => void): void -/** @group PocketBase */declare function onCollectionAfterDeleteRequest(handler: (e: core.CollectionDeleteEvent) => void): void -/** @group PocketBase */declare function onCollectionAfterUpdateRequest(handler: (e: core.CollectionUpdateEvent) => void): void -/** @group PocketBase */declare function onCollectionBeforeCreateRequest(handler: (e: core.CollectionCreateEvent) => void): void -/** @group PocketBase */declare function onCollectionBeforeDeleteRequest(handler: (e: core.CollectionDeleteEvent) => void): void -/** @group PocketBase */declare function onCollectionBeforeUpdateRequest(handler: (e: core.CollectionUpdateEvent) => void): void -/** @group PocketBase */declare function onCollectionViewRequest(handler: (e: core.CollectionViewEvent) => void): void -/** @group PocketBase */declare function onCollectionsAfterImportRequest(handler: (e: core.CollectionsImportEvent) => void): void -/** @group PocketBase */declare function onCollectionsBeforeImportRequest(handler: (e: core.CollectionsImportEvent) => void): void -/** @group PocketBase */declare function onCollectionsListRequest(handler: (e: core.CollectionsListEvent) => void): void -/** @group PocketBase */declare function onFileAfterTokenRequest(handler: (e: core.FileTokenEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onFileBeforeTokenRequest(handler: (e: core.FileTokenEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onFileDownloadRequest(handler: (e: core.FileDownloadEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerAfterAdminResetPasswordSend(handler: (e: core.MailerAdminEvent) => void): void -/** @group PocketBase */declare function onMailerAfterRecordChangeEmailSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerAfterRecordResetPasswordSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerAfterRecordVerificationSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerBeforeAdminResetPasswordSend(handler: (e: core.MailerAdminEvent) => void): void -/** @group PocketBase */declare function onMailerBeforeRecordChangeEmailSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerBeforeRecordResetPasswordSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onMailerBeforeRecordVerificationSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelAfterCreate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelAfterDelete(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelAfterUpdate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelBeforeCreate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelBeforeDelete(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onModelBeforeUpdate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRealtimeAfterMessageSend(handler: (e: core.RealtimeMessageEvent) => void): void -/** @group PocketBase */declare function onRealtimeAfterSubscribeRequest(handler: (e: core.RealtimeSubscribeEvent) => void): void -/** @group PocketBase */declare function onRealtimeBeforeMessageSend(handler: (e: core.RealtimeMessageEvent) => void): void -/** @group PocketBase */declare function onRealtimeBeforeSubscribeRequest(handler: (e: core.RealtimeSubscribeEvent) => void): void -/** @group PocketBase */declare function onRealtimeConnectRequest(handler: (e: core.RealtimeConnectEvent) => void): void -/** @group PocketBase */declare function onRealtimeDisconnectRequest(handler: (e: core.RealtimeDisconnectEvent) => void): void -/** @group PocketBase */declare function onRecordAfterAuthRefreshRequest(handler: (e: core.RecordAuthRefreshEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterAuthWithOAuth2Request(handler: (e: core.RecordAuthWithOAuth2Event) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterAuthWithPasswordRequest(handler: (e: core.RecordAuthWithPasswordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterConfirmEmailChangeRequest(handler: (e: core.RecordConfirmEmailChangeEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterConfirmPasswordResetRequest(handler: (e: core.RecordConfirmPasswordResetEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterConfirmVerificationRequest(handler: (e: core.RecordConfirmVerificationEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterCreateRequest(handler: (e: core.RecordCreateEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterDeleteRequest(handler: (e: core.RecordDeleteEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterRequestEmailChangeRequest(handler: (e: core.RecordRequestEmailChangeEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterRequestPasswordResetRequest(handler: (e: core.RecordRequestPasswordResetEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterRequestVerificationRequest(handler: (e: core.RecordRequestVerificationEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterUnlinkExternalAuthRequest(handler: (e: core.RecordUnlinkExternalAuthEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAfterUpdateRequest(handler: (e: core.RecordUpdateEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordAuthRequest(handler: (e: core.RecordAuthEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeAuthRefreshRequest(handler: (e: core.RecordAuthRefreshEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeAuthWithOAuth2Request(handler: (e: core.RecordAuthWithOAuth2Event) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeAuthWithPasswordRequest(handler: (e: core.RecordAuthWithPasswordEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeConfirmEmailChangeRequest(handler: (e: core.RecordConfirmEmailChangeEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeConfirmPasswordResetRequest(handler: (e: core.RecordConfirmPasswordResetEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeConfirmVerificationRequest(handler: (e: core.RecordConfirmVerificationEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeCreateRequest(handler: (e: core.RecordCreateEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeDeleteRequest(handler: (e: core.RecordDeleteEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeRequestEmailChangeRequest(handler: (e: core.RecordRequestEmailChangeEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeRequestPasswordResetRequest(handler: (e: core.RecordRequestPasswordResetEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeRequestVerificationRequest(handler: (e: core.RecordRequestVerificationEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeUnlinkExternalAuthRequest(handler: (e: core.RecordUnlinkExternalAuthEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordBeforeUpdateRequest(handler: (e: core.RecordUpdateEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordListExternalAuthsRequest(handler: (e: core.RecordListExternalAuthsEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordViewRequest(handler: (e: core.RecordViewEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onRecordsListRequest(handler: (e: core.RecordsListEvent) => void, ...tags: string[]): void -/** @group PocketBase */declare function onSettingsAfterUpdateRequest(handler: (e: core.SettingsUpdateEvent) => void): void -/** @group PocketBase */declare function onSettingsBeforeUpdateRequest(handler: (e: core.SettingsUpdateEvent) => void): void -/** @group PocketBase */declare function onSettingsListRequest(handler: (e: core.SettingsListEvent) => void): void +/** @group PocketBase */declare function onBackupCreate(handler: (e: core.BackupEvent) => void): void +/** @group PocketBase */declare function onBackupRestore(handler: (e: core.BackupEvent) => void): void +/** @group PocketBase */declare function onBatchRequest(handler: (e: core.BatchRequestEvent) => void): void +/** @group PocketBase */declare function onBootstrap(handler: (e: core.BootstrapEvent) => void): void +/** @group PocketBase */declare function onCollectionAfterCreateError(handler: (e: core.CollectionErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionAfterCreateSuccess(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionAfterDeleteError(handler: (e: core.CollectionErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionAfterDeleteSuccess(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionAfterUpdateError(handler: (e: core.CollectionErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionAfterUpdateSuccess(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionCreate(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionCreateExecute(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionCreateRequest(handler: (e: core.CollectionRequestEvent) => void): void +/** @group PocketBase */declare function onCollectionDelete(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionDeleteExecute(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionDeleteRequest(handler: (e: core.CollectionRequestEvent) => void): void +/** @group PocketBase */declare function onCollectionUpdate(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionUpdateExecute(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionUpdateRequest(handler: (e: core.CollectionRequestEvent) => void): void +/** @group PocketBase */declare function onCollectionValidate(handler: (e: core.CollectionEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onCollectionViewRequest(handler: (e: core.CollectionRequestEvent) => void): void +/** @group PocketBase */declare function onCollectionsImportRequest(handler: (e: core.CollectionsImportRequestEvent) => void): void +/** @group PocketBase */declare function onCollectionsListRequest(handler: (e: core.CollectionsListRequestEvent) => void): void +/** @group PocketBase */declare function onFileDownloadRequest(handler: (e: core.FileDownloadRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onFileTokenRequest(handler: (e: core.FileTokenRequestEvent) => void): void +/** @group PocketBase */declare function onMailerRecordAuthAlertSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onMailerRecordEmailChangeSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onMailerRecordOTPSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onMailerRecordPasswordResetSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onMailerRecordVerificationSend(handler: (e: core.MailerRecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onMailerSend(handler: (e: core.MailerEvent) => void): void +/** @group PocketBase */declare function onModelAfterCreateError(handler: (e: core.ModelErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelAfterCreateSuccess(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelAfterDeleteError(handler: (e: core.ModelErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelAfterDeleteSuccess(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelAfterUpdateError(handler: (e: core.ModelErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelAfterUpdateSuccess(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelCreate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelCreateExecute(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelDelete(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelDeleteExecute(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelUpdate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelUpdateExecute(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onModelValidate(handler: (e: core.ModelEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRealtimeConnectRequest(handler: (e: core.RealtimeConnectRequestEvent) => void): void +/** @group PocketBase */declare function onRealtimeMessageSend(handler: (e: core.RealtimeMessageEvent) => void): void +/** @group PocketBase */declare function onRealtimeSubscribeRequest(handler: (e: core.RealtimeSubscribeRequestEvent) => void): void +/** @group PocketBase */declare function onRecordAfterCreateError(handler: (e: core.RecordErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAfterCreateSuccess(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAfterDeleteError(handler: (e: core.RecordErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAfterDeleteSuccess(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAfterUpdateError(handler: (e: core.RecordErrorEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAfterUpdateSuccess(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAuthRefreshRequest(handler: (e: core.RecordAuthRefreshRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAuthRequest(handler: (e: core.RecordAuthRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAuthWithOAuth2Request(handler: (e: core.RecordAuthWithOAuth2RequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAuthWithOTPRequest(handler: (e: core.RecordAuthWithOTPRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordAuthWithPasswordRequest(handler: (e: core.RecordAuthWithPasswordRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordConfirmEmailChangeRequest(handler: (e: core.RecordConfirmEmailChangeRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordConfirmPasswordResetRequest(handler: (e: core.RecordConfirmPasswordResetRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordConfirmVerificationRequest(handler: (e: core.RecordConfirmVerificationRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordCreate(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordCreateExecute(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordCreateRequest(handler: (e: core.RecordRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordDelete(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordDeleteExecute(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordDeleteRequest(handler: (e: core.RecordRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordEnrich(handler: (e: core.RecordEnrichEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordRequestEmailChangeRequest(handler: (e: core.RecordRequestEmailChangeRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordRequestOTPRequest(handler: (e: core.RecordCreateOTPRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordRequestPasswordResetRequest(handler: (e: core.RecordRequestPasswordResetRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordRequestVerificationRequest(handler: (e: core.RecordRequestVerificationRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordUpdate(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordUpdateExecute(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordUpdateRequest(handler: (e: core.RecordRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordValidate(handler: (e: core.RecordEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordViewRequest(handler: (e: core.RecordRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onRecordsListRequest(handler: (e: core.RecordsListRequestEvent) => void, ...tags: string[]): void +/** @group PocketBase */declare function onSettingsListRequest(handler: (e: core.SettingsListRequestEvent) => void): void +/** @group PocketBase */declare function onSettingsReload(handler: (e: core.SettingsReloadEvent) => void): void +/** @group PocketBase */declare function onSettingsUpdateRequest(handler: (e: core.SettingsUpdateRequestEvent) => void): void /** @group PocketBase */declare function onTerminate(handler: (e: core.TerminateEvent) => void): void type _TygojaDict = { [key:string | number | symbol]: any; } type _TygojaAny = any @@ -1148,9 +1201,9 @@ type _TygojaAny = any * functionality. The design is Unix-like, although the error handling is * Go-like; failing calls return values of type error rather than error numbers. * Often, more information is available within the error. For example, - * if a call that takes a file name fails, such as Open or Stat, the error + * if a call that takes a file name fails, such as [Open] or [Stat], the error * will include the failing file name when printed and will be of type - * *PathError, which may be unpacked for more information. + * [*PathError], which may be unpacked for more information. * * The os interface is intended to be uniform across all operating systems. * Features not generally available appear in the system-specific package syscall. @@ -1182,22 +1235,26 @@ type _TygojaAny = any * fmt.Printf("read %d bytes: %q\n", count, data[:count]) * ``` * - * Note: The maximum number of concurrent operations on a File may be limited by - * the OS or the system. The number should be high, but exceeding it may degrade - * performance or cause other issues. + * # Concurrency + * + * The methods of [File] correspond to file system operations. All are + * safe for concurrent use. The maximum number of concurrent + * operations on a File may be limited by the OS or the system. The + * number should be high, but exceeding it may degrade performance or + * cause other issues. */ namespace os { interface readdirMode extends Number{} interface File { /** * Readdir reads the contents of the directory associated with file and - * returns a slice of up to n FileInfo values, as would be returned - * by Lstat, in directory order. Subsequent calls on the same file will yield + * returns a slice of up to n [FileInfo] values, as would be returned + * by [Lstat], in directory order. Subsequent calls on the same file will yield * further FileInfos. * * If n > 0, Readdir returns at most n FileInfo structures. In this case, if * Readdir returns an empty slice, it will return a non-nil error - * explaining why. At the end of a directory, the error is io.EOF. + * explaining why. At the end of a directory, the error is [io.EOF]. * * If n <= 0, Readdir returns all the FileInfo from the directory in * a single slice. In this case, if Readdir succeeds (reads all @@ -1219,7 +1276,7 @@ namespace os { * * If n > 0, Readdirnames returns at most n names. In this case, if * Readdirnames returns an empty slice, it will return a non-nil error - * explaining why. At the end of a directory, the error is io.EOF. + * explaining why. At the end of a directory, the error is [io.EOF]. * * If n <= 0, Readdirnames returns all the names from the directory in * a single slice. In this case, if Readdirnames succeeds (reads all @@ -1232,18 +1289,18 @@ namespace os { } /** * A DirEntry is an entry read from a directory - * (using the ReadDir function or a File's ReadDir method). + * (using the [ReadDir] function or a [File.ReadDir] method). */ interface DirEntry extends fs.DirEntry{} interface File { /** * ReadDir reads the contents of the directory associated with the file f - * and returns a slice of DirEntry values in directory order. + * and returns a slice of [DirEntry] values in directory order. * Subsequent calls on the same file will yield later DirEntry records in the directory. * * If n > 0, ReadDir returns at most n DirEntry records. * In this case, if ReadDir returns an empty slice, it will return an error explaining why. - * At the end of a directory, the error is io.EOF. + * At the end of a directory, the error is [io.EOF]. * * If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. * When it succeeds, it returns a nil error (not io.EOF). @@ -1260,6 +1317,28 @@ namespace os { */ (name: string): Array } + interface copyFS { + /** + * CopyFS copies the file system fsys into the directory dir, + * creating dir if necessary. + * + * Files are created with mode 0o666 plus any execute permissions + * from the source, and directories are created with mode 0o777 + * (before umask). + * + * CopyFS will not overwrite existing files. If a file name in fsys + * already exists in the destination, CopyFS will return an error + * such that errors.Is(err, fs.ErrExist) will be true. + * + * Symbolic links in fsys are not supported. A *PathError with Err set + * to ErrInvalid is returned when copying from a symbolic link. + * + * Symbolic links in dir are followed. + * + * Copying stops at and returns the first error encountered. + */ + (dir: string, fsys: fs.FS): void + } /** * Auxiliary information if the File describes a directory */ @@ -1268,7 +1347,7 @@ namespace os { interface expand { /** * Expand replaces ${var} or $var in the string based on the mapping function. - * For example, os.ExpandEnv(s) is equivalent to os.Expand(s, os.Getenv). + * For example, [os.ExpandEnv](s) is equivalent to [os.Expand](s, [os.Getenv]). */ (s: string, mapping: (_arg0: string) => string): string } @@ -1284,7 +1363,7 @@ namespace os { /** * Getenv retrieves the value of the environment variable named by the key. * It returns the value, which will be empty if the variable is not present. - * To distinguish between an empty value and an unset value, use LookupEnv. + * To distinguish between an empty value and an unset value, use [LookupEnv]. */ (key: string): string } @@ -1353,7 +1432,7 @@ namespace os { } interface newSyscallError { /** - * NewSyscallError returns, as an error, a new SyscallError + * NewSyscallError returns, as an error, a new [SyscallError] * with the given system call name and error details. * As a convenience, if err is nil, NewSyscallError returns nil. */ @@ -1361,53 +1440,55 @@ namespace os { } interface isExist { /** - * IsExist returns a boolean indicating whether the error is known to report - * that a file or directory already exists. It is satisfied by ErrExist as + * IsExist returns a boolean indicating whether its argument is known to report + * that a file or directory already exists. It is satisfied by [ErrExist] as * well as some syscall errors. * - * This function predates errors.Is. It only supports errors returned by + * This function predates [errors.Is]. It only supports errors returned by * the os package. New code should use errors.Is(err, fs.ErrExist). */ (err: Error): boolean } interface isNotExist { /** - * IsNotExist returns a boolean indicating whether the error is known to + * IsNotExist returns a boolean indicating whether its argument is known to * report that a file or directory does not exist. It is satisfied by - * ErrNotExist as well as some syscall errors. + * [ErrNotExist] as well as some syscall errors. * - * This function predates errors.Is. It only supports errors returned by + * This function predates [errors.Is]. It only supports errors returned by * the os package. New code should use errors.Is(err, fs.ErrNotExist). */ (err: Error): boolean } interface isPermission { /** - * IsPermission returns a boolean indicating whether the error is known to - * report that permission is denied. It is satisfied by ErrPermission as well + * IsPermission returns a boolean indicating whether its argument is known to + * report that permission is denied. It is satisfied by [ErrPermission] as well * as some syscall errors. * - * This function predates errors.Is. It only supports errors returned by + * This function predates [errors.Is]. It only supports errors returned by * the os package. New code should use errors.Is(err, fs.ErrPermission). */ (err: Error): boolean } interface isTimeout { /** - * IsTimeout returns a boolean indicating whether the error is known + * IsTimeout returns a boolean indicating whether its argument is known * to report that a timeout occurred. * - * This function predates errors.Is, and the notion of whether an + * This function predates [errors.Is], and the notion of whether an * error indicates a timeout can be ambiguous. For example, the Unix * error EWOULDBLOCK sometimes indicates a timeout and sometimes does not. * New code should use errors.Is with a value appropriate to the call - * returning the error, such as os.ErrDeadlineExceeded. + * returning the error, such as [os.ErrDeadlineExceeded]. */ (err: Error): boolean } interface syscallErrorType extends syscall.Errno{} + interface processMode extends Number{} + interface processStatus extends Number{} /** - * Process stores the information about a process created by StartProcess. + * Process stores the information about a process created by [StartProcess]. */ interface Process { pid: number @@ -1473,7 +1554,7 @@ namespace os { /** * FindProcess looks for a running process by its pid. * - * The Process it returns can be used to obtain information + * The [Process] it returns can be used to obtain information * about the underlying operating system process. * * On Unix systems, FindProcess always succeeds and returns a Process @@ -1486,32 +1567,32 @@ namespace os { interface startProcess { /** * StartProcess starts a new process with the program, arguments and attributes - * specified by name, argv and attr. The argv slice will become os.Args in the + * specified by name, argv and attr. The argv slice will become [os.Args] in the * new process, so it normally starts with the program name. * * If the calling goroutine has locked the operating system thread - * with runtime.LockOSThread and modified any inheritable OS-level + * with [runtime.LockOSThread] and modified any inheritable OS-level * thread state (for example, Linux or Plan 9 name spaces), the new * process will inherit the caller's thread state. * - * StartProcess is a low-level interface. The os/exec package provides + * StartProcess is a low-level interface. The [os/exec] package provides * higher-level interfaces. * - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. */ (name: string, argv: Array, attr: ProcAttr): (Process) } interface Process { /** - * Release releases any resources associated with the Process p, + * Release releases any resources associated with the [Process] p, * rendering it unusable in the future. - * Release only needs to be called if Wait is not. + * Release only needs to be called if [Process.Wait] is not. */ release(): void } interface Process { /** - * Kill causes the Process to exit immediately. Kill does not wait until + * Kill causes the [Process] to exit immediately. Kill does not wait until * the Process has actually exited. This only kills the Process itself, * not any other processes it may have started. */ @@ -1519,7 +1600,7 @@ namespace os { } interface Process { /** - * Wait waits for the Process to exit, and then returns a + * Wait waits for the [Process] to exit, and then returns a * ProcessState describing its status and an error, if any. * Wait releases any resources associated with the Process. * On most operating systems, the Process must be a child @@ -1529,8 +1610,8 @@ namespace os { } interface Process { /** - * Signal sends a signal to the Process. - * Sending Interrupt on Windows is not implemented. + * Signal sends a signal to the [Process]. + * Sending [Interrupt] on Windows is not implemented. */ signal(sig: Signal): void } @@ -1565,7 +1646,7 @@ namespace os { /** * Sys returns system-dependent exit information about * the process. Convert it to the appropriate underlying - * type, such as syscall.WaitStatus on Unix, to access its contents. + * type, such as [syscall.WaitStatus] on Unix, to access its contents. */ sys(): any } @@ -1573,7 +1654,7 @@ namespace os { /** * SysUsage returns system-dependent resource usage information about * the exited process. Convert it to the appropriate underlying - * type, such as *syscall.Rusage on Unix, to access its contents. + * type, such as [*syscall.Rusage] on Unix, to access its contents. * (On Unix, *syscall.Rusage matches struct rusage as defined in the * getrusage(2) manual page.) */ @@ -1607,7 +1688,7 @@ namespace os { * pointing to the correct executable. If a symlink was used to start * the process, depending on the operating system, the result might * be the symlink or the path it pointed to. If a stable result is - * needed, path/filepath.EvalSymlinks might help. + * needed, [path/filepath.EvalSymlinks] might help. * * Executable returns an absolute path unless an error occurred. * @@ -1619,6 +1700,8 @@ namespace os { interface File { /** * Name returns the name of the file as presented to Open. + * + * It is safe to call Name after [Close]. */ name(): string } @@ -1679,8 +1762,8 @@ namespace os { * than ReadFrom. This is used to permit ReadFrom to call io.Copy * without leading to a recursive call to ReadFrom. */ - type _subezgYh = noReadFrom&File - interface fileWithoutReadFrom extends _subezgYh { + type _subrrRcn = noReadFrom&File + interface fileWithoutReadFrom extends _subrrRcn { } interface File { /** @@ -1724,8 +1807,8 @@ namespace os { * than WriteTo. This is used to permit WriteTo to call io.Copy * without leading to a recursive call to WriteTo. */ - type _subJsbVf = noWriteTo&File - interface fileWithoutWriteTo extends _subJsbVf { + type _subSJIke = noWriteTo&File + interface fileWithoutWriteTo extends _subSJIke { } interface File { /** @@ -1771,7 +1854,7 @@ namespace os { interface create { /** * Create creates or truncates the named file. If the file already exists, - * it is truncated. If the file does not exist, it is created with mode 0666 + * it is truncated. If the file does not exist, it is created with mode 0o666 * (before umask). If successful, methods on the returned File can * be used for I/O; the associated file descriptor has mode O_RDWR. * If there is an error, it will be of type *PathError. @@ -1884,11 +1967,11 @@ namespace os { * On Unix, the mode's permission bits, ModeSetuid, ModeSetgid, and * ModeSticky are used. * - * On Windows, only the 0200 bit (owner writable) of mode is used; it + * On Windows, only the 0o200 bit (owner writable) of mode is used; it * controls whether the file's read-only attribute is set or cleared. * The other bits are currently unused. For compatibility with Go 1.12 - * and earlier, use a non-zero mode. Use mode 0400 for a read-only - * file and 0600 for a readable+writable file. + * and earlier, use a non-zero mode. Use mode 0o400 for a read-only + * file and 0o600 for a readable+writable file. * * On Plan 9, the mode's permission bits, ModeAppend, ModeExclusive, * and ModeTemporary are used. @@ -2022,9 +2105,9 @@ namespace os { } interface File { /** - * Close closes the File, rendering it unusable for I/O. - * On files that support SetDeadline, any pending I/O operations will - * be canceled and return immediately with an ErrClosed error. + * Close closes the [File], rendering it unusable for I/O. + * On files that support [File.SetDeadline], any pending I/O operations will + * be canceled and return immediately with an [ErrClosed] error. * Close will return an error if it has already been called. */ close(): void @@ -2034,9 +2117,9 @@ namespace os { * Chown changes the numeric uid and gid of the named file. * If the file is a symbolic link, it changes the uid and gid of the link's target. * A uid or gid of -1 means to not change that value. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. * - * On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or + * On Windows or Plan 9, Chown always returns the [syscall.EWINDOWS] or * EPLAN9 error, wrapped in *PathError. */ (name: string, uid: number, gid: number): void @@ -2045,9 +2128,9 @@ namespace os { /** * Lchown changes the numeric uid and gid of the named file. * If the file is a symbolic link, it changes the uid and gid of the link itself. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. * - * On Windows, it always returns the syscall.EWINDOWS error, wrapped + * On Windows, it always returns the [syscall.EWINDOWS] error, wrapped * in *PathError. */ (name: string, uid: number, gid: number): void @@ -2055,9 +2138,9 @@ namespace os { interface File { /** * Chown changes the numeric uid and gid of the named file. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. * - * On Windows, it always returns the syscall.EWINDOWS error, wrapped + * On Windows, it always returns the [syscall.EWINDOWS] error, wrapped * in *PathError. */ chown(uid: number, gid: number): void @@ -2066,7 +2149,7 @@ namespace os { /** * Truncate changes the size of the file. * It does not change the I/O offset. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. */ truncate(size: number): void } @@ -2082,11 +2165,11 @@ namespace os { /** * Chtimes changes the access and modification times of the named * file, similar to the Unix utime() or utimes() functions. - * A zero time.Time value will leave the corresponding file time unchanged. + * A zero [time.Time] value will leave the corresponding file time unchanged. * * The underlying filesystem may truncate or round the values to a * less precise time unit. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. */ (name: string, atime: time.Time, mtime: time.Time): void } @@ -2094,7 +2177,7 @@ namespace os { /** * Chdir changes the current working directory to the file, * which must be a directory. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. */ chdir(): void } @@ -2111,11 +2194,11 @@ namespace os { * Fd returns the integer Unix file descriptor referencing the open file. * If f is closed, the file descriptor becomes invalid. * If f is garbage collected, a finalizer may close the file descriptor, - * making it invalid; see runtime.SetFinalizer for more information on when - * a finalizer might be run. On Unix systems this will cause the SetDeadline + * making it invalid; see [runtime.SetFinalizer] for more information on when + * a finalizer might be run. On Unix systems this will cause the [File.SetDeadline] * methods to stop working. * Because file descriptors can be reused, the returned file descriptor may - * only be closed through the Close method of f, or by its finalizer during + * only be closed through the [File.Close] method of f, or by its finalizer during * garbage collection. Otherwise, during garbage collection the finalizer * may close an unrelated file descriptor with the same (reused) number. * @@ -2216,7 +2299,7 @@ namespace os { * It removes everything it can but returns the first error * it encounters. If the path does not exist, RemoveAll * returns nil (no error). - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. */ (path: string): void } @@ -2269,7 +2352,7 @@ namespace os { /** * Getgroups returns a list of the numeric ids of groups that the caller belongs to. * - * On Windows, it returns syscall.EWINDOWS. See the os/user package + * On Windows, it returns [syscall.EWINDOWS]. See the [os/user] package * for a possible alternative. */ (): Array @@ -2300,17 +2383,17 @@ namespace os { } interface stat { /** - * Stat returns a FileInfo describing the named file. - * If there is an error, it will be of type *PathError. + * Stat returns a [FileInfo] describing the named file. + * If there is an error, it will be of type [*PathError]. */ (name: string): FileInfo } interface lstat { /** - * Lstat returns a FileInfo describing the named file. + * Lstat returns a [FileInfo] describing the named file. * If the file is a symbolic link, the returned FileInfo * describes the symbolic link. Lstat makes no attempt to follow the link. - * If there is an error, it will be of type *PathError. + * If there is an error, it will be of type [*PathError]. * * On Windows, if the file is a reparse point that is a surrogate for another * named entity (such as a symbolic link or mounted folder), the returned @@ -2320,8 +2403,8 @@ namespace os { } interface File { /** - * Stat returns the FileInfo structure describing file. - * If there is an error, it will be of type *PathError. + * Stat returns the [FileInfo] structure describing file. + * If there is an error, it will be of type [*PathError]. */ stat(): FileInfo } @@ -2337,7 +2420,8 @@ namespace os { * opens the file for reading and writing, and returns the resulting file. * The filename is generated by taking pattern and adding a random string to the end. * If pattern includes a "*", the random string replaces the last "*". - * If dir is the empty string, CreateTemp uses the default directory for temporary files, as returned by TempDir. + * The file is created with mode 0o600 (before umask). + * If dir is the empty string, CreateTemp uses the default directory for temporary files, as returned by [TempDir]. * Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file. * The caller can use the file's Name method to find the pathname of the file. * It is the caller's responsibility to remove the file when it is no longer needed. @@ -2350,6 +2434,7 @@ namespace os { * and returns the pathname of the new directory. * The new directory's name is generated by adding a random string to the end of pattern. * If pattern includes a "*", the random string replaces the last "*" instead. + * The directory is created with mode 0o700 (before umask). * If dir is the empty string, MkdirTemp uses the default directory for temporary files, as returned by TempDir. * Multiple programs or goroutines calling MkdirTemp simultaneously will not choose the same directory. * It is the caller's responsibility to remove the directory when it is no longer needed. @@ -2364,12 +2449,14 @@ namespace os { } /** * File represents an open file descriptor. + * + * The methods of File are safe for concurrent use. */ - type _subzNURo = file - interface File extends _subzNURo { + type _subcHuOE = file + interface File extends _subcHuOE { } /** - * A FileInfo describes a file and is returned by Stat and Lstat. + * A FileInfo describes a file and is returned by [Stat] and [Lstat]. */ interface FileInfo extends fs.FileInfo{} /** @@ -2377,7 +2464,7 @@ namespace os { * The bits have the same definition on all systems, so that * information about files can be moved from one system * to another portably. Not all bits apply to all systems. - * The only required bit is ModeDir for directories. + * The only required bit is [ModeDir] for directories. */ interface FileMode extends fs.FileMode{} interface fileStat { @@ -2392,7 +2479,7 @@ namespace os { * For example, on Unix this means that the device and inode fields * of the two underlying structures are identical; on other systems * the decision may be based on the path names. - * SameFile only applies to results returned by this package's Stat. + * SameFile only applies to results returned by this package's [Stat]. * It returns false in other cases. */ (fi1: FileInfo, fi2: FileInfo): boolean @@ -2470,14 +2557,6 @@ namespace filepath { */ (pattern: string): Array } - /** - * A lazybuf is a lazily constructed path buffer. - * It supports append, reading previously appended bytes, - * and retrieving the final string. It does not allocate a buffer - * to hold the output until that output diverges from s. - */ - interface lazybuf { - } interface clean { /** * Clean returns the shortest path name equivalent to path @@ -2535,6 +2614,19 @@ namespace filepath { */ (path: string): boolean } + interface localize { + /** + * Localize converts a slash-separated path into an operating system path. + * The input path must be a valid path as reported by [io/fs.ValidPath]. + * + * Localize returns an error if the path cannot be represented by the operating system. + * For example, the path a\b is rejected on Windows, on which \ is a separator + * character and cannot be part of a filename. + * + * The path returned by Localize will always be local, as reported by IsLocal. + */ + (path: string): string + } interface toSlash { /** * ToSlash returns the result of replacing each separator character @@ -2548,6 +2640,9 @@ namespace filepath { * FromSlash returns the result of replacing each slash ('/') character * in path with a separator character. Multiple slashes are replaced * by multiple separators. + * + * See also the Localize function, which converts a slash-separated path + * as used by the io/fs package to an operating system path. */ (path: string): string } @@ -2601,6 +2696,12 @@ namespace filepath { */ (path: string): string } + interface isAbs { + /** + * IsAbs reports whether the path is absolute. + */ + (path: string): boolean + } interface abs { /** * Abs returns an absolute representation of path. @@ -2733,12 +2834,6 @@ namespace filepath { */ (path: string): string } - interface isAbs { - /** - * IsAbs reports whether the path is absolute. - */ - (path: string): boolean - } interface hasPrefix { /** * HasPrefix exists for historical compatibility and should not be used. @@ -2761,7 +2856,7 @@ namespace filepath { * pipelines, or redirections typically done by shells. The package * behaves more like C's "exec" family of functions. To expand glob * patterns, either call the shell directly, taking care to escape any - * dangerous input, or use the path/filepath package's Glob function. + * dangerous input, or use the [path/filepath] package's Glob function. * To expand environment variables, use package os's ExpandEnv. * * Note that the examples in this package assume a Unix system. @@ -2770,7 +2865,7 @@ namespace filepath { * * # Executables in the current directory * - * The functions Command and LookPath look for a program + * The functions [Command] and [LookPath] look for a program * in the directories listed in the current path, following the * conventions of the host operating system. * Operating systems have for decades included the current @@ -2781,10 +2876,10 @@ namespace filepath { * * To avoid those security problems, as of Go 1.19, this package will not resolve a program * using an implicit or explicit path entry relative to the current directory. - * That is, if you run exec.LookPath("go"), it will not successfully return + * That is, if you run [LookPath]("go"), it will not successfully return * ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. * Instead, if the usual path algorithms would result in that answer, - * these functions return an error err satisfying errors.Is(err, ErrDot). + * these functions return an error err satisfying [errors.Is](err, [ErrDot]). * * For example, consider these two program snippets: * @@ -2849,12 +2944,12 @@ namespace filepath { namespace exec { interface command { /** - * Command returns the Cmd struct to execute the named program with + * Command returns the [Cmd] struct to execute the named program with * the given arguments. * * It sets only the Path and Args in the returned structure. * - * If name contains no path separators, Command uses LookPath to + * If name contains no path separators, Command uses [LookPath] to * resolve name to a complete path if possible. Otherwise it uses name * directly as Path. * @@ -2876,471 +2971,6 @@ namespace exec { } } -namespace security { - interface s256Challenge { - /** - * S256Challenge creates base64 encoded sha256 challenge string derived from code. - * The padding of the result base64 string is stripped per [RFC 7636]. - * - * [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 - */ - (code: string): string - } - interface md5 { - /** - * MD5 creates md5 hash from the provided plain text. - */ - (text: string): string - } - interface sha256 { - /** - * SHA256 creates sha256 hash as defined in FIPS 180-4 from the provided text. - */ - (text: string): string - } - interface sha512 { - /** - * SHA512 creates sha512 hash as defined in FIPS 180-4 from the provided text. - */ - (text: string): string - } - interface hs256 { - /** - * HS256 creates a HMAC hash with sha256 digest algorithm. - */ - (text: string, secret: string): string - } - interface hs512 { - /** - * HS512 creates a HMAC hash with sha512 digest algorithm. - */ - (text: string, secret: string): string - } - interface equal { - /** - * Equal compares two hash strings for equality without leaking timing information. - */ - (hash1: string, hash2: string): boolean - } - // @ts-ignore - import crand = rand - interface encrypt { - /** - * Encrypt encrypts "data" with the specified "key" (must be valid 32 char AES key). - * - * This method uses AES-256-GCM block cypher mode. - */ - (data: string|Array, key: string): string - } - interface decrypt { - /** - * Decrypt decrypts encrypted text with key (must be valid 32 chars AES key). - * - * This method uses AES-256-GCM block cypher mode. - */ - (cipherText: string, key: string): string|Array - } - interface parseUnverifiedJWT { - /** - * ParseUnverifiedJWT parses JWT and returns its claims - * but DOES NOT verify the signature. - * - * It verifies only the exp, iat and nbf claims. - */ - (token: string): jwt.MapClaims - } - interface parseJWT { - /** - * ParseJWT verifies and parses JWT and returns its claims. - */ - (token: string, verificationKey: string): jwt.MapClaims - } - interface newJWT { - /** - * NewJWT generates and returns new HS256 signed JWT. - */ - (payload: jwt.MapClaims, signingKey: string, secondsDuration: number): string - } - interface newToken { - /** - * Deprecated: - * Consider replacing with NewJWT(). - * - * NewToken is a legacy alias for NewJWT that generates a HS256 signed JWT. - */ - (payload: jwt.MapClaims, signingKey: string, secondsDuration: number): string - } - // @ts-ignore - import cryptoRand = rand - // @ts-ignore - import mathRand = rand - interface randomString { - /** - * RandomString generates a cryptographically random string with the specified length. - * - * The generated string matches [A-Za-z0-9]+ and it's transparent to URL-encoding. - */ - (length: number): string - } - interface randomStringWithAlphabet { - /** - * RandomStringWithAlphabet generates a cryptographically random string - * with the specified length and characters set. - * - * It panics if for some reason rand.Int returns a non-nil error. - */ - (length: number, alphabet: string): string - } - interface pseudorandomString { - /** - * PseudorandomString generates a pseudorandom string with the specified length. - * - * The generated string matches [A-Za-z0-9]+ and it's transparent to URL-encoding. - * - * For a cryptographically random string (but a little bit slower) use RandomString instead. - */ - (length: number): string - } - interface pseudorandomStringWithAlphabet { - /** - * PseudorandomStringWithAlphabet generates a pseudorandom string - * with the specified length and characters set. - * - * For a cryptographically random (but a little bit slower) use RandomStringWithAlphabet instead. - */ - (length: number, alphabet: string): string - } -} - -namespace filesystem { - /** - * FileReader defines an interface for a file resource reader. - */ - interface FileReader { - [key:string]: any; - open(): io.ReadSeekCloser - } - /** - * File defines a single file [io.ReadSeekCloser] resource. - * - * The file could be from a local path, multipart/form-data header, etc. - */ - interface File { - reader: FileReader - name: string - originalName: string - size: number - } - interface newFileFromPath { - /** - * NewFileFromPath creates a new File instance from the provided local file path. - */ - (path: string): (File) - } - interface newFileFromBytes { - /** - * NewFileFromBytes creates a new File instance from the provided byte slice. - */ - (b: string|Array, name: string): (File) - } - interface newFileFromMultipart { - /** - * NewFileFromMultipart creates a new File from the provided multipart header. - */ - (mh: multipart.FileHeader): (File) - } - interface newFileFromUrl { - /** - * NewFileFromUrl creates a new File from the provided url by - * downloading the resource and load it as BytesReader. - * - * Example - * - * ``` - * ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - * defer cancel() - * - * file, err := filesystem.NewFileFromUrl(ctx, "https://example.com/image.png") - * ``` - */ - (ctx: context.Context, url: string): (File) - } - /** - * MultipartReader defines a FileReader from [multipart.FileHeader]. - */ - interface MultipartReader { - header?: multipart.FileHeader - } - interface MultipartReader { - /** - * Open implements the [filesystem.FileReader] interface. - */ - open(): io.ReadSeekCloser - } - /** - * PathReader defines a FileReader from a local file path. - */ - interface PathReader { - path: string - } - interface PathReader { - /** - * Open implements the [filesystem.FileReader] interface. - */ - open(): io.ReadSeekCloser - } - /** - * BytesReader defines a FileReader from bytes content. - */ - interface BytesReader { - bytes: string|Array - } - interface BytesReader { - /** - * Open implements the [filesystem.FileReader] interface. - */ - open(): io.ReadSeekCloser - } - type _subRtcDW = bytes.Reader - interface bytesReadSeekCloser extends _subRtcDW { - } - interface bytesReadSeekCloser { - /** - * Close implements the [io.ReadSeekCloser] interface. - */ - close(): void - } - interface System { - } - interface newS3 { - /** - * NewS3 initializes an S3 filesystem instance. - * - * NB! Make sure to call `Close()` after you are done working with it. - */ - (bucketName: string, region: string, endpoint: string, accessKey: string, secretKey: string, s3ForcePathStyle: boolean): (System) - } - interface newLocal { - /** - * NewLocal initializes a new local filesystem instance. - * - * NB! Make sure to call `Close()` after you are done working with it. - */ - (dirPath: string): (System) - } - interface System { - /** - * SetContext assigns the specified context to the current filesystem. - */ - setContext(ctx: context.Context): void - } - interface System { - /** - * Close releases any resources used for the related filesystem. - */ - close(): void - } - interface System { - /** - * Exists checks if file with fileKey path exists or not. - */ - exists(fileKey: string): boolean - } - interface System { - /** - * Attributes returns the attributes for the file with fileKey path. - */ - attributes(fileKey: string): (blob.Attributes) - } - interface System { - /** - * GetFile returns a file content reader for the given fileKey. - * - * NB! Make sure to call `Close()` after you are done working with it. - */ - getFile(fileKey: string): (blob.Reader) - } - interface System { - /** - * Copy copies the file stored at srcKey to dstKey. - * - * If dstKey file already exists, it is overwritten. - */ - copy(srcKey: string, dstKey: string): void - } - interface System { - /** - * List returns a flat list with info for all files under the specified prefix. - */ - list(prefix: string): Array<(blob.ListObject | undefined)> - } - interface System { - /** - * Upload writes content into the fileKey location. - */ - upload(content: string|Array, fileKey: string): void - } - interface System { - /** - * UploadFile uploads the provided multipart file to the fileKey location. - */ - uploadFile(file: File, fileKey: string): void - } - interface System { - /** - * UploadMultipart uploads the provided multipart file to the fileKey location. - */ - uploadMultipart(fh: multipart.FileHeader, fileKey: string): void - } - interface System { - /** - * Delete deletes stored file at fileKey location. - */ - delete(fileKey: string): void - } - interface System { - /** - * DeletePrefix deletes everything starting with the specified prefix. - */ - deletePrefix(prefix: string): Array - } - interface System { - /** - * Serve serves the file at fileKey location to an HTTP response. - * - * If the `download` query parameter is used the file will be always served for - * download no matter of its type (aka. with "Content-Disposition: attachment"). - */ - serve(res: http.ResponseWriter, req: http.Request, fileKey: string, name: string): void - } - interface System { - /** - * CreateThumb creates a new thumb image for the file at originalKey location. - * The new thumb file is stored at thumbKey location. - * - * thumbSize is in the format: - * - 0xH (eg. 0x100) - resize to H height preserving the aspect ratio - * - Wx0 (eg. 300x0) - resize to W width preserving the aspect ratio - * - WxH (eg. 300x100) - resize and crop to WxH viewbox (from center) - * - WxHt (eg. 300x100t) - resize and crop to WxH viewbox (from top) - * - WxHb (eg. 300x100b) - resize and crop to WxH viewbox (from bottom) - * - WxHf (eg. 300x100f) - fit inside a WxH viewbox (without cropping) - */ - createThumb(originalKey: string, thumbKey: string, thumbSize: string): void - } - // @ts-ignore - import v4 = signer - // @ts-ignore - import smithyhttp = http - interface ignoredHeadersKey { - } -} - -/** - * Package template is a thin wrapper around the standard html/template - * and text/template packages that implements a convenient registry to - * load and cache templates on the fly concurrently. - * - * It was created to assist the JSVM plugin HTML rendering, but could be used in other Go code. - * - * Example: - * - * ``` - * registry := template.NewRegistry() - * - * html1, err := registry.LoadFiles( - * // the files set wil be parsed only once and then cached - * "layout.html", - * "content.html", - * ).Render(map[string]any{"name": "John"}) - * - * html2, err := registry.LoadFiles( - * // reuse the already parsed and cached files set - * "layout.html", - * "content.html", - * ).Render(map[string]any{"name": "Jane"}) - * ``` - */ -namespace template { - interface newRegistry { - /** - * NewRegistry creates and initializes a new templates registry with - * some defaults (eg. global "raw" template function for unescaped HTML). - * - * Use the Registry.Load* methods to load templates into the registry. - */ - (): (Registry) - } - /** - * Registry defines a templates registry that is safe to be used by multiple goroutines. - * - * Use the Registry.Load* methods to load templates into the registry. - */ - interface Registry { - } - interface Registry { - /** - * AddFuncs registers new global template functions. - * - * The key of each map entry is the function name that will be used in the templates. - * If a function with the map entry name already exists it will be replaced with the new one. - * - * The value of each map entry is a function that must have either a - * single return value, or two return values of which the second has type error. - * - * Example: - * - * r.AddFuncs(map[string]any{ - * ``` - * "toUpper": func(str string) string { - * return strings.ToUppser(str) - * }, - * ... - * ``` - * }) - */ - addFuncs(funcs: _TygojaDict): (Registry) - } - interface Registry { - /** - * LoadFiles caches (if not already) the specified filenames set as a - * single template and returns a ready to use Renderer instance. - * - * There must be at least 1 filename specified. - */ - loadFiles(...filenames: string[]): (Renderer) - } - interface Registry { - /** - * LoadString caches (if not already) the specified inline string as a - * single template and returns a ready to use Renderer instance. - */ - loadString(text: string): (Renderer) - } - interface Registry { - /** - * LoadFS caches (if not already) the specified fs and globPatterns - * pair as single template and returns a ready to use Renderer instance. - * - * There must be at least 1 file matching the provided globPattern(s) - * (note that most file names serves as glob patterns matching themselves). - */ - loadFS(fsys: fs.FS, ...globPatterns: string[]): (Renderer) - } - /** - * Renderer defines a single parsed template. - */ - interface Renderer { - } - interface Renderer { - /** - * Render executes the template with the specified data as the dot object - * and returns the result as plain string. - */ - render(data: any): string - } -} - /** * Package validation provides configurable and extensible rules for validating data of various types. */ @@ -3359,25 +2989,6 @@ namespace ozzo_validation { } } -namespace middleware { - interface bodyLimit { - /** - * BodyLimit returns a BodyLimit middleware. - * - * BodyLimit middleware sets the maximum allowed size for a request body, if the size exceeds the configured limit, it - * sends "413 - Request Entity Too Large" response. The BodyLimit is determined based on both `Content-Length` request - * header and actual content read, which makes it super secure. - */ - (limitBytes: number): echo.MiddlewareFunc - } - interface gzip { - /** - * Gzip returns a middleware which compresses HTTP response using gzip compression scheme. - */ - (): echo.MiddlewareFunc - } -} - /** * Package dbx provides a set of DB-agnostic and easy-to-use query building methods for relational databases. */ @@ -3714,14 +3325,14 @@ namespace dbx { /** * MssqlBuilder is the builder for SQL Server databases. */ - type _subrFKDD = BaseBuilder - interface MssqlBuilder extends _subrFKDD { + type _subtdfax = BaseBuilder + interface MssqlBuilder extends _subtdfax { } /** * MssqlQueryBuilder is the query builder for SQL Server databases. */ - type _submHtvV = BaseQueryBuilder - interface MssqlQueryBuilder extends _submHtvV { + type _sublQypF = BaseQueryBuilder + interface MssqlQueryBuilder extends _sublQypF { } interface newMssqlBuilder { /** @@ -3792,8 +3403,8 @@ namespace dbx { /** * MysqlBuilder is the builder for MySQL databases. */ - type _subIVLoN = BaseBuilder - interface MysqlBuilder extends _subIVLoN { + type _subtKrsG = BaseBuilder + interface MysqlBuilder extends _subtKrsG { } interface newMysqlBuilder { /** @@ -3868,14 +3479,14 @@ namespace dbx { /** * OciBuilder is the builder for Oracle databases. */ - type _subfiTVV = BaseBuilder - interface OciBuilder extends _subfiTVV { + type _subTevke = BaseBuilder + interface OciBuilder extends _subTevke { } /** * OciQueryBuilder is the query builder for Oracle databases. */ - type _subrSBRI = BaseQueryBuilder - interface OciQueryBuilder extends _subrSBRI { + type _subFRAPn = BaseQueryBuilder + interface OciQueryBuilder extends _subFRAPn { } interface newOciBuilder { /** @@ -3938,8 +3549,8 @@ namespace dbx { /** * PgsqlBuilder is the builder for PostgreSQL databases. */ - type _subtFJti = BaseBuilder - interface PgsqlBuilder extends _subtFJti { + type _subKKaBH = BaseBuilder + interface PgsqlBuilder extends _subKKaBH { } interface newPgsqlBuilder { /** @@ -4006,8 +3617,8 @@ namespace dbx { /** * SqliteBuilder is the builder for SQLite databases. */ - type _subrBNop = BaseBuilder - interface SqliteBuilder extends _subrBNop { + type _subRyJlz = BaseBuilder + interface SqliteBuilder extends _subRyJlz { } interface newSqliteBuilder { /** @@ -4106,8 +3717,8 @@ namespace dbx { /** * StandardBuilder is the builder that is used by DB for an unknown driver. */ - type _subesQFA = BaseBuilder - interface StandardBuilder extends _subesQFA { + type _subkIcuU = BaseBuilder + interface StandardBuilder extends _subkIcuU { } interface newStandardBuilder { /** @@ -4173,8 +3784,8 @@ namespace dbx { * DB enhances sql.DB by providing a set of DB-agnostic query building methods. * DB allows easier query building and population of data into Go variables. */ - type _subkWabA = Builder - interface DB extends _subkWabA { + type _subExmlQ = Builder + interface DB extends _subExmlQ { /** * FieldMapper maps struct fields to DB columns. Defaults to DefaultFieldMapFunc. */ @@ -4978,8 +4589,8 @@ namespace dbx { * Rows enhances sql.Rows by providing additional data query methods. * Rows can be obtained by calling Query.Rows(). It is mainly used to populate data row by row. */ - type _subrMzGi = sql.Rows - interface Rows extends _subrMzGi { + type _subSZGyU = sql.Rows + interface Rows extends _subSZGyU { } interface Rows { /** @@ -5337,8 +4948,8 @@ namespace dbx { }): string } interface structInfo { } - type _subSCEkW = structInfo - interface structValue extends _subSCEkW { + type _subOCBls = structInfo + interface structValue extends _subOCBls { } interface fieldInfo { } @@ -5377,8 +4988,8 @@ namespace dbx { /** * Tx enhances sql.Tx with additional querying methods. */ - type _subXvIkD = Builder - interface Tx extends _subXvIkD { + type _subWmBdO = Builder + interface Tx extends _subWmBdO { } interface Tx { /** @@ -5394,57 +5005,403 @@ namespace dbx { } } -/** - * Package tokens implements various user and admin tokens generation methods. - */ -namespace tokens { - interface newAdminAuthToken { +namespace security { + interface s256Challenge { /** - * NewAdminAuthToken generates and returns a new admin authentication token. + * S256Challenge creates base64 encoded sha256 challenge string derived from code. + * The padding of the result base64 string is stripped per [RFC 7636]. + * + * [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 */ - (app: CoreApp, admin: models.Admin): string + (code: string): string } - interface newAdminResetPasswordToken { + interface md5 { /** - * NewAdminResetPasswordToken generates and returns a new admin password reset request token. + * MD5 creates md5 hash from the provided plain text. */ - (app: CoreApp, admin: models.Admin): string + (text: string): string } - interface newAdminFileToken { + interface sha256 { /** - * NewAdminFileToken generates and returns a new admin private file access token. + * SHA256 creates sha256 hash as defined in FIPS 180-4 from the provided text. */ - (app: CoreApp, admin: models.Admin): string + (text: string): string } - interface newRecordAuthToken { + interface sha512 { /** - * NewRecordAuthToken generates and returns a new auth record authentication token. + * SHA512 creates sha512 hash as defined in FIPS 180-4 from the provided text. */ - (app: CoreApp, record: models.Record): string + (text: string): string } - interface newRecordVerifyToken { + interface hs256 { /** - * NewRecordVerifyToken generates and returns a new record verification token. + * HS256 creates a HMAC hash with sha256 digest algorithm. */ - (app: CoreApp, record: models.Record): string + (text: string, secret: string): string } - interface newRecordResetPasswordToken { + interface hs512 { /** - * NewRecordResetPasswordToken generates and returns a new auth record password reset request token. + * HS512 creates a HMAC hash with sha512 digest algorithm. */ - (app: CoreApp, record: models.Record): string + (text: string, secret: string): string } - interface newRecordChangeEmailToken { + interface equal { /** - * NewRecordChangeEmailToken generates and returns a new auth record change email request token. + * Equal compares two hash strings for equality without leaking timing information. */ - (app: CoreApp, record: models.Record, newEmail: string): string + (hash1: string, hash2: string): boolean } - interface newRecordFileToken { + // @ts-ignore + import crand = rand + interface encrypt { /** - * NewRecordFileToken generates and returns a new record private file access token. + * Encrypt encrypts "data" with the specified "key" (must be valid 32 char AES key). + * + * This method uses AES-256-GCM block cypher mode. */ - (app: CoreApp, record: models.Record): string + (data: string|Array, key: string): string + } + interface decrypt { + /** + * Decrypt decrypts encrypted text with key (must be valid 32 chars AES key). + * + * This method uses AES-256-GCM block cypher mode. + */ + (cipherText: string, key: string): string|Array + } + interface parseUnverifiedJWT { + /** + * ParseUnverifiedJWT parses JWT and returns its claims + * but DOES NOT verify the signature. + * + * It verifies only the exp, iat and nbf claims. + */ + (token: string): jwt.MapClaims + } + interface parseJWT { + /** + * ParseJWT verifies and parses JWT and returns its claims. + */ + (token: string, verificationKey: string): jwt.MapClaims + } + interface newJWT { + /** + * NewJWT generates and returns new HS256 signed JWT. + */ + (payload: jwt.MapClaims, signingKey: string, duration: time.Duration): string + } + // @ts-ignore + import cryptoRand = rand + // @ts-ignore + import mathRand = rand + interface randomString { + /** + * RandomString generates a cryptographically random string with the specified length. + * + * The generated string matches [A-Za-z0-9]+ and it's transparent to URL-encoding. + */ + (length: number): string + } + interface randomStringWithAlphabet { + /** + * RandomStringWithAlphabet generates a cryptographically random string + * with the specified length and characters set. + * + * It panics if for some reason rand.Int returns a non-nil error. + */ + (length: number, alphabet: string): string + } + interface pseudorandomString { + /** + * PseudorandomString generates a pseudorandom string with the specified length. + * + * The generated string matches [A-Za-z0-9]+ and it's transparent to URL-encoding. + * + * For a cryptographically random string (but a little bit slower) use RandomString instead. + */ + (length: number): string + } + interface pseudorandomStringWithAlphabet { + /** + * PseudorandomStringWithAlphabet generates a pseudorandom string + * with the specified length and characters set. + * + * For a cryptographically random (but a little bit slower) use RandomStringWithAlphabet instead. + */ + (length: number, alphabet: string): string + } + interface randomStringByRegex { + /** + * RandomStringByRegex generates a random string matching the regex pattern. + * If optFlags is not set, fallbacks to [syntax.Perl]. + * + * NB! While the source of the randomness comes from [crypto/rand] this method + * is not recommended to be used on its own in critical secure contexts because + * the generated length could vary too much on the used pattern and may not be + * as secure as simply calling [security.RandomString]. + * If you still insist on using it for such purposes, consider at least + * a large enough minimum length for the generated string, e.g. `[a-z0-9]{30}`. + * + * This function is inspired by github.com/pipe01/revregexp, github.com/lucasjones/reggen and other similar packages. + */ + (pattern: string, ...optFlags: syntax.Flags[]): string + } +} + +namespace filesystem { + /** + * FileReader defines an interface for a file resource reader. + */ + interface FileReader { + [key:string]: any; + open(): io.ReadSeekCloser + } + /** + * File defines a single file [io.ReadSeekCloser] resource. + * + * The file could be from a local path, multipart/form-data header, etc. + */ + interface File { + reader: FileReader + name: string + originalName: string + size: number + } + interface File { + /** + * AsMap implements [core.mapExtractor] and returns a value suitable + * to be used in an API rule expression. + */ + asMap(): _TygojaDict + } + interface newFileFromPath { + /** + * NewFileFromPath creates a new File instance from the provided local file path. + */ + (path: string): (File) + } + interface newFileFromBytes { + /** + * NewFileFromBytes creates a new File instance from the provided byte slice. + */ + (b: string|Array, name: string): (File) + } + interface newFileFromMultipart { + /** + * NewFileFromMultipart creates a new File from the provided multipart header. + */ + (mh: multipart.FileHeader): (File) + } + interface newFileFromURL { + /** + * NewFileFromURL creates a new File from the provided url by + * downloading the resource and load it as BytesReader. + * + * Example + * + * ``` + * ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + * defer cancel() + * + * file, err := filesystem.NewFileFromURL(ctx, "https://example.com/image.png") + * ``` + */ + (ctx: context.Context, url: string): (File) + } + /** + * MultipartReader defines a FileReader from [multipart.FileHeader]. + */ + interface MultipartReader { + header?: multipart.FileHeader + } + interface MultipartReader { + /** + * Open implements the [filesystem.FileReader] interface. + */ + open(): io.ReadSeekCloser + } + /** + * PathReader defines a FileReader from a local file path. + */ + interface PathReader { + path: string + } + interface PathReader { + /** + * Open implements the [filesystem.FileReader] interface. + */ + open(): io.ReadSeekCloser + } + /** + * BytesReader defines a FileReader from bytes content. + */ + interface BytesReader { + bytes: string|Array + } + interface BytesReader { + /** + * Open implements the [filesystem.FileReader] interface. + */ + open(): io.ReadSeekCloser + } + type _subteRxd = bytes.Reader + interface bytesReadSeekCloser extends _subteRxd { + } + interface bytesReadSeekCloser { + /** + * Close implements the [io.ReadSeekCloser] interface. + */ + close(): void + } + interface System { + } + interface newS3 { + /** + * NewS3 initializes an S3 filesystem instance. + * + * NB! Make sure to call `Close()` after you are done working with it. + */ + (bucketName: string, region: string, endpoint: string, accessKey: string, secretKey: string, s3ForcePathStyle: boolean): (System) + } + interface newLocal { + /** + * NewLocal initializes a new local filesystem instance. + * + * NB! Make sure to call `Close()` after you are done working with it. + */ + (dirPath: string): (System) + } + interface System { + /** + * SetContext assigns the specified context to the current filesystem. + */ + setContext(ctx: context.Context): void + } + interface System { + /** + * Close releases any resources used for the related filesystem. + */ + close(): void + } + interface System { + /** + * Exists checks if file with fileKey path exists or not. + * + * If the file doesn't exist returns false and ErrNotFound. + */ + exists(fileKey: string): boolean + } + interface System { + /** + * Attributes returns the attributes for the file with fileKey path. + * + * If the file doesn't exist it returns ErrNotFound. + */ + attributes(fileKey: string): (blob.Attributes) + } + interface System { + /** + * GetFile returns a file content reader for the given fileKey. + * + * NB! Make sure to call Close() on the file after you are done working with it. + * + * If the file doesn't exist returns ErrNotFound. + */ + getFile(fileKey: string): (blob.Reader) + } + interface System { + /** + * Copy copies the file stored at srcKey to dstKey. + * + * If srcKey file doesn't exist, it returns ErrNotFound. + * + * If dstKey file already exists, it is overwritten. + */ + copy(srcKey: string, dstKey: string): void + } + interface System { + /** + * List returns a flat list with info for all files under the specified prefix. + */ + list(prefix: string): Array<(blob.ListObject | undefined)> + } + interface System { + /** + * Upload writes content into the fileKey location. + */ + upload(content: string|Array, fileKey: string): void + } + interface System { + /** + * UploadFile uploads the provided File to the fileKey location. + */ + uploadFile(file: File, fileKey: string): void + } + interface System { + /** + * UploadMultipart uploads the provided multipart file to the fileKey location. + */ + uploadMultipart(fh: multipart.FileHeader, fileKey: string): void + } + interface System { + /** + * Delete deletes stored file at fileKey location. + * + * If the file doesn't exist returns ErrNotFound. + */ + delete(fileKey: string): void + } + interface System { + /** + * DeletePrefix deletes everything starting with the specified prefix. + * + * The prefix could be subpath (ex. "/a/b/") or filename prefix (ex. "/a/b/file_"). + */ + deletePrefix(prefix: string): Array + } + interface System { + /** + * Checks if the provided dir prefix doesn't have any files. + * + * A trailing slash will be appended to a non-empty dir string argument + * to ensure that the checked prefix is a "directory". + * + * Returns "false" in case the has at least one file, otherwise - "true". + */ + isEmptyDir(dir: string): boolean + } + interface System { + /** + * Serve serves the file at fileKey location to an HTTP response. + * + * If the `download` query parameter is used the file will be always served for + * download no matter of its type (aka. with "Content-Disposition: attachment"). + * + * Internally this method uses [http.ServeContent] so Range requests, + * If-Match, If-Unmodified-Since, etc. headers are handled transparently. + */ + serve(res: http.ResponseWriter, req: http.Request, fileKey: string, name: string): void + } + interface System { + /** + * CreateThumb creates a new thumb image for the file at originalKey location. + * The new thumb file is stored at thumbKey location. + * + * thumbSize is in the format: + * - 0xH (eg. 0x100) - resize to H height preserving the aspect ratio + * - Wx0 (eg. 300x0) - resize to W width preserving the aspect ratio + * - WxH (eg. 300x100) - resize and crop to WxH viewbox (from center) + * - WxHt (eg. 300x100t) - resize and crop to WxH viewbox (from top) + * - WxHb (eg. 300x100b) - resize and crop to WxH viewbox (from bottom) + * - WxHf (eg. 300x100f) - fit inside a WxH viewbox (without cropping) + */ + createThumb(originalKey: string, thumbKey: string, thumbSize: string): void + } + // @ts-ignore + import v4 = signer + // @ts-ignore + import smithyhttp = http + interface ignoredHeadersKey { } } @@ -5453,203 +5410,146 @@ namespace tokens { * emails like forgotten password, verification, etc. */ namespace mails { - interface sendAdminPasswordReset { + interface sendRecordAuthAlert { /** - * SendAdminPasswordReset sends a password reset request email to the specified admin. + * SendRecordAuthAlert sends a new device login alert to the specified auth record. */ - (app: CoreApp, admin: models.Admin): void + (app: CoreApp, authRecord: core.Record): void + } + interface sendRecordOTP { + /** + * SendRecordOTP sends OTP email to the specified auth record. + */ + (app: CoreApp, authRecord: core.Record, otpId: string, pass: string): void } interface sendRecordPasswordReset { /** - * SendRecordPasswordReset sends a password reset request email to the specified user. + * SendRecordPasswordReset sends a password reset request email to the specified auth record. */ - (app: CoreApp, authRecord: models.Record): void + (app: CoreApp, authRecord: core.Record): void } interface sendRecordVerification { /** - * SendRecordVerification sends a verification request email to the specified user. + * SendRecordVerification sends a verification request email to the specified auth record. */ - (app: CoreApp, authRecord: models.Record): void + (app: CoreApp, authRecord: core.Record): void } interface sendRecordChangeEmail { /** - * SendRecordChangeEmail sends a change email confirmation email to the specified user. + * SendRecordChangeEmail sends a change email confirmation email to the specified auth record. */ - (app: CoreApp, record: models.Record, newEmail: string): void + (app: CoreApp, authRecord: core.Record, newEmail: string): void } } /** - * Package models implements various services used for request data - * validation and applying changes to existing DB models through the app Dao. + * Package template is a thin wrapper around the standard html/template + * and text/template packages that implements a convenient registry to + * load and cache templates on the fly concurrently. + * + * It was created to assist the JSVM plugin HTML rendering, but could be used in other Go code. + * + * Example: + * + * ``` + * registry := template.NewRegistry() + * + * html1, err := registry.LoadFiles( + * // the files set wil be parsed only once and then cached + * "layout.html", + * "content.html", + * ).Render(map[string]any{"name": "John"}) + * + * html2, err := registry.LoadFiles( + * // reuse the already parsed and cached files set + * "layout.html", + * "content.html", + * ).Render(map[string]any{"name": "Jane"}) + * ``` */ +namespace template { + interface newRegistry { + /** + * NewRegistry creates and initializes a new templates registry with + * some defaults (eg. global "raw" template function for unescaped HTML). + * + * Use the Registry.Load* methods to load templates into the registry. + */ + (): (Registry) + } + /** + * Registry defines a templates registry that is safe to be used by multiple goroutines. + * + * Use the Registry.Load* methods to load templates into the registry. + */ + interface Registry { + } + interface Registry { + /** + * AddFuncs registers new global template functions. + * + * The key of each map entry is the function name that will be used in the templates. + * If a function with the map entry name already exists it will be replaced with the new one. + * + * The value of each map entry is a function that must have either a + * single return value, or two return values of which the second has type error. + * + * Example: + * + * ``` + * r.AddFuncs(map[string]any{ + * "toUpper": func(str string) string { + * return strings.ToUppser(str) + * }, + * ... + * }) + * ``` + */ + addFuncs(funcs: _TygojaDict): (Registry) + } + interface Registry { + /** + * LoadFiles caches (if not already) the specified filenames set as a + * single template and returns a ready to use Renderer instance. + * + * There must be at least 1 filename specified. + */ + loadFiles(...filenames: string[]): (Renderer) + } + interface Registry { + /** + * LoadString caches (if not already) the specified inline string as a + * single template and returns a ready to use Renderer instance. + */ + loadString(text: string): (Renderer) + } + interface Registry { + /** + * LoadFS caches (if not already) the specified fs and globPatterns + * pair as single template and returns a ready to use Renderer instance. + * + * There must be at least 1 file matching the provided globPattern(s) + * (note that most file names serves as glob patterns matching themselves). + */ + loadFS(fsys: fs.FS, ...globPatterns: string[]): (Renderer) + } + /** + * Renderer defines a single parsed template. + */ + interface Renderer { + } + interface Renderer { + /** + * Render executes the template with the specified data as the dot object + * and returns the result as plain string. + */ + render(data: any): string + } +} + namespace forms { // @ts-ignore import validation = ozzo_validation - /** - * AdminLogin is an admin email/pass login form. - */ - interface AdminLogin { - identity: string - password: string - } - interface newAdminLogin { - /** - * NewAdminLogin creates a new [AdminLogin] form initialized with - * the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp): (AdminLogin) - } - interface AdminLogin { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface AdminLogin { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface AdminLogin { - /** - * Submit validates and submits the admin form. - * On success returns the authorized admin model. - * - * You can optionally provide a list of InterceptorFunc to - * further modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Admin) - } - /** - * AdminPasswordResetConfirm is an admin password reset confirmation form. - */ - interface AdminPasswordResetConfirm { - token: string - password: string - passwordConfirm: string - } - interface newAdminPasswordResetConfirm { - /** - * NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp): (AdminPasswordResetConfirm) - } - interface AdminPasswordResetConfirm { - /** - * SetDao replaces the form Dao instance with the provided one. - * - * This is useful if you want to use a specific transaction Dao instance - * instead of the default app.Dao(). - */ - setDao(dao: daos.Dao): void - } - interface AdminPasswordResetConfirm { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface AdminPasswordResetConfirm { - /** - * Submit validates and submits the admin password reset confirmation form. - * On success returns the updated admin model associated to `form.Token`. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Admin) - } - /** - * AdminPasswordResetRequest is an admin password reset request form. - */ - interface AdminPasswordResetRequest { - email: string - } - interface newAdminPasswordResetRequest { - /** - * NewAdminPasswordResetRequest creates a new [AdminPasswordResetRequest] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp): (AdminPasswordResetRequest) - } - interface AdminPasswordResetRequest { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface AdminPasswordResetRequest { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - * - * This method doesn't verify that admin with `form.Email` exists (this is done on Submit). - */ - validate(): void - } - interface AdminPasswordResetRequest { - /** - * Submit validates and submits the form. - * On success sends a password reset email to the `form.Email` admin. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * AdminUpsert is a [models.Admin] upsert (create/update) form. - */ - interface AdminUpsert { - id: string - avatar: number - email: string - password: string - passwordConfirm: string - } - interface newAdminUpsert { - /** - * NewAdminUpsert creates a new [AdminUpsert] form with initializer - * config created from the provided [CoreApp] and [models.Admin] instances - * (for create you could pass a pointer to an empty Admin - `&models.Admin{}`). - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, admin: models.Admin): (AdminUpsert) - } - interface AdminUpsert { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface AdminUpsert { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface AdminUpsert { - /** - * Submit validates the form and upserts the form admin model. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } /** * AppleClientSecretCreate is a form struct to generate a new Apple Client Secret. * @@ -5700,744 +5600,98 @@ namespace forms { */ submit(): string } - /** - * BackupCreate is a request form for creating a new app backup. - */ - interface BackupCreate { - name: string - } - interface newBackupCreate { - /** - * NewBackupCreate creates new BackupCreate request form. - */ - (app: CoreApp): (BackupCreate) - } - interface BackupCreate { - /** - * SetContext replaces the default form context with the provided one. - */ - setContext(ctx: context.Context): void - } - interface BackupCreate { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface BackupCreate { - /** - * Submit validates the form and creates the app backup. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before creating the backup. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * BackupUpload is a request form for uploading a new app backup. - */ - interface BackupUpload { - file?: filesystem.File - } - interface newBackupUpload { - /** - * NewBackupUpload creates new BackupUpload request form. - */ - (app: CoreApp): (BackupUpload) - } - interface BackupUpload { - /** - * SetContext replaces the default form upload context with the provided one. - */ - setContext(ctx: context.Context): void - } - interface BackupUpload { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface BackupUpload { - /** - * Submit validates the form and upload the backup file. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before uploading the backup. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * InterceptorNextFunc is a interceptor handler function. - * Usually used in combination with InterceptorFunc. - */ - interface InterceptorNextFunc {(t: T): void } - /** - * InterceptorFunc defines a single interceptor function that - * will execute the provided next func handler. - */ - interface InterceptorFunc {(next: InterceptorNextFunc): InterceptorNextFunc } - /** - * CollectionUpsert is a [models.Collection] upsert (create/update) form. - */ - interface CollectionUpsert { - id: string - type: string - name: string - system: boolean - schema: schema.Schema - indexes: types.JsonArray - listRule?: string - viewRule?: string - createRule?: string - updateRule?: string - deleteRule?: string - options: types.JsonMap - } - interface newCollectionUpsert { - /** - * NewCollectionUpsert creates a new [CollectionUpsert] form with initializer - * config created from the provided [CoreApp] and [models.Collection] instances - * (for create you could pass a pointer to an empty Collection - `&models.Collection{}`). - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (CollectionUpsert) - } - interface CollectionUpsert { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface CollectionUpsert { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface CollectionUpsert { - /** - * Submit validates the form and upserts the form's Collection model. - * - * On success the related record table schema will be auto updated. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * CollectionsImport is a form model to bulk import - * (create, replace and delete) collections from a user provided list. - */ - interface CollectionsImport { - collections: Array<(models.Collection | undefined)> - deleteMissing: boolean - } - interface newCollectionsImport { - /** - * NewCollectionsImport creates a new [CollectionsImport] form with - * initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp): (CollectionsImport) - } - interface CollectionsImport { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface CollectionsImport { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface CollectionsImport { - /** - * Submit applies the import, aka.: - * - imports the form collections (create or replace) - * - sync the collection changes with their related records table - * - ensures the integrity of the imported structure (aka. run validations for each collection) - * - if [form.DeleteMissing] is set, deletes all local collections that are not found in the imports list - * - * All operations are wrapped in a single transaction that are - * rollbacked on the first encountered error. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc>[]): void - } - /** - * RealtimeSubscribe is a realtime subscriptions request form. - */ - interface RealtimeSubscribe { - clientId: string - subscriptions: Array - } - interface newRealtimeSubscribe { - /** - * NewRealtimeSubscribe creates new RealtimeSubscribe request form. - */ - (): (RealtimeSubscribe) - } - interface RealtimeSubscribe { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - /** - * RecordEmailChangeConfirm is an auth record email change confirmation form. - */ - interface RecordEmailChangeConfirm { - token: string - password: string - } - interface newRecordEmailChangeConfirm { - /** - * NewRecordEmailChangeConfirm creates a new [RecordEmailChangeConfirm] form - * initialized with from the provided [CoreApp] and [models.Collection] instances. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordEmailChangeConfirm) - } - interface RecordEmailChangeConfirm { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordEmailChangeConfirm { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordEmailChangeConfirm { - /** - * Submit validates and submits the auth record email change confirmation form. - * On success returns the updated auth record associated to `form.Token`. - * - * You can optionally provide a list of InterceptorFunc to - * further modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Record) - } - /** - * RecordEmailChangeRequest is an auth record email change request form. - */ - interface RecordEmailChangeRequest { - newEmail: string - } - interface newRecordEmailChangeRequest { - /** - * NewRecordEmailChangeRequest creates a new [RecordEmailChangeRequest] form - * initialized with from the provided [CoreApp] and [models.Record] instances. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, record: models.Record): (RecordEmailChangeRequest) - } - interface RecordEmailChangeRequest { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordEmailChangeRequest { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordEmailChangeRequest { - /** - * Submit validates and sends the change email request. - * - * You can optionally provide a list of InterceptorFunc to - * further modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * RecordOAuth2LoginData defines the OA - */ - interface RecordOAuth2LoginData { - externalAuth?: models.ExternalAuth - record?: models.Record - oAuth2User?: auth.AuthUser - providerClient: auth.Provider - } - /** - * BeforeOAuth2RecordCreateFunc defines a callback function that will - * be called before OAuth2 new Record creation. - */ - interface BeforeOAuth2RecordCreateFunc {(createForm: RecordUpsert, authRecord: models.Record, authUser: auth.AuthUser): void } - /** - * RecordOAuth2Login is an auth record OAuth2 login form. - */ - interface RecordOAuth2Login { - /** - * The name of the OAuth2 client provider (eg. "google") - */ - provider: string - /** - * The authorization code returned from the initial request. - */ - code: string - /** - * The optional PKCE code verifier as part of the code_challenge sent with the initial request. - */ - codeVerifier: string - /** - * The redirect url sent with the initial request. - */ - redirectUrl: string - /** - * Additional data that will be used for creating a new auth record - * if an existing OAuth2 account doesn't exist. - */ - createData: _TygojaDict - } - interface newRecordOAuth2Login { - /** - * NewRecordOAuth2Login creates a new [RecordOAuth2Login] form with - * initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection, optAuthRecord: models.Record): (RecordOAuth2Login) - } - interface RecordOAuth2Login { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordOAuth2Login { - /** - * SetBeforeNewRecordCreateFunc sets a before OAuth2 record create callback handler. - */ - setBeforeNewRecordCreateFunc(f: BeforeOAuth2RecordCreateFunc): void - } - interface RecordOAuth2Login { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordOAuth2Login { - /** - * Submit validates and submits the form. - * - * If an auth record doesn't exist, it will make an attempt to create it - * based on the fetched OAuth2 profile data via a local [RecordUpsert] form. - * You can intercept/modify the Record create form with [form.SetBeforeNewRecordCreateFunc()]. - * - * You can also optionally provide a list of InterceptorFunc to - * further modify the form behavior before persisting it. - * - * On success returns the authorized record model and the fetched provider's data. - */ - submit(...interceptors: InterceptorFunc[]): [(models.Record), (auth.AuthUser)] - } - /** - * RecordPasswordLogin is record username/email + password login form. - */ - interface RecordPasswordLogin { - identity: string - password: string - } - interface newRecordPasswordLogin { - /** - * NewRecordPasswordLogin creates a new [RecordPasswordLogin] form initialized - * with from the provided [CoreApp] and [models.Collection] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordPasswordLogin) - } - interface RecordPasswordLogin { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordPasswordLogin { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordPasswordLogin { - /** - * Submit validates and submits the form. - * On success returns the authorized record model. - * - * You can optionally provide a list of InterceptorFunc to - * further modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Record) - } - /** - * RecordPasswordResetConfirm is an auth record password reset confirmation form. - */ - interface RecordPasswordResetConfirm { - token: string - password: string - passwordConfirm: string - } - interface newRecordPasswordResetConfirm { - /** - * NewRecordPasswordResetConfirm creates a new [RecordPasswordResetConfirm] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordPasswordResetConfirm) - } - interface RecordPasswordResetConfirm { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordPasswordResetConfirm { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordPasswordResetConfirm { - /** - * Submit validates and submits the form. - * On success returns the updated auth record associated to `form.Token`. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Record) - } - /** - * RecordPasswordResetRequest is an auth record reset password request form. - */ - interface RecordPasswordResetRequest { - email: string - } - interface newRecordPasswordResetRequest { - /** - * NewRecordPasswordResetRequest creates a new [RecordPasswordResetRequest] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordPasswordResetRequest) - } - interface RecordPasswordResetRequest { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordPasswordResetRequest { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - * - * This method doesn't check whether auth record with `form.Email` exists (this is done on Submit). - */ - validate(): void - } - interface RecordPasswordResetRequest { - /** - * Submit validates and submits the form. - * On success, sends a password reset email to the `form.Email` auth record. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * RecordUpsert is a [models.Record] upsert (create/update) form. - */ interface RecordUpsert { /** - * base model fields + * extra password fields */ - id: string - /** - * auth collection fields - * --- - */ - username: string - email: string - emailVisibility: boolean - verified: boolean password: string passwordConfirm: string oldPassword: string } interface newRecordUpsert { /** - * NewRecordUpsert creates a new [RecordUpsert] form with initializer - * config created from the provided [CoreApp] and [models.Record] instances - * (for create you could pass a pointer to an empty Record - models.NewRecord(collection)). - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. + * NewRecordUpsert creates a new [RecordUpsert] form from the provided [CoreApp] and [core.Record] instances + * (for create you could pass a pointer to an empty Record - core.NewRecord(collection)). */ - (app: CoreApp, record: models.Record): (RecordUpsert) + (app: CoreApp, record: core.Record): (RecordUpsert) } interface RecordUpsert { /** - * Data returns the loaded form's data. + * SetContext assigns ctx as context of the current form. */ - data(): _TygojaDict + setContext(ctx: context.Context): void } interface RecordUpsert { /** - * SetFullManageAccess sets the manageAccess bool flag of the current - * form to enable/disable directly changing some system record fields - * (often used with auth collection records). + * SetApp replaces the current form app instance. + * + * This could be used for example if you want to change at later stage + * before submission to change from regular -> transactional app instance. */ - setFullManageAccess(fullManageAccess: boolean): void + setApp(app: CoreApp): void } interface RecordUpsert { /** - * SetDao replaces the default form Dao instance with the provided one. + * SetRecord replaces the current form record instance. */ - setDao(dao: daos.Dao): void + setRecord(record: core.Record): void } interface RecordUpsert { /** - * LoadRequest extracts the json or multipart/form-data request data - * and lods it into the form. - * - * File upload is supported only via multipart/form-data. + * ResetAccess resets the form access level to the accessLevelDefault. */ - loadRequest(r: http.Request, keyPrefix: string): void + resetAccess(): void } interface RecordUpsert { /** - * FilesToUpload returns the parsed request files ready for upload. + * GrantManagerAccess updates the form access level to "manager" allowing + * directly changing some system record fields (often used with auth collection records). */ - filesToUpload(): _TygojaDict + grantManagerAccess(): void } interface RecordUpsert { /** - * FilesToUpload returns the parsed request filenames ready to be deleted. + * GrantSuperuserAccess updates the form access level to "superuser" allowing + * directly changing all system record fields, including those marked as "Hidden". */ - filesToDelete(): Array + grantSuperuserAccess(): void } interface RecordUpsert { /** - * AddFiles adds the provided file(s) to the specified file field. - * - * If the file field is a SINGLE-value file field (aka. "Max Select = 1"), - * then the newly added file will REPLACE the existing one. - * In this case if you pass more than 1 files only the first one will be assigned. - * - * If the file field is a MULTI-value file field (aka. "Max Select > 1"), - * then the newly added file(s) will be APPENDED to the existing one(s). - * - * Example - * - * ``` - * f1, _ := filesystem.NewFileFromPath("/path/to/file1.txt") - * f2, _ := filesystem.NewFileFromPath("/path/to/file2.txt") - * form.AddFiles("documents", f1, f2) - * ``` + * HasManageAccess reports whether the form has "manager" or "superuser" level access. */ - addFiles(key: string, ...files: (filesystem.File | undefined)[]): void + hasManageAccess(): boolean } interface RecordUpsert { /** - * RemoveFiles removes a single or multiple file from the specified file field. - * - * NB! If filesToDelete is not set it will remove all existing files - * assigned to the file field (including those assigned with AddFiles)! - * - * Example - * - * ``` - * // mark only only 2 files for removal - * form.RemoveFiles("documents", "file1_aw4bdrvws6.txt", "file2_xwbs36bafv.txt") - * - * // mark all "documents" files for removal - * form.RemoveFiles("documents") - * ``` + * Load loads the provided data into the form and the related record. */ - removeFiles(key: string, ...toDelete: string[]): void + load(data: _TygojaDict): void } interface RecordUpsert { /** - * LoadData loads and normalizes the provided regular record data fields into the form. + * @todo consider removing and executing the Create API rule without dummy insert. + * + * DrySubmit performs a temp form submit within a transaction and reverts it at the end. + * For actual record persistence, check the [RecordUpsert.Submit()] method. + * + * This method doesn't perform validations, handle file uploads/deletes or trigger app save events! */ - loadData(requestData: _TygojaDict): void + drySubmit(callback: (txApp: CoreApp, drySavedRecord: core.Record) => void): void } interface RecordUpsert { /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. + * Submit validates the form specific validations and attempts to save the form record. */ - validate(): void - } - interface RecordUpsert { - validateAndFill(): void - } - interface RecordUpsert { - /** - * DrySubmit performs a form submit within a transaction and reverts it. - * For actual record persistence, check the `form.Submit()` method. - * - * This method doesn't handle file uploads/deletes or trigger any app events! - */ - drySubmit(callback: (txDao: daos.Dao) => void): void - } - interface RecordUpsert { - /** - * Submit validates the form and upserts the form Record model. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * RecordVerificationConfirm is an auth record email verification confirmation form. - */ - interface RecordVerificationConfirm { - token: string - } - interface newRecordVerificationConfirm { - /** - * NewRecordVerificationConfirm creates a new [RecordVerificationConfirm] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordVerificationConfirm) - } - interface RecordVerificationConfirm { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordVerificationConfirm { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface RecordVerificationConfirm { - /** - * Submit validates and submits the form. - * On success returns the verified auth record associated to `form.Token`. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): (models.Record) - } - /** - * RecordVerificationRequest is an auth record email verification request form. - */ - interface RecordVerificationRequest { - email: string - } - interface newRecordVerificationRequest { - /** - * NewRecordVerificationRequest creates a new [RecordVerificationRequest] - * form initialized with from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp, collection: models.Collection): (RecordVerificationRequest) - } - interface RecordVerificationRequest { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface RecordVerificationRequest { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - * - * // This method doesn't verify that auth record with `form.Email` exists (this is done on Submit). - */ - validate(): void - } - interface RecordVerificationRequest { - /** - * Submit validates and sends a verification request email - * to the `form.Email` auth record. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void - } - /** - * SettingsUpsert is a [settings.Settings] upsert (create/update) form. - */ - type _subxZPsK = settings.Settings - interface SettingsUpsert extends _subxZPsK { - } - interface newSettingsUpsert { - /** - * NewSettingsUpsert creates a new [SettingsUpsert] form with initializer - * config created from the provided [CoreApp] instance. - * - * If you want to submit the form as part of a transaction, - * you can change the default Dao via [SetDao()]. - */ - (app: CoreApp): (SettingsUpsert) - } - interface SettingsUpsert { - /** - * SetDao replaces the default form Dao instance with the provided one. - */ - setDao(dao: daos.Dao): void - } - interface SettingsUpsert { - /** - * Validate makes the form validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface SettingsUpsert { - /** - * Submit validates the form and upserts the loaded settings. - * - * On success the app settings will be refreshed with the form ones. - * - * You can optionally provide a list of InterceptorFunc to further - * modify the form behavior before persisting it. - */ - submit(...interceptors: InterceptorFunc[]): void + submit(): void } /** * TestEmailSend is a email template test request form. */ interface TestEmailSend { - template: string email: string + template: string + collection: string // optional, fallbacks to _superusers } interface newTestEmailSend { /** @@ -6486,230 +5740,470 @@ namespace forms { } } -/** - * Package apis implements the default PocketBase api services and middlewares. - */ namespace apis { - interface adminApi { - } - // @ts-ignore - import validation = ozzo_validation - /** - * ApiError defines the struct for a basic api error response. - */ - interface ApiError { - code: number - message: string - data: _TygojaDict - } - interface ApiError { + interface newApiError { /** - * Error makes it compatible with the `error` interface. + * NewApiError is an alias for [router.NewApiError]. */ - error(): string - } - interface ApiError { - /** - * RawData returns the unformatted error data (could be an internal error, text, etc.) - */ - rawData(): any - } - interface newNotFoundError { - /** - * NewNotFoundError creates and returns 404 `ApiError`. - */ - (message: string, data: any): (ApiError) + (status: number, message: string, errData: any): (router.ApiError) } interface newBadRequestError { /** - * NewBadRequestError creates and returns 400 `ApiError`. + * NewBadRequestError is an alias for [router.NewBadRequestError]. */ - (message: string, data: any): (ApiError) + (message: string, errData: any): (router.ApiError) + } + interface newNotFoundError { + /** + * NewNotFoundError is an alias for [router.NewNotFoundError]. + */ + (message: string, errData: any): (router.ApiError) } interface newForbiddenError { /** - * NewForbiddenError creates and returns 403 `ApiError`. + * NewForbiddenError is an alias for [router.NewForbiddenError]. */ - (message: string, data: any): (ApiError) + (message: string, errData: any): (router.ApiError) } interface newUnauthorizedError { /** - * NewUnauthorizedError creates and returns 401 `ApiError`. + * NewUnauthorizedError is an alias for [router.NewUnauthorizedError]. */ - (message: string, data: any): (ApiError) + (message: string, errData: any): (router.ApiError) } - interface newApiError { + interface newTooManyRequestsError { /** - * NewApiError creates and returns new normalized `ApiError` instance. + * NewTooManyRequestsError is an alias for [router.NewTooManyRequestsError]. */ - (status: number, message: string, data: any): (ApiError) + (message: string, errData: any): (router.ApiError) } - interface backupApi { - } - interface initApi { + interface newInternalServerError { /** - * InitApi creates a configured echo instance with registered - * system and app specific routes and middlewares. + * NewInternalServerError is an alias for [router.NewInternalServerError]. */ - (app: CoreApp): (echo.Echo) + (message: string, errData: any): (router.ApiError) } - interface staticDirectoryHandler { + interface backupFileInfo { + modified: types.DateTime + key: string + size: number + } + // @ts-ignore + import validation = ozzo_validation + interface backupCreateForm { + name: string + } + interface backupUploadForm { + file?: filesystem.File + } + interface newRouter { /** - * StaticDirectoryHandler is similar to `echo.StaticDirectoryHandler` - * but without the directory redirect which conflicts with RemoveTrailingSlash middleware. + * NewRouter returns a new router instance loaded with the default app middlewares and api routes. + */ + (app: CoreApp): (router.Router) + } + interface wrapStdHandler { + /** + * WrapStdHandler wraps Go [http.Handler] into a PocketBase handler func. + */ + (h: http.Handler): hook.HandlerFunc + } + interface wrapStdMiddleware { + /** + * WrapStdMiddleware wraps Go [func(http.Handler) http.Handle] into a PocketBase middleware func. + */ + (m: (_arg0: http.Handler) => http.Handler): hook.HandlerFunc + } + interface mustSubFS { + /** + * MustSubFS returns an [fs.FS] corresponding to the subtree rooted at fsys's dir. + * + * This is similar to [fs.Sub] but panics on failure. + */ + (fsys: fs.FS, dir: string): fs.FS + } + interface _static { + /** + * Static is a handler function to serve static directory content from fsys. * * If a file resource is missing and indexFallback is set, the request - * will be forwarded to the base index.html (useful also for SPA). + * will be forwarded to the base index.html (useful for SPA with pretty urls). * - * @see https://github.com/labstack/echo/issues/2211 + * NB! Expects the route to have a "{path...}" wildcard parameter. + * + * Special redirects: + * ``` + * - if "path" is a file that ends in index.html, it is redirected to its non-index.html version (eg. /test/index.html -> /test/) + * - if "path" is a directory that has index.html, the index.html file is rendered, + * otherwise if missing - returns 404 or fallback to the root index.html if indexFallback is set + * ``` + * + * Example: + * + * ``` + * fsys := os.DirFS("./pb_public") + * router.GET("/files/{path...}", apis.Static(fsys, false)) + * ``` */ - (fileSystem: fs.FS, indexFallback: boolean): echo.HandlerFunc + (fsys: fs.FS, indexFallback: boolean): hook.HandlerFunc } - interface collectionApi { + interface findUploadedFiles { + /** + * FindUploadedFiles extracts all form files of "key" from a http request + * and returns a slice with filesystem.File instances (if any). + */ + (r: http.Request, key: string): Array<(filesystem.File | undefined)> + } + interface HandleFunc {(e: core.RequestEvent): void } + interface BatchActionHandlerFunc {(app: CoreApp, ir: core.InternalRequest, params: _TygojaDict, next: () => void): HandleFunc } + interface BatchRequestResult { + body: any + status: number + } + interface batchRequestsForm { + requests: Array<(core.InternalRequest | undefined)> + } + interface batchProcessor { + } + interface batchProcessor { + process(batch: Array<(core.InternalRequest | undefined)>, timeout: time.Duration): void + } + interface BatchResponseError { + } + interface BatchResponseError { + error(): string + } + interface BatchResponseError { + code(): string + } + interface BatchResponseError { + resolve(errData: _TygojaDict): any + } + interface BatchResponseError { + marshalJSON(): string|Array + } + interface collectionsImportForm { + collections: Array<_TygojaDict> + deleteMissing: boolean } interface fileApi { } - interface healthApi { - } - interface healthCheckResponse { - message: string - code: number - data: { - canBackup: boolean - } - } - interface logsApi { - } interface requireGuestOnly { /** * RequireGuestOnly middleware requires a request to NOT have a valid * Authorization header. * - * This middleware is the opposite of [apis.RequireAdminOrRecordAuth()]. + * This middleware is the opposite of [apis.RequireAuth()]. */ - (): echo.MiddlewareFunc + (): (hook.Handler) } - interface requireRecordAuth { + interface requireAuth { /** - * RequireRecordAuth middleware requires a request to have - * a valid record auth Authorization header. + * RequireAuth middleware requires a request to have a valid record Authorization header. * * The auth record could be from any collection. - * - * You can further filter the allowed record auth collections by - * specifying their names. + * You can further filter the allowed record auth collections by specifying their names. * * Example: * * ``` - * apis.RequireRecordAuth() + * apis.RequireAuth() // any auth collection + * apis.RequireAuth("_superusers", "users") // only the listed auth collections * ``` - * - * Or: - * - * ``` - * apis.RequireRecordAuth("users", "supervisors") - * ``` - * - * To restrict the auth record only to the loaded context collection, - * use [apis.RequireSameContextRecordAuth()] instead. */ - (...optCollectionNames: string[]): echo.MiddlewareFunc + (...optCollectionNames: string[]): (hook.Handler) } - interface requireSameContextRecordAuth { + interface requireSuperuserAuth { /** - * RequireSameContextRecordAuth middleware requires a request to have - * a valid record Authorization header. - * - * The auth record must be from the same collection already loaded in the context. + * RequireSuperuserAuth middleware requires a request to have + * a valid superuser Authorization header. */ - (): echo.MiddlewareFunc + (): (hook.Handler) } - interface requireAdminAuth { + interface requireSuperuserAuthOnlyIfAny { /** - * RequireAdminAuth middleware requires a request to have - * a valid admin Authorization header. + * RequireSuperuserAuthOnlyIfAny middleware requires a request to have + * a valid superuser Authorization header ONLY if the application has + * at least 1 existing superuser. */ - (): echo.MiddlewareFunc + (): (hook.Handler) } - interface requireAdminAuthOnlyIfAny { + interface requireSuperuserOrOwnerAuth { /** - * RequireAdminAuthOnlyIfAny middleware requires a request to have - * a valid admin Authorization header ONLY if the application has - * at least 1 existing Admin model. - */ - (app: CoreApp): echo.MiddlewareFunc - } - interface requireAdminOrRecordAuth { - /** - * RequireAdminOrRecordAuth middleware requires a request to have - * a valid admin or record Authorization header set. + * RequireSuperuserOrOwnerAuth middleware requires a request to have + * a valid superuser or regular record owner Authorization header set. * - * You can further filter the allowed auth record collections by providing their names. - * - * This middleware is the opposite of [apis.RequireGuestOnly()]. - */ - (...optCollectionNames: string[]): echo.MiddlewareFunc - } - interface requireAdminOrOwnerAuth { - /** - * RequireAdminOrOwnerAuth middleware requires a request to have - * a valid admin or auth record owner Authorization header set. - * - * This middleware is similar to [apis.RequireAdminOrRecordAuth()] but + * This middleware is similar to [apis.RequireAuth()] but * for the auth record token expects to have the same id as the path - * parameter ownerIdParam (default to "id" if empty). + * parameter ownerIdPathParam (default to "id" if empty). */ - (ownerIdParam: string): echo.MiddlewareFunc + (ownerIdPathParam: string): (hook.Handler) } - interface loadAuthContext { + interface requireSameCollectionContextAuth { /** - * LoadAuthContext middleware reads the Authorization request header - * and loads the token related record or admin instance into the - * request's context. - * - * This middleware is expected to be already registered by default for all routes. + * RequireSameCollectionContextAuth middleware requires a request to have + * a valid record Authorization header and the auth record's collection to + * match the one from the route path parameter (default to "collection" if collectionParam is empty). */ - (app: CoreApp): echo.MiddlewareFunc + (collectionPathParam: string): (hook.Handler) } - interface loadCollectionContext { + interface skipSuccessActivityLog { /** - * LoadCollectionContext middleware finds the collection with related - * path identifier and loads it into the request context. - * - * Set optCollectionTypes to further filter the found collection by its type. + * SkipSuccessActivityLog is a helper middleware that instructs the global + * activity logger to log only requests that have failed/returned an error. */ - (app: CoreApp, ...optCollectionTypes: string[]): echo.MiddlewareFunc + (): (hook.Handler) } - interface activityLogger { + interface bodyLimit { /** - * ActivityLogger middleware takes care to save the request information - * into the logs database. + * BodyLimit returns a middleware function that changes the default request body size limit. * - * The middleware does nothing if the app logs retention period is zero - * (aka. app.Settings().Logs.MaxDays = 0). + * 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, + * it sends 413 error response. */ - (app: CoreApp): echo.MiddlewareFunc + (limitBytes: number): (hook.Handler) } - interface realtimeApi { + type _subWJQoF = io.ReadCloser + interface limitedReader extends _subWJQoF { + } + interface limitedReader { + read(b: string|Array): number + } + interface limitedReader { + reread(): void + } + /** + * CORSConfig defines the config for CORS middleware. + */ + interface CORSConfig { + /** + * AllowOrigins determines the value of the Access-Control-Allow-Origin + * response header. This header defines a list of origins that may access the + * resource. The wildcard characters '*' and '?' are supported and are + * converted to regex fragments '.*' and '.' accordingly. + * + * Security: use extreme caution when handling the origin, and carefully + * validate any logic. Remember that attackers may register hostile domain names. + * See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + * + * Optional. Default value []string{"*"}. + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + */ + allowOrigins: Array + /** + * AllowOriginFunc is a custom function to validate the origin. It takes the + * origin as an argument and returns true if allowed or false otherwise. If + * an error is returned, it is returned by the handler. If this option is + * set, AllowOrigins is ignored. + * + * Security: use extreme caution when handling the origin, and carefully + * validate any logic. Remember that attackers may register hostile domain names. + * See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + * + * Optional. + */ + allowOriginFunc: (origin: string) => boolean + /** + * AllowMethods determines the value of the Access-Control-Allow-Methods + * response header. This header specified the list of methods allowed when + * accessing the resource. This is used in response to a preflight request. + * + * Optional. Default value DefaultCORSConfig.AllowMethods. + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + */ + allowMethods: Array + /** + * AllowHeaders determines the value of the Access-Control-Allow-Headers + * response header. This header is used in response to a preflight request to + * indicate which HTTP headers can be used when making the actual request. + * + * Optional. Default value []string{}. + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + */ + allowHeaders: Array + /** + * AllowCredentials determines the value of the + * Access-Control-Allow-Credentials response header. This header indicates + * whether or not the response to the request can be exposed when the + * credentials mode (Request.credentials) is true. When used as part of a + * response to a preflight request, this indicates whether or not the actual + * request can be made using credentials. See also + * [MDN: Access-Control-Allow-Credentials]. + * + * Optional. Default value false, in which case the header is not set. + * + * Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`. + * See "Exploiting CORS misconfigurations for Bitcoins and bounties", + * https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + */ + allowCredentials: boolean + /** + * UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials + * flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header. + * + * This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties) + * attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject. + * + * Optional. Default value is false. + */ + unsafeWildcardOriginWithAllowCredentials: boolean + /** + * ExposeHeaders determines the value of Access-Control-Expose-Headers, which + * defines a list of headers that clients are allowed to access. + * + * Optional. Default value []string{}, in which case the header is not set. + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header + */ + exposeHeaders: Array + /** + * MaxAge determines the value of the Access-Control-Max-Age response header. + * This header indicates how long (in seconds) the results of a preflight + * request can be cached. + * The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response. + * + * Optional. Default value 0 - meaning header is not sent. + * + * See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + */ + maxAge: number + } + interface corsWithConfig { + /** + * CORSWithConfig returns a CORS middleware with config. + */ + (config: CORSConfig): hook.HandlerFunc + } + /** + * GzipConfig defines the config for Gzip middleware. + */ + interface GzipConfig { + /** + * Gzip compression level. + * Optional. Default value -1. + */ + level: number + /** + * Length threshold before gzip compression is applied. + * Optional. Default value 0. + * + * Most of the time you will not need to change the default. Compressing + * a short response might increase the transmitted data because of the + * gzip format overhead. Compressing the response will also consume CPU + * and time on the server and the client (for decompressing). Depending on + * your use case such a threshold might be useful. + * + * See also: + * https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits + */ + minLength: number + } + interface gzip { + /** + * Gzip returns a middleware which compresses HTTP response using gzip compression scheme. + */ + (): hook.HandlerFunc + } + interface gzipWithConfig { + /** + * GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme. + */ + (config: GzipConfig): hook.HandlerFunc + } + type _subpcIaI = http.ResponseWriter&io.Writer + interface gzipResponseWriter extends _subpcIaI { + } + interface gzipResponseWriter { + writeHeader(code: number): void + } + interface gzipResponseWriter { + write(b: string|Array): number + } + interface gzipResponseWriter { + flush(): void + } + interface gzipResponseWriter { + hijack(): [net.Conn, (bufio.ReadWriter)] + } + interface gzipResponseWriter { + push(target: string, opts: http.PushOptions): void + } + interface gzipResponseWriter { + readFrom(r: io.Reader): number + } + interface gzipResponseWriter { + unwrap(): http.ResponseWriter + } + type _subaqIgM = sync.RWMutex + interface rateLimiter extends _subaqIgM { + } + type _subPpcNi = sync.Mutex + interface fixedWindow extends _subPpcNi { + } + interface realtimeSubscribeForm { + clientId: string + subscriptions: Array } /** * recordData represents the broadcasted record subscrition message data. */ interface recordData { - record: any // map or models.Record + record: any // map or core.Record action: string } interface getter { [key:string]: any; get(_arg0: string): any } - interface recordAuthApi { + interface EmailChangeConfirmForm { + token: string + password: string + } + interface emailChangeRequestForm { + newEmail: string + } + interface impersonateForm { + /** + * Duration is the optional custom token duration in seconds. + */ + duration: number + } + interface otpResponse { + enabled: boolean + duration: number // in seconds + } + interface mfaResponse { + enabled: boolean + duration: number // in seconds + } + interface passwordResponse { + identityFields: Array + enabled: boolean + } + interface oauth2Response { + providers: Array + enabled: boolean } interface providerInfo { name: string displayName: string state: string + authURL: string + /** + * @todo + * deprecated: use AuthURL instead + * AuthUrl will be removed after dropping v0.22 support + */ authUrl: string /** * technically could be omitted if the provider doesn't support PKCE, @@ -6719,32 +6213,95 @@ namespace apis { codeChallenge: string codeChallengeMethod: string } - interface oauth2EventMessage { + interface authMethodsResponse { + password: passwordResponse + oAuth2: oauth2Response + mfa: mfaResponse + otp: otpResponse + /** + * legacy fields + * @todo remove after dropping v0.22 support + */ + authProviders: Array + usernamePassword: boolean + emailPassword: boolean + } + interface createOTPForm { + email: string + } + interface recordConfirmPasswordResetForm { + token: string + password: string + passwordConfirm: string + } + interface recordRequestPasswordResetForm { + email: string + } + interface recordConfirmVerificationForm { + token: string + } + interface recordRequestVerificationForm { + email: string + } + interface recordOAuth2LoginForm { + /** + * Additional data that will be used for creating a new auth record + * if an existing OAuth2 account doesn't exist. + */ + createData: _TygojaDict + /** + * The name of the OAuth2 client provider (eg. "google") + */ + provider: string + /** + * The authorization code returned from the initial request. + */ + code: string + /** + * The optional PKCE code verifier as part of the code_challenge sent with the initial request. + */ + codeVerifier: string + /** + * The redirect url sent with the initial request. + */ + redirectURL: string + /** + * @todo + * deprecated: use RedirectURL instead + * RedirectUrl will be removed after dropping v0.22 support + */ + redirectUrl: string + } + interface oauth2RedirectData { state: string code: string error: string } - interface recordApi { + interface authWithOTPForm { + otpId: string + password: string } - interface requestData { + interface authWithPasswordForm { + identity: string + password: string /** - * Deprecated: Use RequestInfo instead. + * IdentityField specifies the field to use to search for the identity + * (leave it empty for "auto" detection). */ - (c: echo.Context): (models.RequestInfo) - } - interface requestInfo { - /** - * RequestInfo exports cached common request data fields - * (query, body, logged auth state, etc.) from the provided context. - */ - (c: echo.Context): (models.RequestInfo) + identityField: string } interface recordAuthResponse { /** - * RecordAuthResponse writes standardised json record auth response + * RecordAuthResponse writes standardized json record auth response * into the specified request context. + * + * The authMethod argument specify the name of the current authentication method (eg. password, oauth2, etc.) + * that it is used primarily as an auth identifier during MFA and for login alerts. + * + * Set authMethod to empty string if you want to ignore the MFA checks and the login alerts + * (can be also adjusted additionally via the OnRecordAuthRequest hook). */ - (app: CoreApp, c: echo.Context, authRecord: models.Record, meta: any, ...finalizers: ((token: string) => void)[]): void + (e: core.RequestEvent, authRecord: core.Record, authMethod: string, meta: any): void } interface enrichRecord { /** @@ -6752,10 +6309,10 @@ namespace apis { * ``` * - expands relations (if defaultExpands and/or ?expand query param is set) * - ensures that the emails of the auth record and its expanded auth relations - * are visible only for the current logged admin, record owner or record with manage access + * are visible only for the current logged superuser, record owner or record with manage access * ``` */ - (c: echo.Context, dao: daos.Dao, record: models.Record, ...defaultExpands: string[]): void + (e: core.RequestEvent, record: core.Record, ...defaultExpands: string[]): void } interface enrichRecords { /** @@ -6763,10 +6320,14 @@ namespace apis { * ``` * - expands relations (if defaultExpands and/or ?expand query param is set) * - ensures that the emails of the auth records and their expanded auth relations - * are visible only for the current logged admin, record owner or record with manage access + * are visible only for the current logged superuser, record owner or record with manage access * ``` + * + * Note: Expects all records to be from the same collection! */ - (c: echo.Context, dao: daos.Dao, records: Array<(models.Record | undefined)>, ...defaultExpands: string[]): void + (e: core.RequestEvent, records: Array<(core.Record | undefined)>, ...defaultExpands: string[]): void + } + interface iterator { } /** * ServeConfig defines a configuration struct for apis.Serve(). @@ -6777,11 +6338,18 @@ namespace apis { */ showStartBanner: boolean /** - * HttpAddr is the TCP address to listen for the HTTP server (eg. `127.0.0.1:80`). + * DashboardPath specifies the route path to the superusers dashboard interface + * (default to "/_/{path...}"). + * + * Note: Must include the "{path...}" wildcard parameter. + */ + dashboardPath: string + /** + * HttpAddr is the TCP address to listen for the HTTP server (eg. "127.0.0.1:80"). */ httpAddr: string /** - * HttpsAddr is the TCP address to listen for the HTTPS server (eg. `127.0.0.1:443`). + * HttpsAddr is the TCP address to listen for the HTTPS server (eg. "127.0.0.1:443"). */ httpsAddr: string /** @@ -6814,31 +6382,24 @@ namespace apis { * }) * ``` */ - (app: CoreApp, config: ServeConfig): (http.Server) + (app: CoreApp, config: ServeConfig): void } - interface migrationsConnection { - db?: dbx.DB - migrationsList: migrate.MigrationsList + interface serverErrorLogWriter { } - interface settingsApi { + interface serverErrorLogWriter { + write(p: string|Array): number } } namespace pocketbase { - /** - * appWrapper serves as a private CoreApp instance wrapper. - */ - type _subXbotK = CoreApp - interface appWrapper extends _subXbotK { - } /** * PocketBase defines a PocketBase app launcher. * * It implements [CoreApp] via embedding and all of the app interface methods * could be accessed directly through the instance (eg. PocketBase.DataDir()). */ - type _subYRHBu = appWrapper - interface PocketBase extends _subYRHBu { + type _subltdOS = CoreApp + interface PocketBase extends _subltdOS { /** * RootCmd is the main console command */ @@ -6848,23 +6409,25 @@ namespace pocketbase { * Config is the PocketBase initialization config struct. */ interface Config { + /** + * hide the default console server info on app startup + */ + hideStartBanner: boolean /** * optional default values for the console flags */ defaultDev: boolean defaultDataDir: string // if not set, it will fallback to "./pb_data" defaultEncryptionEnv: string - /** - * hide the default console server info on app startup - */ - hideStartBanner: boolean /** * optional DB configurations */ dataMaxOpenConns: number // default to core.DefaultDataMaxOpenConns dataMaxIdleConns: number // default to core.DefaultDataMaxIdleConns - logsMaxOpenConns: number // default to core.DefaultLogsMaxOpenConns - logsMaxIdleConns: number // default to core.DefaultLogsMaxIdleConns + auxMaxOpenConns: number // default to core.DefaultAuxMaxOpenConns + auxMaxIdleConns: number // default to core.DefaultAuxMaxIdleConns + queryTimeout: number // default to core.DefaultQueryTimeout (in seconds) + dbConnect: core.DBConnectFunc // default to core.dbConnect } interface _new { /** @@ -6922,159 +6485,149 @@ namespace pocketbase { } /** - * Package io provides basic interfaces to I/O primitives. - * Its primary job is to wrap existing implementations of such primitives, - * such as those in package os, into shared public interfaces that - * abstract the functionality, plus some other related primitives. + * Package sync provides basic synchronization primitives such as mutual + * exclusion locks. Other than the [Once] and [WaitGroup] types, most are intended + * for use by low-level library routines. Higher-level synchronization is + * better done via channels and communication. * - * Because these interfaces and primitives wrap lower-level operations with - * various implementations, unless otherwise informed clients should not - * assume they are safe for parallel execution. + * Values containing the types defined in this package should not be copied. */ -namespace io { +namespace sync { /** - * Reader is the interface that wraps the basic Read method. + * A Mutex is a mutual exclusion lock. + * The zero value for a Mutex is an unlocked mutex. * - * Read reads up to len(p) bytes into p. It returns the number of bytes - * read (0 <= n <= len(p)) and any error encountered. Even if Read - * returns n < len(p), it may use all of p as scratch space during the call. - * If some data is available but not len(p) bytes, Read conventionally - * returns what is available instead of waiting for more. + * A Mutex must not be copied after first use. * - * When Read encounters an error or end-of-file condition after - * successfully reading n > 0 bytes, it returns the number of - * bytes read. It may return the (non-nil) error from the same call - * or return the error (and n == 0) from a subsequent call. - * An instance of this general case is that a Reader returning - * a non-zero number of bytes at the end of the input stream may - * return either err == EOF or err == nil. The next Read should - * return 0, EOF. + * In the terminology of [the Go memory model], + * the n'th call to [Mutex.Unlock] “synchronizes before” the m'th call to [Mutex.Lock] + * for any n < m. + * A successful call to [Mutex.TryLock] is equivalent to a call to Lock. + * A failed call to TryLock does not establish any “synchronizes before” + * relation at all. * - * Callers should always process the n > 0 bytes returned before - * considering the error err. Doing so correctly handles I/O errors - * that happen after reading some bytes and also both of the - * allowed EOF behaviors. - * - * If len(p) == 0, Read should always return n == 0. It may return a - * non-nil error if some error condition is known, such as EOF. - * - * Implementations of Read are discouraged from returning a - * zero byte count with a nil error, except when len(p) == 0. - * Callers should treat a return of 0 and nil as indicating that - * nothing happened; in particular it does not indicate EOF. - * - * Implementations must not retain p. + * [the Go memory model]: https://go.dev/ref/mem */ - interface Reader { - [key:string]: any; - read(p: string|Array): number + interface Mutex { + } + interface Mutex { + /** + * Lock locks m. + * If the lock is already in use, the calling goroutine + * blocks until the mutex is available. + */ + lock(): void + } + interface Mutex { + /** + * TryLock tries to lock m and reports whether it succeeded. + * + * Note that while correct uses of TryLock do exist, they are rare, + * and use of TryLock is often a sign of a deeper problem + * in a particular use of mutexes. + */ + tryLock(): boolean + } + interface Mutex { + /** + * Unlock unlocks m. + * It is a run-time error if m is not locked on entry to Unlock. + * + * A locked [Mutex] is not associated with a particular goroutine. + * It is allowed for one goroutine to lock a Mutex and then + * arrange for another goroutine to unlock it. + */ + unlock(): void } /** - * Writer is the interface that wraps the basic Write method. + * A RWMutex is a reader/writer mutual exclusion lock. + * The lock can be held by an arbitrary number of readers or a single writer. + * The zero value for a RWMutex is an unlocked mutex. * - * Write writes len(p) bytes from p to the underlying data stream. - * It returns the number of bytes written from p (0 <= n <= len(p)) - * and any error encountered that caused the write to stop early. - * Write must return a non-nil error if it returns n < len(p). - * Write must not modify the slice data, even temporarily. + * A RWMutex must not be copied after first use. * - * Implementations must not retain p. + * If any goroutine calls [RWMutex.Lock] while the lock is already held by + * one or more readers, concurrent calls to [RWMutex.RLock] will block until + * the writer has acquired (and released) the lock, to ensure that + * the lock eventually becomes available to the writer. + * Note that this prohibits recursive read-locking. + * + * In the terminology of [the Go memory model], + * the n'th call to [RWMutex.Unlock] “synchronizes before” the m'th call to Lock + * for any n < m, just as for [Mutex]. + * For any call to RLock, there exists an n such that + * the n'th call to Unlock “synchronizes before” that call to RLock, + * and the corresponding call to [RWMutex.RUnlock] “synchronizes before” + * the n+1'th call to Lock. + * + * [the Go memory model]: https://go.dev/ref/mem */ - interface Writer { - [key:string]: any; - write(p: string|Array): number + interface RWMutex { } - /** - * ReadSeekCloser is the interface that groups the basic Read, Seek and Close - * methods. - */ - interface ReadSeekCloser { - [key:string]: any; - } -} - -/** - * Package bytes implements functions for the manipulation of byte slices. - * It is analogous to the facilities of the [strings] package. - */ -namespace bytes { - /** - * A Reader implements the io.Reader, io.ReaderAt, io.WriterTo, io.Seeker, - * io.ByteScanner, and io.RuneScanner interfaces by reading from - * a byte slice. - * Unlike a [Buffer], a Reader is read-only and supports seeking. - * The zero value for Reader operates like a Reader of an empty slice. - */ - interface Reader { - } - interface Reader { + interface RWMutex { /** - * Len returns the number of bytes of the unread portion of the - * slice. + * RLock locks rw for reading. + * + * It should not be used for recursive read locking; a blocked Lock + * call excludes new readers from acquiring the lock. See the + * documentation on the [RWMutex] type. */ - len(): number + rLock(): void } - interface Reader { + interface RWMutex { /** - * Size returns the original length of the underlying byte slice. - * Size is the number of bytes available for reading via [Reader.ReadAt]. - * The result is unaffected by any method calls except [Reader.Reset]. + * TryRLock tries to lock rw for reading and reports whether it succeeded. + * + * Note that while correct uses of TryRLock do exist, they are rare, + * and use of TryRLock is often a sign of a deeper problem + * in a particular use of mutexes. */ - size(): number + tryRLock(): boolean } - interface Reader { + interface RWMutex { /** - * Read implements the [io.Reader] interface. + * RUnlock undoes a single [RWMutex.RLock] call; + * it does not affect other simultaneous readers. + * It is a run-time error if rw is not locked for reading + * on entry to RUnlock. */ - read(b: string|Array): number + rUnlock(): void } - interface Reader { + interface RWMutex { /** - * ReadAt implements the [io.ReaderAt] interface. + * Lock locks rw for writing. + * If the lock is already locked for reading or writing, + * Lock blocks until the lock is available. */ - readAt(b: string|Array, off: number): number + lock(): void } - interface Reader { + interface RWMutex { /** - * ReadByte implements the [io.ByteReader] interface. + * TryLock tries to lock rw for writing and reports whether it succeeded. + * + * Note that while correct uses of TryLock do exist, they are rare, + * and use of TryLock is often a sign of a deeper problem + * in a particular use of mutexes. */ - readByte(): number + tryLock(): boolean } - interface Reader { + interface RWMutex { /** - * UnreadByte complements [Reader.ReadByte] in implementing the [io.ByteScanner] interface. + * Unlock unlocks rw for writing. It is a run-time error if rw is + * not locked for writing on entry to Unlock. + * + * As with Mutexes, a locked [RWMutex] is not associated with a particular + * goroutine. One goroutine may [RWMutex.RLock] ([RWMutex.Lock]) a RWMutex and then + * arrange for another goroutine to [RWMutex.RUnlock] ([RWMutex.Unlock]) it. */ - unreadByte(): void + unlock(): void } - interface Reader { + interface RWMutex { /** - * ReadRune implements the [io.RuneReader] interface. + * RLocker returns a [Locker] interface that implements + * the [Locker.Lock] and [Locker.Unlock] methods by calling rw.RLock and rw.RUnlock. */ - readRune(): [number, number] - } - interface Reader { - /** - * UnreadRune complements [Reader.ReadRune] in implementing the [io.RuneScanner] interface. - */ - unreadRune(): void - } - interface Reader { - /** - * Seek implements the [io.Seeker] interface. - */ - seek(offset: number, whence: number): number - } - interface Reader { - /** - * WriteTo implements the [io.WriterTo] interface. - */ - writeTo(w: io.Writer): number - } - interface Reader { - /** - * Reset resets the [Reader.Reader] to be reading from b. - */ - reset(b: string|Array): void + rLocker(): Locker } } @@ -7093,7 +6646,7 @@ namespace bytes { * the manuals for the appropriate operating system. * These calls return err == nil to indicate success; otherwise * err is an operating system error describing the failure. - * On most systems, that error has type syscall.Errno. + * On most systems, that error has type [Errno]. * * NOTE: Most of the functions, types, and constants defined in * this package are also available in the [golang.org/x/sys] package. @@ -7193,6 +6746,8 @@ namespace syscall { */ write(f: (fd: number) => boolean): void } + // @ts-ignore + import runtimesyscall = syscall /** * An Errno is an unsigned number describing an error condition. * It implements the error interface. The zero Errno is by convention @@ -7205,7 +6760,7 @@ namespace syscall { * } * ``` * - * Errno values can be tested against error values using errors.Is. + * Errno values can be tested against error values using [errors.Is]. * For example: * * ``` @@ -7228,6 +6783,169 @@ namespace syscall { } } +/** + * Package io provides basic interfaces to I/O primitives. + * Its primary job is to wrap existing implementations of such primitives, + * such as those in package os, into shared public interfaces that + * abstract the functionality, plus some other related primitives. + * + * Because these interfaces and primitives wrap lower-level operations with + * various implementations, unless otherwise informed clients should not + * assume they are safe for parallel execution. + */ +namespace io { + /** + * Reader is the interface that wraps the basic Read method. + * + * Read reads up to len(p) bytes into p. It returns the number of bytes + * read (0 <= n <= len(p)) and any error encountered. Even if Read + * returns n < len(p), it may use all of p as scratch space during the call. + * If some data is available but not len(p) bytes, Read conventionally + * returns what is available instead of waiting for more. + * + * When Read encounters an error or end-of-file condition after + * successfully reading n > 0 bytes, it returns the number of + * bytes read. It may return the (non-nil) error from the same call + * or return the error (and n == 0) from a subsequent call. + * An instance of this general case is that a Reader returning + * a non-zero number of bytes at the end of the input stream may + * return either err == EOF or err == nil. The next Read should + * return 0, EOF. + * + * Callers should always process the n > 0 bytes returned before + * considering the error err. Doing so correctly handles I/O errors + * that happen after reading some bytes and also both of the + * allowed EOF behaviors. + * + * If len(p) == 0, Read should always return n == 0. It may return a + * non-nil error if some error condition is known, such as EOF. + * + * Implementations of Read are discouraged from returning a + * zero byte count with a nil error, except when len(p) == 0. + * Callers should treat a return of 0 and nil as indicating that + * nothing happened; in particular it does not indicate EOF. + * + * Implementations must not retain p. + */ + interface Reader { + [key:string]: any; + read(p: string|Array): number + } + /** + * Writer is the interface that wraps the basic Write method. + * + * Write writes len(p) bytes from p to the underlying data stream. + * It returns the number of bytes written from p (0 <= n <= len(p)) + * and any error encountered that caused the write to stop early. + * Write must return a non-nil error if it returns n < len(p). + * Write must not modify the slice data, even temporarily. + * + * Implementations must not retain p. + */ + interface Writer { + [key:string]: any; + write(p: string|Array): number + } + /** + * ReadCloser is the interface that groups the basic Read and Close methods. + */ + interface ReadCloser { + [key:string]: any; + } + /** + * ReadSeekCloser is the interface that groups the basic Read, Seek and Close + * methods. + */ + interface ReadSeekCloser { + [key:string]: any; + } +} + +/** + * Package bytes implements functions for the manipulation of byte slices. + * It is analogous to the facilities of the [strings] package. + */ +namespace bytes { + /** + * A Reader implements the [io.Reader], [io.ReaderAt], [io.WriterTo], [io.Seeker], + * [io.ByteScanner], and [io.RuneScanner] interfaces by reading from + * a byte slice. + * Unlike a [Buffer], a Reader is read-only and supports seeking. + * The zero value for Reader operates like a Reader of an empty slice. + */ + interface Reader { + } + interface Reader { + /** + * Len returns the number of bytes of the unread portion of the + * slice. + */ + len(): number + } + interface Reader { + /** + * Size returns the original length of the underlying byte slice. + * Size is the number of bytes available for reading via [Reader.ReadAt]. + * The result is unaffected by any method calls except [Reader.Reset]. + */ + size(): number + } + interface Reader { + /** + * Read implements the [io.Reader] interface. + */ + read(b: string|Array): number + } + interface Reader { + /** + * ReadAt implements the [io.ReaderAt] interface. + */ + readAt(b: string|Array, off: number): number + } + interface Reader { + /** + * ReadByte implements the [io.ByteReader] interface. + */ + readByte(): number + } + interface Reader { + /** + * UnreadByte complements [Reader.ReadByte] in implementing the [io.ByteScanner] interface. + */ + unreadByte(): void + } + interface Reader { + /** + * ReadRune implements the [io.RuneReader] interface. + */ + readRune(): [number, number] + } + interface Reader { + /** + * UnreadRune complements [Reader.ReadRune] in implementing the [io.RuneScanner] interface. + */ + unreadRune(): void + } + interface Reader { + /** + * Seek implements the [io.Seeker] interface. + */ + seek(offset: number, whence: number): number + } + interface Reader { + /** + * WriteTo implements the [io.WriterTo] interface. + */ + writeTo(w: io.Writer): number + } + interface Reader { + /** + * Reset resets the [Reader] to be reading from b. + */ + reset(b: string|Array): void + } +} + /** * Package time provides functionality for measuring and displaying time. * @@ -7240,7 +6958,7 @@ namespace syscall { * changes for clock synchronization, and a “monotonic clock,” which is * not. The general rule is that the wall clock is for telling time and * the monotonic clock is for measuring time. Rather than split the API, - * in this package the Time returned by time.Now contains both a wall + * in this package the Time returned by [time.Now] contains both a wall * clock reading and a monotonic clock reading; later time-telling * operations use the wall clock reading, but later time-measuring * operations, specifically comparisons and subtractions, use the @@ -7257,7 +6975,7 @@ namespace syscall { * elapsed := t.Sub(start) * ``` * - * Other idioms, such as time.Since(start), time.Until(deadline), and + * Other idioms, such as [time.Since](start), [time.Until](deadline), and * time.Now().Before(deadline), are similarly robust against wall clock * resets. * @@ -7282,23 +7000,26 @@ namespace syscall { * * On some systems the monotonic clock will stop if the computer goes to sleep. * On such a system, t.Sub(u) may not accurately reflect the actual - * time that passed between t and u. + * time that passed between t and u. The same applies to other functions and + * methods that subtract times, such as [Since], [Until], [Before], [After], + * [Add], [Sub], [Equal] and [Compare]. In some cases, you may need to strip + * the monotonic clock to get accurate results. * * Because the monotonic clock reading has no meaning outside * the current process, the serialized forms generated by t.GobEncode, * t.MarshalBinary, t.MarshalJSON, and t.MarshalText omit the monotonic * clock reading, and t.Format provides no format for it. Similarly, the - * constructors time.Date, time.Parse, time.ParseInLocation, and time.Unix, + * constructors [time.Date], [time.Parse], [time.ParseInLocation], and [time.Unix], * as well as the unmarshalers t.GobDecode, t.UnmarshalBinary. * t.UnmarshalJSON, and t.UnmarshalText always create times with * no monotonic clock reading. * - * The monotonic clock reading exists only in Time values. It is not - * a part of Duration values or the Unix times returned by t.Unix and + * The monotonic clock reading exists only in [Time] values. It is not + * a part of [Duration] values or the Unix times returned by t.Unix and * friends. * * Note that the Go == operator compares not just the time instant but - * also the Location and the monotonic clock reading. See the + * also the [Location] and the monotonic clock reading. See the * documentation for the Time type for a discussion of equality * testing for Time values. * @@ -7308,10 +7029,11 @@ namespace syscall { * * # Timer Resolution * - * Timer resolution varies depending on the Go runtime, the operating system + * [Timer] resolution varies depending on the Go runtime, the operating system * and the underlying hardware. - * On Unix, the resolution is approximately 1ms. - * On Windows, the default resolution is approximately 16ms, but + * On Unix, the resolution is ~1ms. + * On Windows version 1803 and newer, the resolution is ~0.5ms. + * On older Windows versions, the default resolution is ~16ms, but * a higher resolution may be requested using [golang.org/x/sys/windows.TimeBeginPeriod]. */ namespace time { @@ -7335,7 +7057,7 @@ namespace time { } interface Time { /** - * GoString implements fmt.GoStringer and formats t to be printed in Go source + * GoString implements [fmt.GoStringer] and formats t to be printed in Go source * code. */ goString(): string @@ -7344,16 +7066,16 @@ namespace time { /** * Format returns a textual representation of the time value formatted according * to the layout defined by the argument. See the documentation for the - * constant called Layout to see how to represent the layout format. + * constant called [Layout] to see how to represent the layout format. * - * The executable example for Time.Format demonstrates the working + * The executable example for [Time.Format] demonstrates the working * of the layout string in detail and is a good reference. */ format(layout: string): string } interface Time { /** - * AppendFormat is like Format but appends the textual + * AppendFormat is like [Time.Format] but appends the textual * representation to b and returns the extended buffer. */ appendFormat(b: string|Array, layout: string): string|Array @@ -7363,27 +7085,27 @@ namespace time { * * Programs using times should typically store and pass them as values, * not pointers. That is, time variables and struct fields should be of - * type time.Time, not *time.Time. + * type [time.Time], not *time.Time. * * A Time value can be used by multiple goroutines simultaneously except - * that the methods GobDecode, UnmarshalBinary, UnmarshalJSON and - * UnmarshalText are not concurrency-safe. + * that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and + * [Time.UnmarshalText] are not concurrency-safe. * - * Time instants can be compared using the Before, After, and Equal methods. - * The Sub method subtracts two instants, producing a Duration. - * The Add method adds a Time and a Duration, producing a Time. + * Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods. + * The [Time.Sub] method subtracts two instants, producing a [Duration]. + * The [Time.Add] method adds a Time and a Duration, producing a Time. * * The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC. - * As this time is unlikely to come up in practice, the IsZero method gives + * As this time is unlikely to come up in practice, the [Time.IsZero] method gives * a simple way of detecting a time that has not been initialized explicitly. * - * Each time has an associated Location. The methods Local, UTC, and In return a + * Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a * Time with a specific Location. Changing the Location of a Time value with * these methods does not change the actual instant it represents, only the time * zone in which to interpret it. * - * Representations of a Time value saved by the GobEncode, MarshalBinary, - * MarshalJSON, and MarshalText methods store the Time.Location's offset, but not + * Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], + * [Time.MarshalJSON], and [Time.MarshalText] methods store the [Time.Location]'s offset, but not * the location name. They therefore lose information about Daylight Saving Time. * * In addition to the required “wall clock” reading, a Time may contain an optional @@ -7579,7 +7301,7 @@ namespace time { * Round returns the result of rounding d to the nearest multiple of m. * The rounding behavior for halfway values is to round away from zero. * If the result exceeds the maximum (or minimum) - * value that can be stored in a Duration, + * value that can be stored in a [Duration], * Round returns the maximum (or minimum) duration. * If m <= 0, Round returns d unchanged. */ @@ -7588,7 +7310,7 @@ namespace time { interface Duration { /** * Abs returns the absolute value of d. - * As a special case, math.MinInt64 is converted to math.MaxInt64. + * As a special case, [math.MinInt64] is converted to [math.MaxInt64]. */ abs(): Duration } @@ -7601,7 +7323,7 @@ namespace time { interface Time { /** * Sub returns the duration t-u. If the result exceeds the maximum (or minimum) - * value that can be stored in a Duration, the maximum (or minimum) duration + * value that can be stored in a [Duration], the maximum (or minimum) duration * will be returned. * To compute t-d for a duration d, use t.Add(-d). */ @@ -7742,7 +7464,7 @@ namespace time { } interface Time { /** - * MarshalJSON implements the json.Marshaler interface. + * MarshalJSON implements the [json.Marshaler] interface. * The time is a quoted string in the RFC 3339 format with sub-second precision. * If the timestamp cannot be represented as valid RFC 3339 * (e.g., the year is out of range), then an error is reported. @@ -7751,14 +7473,14 @@ namespace time { } interface Time { /** - * UnmarshalJSON implements the json.Unmarshaler interface. + * UnmarshalJSON implements the [json.Unmarshaler] interface. * The time must be a quoted string in the RFC 3339 format. */ unmarshalJSON(data: string|Array): void } interface Time { /** - * MarshalText implements the encoding.TextMarshaler interface. + * MarshalText implements the [encoding.TextMarshaler] interface. * The time is formatted in RFC 3339 format with sub-second precision. * If the timestamp cannot be represented as valid RFC 3339 * (e.g., the year is out of range), then an error is reported. @@ -7767,7 +7489,7 @@ namespace time { } interface Time { /** - * UnmarshalText implements the encoding.TextUnmarshaler interface. + * UnmarshalText implements the [encoding.TextUnmarshaler] interface. * The time must be in the RFC 3339 format. */ unmarshalText(data: string|Array): void @@ -7805,6 +7527,169 @@ namespace time { } } +/** + * Package context defines the Context type, which carries deadlines, + * cancellation signals, and other request-scoped values across API boundaries + * and between processes. + * + * Incoming requests to a server should create a [Context], and outgoing + * calls to servers should accept a Context. The chain of function + * calls between them must propagate the Context, optionally replacing + * it with a derived Context created using [WithCancel], [WithDeadline], + * [WithTimeout], or [WithValue]. When a Context is canceled, all + * Contexts derived from it are also canceled. + * + * The [WithCancel], [WithDeadline], and [WithTimeout] functions take a + * Context (the parent) and return a derived Context (the child) and a + * [CancelFunc]. Calling the CancelFunc cancels the child and its + * children, removes the parent's reference to the child, and stops + * any associated timers. Failing to call the CancelFunc leaks the + * child and its children until the parent is canceled or the timer + * fires. The go vet tool checks that CancelFuncs are used on all + * control-flow paths. + * + * The [WithCancelCause] function returns a [CancelCauseFunc], which + * takes an error and records it as the cancellation cause. Calling + * [Cause] on the canceled context or any of its children retrieves + * the cause. If no cause is specified, Cause(ctx) returns the same + * value as ctx.Err(). + * + * Programs that use Contexts should follow these rules to keep interfaces + * consistent across packages and enable static analysis tools to check context + * propagation: + * + * Do not store Contexts inside a struct type; instead, pass a Context + * explicitly to each function that needs it. The Context should be the first + * parameter, typically named ctx: + * + * ``` + * func DoSomething(ctx context.Context, arg Arg) error { + * // ... use ctx ... + * } + * ``` + * + * Do not pass a nil [Context], even if a function permits it. Pass [context.TODO] + * if you are unsure about which Context to use. + * + * Use context Values only for request-scoped data that transits processes and + * APIs, not for passing optional parameters to functions. + * + * The same Context may be passed to functions running in different goroutines; + * Contexts are safe for simultaneous use by multiple goroutines. + * + * See https://blog.golang.org/context for example code for a server that uses + * Contexts. + */ +namespace context { + /** + * A Context carries a deadline, a cancellation signal, and other values across + * API boundaries. + * + * Context's methods may be called by multiple goroutines simultaneously. + */ + interface Context { + [key:string]: any; + /** + * Deadline returns the time when work done on behalf of this context + * should be canceled. Deadline returns ok==false when no deadline is + * set. Successive calls to Deadline return the same results. + */ + deadline(): [time.Time, boolean] + /** + * Done returns a channel that's closed when work done on behalf of this + * context should be canceled. Done may return nil if this context can + * never be canceled. Successive calls to Done return the same value. + * The close of the Done channel may happen asynchronously, + * after the cancel function returns. + * + * WithCancel arranges for Done to be closed when cancel is called; + * WithDeadline arranges for Done to be closed when the deadline + * expires; WithTimeout arranges for Done to be closed when the timeout + * elapses. + * + * Done is provided for use in select statements: + * + * // Stream generates values with DoSomething and sends them to out + * // until DoSomething returns an error or ctx.Done is closed. + * func Stream(ctx context.Context, out chan<- Value) error { + * for { + * v, err := DoSomething(ctx) + * if err != nil { + * return err + * } + * select { + * case <-ctx.Done(): + * return ctx.Err() + * case out <- v: + * } + * } + * } + * + * See https://blog.golang.org/pipelines for more examples of how to use + * a Done channel for cancellation. + */ + done(): undefined + /** + * If Done is not yet closed, Err returns nil. + * If Done is closed, Err returns a non-nil error explaining why: + * Canceled if the context was canceled + * or DeadlineExceeded if the context's deadline passed. + * After Err returns a non-nil error, successive calls to Err return the same error. + */ + err(): void + /** + * Value returns the value associated with this context for key, or nil + * if no value is associated with key. Successive calls to Value with + * the same key returns the same result. + * + * Use context values only for request-scoped data that transits + * processes and API boundaries, not for passing optional parameters to + * functions. + * + * A key identifies a specific value in a Context. Functions that wish + * to store values in Context typically allocate a key in a global + * variable then use that key as the argument to context.WithValue and + * Context.Value. A key can be any type that supports equality; + * packages should define keys as an unexported type to avoid + * collisions. + * + * Packages that define a Context key should provide type-safe accessors + * for the values stored using that key: + * + * ``` + * // Package user defines a User type that's stored in Contexts. + * package user + * + * import "context" + * + * // User is the type of value stored in the Contexts. + * type User struct {...} + * + * // key is an unexported type for keys defined in this package. + * // This prevents collisions with keys defined in other packages. + * type key int + * + * // userKey is the key for user.User values in Contexts. It is + * // unexported; clients use user.NewContext and user.FromContext + * // instead of using this key directly. + * var userKey key + * + * // NewContext returns a new Context that carries value u. + * func NewContext(ctx context.Context, u *User) context.Context { + * return context.WithValue(ctx, userKey, u) + * } + * + * // FromContext returns the User value stored in ctx, if any. + * func FromContext(ctx context.Context) (*User, bool) { + * u, ok := ctx.Value(userKey).(*User) + * return u, ok + * } + * ``` + */ + value(key: any): any + } +} + /** * Package fs defines basic interfaces to a file system. * A file system can be provided by the host operating system @@ -8005,169 +7890,6 @@ namespace fs { interface WalkDirFunc {(path: string, d: DirEntry, err: Error): void } } -/** - * Package context defines the Context type, which carries deadlines, - * cancellation signals, and other request-scoped values across API boundaries - * and between processes. - * - * Incoming requests to a server should create a [Context], and outgoing - * calls to servers should accept a Context. The chain of function - * calls between them must propagate the Context, optionally replacing - * it with a derived Context created using [WithCancel], [WithDeadline], - * [WithTimeout], or [WithValue]. When a Context is canceled, all - * Contexts derived from it are also canceled. - * - * The [WithCancel], [WithDeadline], and [WithTimeout] functions take a - * Context (the parent) and return a derived Context (the child) and a - * [CancelFunc]. Calling the CancelFunc cancels the child and its - * children, removes the parent's reference to the child, and stops - * any associated timers. Failing to call the CancelFunc leaks the - * child and its children until the parent is canceled or the timer - * fires. The go vet tool checks that CancelFuncs are used on all - * control-flow paths. - * - * The [WithCancelCause] function returns a [CancelCauseFunc], which - * takes an error and records it as the cancellation cause. Calling - * [Cause] on the canceled context or any of its children retrieves - * the cause. If no cause is specified, Cause(ctx) returns the same - * value as ctx.Err(). - * - * Programs that use Contexts should follow these rules to keep interfaces - * consistent across packages and enable static analysis tools to check context - * propagation: - * - * Do not store Contexts inside a struct type; instead, pass a Context - * explicitly to each function that needs it. The Context should be the first - * parameter, typically named ctx: - * - * ``` - * func DoSomething(ctx context.Context, arg Arg) error { - * // ... use ctx ... - * } - * ``` - * - * Do not pass a nil [Context], even if a function permits it. Pass [context.TODO] - * if you are unsure about which Context to use. - * - * Use context Values only for request-scoped data that transits processes and - * APIs, not for passing optional parameters to functions. - * - * The same Context may be passed to functions running in different goroutines; - * Contexts are safe for simultaneous use by multiple goroutines. - * - * See https://blog.golang.org/context for example code for a server that uses - * Contexts. - */ -namespace context { - /** - * A Context carries a deadline, a cancellation signal, and other values across - * API boundaries. - * - * Context's methods may be called by multiple goroutines simultaneously. - */ - interface Context { - [key:string]: any; - /** - * Deadline returns the time when work done on behalf of this context - * should be canceled. Deadline returns ok==false when no deadline is - * set. Successive calls to Deadline return the same results. - */ - deadline(): [time.Time, boolean] - /** - * Done returns a channel that's closed when work done on behalf of this - * context should be canceled. Done may return nil if this context can - * never be canceled. Successive calls to Done return the same value. - * The close of the Done channel may happen asynchronously, - * after the cancel function returns. - * - * WithCancel arranges for Done to be closed when cancel is called; - * WithDeadline arranges for Done to be closed when the deadline - * expires; WithTimeout arranges for Done to be closed when the timeout - * elapses. - * - * Done is provided for use in select statements: - * - * // Stream generates values with DoSomething and sends them to out - * // until DoSomething returns an error or ctx.Done is closed. - * func Stream(ctx context.Context, out chan<- Value) error { - * for { - * v, err := DoSomething(ctx) - * if err != nil { - * return err - * } - * select { - * case <-ctx.Done(): - * return ctx.Err() - * case out <- v: - * } - * } - * } - * - * See https://blog.golang.org/pipelines for more examples of how to use - * a Done channel for cancellation. - */ - done(): undefined - /** - * If Done is not yet closed, Err returns nil. - * If Done is closed, Err returns a non-nil error explaining why: - * Canceled if the context was canceled - * or DeadlineExceeded if the context's deadline passed. - * After Err returns a non-nil error, successive calls to Err return the same error. - */ - err(): void - /** - * Value returns the value associated with this context for key, or nil - * if no value is associated with key. Successive calls to Value with - * the same key returns the same result. - * - * Use context values only for request-scoped data that transits - * processes and API boundaries, not for passing optional parameters to - * functions. - * - * A key identifies a specific value in a Context. Functions that wish - * to store values in Context typically allocate a key in a global - * variable then use that key as the argument to context.WithValue and - * Context.Value. A key can be any type that supports equality; - * packages should define keys as an unexported type to avoid - * collisions. - * - * Packages that define a Context key should provide type-safe accessors - * for the values stored using that key: - * - * ``` - * // Package user defines a User type that's stored in Contexts. - * package user - * - * import "context" - * - * // User is the type of value stored in the Contexts. - * type User struct {...} - * - * // key is an unexported type for keys defined in this package. - * // This prevents collisions with keys defined in other packages. - * type key int - * - * // userKey is the key for user.User values in Contexts. It is - * // unexported; clients use user.NewContext and user.FromContext - * // instead of using this key directly. - * var userKey key - * - * // NewContext returns a new Context that carries value u. - * func NewContext(ctx context.Context, u *User) context.Context { - * return context.WithValue(ctx, userKey, u) - * } - * - * // FromContext returns the User value stored in ctx, if any. - * func FromContext(ctx context.Context) (*User, bool) { - * u, ok := ctx.Value(userKey).(*User) - * return u, ok - * } - * ``` - */ - value(key: any): any - } -} - /** * Package sql provides a generic interface around SQL (or SQL-like) * databases. @@ -8811,6 +8533,781 @@ namespace sql { } } +/** + * Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer + * object, creating another object (Reader or Writer) that also implements + * the interface but provides buffering and some help for textual I/O. + */ +namespace bufio { + /** + * ReadWriter stores pointers to a [Reader] and a [Writer]. + * It implements [io.ReadWriter]. + */ + type _subxjvIW = Reader&Writer + interface ReadWriter extends _subxjvIW { + } +} + +/** + * Package syntax parses regular expressions into parse trees and compiles + * parse trees into programs. Most clients of regular expressions will use the + * facilities of package [regexp] (such as [regexp.Compile] and [regexp.Match]) instead of this package. + * + * # Syntax + * + * The regular expression syntax understood by this package when parsing with the [Perl] flag is as follows. + * Parts of the syntax can be disabled by passing alternate flags to [Parse]. + * + * Single characters: + * + * ``` + * . any character, possibly including newline (flag s=true) + * [xyz] character class + * [^xyz] negated character class + * \d Perl character class + * \D negated Perl character class + * [[:alpha:]] ASCII character class + * [[:^alpha:]] negated ASCII character class + * \pN Unicode character class (one-letter name) + * \p{Greek} Unicode character class + * \PN negated Unicode character class (one-letter name) + * \P{Greek} negated Unicode character class + * ``` + * + * Composites: + * + * ``` + * xy x followed by y + * x|y x or y (prefer x) + * ``` + * + * Repetitions: + * + * ``` + * x* zero or more x, prefer more + * x+ one or more x, prefer more + * x? zero or one x, prefer one + * x{n,m} n or n+1 or ... or m x, prefer more + * x{n,} n or more x, prefer more + * x{n} exactly n x + * x*? zero or more x, prefer fewer + * x+? one or more x, prefer fewer + * x?? zero or one x, prefer zero + * x{n,m}? n or n+1 or ... or m x, prefer fewer + * x{n,}? n or more x, prefer fewer + * x{n}? exactly n x + * ``` + * + * Implementation restriction: The counting forms x{n,m}, x{n,}, and x{n} + * reject forms that create a minimum or maximum repetition count above 1000. + * Unlimited repetitions are not subject to this restriction. + * + * Grouping: + * + * ``` + * (re) numbered capturing group (submatch) + * (?Pre) named & numbered capturing group (submatch) + * (?re) named & numbered capturing group (submatch) + * (?:re) non-capturing group + * (?flags) set flags within current group; non-capturing + * (?flags:re) set flags during re; non-capturing + * + * Flag syntax is xyz (set) or -xyz (clear) or xy-z (set xy, clear z). The flags are: + * + * i case-insensitive (default false) + * m multi-line mode: ^ and $ match begin/end line in addition to begin/end text (default false) + * s let . match \n (default false) + * U ungreedy: swap meaning of x* and x*?, x+ and x+?, etc (default false) + * ``` + * + * Empty strings: + * + * ``` + * ^ at beginning of text or line (flag m=true) + * $ at end of text (like \z not \Z) or line (flag m=true) + * \A at beginning of text + * \b at ASCII word boundary (\w on one side and \W, \A, or \z on the other) + * \B not at ASCII word boundary + * \z at end of text + * ``` + * + * Escape sequences: + * + * ``` + * \a bell (== \007) + * \f form feed (== \014) + * \t horizontal tab (== \011) + * \n newline (== \012) + * \r carriage return (== \015) + * \v vertical tab character (== \013) + * \* literal *, for any punctuation character * + * \123 octal character code (up to three digits) + * \x7F hex character code (exactly two digits) + * \x{10FFFF} hex character code + * \Q...\E literal text ... even if ... has punctuation + * ``` + * + * Character class elements: + * + * ``` + * x single character + * A-Z character range (inclusive) + * \d Perl character class + * [:foo:] ASCII character class foo + * \p{Foo} Unicode character class Foo + * \pF Unicode character class F (one-letter name) + * ``` + * + * Named character classes as character class elements: + * + * ``` + * [\d] digits (== \d) + * [^\d] not digits (== \D) + * [\D] not digits (== \D) + * [^\D] not not digits (== \d) + * [[:name:]] named ASCII class inside character class (== [:name:]) + * [^[:name:]] named ASCII class inside negated character class (== [:^name:]) + * [\p{Name}] named Unicode property inside character class (== \p{Name}) + * [^\p{Name}] named Unicode property inside negated character class (== \P{Name}) + * ``` + * + * Perl character classes (all ASCII-only): + * + * ``` + * \d digits (== [0-9]) + * \D not digits (== [^0-9]) + * \s whitespace (== [\t\n\f\r ]) + * \S not whitespace (== [^\t\n\f\r ]) + * \w word characters (== [0-9A-Za-z_]) + * \W not word characters (== [^0-9A-Za-z_]) + * ``` + * + * ASCII character classes: + * + * ``` + * [[:alnum:]] alphanumeric (== [0-9A-Za-z]) + * [[:alpha:]] alphabetic (== [A-Za-z]) + * [[:ascii:]] ASCII (== [\x00-\x7F]) + * [[:blank:]] blank (== [\t ]) + * [[:cntrl:]] control (== [\x00-\x1F\x7F]) + * [[:digit:]] digits (== [0-9]) + * [[:graph:]] graphical (== [!-~] == [A-Za-z0-9!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]) + * [[:lower:]] lower case (== [a-z]) + * [[:print:]] printable (== [ -~] == [ [:graph:]]) + * [[:punct:]] punctuation (== [!-/:-@[-`{-~]) + * [[:space:]] whitespace (== [\t\n\v\f\r ]) + * [[:upper:]] upper case (== [A-Z]) + * [[:word:]] word characters (== [0-9A-Za-z_]) + * [[:xdigit:]] hex digit (== [0-9A-Fa-f]) + * ``` + * + * Unicode character classes are those in [unicode.Categories] and [unicode.Scripts]. + */ +namespace syntax { + /** + * Flags control the behavior of the parser and record information about regexp context. + */ + interface Flags extends Number{} +} + +/** + * Package exec runs external commands. It wraps os.StartProcess to make it + * easier to remap stdin and stdout, connect I/O with pipes, and do other + * adjustments. + * + * Unlike the "system" library call from C and other languages, the + * os/exec package intentionally does not invoke the system shell and + * does not expand any glob patterns or handle other expansions, + * pipelines, or redirections typically done by shells. The package + * behaves more like C's "exec" family of functions. To expand glob + * patterns, either call the shell directly, taking care to escape any + * dangerous input, or use the [path/filepath] package's Glob function. + * To expand environment variables, use package os's ExpandEnv. + * + * Note that the examples in this package assume a Unix system. + * They may not run on Windows, and they do not run in the Go Playground + * used by golang.org and godoc.org. + * + * # Executables in the current directory + * + * The functions [Command] and [LookPath] look for a program + * in the directories listed in the current path, following the + * conventions of the host operating system. + * Operating systems have for decades included the current + * directory in this search, sometimes implicitly and sometimes + * configured explicitly that way by default. + * Modern practice is that including the current directory + * is usually unexpected and often leads to security problems. + * + * To avoid those security problems, as of Go 1.19, this package will not resolve a program + * using an implicit or explicit path entry relative to the current directory. + * That is, if you run [LookPath]("go"), it will not successfully return + * ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. + * Instead, if the usual path algorithms would result in that answer, + * these functions return an error err satisfying [errors.Is](err, [ErrDot]). + * + * For example, consider these two program snippets: + * + * ``` + * path, err := exec.LookPath("prog") + * if err != nil { + * log.Fatal(err) + * } + * use(path) + * ``` + * + * and + * + * ``` + * cmd := exec.Command("prog") + * if err := cmd.Run(); err != nil { + * log.Fatal(err) + * } + * ``` + * + * These will not find and run ./prog or .\prog.exe, + * no matter how the current path is configured. + * + * Code that always wants to run a program from the current directory + * can be rewritten to say "./prog" instead of "prog". + * + * Code that insists on including results from relative path entries + * can instead override the error using an errors.Is check: + * + * ``` + * path, err := exec.LookPath("prog") + * if errors.Is(err, exec.ErrDot) { + * err = nil + * } + * if err != nil { + * log.Fatal(err) + * } + * use(path) + * ``` + * + * and + * + * ``` + * cmd := exec.Command("prog") + * if errors.Is(cmd.Err, exec.ErrDot) { + * cmd.Err = nil + * } + * if err := cmd.Run(); err != nil { + * log.Fatal(err) + * } + * ``` + * + * Setting the environment variable GODEBUG=execerrdot=0 + * disables generation of ErrDot entirely, temporarily restoring the pre-Go 1.19 + * behavior for programs that are unable to apply more targeted fixes. + * A future version of Go may remove support for this variable. + * + * Before adding such overrides, make sure you understand the + * security implications of doing so. + * See https://go.dev/blog/path-security for more information. + */ +namespace exec { + /** + * Cmd represents an external command being prepared or run. + * + * A Cmd cannot be reused after calling its [Cmd.Run], [Cmd.Output] or [Cmd.CombinedOutput] + * methods. + */ + interface Cmd { + /** + * Path is the path of the command to run. + * + * This is the only field that must be set to a non-zero + * value. If Path is relative, it is evaluated relative + * to Dir. + */ + path: string + /** + * Args holds command line arguments, including the command as Args[0]. + * If the Args field is empty or nil, Run uses {Path}. + * + * In typical use, both Path and Args are set by calling Command. + */ + args: Array + /** + * Env specifies the environment of the process. + * Each entry is of the form "key=value". + * If Env is nil, the new process uses the current process's + * environment. + * If Env contains duplicate environment keys, only the last + * value in the slice for each duplicate key is used. + * As a special case on Windows, SYSTEMROOT is always added if + * missing and not explicitly set to the empty string. + */ + env: Array + /** + * Dir specifies the working directory of the command. + * If Dir is the empty string, Run runs the command in the + * calling process's current directory. + */ + dir: string + /** + * Stdin specifies the process's standard input. + * + * If Stdin is nil, the process reads from the null device (os.DevNull). + * + * If Stdin is an *os.File, the process's standard input is connected + * directly to that file. + * + * Otherwise, during the execution of the command a separate + * goroutine reads from Stdin and delivers that data to the command + * over a pipe. In this case, Wait does not complete until the goroutine + * stops copying, either because it has reached the end of Stdin + * (EOF or a read error), or because writing to the pipe returned an error, + * or because a nonzero WaitDelay was set and expired. + */ + stdin: io.Reader + /** + * Stdout and Stderr specify the process's standard output and error. + * + * If either is nil, Run connects the corresponding file descriptor + * to the null device (os.DevNull). + * + * If either is an *os.File, the corresponding output from the process + * is connected directly to that file. + * + * Otherwise, during the execution of the command a separate goroutine + * reads from the process over a pipe and delivers that data to the + * corresponding Writer. In this case, Wait does not complete until the + * goroutine reaches EOF or encounters an error or a nonzero WaitDelay + * expires. + * + * If Stdout and Stderr are the same writer, and have a type that can + * be compared with ==, at most one goroutine at a time will call Write. + */ + stdout: io.Writer + stderr: io.Writer + /** + * ExtraFiles specifies additional open files to be inherited by the + * new process. It does not include standard input, standard output, or + * standard error. If non-nil, entry i becomes file descriptor 3+i. + * + * ExtraFiles is not supported on Windows. + */ + extraFiles: Array<(os.File | undefined)> + /** + * SysProcAttr holds optional, operating system-specific attributes. + * Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + */ + sysProcAttr?: syscall.SysProcAttr + /** + * Process is the underlying process, once started. + */ + process?: os.Process + /** + * ProcessState contains information about an exited process. + * If the process was started successfully, Wait or Run will + * populate its ProcessState when the command completes. + */ + processState?: os.ProcessState + err: Error // LookPath error, if any. + /** + * If Cancel is non-nil, the command must have been created with + * CommandContext and Cancel will be called when the command's + * Context is done. By default, CommandContext sets Cancel to + * call the Kill method on the command's Process. + * + * Typically a custom Cancel will send a signal to the command's + * Process, but it may instead take other actions to initiate cancellation, + * such as closing a stdin or stdout pipe or sending a shutdown request on a + * network socket. + * + * If the command exits with a success status after Cancel is + * called, and Cancel does not return an error equivalent to + * os.ErrProcessDone, then Wait and similar methods will return a non-nil + * error: either an error wrapping the one returned by Cancel, + * or the error from the Context. + * (If the command exits with a non-success status, or Cancel + * returns an error that wraps os.ErrProcessDone, Wait and similar methods + * continue to return the command's usual exit status.) + * + * If Cancel is set to nil, nothing will happen immediately when the command's + * Context is done, but a nonzero WaitDelay will still take effect. That may + * be useful, for example, to work around deadlocks in commands that do not + * support shutdown signals but are expected to always finish quickly. + * + * Cancel will not be called if Start returns a non-nil error. + */ + cancel: () => void + /** + * If WaitDelay is non-zero, it bounds the time spent waiting on two sources + * of unexpected delay in Wait: a child process that fails to exit after the + * associated Context is canceled, and a child process that exits but leaves + * its I/O pipes unclosed. + * + * The WaitDelay timer starts when either the associated Context is done or a + * call to Wait observes that the child process has exited, whichever occurs + * first. When the delay has elapsed, the command shuts down the child process + * and/or its I/O pipes. + * + * If the child process has failed to exit — perhaps because it ignored or + * failed to receive a shutdown signal from a Cancel function, or because no + * Cancel function was set — then it will be terminated using os.Process.Kill. + * + * Then, if the I/O pipes communicating with the child process are still open, + * those pipes are closed in order to unblock any goroutines currently blocked + * on Read or Write calls. + * + * If pipes are closed due to WaitDelay, no Cancel call has occurred, + * and the command has otherwise exited with a successful status, Wait and + * similar methods will return ErrWaitDelay instead of nil. + * + * If WaitDelay is zero (the default), I/O pipes will be read until EOF, + * which might not occur until orphaned subprocesses of the command have + * also closed their descriptors for the pipes. + */ + waitDelay: time.Duration + } + interface Cmd { + /** + * String returns a human-readable description of c. + * It is intended only for debugging. + * In particular, it is not suitable for use as input to a shell. + * The output of String may vary across Go releases. + */ + string(): string + } + interface Cmd { + /** + * Run starts the specified command and waits for it to complete. + * + * The returned error is nil if the command runs, has no problems + * copying stdin, stdout, and stderr, and exits with a zero exit + * status. + * + * If the command starts but does not complete successfully, the error is of + * type [*ExitError]. Other error types may be returned for other situations. + * + * If the calling goroutine has locked the operating system thread + * with [runtime.LockOSThread] and modified any inheritable OS-level + * thread state (for example, Linux or Plan 9 name spaces), the new + * process will inherit the caller's thread state. + */ + run(): void + } + interface Cmd { + /** + * Start starts the specified command but does not wait for it to complete. + * + * If Start returns successfully, the c.Process field will be set. + * + * After a successful call to Start the [Cmd.Wait] method must be called in + * order to release associated system resources. + */ + start(): void + } + interface Cmd { + /** + * Wait waits for the command to exit and waits for any copying to + * stdin or copying from stdout or stderr to complete. + * + * The command must have been started by [Cmd.Start]. + * + * The returned error is nil if the command runs, has no problems + * copying stdin, stdout, and stderr, and exits with a zero exit + * status. + * + * If the command fails to run or doesn't complete successfully, the + * error is of type [*ExitError]. Other error types may be + * returned for I/O problems. + * + * If any of c.Stdin, c.Stdout or c.Stderr are not an [*os.File], Wait also waits + * for the respective I/O loop copying to or from the process to complete. + * + * Wait releases any resources associated with the [Cmd]. + */ + wait(): void + } + interface Cmd { + /** + * Output runs the command and returns its standard output. + * Any returned error will usually be of type [*ExitError]. + * If c.Stderr was nil, Output populates [ExitError.Stderr]. + */ + output(): string|Array + } + interface Cmd { + /** + * CombinedOutput runs the command and returns its combined standard + * output and standard error. + */ + combinedOutput(): string|Array + } + interface Cmd { + /** + * StdinPipe returns a pipe that will be connected to the command's + * standard input when the command starts. + * The pipe will be closed automatically after [Cmd.Wait] sees the command exit. + * A caller need only call Close to force the pipe to close sooner. + * For example, if the command being run will not exit until standard input + * is closed, the caller must close the pipe. + */ + stdinPipe(): io.WriteCloser + } + interface Cmd { + /** + * StdoutPipe returns a pipe that will be connected to the command's + * standard output when the command starts. + * + * [Cmd.Wait] will close the pipe after seeing the command exit, so most callers + * need not close the pipe themselves. It is thus incorrect to call Wait + * before all reads from the pipe have completed. + * For the same reason, it is incorrect to call [Cmd.Run] when using StdoutPipe. + * See the example for idiomatic usage. + */ + stdoutPipe(): io.ReadCloser + } + interface Cmd { + /** + * StderrPipe returns a pipe that will be connected to the command's + * standard error when the command starts. + * + * [Cmd.Wait] will close the pipe after seeing the command exit, so most callers + * need not close the pipe themselves. It is thus incorrect to call Wait + * before all reads from the pipe have completed. + * For the same reason, it is incorrect to use [Cmd.Run] when using StderrPipe. + * See the StdoutPipe example for idiomatic usage. + */ + stderrPipe(): io.ReadCloser + } + interface Cmd { + /** + * Environ returns a copy of the environment in which the command would be run + * as it is currently configured. + */ + environ(): Array + } +} + +/** + * Package net provides a portable interface for network I/O, including + * TCP/IP, UDP, domain name resolution, and Unix domain sockets. + * + * Although the package provides access to low-level networking + * primitives, most clients will need only the basic interface provided + * by the [Dial], [Listen], and Accept functions and the associated + * [Conn] and [Listener] interfaces. The crypto/tls package uses + * the same interfaces and similar Dial and Listen functions. + * + * The Dial function connects to a server: + * + * ``` + * conn, err := net.Dial("tcp", "golang.org:80") + * if err != nil { + * // handle error + * } + * fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") + * status, err := bufio.NewReader(conn).ReadString('\n') + * // ... + * ``` + * + * The Listen function creates servers: + * + * ``` + * ln, err := net.Listen("tcp", ":8080") + * if err != nil { + * // handle error + * } + * for { + * conn, err := ln.Accept() + * if err != nil { + * // handle error + * } + * go handleConnection(conn) + * } + * ``` + * + * # Name Resolution + * + * The method for resolving domain names, whether indirectly with functions like Dial + * or directly with functions like [LookupHost] and [LookupAddr], varies by operating system. + * + * On Unix systems, the resolver has two options for resolving names. + * It can use a pure Go resolver that sends DNS requests directly to the servers + * listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C + * library routines such as getaddrinfo and getnameinfo. + * + * On Unix the pure Go resolver is preferred over the cgo resolver, because a blocked DNS + * request consumes only a goroutine, while a blocked C call consumes an operating system thread. + * When cgo is available, the cgo-based resolver is used instead under a variety of + * conditions: on systems that do not let programs make direct DNS requests (OS X), + * when the LOCALDOMAIN environment variable is present (even if empty), + * when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, + * when the ASR_CONFIG environment variable is non-empty (OpenBSD only), + * when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the + * Go resolver does not implement. + * + * On all systems (except Plan 9), when the cgo resolver is being used + * this package applies a concurrent cgo lookup limit to prevent the system + * from running out of system threads. Currently, it is limited to 500 concurrent lookups. + * + * The resolver decision can be overridden by setting the netdns value of the + * GODEBUG environment variable (see package runtime) to go or cgo, as in: + * + * ``` + * export GODEBUG=netdns=go # force pure Go resolver + * export GODEBUG=netdns=cgo # force native resolver (cgo, win32) + * ``` + * + * The decision can also be forced while building the Go source tree + * by setting the netgo or netcgo build tag. + * + * A numeric netdns setting, as in GODEBUG=netdns=1, causes the resolver + * to print debugging information about its decisions. + * To force a particular resolver while also printing debugging information, + * join the two settings by a plus sign, as in GODEBUG=netdns=go+1. + * + * The Go resolver will send an EDNS0 additional header with a DNS request, + * to signal a willingness to accept a larger DNS packet size. + * This can reportedly cause sporadic failures with the DNS server run + * by some modems and routers. Setting GODEBUG=netedns0=0 will disable + * sending the additional header. + * + * On macOS, if Go code that uses the net package is built with + * -buildmode=c-archive, linking the resulting archive into a C program + * requires passing -lresolv when linking the C code. + * + * On Plan 9, the resolver always accesses /net/cs and /net/dns. + * + * On Windows, in Go 1.18.x and earlier, the resolver always used C + * library functions, such as GetAddrInfo and DnsQuery. + */ +namespace net { + /** + * Conn is a generic stream-oriented network connection. + * + * Multiple goroutines may invoke methods on a Conn simultaneously. + */ + interface Conn { + [key:string]: any; + /** + * Read reads data from the connection. + * Read can be made to time out and return an error after a fixed + * time limit; see SetDeadline and SetReadDeadline. + */ + read(b: string|Array): number + /** + * Write writes data to the connection. + * Write can be made to time out and return an error after a fixed + * time limit; see SetDeadline and SetWriteDeadline. + */ + write(b: string|Array): number + /** + * Close closes the connection. + * Any blocked Read or Write operations will be unblocked and return errors. + */ + close(): void + /** + * LocalAddr returns the local network address, if known. + */ + localAddr(): Addr + /** + * RemoteAddr returns the remote network address, if known. + */ + remoteAddr(): Addr + /** + * SetDeadline sets the read and write deadlines associated + * with the connection. It is equivalent to calling both + * SetReadDeadline and SetWriteDeadline. + * + * A deadline is an absolute time after which I/O operations + * fail instead of blocking. The deadline applies to all future + * and pending I/O, not just the immediately following call to + * Read or Write. After a deadline has been exceeded, the + * connection can be refreshed by setting a deadline in the future. + * + * If the deadline is exceeded a call to Read or Write or to other + * I/O methods will return an error that wraps os.ErrDeadlineExceeded. + * This can be tested using errors.Is(err, os.ErrDeadlineExceeded). + * The error's Timeout method will return true, but note that there + * are other possible errors for which the Timeout method will + * return true even if the deadline has not been exceeded. + * + * An idle timeout can be implemented by repeatedly extending + * the deadline after successful Read or Write calls. + * + * A zero value for t means I/O operations will not time out. + */ + setDeadline(t: time.Time): void + /** + * SetReadDeadline sets the deadline for future Read calls + * and any currently-blocked Read call. + * A zero value for t means Read will not time out. + */ + setReadDeadline(t: time.Time): void + /** + * SetWriteDeadline sets the deadline for future Write calls + * and any currently-blocked Write call. + * Even if write times out, it may return n > 0, indicating that + * some of the data was successfully written. + * A zero value for t means Write will not time out. + */ + setWriteDeadline(t: time.Time): void + } +} + +/** + * Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html + * + * See README.md for more info. + */ +namespace jwt { + /** + * MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. + * This is the default claims type if you don't supply one + */ + interface MapClaims extends _TygojaDict{} + interface MapClaims { + /** + * VerifyAudience Compares the aud claim against cmp. + * If required is false, this method will return true if the value matches or is unset + */ + verifyAudience(cmp: string, req: boolean): boolean + } + interface MapClaims { + /** + * VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). + * If req is false, it will return true, if exp is unset. + */ + verifyExpiresAt(cmp: number, req: boolean): boolean + } + interface MapClaims { + /** + * VerifyIssuedAt compares the exp claim against cmp (cmp >= iat). + * If req is false, it will return true, if iat is unset. + */ + verifyIssuedAt(cmp: number, req: boolean): boolean + } + interface MapClaims { + /** + * VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). + * If req is false, it will return true, if nbf is unset. + */ + verifyNotBefore(cmp: number, req: boolean): boolean + } + interface MapClaims { + /** + * VerifyIssuer compares the iss claim against cmp. + * If required is false, this method will return true if the value matches or is unset + */ + verifyIssuer(cmp: string, req: boolean): boolean + } + interface MapClaims { + /** + * Valid validates time based claims "exp, iat, nbf". + * There is no accounting for clock skew. + * As well, if any of the above claims are not in the token, it will still + * be considered a valid claim. + */ + valid(): void + } +} + /** * Package multipart implements MIME multipart parsing, as defined in RFC * 2046. @@ -8823,8 +9320,8 @@ namespace sql { * To protect against malicious inputs, this package sets limits on the size * of the MIME data it processes. * - * Reader.NextPart and Reader.NextRawPart limit the number of headers in a - * part to 10000 and Reader.ReadForm limits the total number of headers in all + * [Reader.NextPart] and [Reader.NextRawPart] limit the number of headers in a + * part to 10000 and [Reader.ReadForm] limits the total number of headers in all * FileHeaders to 10000. * These limits may be adjusted with the GODEBUG=multipartmaxheaders= * setting. @@ -8833,11 +9330,6 @@ namespace sql { * This limit may be adjusted with the GODEBUG=multipartmaxparts= * setting. */ -/** - * Copyright 2023 The Go Authors. All rights reserved. - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ namespace multipart { /** * A FileHeader describes a file part of a multipart request. @@ -8849,7 +9341,7 @@ namespace multipart { } interface FileHeader { /** - * Open opens and returns the FileHeader's associated File. + * Open opens and returns the [FileHeader]'s associated File. */ open(): File } @@ -8977,6 +9469,22 @@ namespace multipart { namespace http { // @ts-ignore import mathrand = rand + /** + * PushOptions describes options for [Pusher.Push]. + */ + interface PushOptions { + /** + * Method specifies the HTTP method for the promised request. + * If set, it must be "GET" or "HEAD". Empty means "GET". + */ + method: string + /** + * Header specifies additional promised request headers. This cannot + * include HTTP/2 pseudo header fields like ":path" and ":scheme", + * which will be added automatically. + */ + header: Header + } // @ts-ignore import urlpkg = url /** @@ -9222,6 +9730,11 @@ namespace http { * redirects. */ response?: Response + /** + * Pattern is the [ServeMux] pattern that matched the request. + * It is empty if the request was not matched against a pattern. + */ + pattern: string } interface Request { /** @@ -9258,6 +9771,8 @@ namespace http { * Clone returns a deep copy of r with its context changed to ctx. * The provided ctx must be non-nil. * + * Clone only makes a shallow copy of the Body field. + * * For an outgoing client request, the context controls the entire * lifetime of a request and its response: obtaining a connection, * sending the request, and reading the response headers and body. @@ -9283,6 +9798,13 @@ namespace http { */ cookies(): Array<(Cookie | undefined)> } + interface Request { + /** + * CookiesNamed parses and returns the named HTTP cookies sent with the request + * or an empty slice if none matched. + */ + cookiesNamed(name: string): Array<(Cookie | undefined)> + } interface Request { /** * Cookie returns the named cookie provided in the request or @@ -9466,6 +9988,36 @@ namespace http { */ setPathValue(name: string, value: string): void } + /** + * A Handler responds to an HTTP request. + * + * [Handler.ServeHTTP] should write reply headers and data to the [ResponseWriter] + * and then return. Returning signals that the request is finished; it + * is not valid to use the [ResponseWriter] or read from the + * [Request.Body] after or concurrently with the completion of the + * ServeHTTP call. + * + * Depending on the HTTP client software, HTTP protocol version, and + * any intermediaries between the client and the Go server, it may not + * be possible to read from the [Request.Body] after writing to the + * [ResponseWriter]. Cautious handlers should read the [Request.Body] + * first, and then reply. + * + * Except for reading the body, handlers should not modify the + * provided Request. + * + * If ServeHTTP panics, the server (the caller of ServeHTTP) assumes + * that the effect of the panic was isolated to the active request. + * It recovers the panic, logs a stack trace to the server error log, + * and either closes the network connection or sends an HTTP/2 + * RST_STREAM, depending on the HTTP protocol. To abort a handler so + * the client sees an interrupted response but the server doesn't log + * an error, panic with the value [ErrAbortHandler]. + */ + interface Handler { + [key:string]: any; + serveHTTP(_arg0: ResponseWriter, _arg1: Request): void + } /** * A ResponseWriter interface is used by an HTTP handler to * construct an HTTP response. @@ -9543,674 +10095,6 @@ namespace http { */ writeHeader(statusCode: number): void } - /** - * A Server defines parameters for running an HTTP server. - * The zero value for Server is a valid configuration. - */ - interface Server { - /** - * Addr optionally specifies the TCP address for the server to listen on, - * in the form "host:port". If empty, ":http" (port 80) is used. - * The service names are defined in RFC 6335 and assigned by IANA. - * See net.Dial for details of the address format. - */ - addr: string - handler: Handler // handler to invoke, http.DefaultServeMux if nil - /** - * DisableGeneralOptionsHandler, if true, passes "OPTIONS *" requests to the Handler, - * otherwise responds with 200 OK and Content-Length: 0. - */ - disableGeneralOptionsHandler: boolean - /** - * TLSConfig optionally provides a TLS configuration for use - * by ServeTLS and ListenAndServeTLS. Note that this value is - * cloned by ServeTLS and ListenAndServeTLS, so it's not - * possible to modify the configuration with methods like - * tls.Config.SetSessionTicketKeys. To use - * SetSessionTicketKeys, use Server.Serve with a TLS Listener - * instead. - */ - tlsConfig?: any - /** - * ReadTimeout is the maximum duration for reading the entire - * request, including the body. A zero or negative value means - * there will be no timeout. - * - * Because ReadTimeout does not let Handlers make per-request - * decisions on each request body's acceptable deadline or - * upload rate, most users will prefer to use - * ReadHeaderTimeout. It is valid to use them both. - */ - readTimeout: time.Duration - /** - * ReadHeaderTimeout is the amount of time allowed to read - * request headers. The connection's read deadline is reset - * after reading the headers and the Handler can decide what - * is considered too slow for the body. If ReadHeaderTimeout - * is zero, the value of ReadTimeout is used. If both are - * zero, there is no timeout. - */ - readHeaderTimeout: time.Duration - /** - * WriteTimeout is the maximum duration before timing out - * writes of the response. It is reset whenever a new - * request's header is read. Like ReadTimeout, it does not - * let Handlers make decisions on a per-request basis. - * A zero or negative value means there will be no timeout. - */ - writeTimeout: time.Duration - /** - * IdleTimeout is the maximum amount of time to wait for the - * next request when keep-alives are enabled. If IdleTimeout - * is zero, the value of ReadTimeout is used. If both are - * zero, there is no timeout. - */ - idleTimeout: time.Duration - /** - * MaxHeaderBytes controls the maximum number of bytes the - * server will read parsing the request header's keys and - * values, including the request line. It does not limit the - * size of the request body. - * If zero, DefaultMaxHeaderBytes is used. - */ - maxHeaderBytes: number - /** - * TLSNextProto optionally specifies a function to take over - * ownership of the provided TLS connection when an ALPN - * protocol upgrade has occurred. The map key is the protocol - * name negotiated. The Handler argument should be used to - * handle HTTP requests and will initialize the Request's TLS - * and RemoteAddr if not already set. The connection is - * automatically closed when the function returns. - * If TLSNextProto is not nil, HTTP/2 support is not enabled - * automatically. - */ - tlsNextProto: _TygojaDict - /** - * ConnState specifies an optional callback function that is - * called when a client connection changes state. See the - * ConnState type and associated constants for details. - */ - connState: (_arg0: net.Conn, _arg1: ConnState) => void - /** - * ErrorLog specifies an optional logger for errors accepting - * connections, unexpected behavior from handlers, and - * underlying FileSystem errors. - * If nil, logging is done via the log package's standard logger. - */ - errorLog?: any - /** - * BaseContext optionally specifies a function that returns - * the base context for incoming requests on this server. - * The provided Listener is the specific Listener that's - * about to start accepting requests. - * If BaseContext is nil, the default is context.Background(). - * If non-nil, it must return a non-nil context. - */ - baseContext: (_arg0: net.Listener) => context.Context - /** - * ConnContext optionally specifies a function that modifies - * the context used for a new connection c. The provided ctx - * is derived from the base context and has a ServerContextKey - * value. - */ - connContext: (ctx: context.Context, c: net.Conn) => context.Context - } - interface Server { - /** - * Close immediately closes all active net.Listeners and any - * connections in state [StateNew], [StateActive], or [StateIdle]. For a - * graceful shutdown, use [Server.Shutdown]. - * - * Close does not attempt to close (and does not even know about) - * any hijacked connections, such as WebSockets. - * - * Close returns any error returned from closing the [Server]'s - * underlying Listener(s). - */ - close(): void - } - interface Server { - /** - * Shutdown gracefully shuts down the server without interrupting any - * active connections. Shutdown works by first closing all open - * listeners, then closing all idle connections, and then waiting - * indefinitely for connections to return to idle and then shut down. - * If the provided context expires before the shutdown is complete, - * Shutdown returns the context's error, otherwise it returns any - * error returned from closing the [Server]'s underlying Listener(s). - * - * When Shutdown is called, [Serve], [ListenAndServe], and - * [ListenAndServeTLS] immediately return [ErrServerClosed]. Make sure the - * program doesn't exit and waits instead for Shutdown to return. - * - * Shutdown does not attempt to close nor wait for hijacked - * connections such as WebSockets. The caller of Shutdown should - * separately notify such long-lived connections of shutdown and wait - * for them to close, if desired. See [Server.RegisterOnShutdown] for a way to - * register shutdown notification functions. - * - * Once Shutdown has been called on a server, it may not be reused; - * future calls to methods such as Serve will return ErrServerClosed. - */ - shutdown(ctx: context.Context): void - } - interface Server { - /** - * RegisterOnShutdown registers a function to call on [Server.Shutdown]. - * This can be used to gracefully shutdown connections that have - * undergone ALPN protocol upgrade or that have been hijacked. - * This function should start protocol-specific graceful shutdown, - * but should not wait for shutdown to complete. - */ - registerOnShutdown(f: () => void): void - } - interface Server { - /** - * ListenAndServe listens on the TCP network address srv.Addr and then - * calls [Serve] to handle requests on incoming connections. - * Accepted connections are configured to enable TCP keep-alives. - * - * If srv.Addr is blank, ":http" is used. - * - * ListenAndServe always returns a non-nil error. After [Server.Shutdown] or [Server.Close], - * the returned error is [ErrServerClosed]. - */ - listenAndServe(): void - } - interface Server { - /** - * Serve accepts incoming connections on the Listener l, creating a - * new service goroutine for each. The service goroutines read requests and - * then call srv.Handler to reply to them. - * - * HTTP/2 support is only enabled if the Listener returns [*tls.Conn] - * connections and they were configured with "h2" in the TLS - * Config.NextProtos. - * - * Serve always returns a non-nil error and closes l. - * After [Server.Shutdown] or [Server.Close], the returned error is [ErrServerClosed]. - */ - serve(l: net.Listener): void - } - interface Server { - /** - * ServeTLS accepts incoming connections on the Listener l, creating a - * new service goroutine for each. The service goroutines perform TLS - * setup and then read requests, calling srv.Handler to reply to them. - * - * Files containing a certificate and matching private key for the - * server must be provided if neither the [Server]'s - * TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. - * If the certificate is signed by a certificate authority, the - * certFile should be the concatenation of the server's certificate, - * any intermediates, and the CA's certificate. - * - * ServeTLS always returns a non-nil error. After [Server.Shutdown] or [Server.Close], the - * returned error is [ErrServerClosed]. - */ - serveTLS(l: net.Listener, certFile: string, keyFile: string): void - } - interface Server { - /** - * SetKeepAlivesEnabled controls whether HTTP keep-alives are enabled. - * By default, keep-alives are always enabled. Only very - * resource-constrained environments or servers in the process of - * shutting down should disable them. - */ - setKeepAlivesEnabled(v: boolean): void - } - interface Server { - /** - * ListenAndServeTLS listens on the TCP network address srv.Addr and - * then calls [ServeTLS] to handle requests on incoming TLS connections. - * Accepted connections are configured to enable TCP keep-alives. - * - * Filenames containing a certificate and matching private key for the - * server must be provided if neither the [Server]'s TLSConfig.Certificates - * nor TLSConfig.GetCertificate are populated. If the certificate is - * signed by a certificate authority, the certFile should be the - * concatenation of the server's certificate, any intermediates, and - * the CA's certificate. - * - * If srv.Addr is blank, ":https" is used. - * - * ListenAndServeTLS always returns a non-nil error. After [Server.Shutdown] or - * [Server.Close], the returned error is [ErrServerClosed]. - */ - listenAndServeTLS(certFile: string, keyFile: string): void - } -} - -/** - * Package exec runs external commands. It wraps os.StartProcess to make it - * easier to remap stdin and stdout, connect I/O with pipes, and do other - * adjustments. - * - * Unlike the "system" library call from C and other languages, the - * os/exec package intentionally does not invoke the system shell and - * does not expand any glob patterns or handle other expansions, - * pipelines, or redirections typically done by shells. The package - * behaves more like C's "exec" family of functions. To expand glob - * patterns, either call the shell directly, taking care to escape any - * dangerous input, or use the path/filepath package's Glob function. - * To expand environment variables, use package os's ExpandEnv. - * - * Note that the examples in this package assume a Unix system. - * They may not run on Windows, and they do not run in the Go Playground - * used by golang.org and godoc.org. - * - * # Executables in the current directory - * - * The functions Command and LookPath look for a program - * in the directories listed in the current path, following the - * conventions of the host operating system. - * Operating systems have for decades included the current - * directory in this search, sometimes implicitly and sometimes - * configured explicitly that way by default. - * Modern practice is that including the current directory - * is usually unexpected and often leads to security problems. - * - * To avoid those security problems, as of Go 1.19, this package will not resolve a program - * using an implicit or explicit path entry relative to the current directory. - * That is, if you run exec.LookPath("go"), it will not successfully return - * ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. - * Instead, if the usual path algorithms would result in that answer, - * these functions return an error err satisfying errors.Is(err, ErrDot). - * - * For example, consider these two program snippets: - * - * ``` - * path, err := exec.LookPath("prog") - * if err != nil { - * log.Fatal(err) - * } - * use(path) - * ``` - * - * and - * - * ``` - * cmd := exec.Command("prog") - * if err := cmd.Run(); err != nil { - * log.Fatal(err) - * } - * ``` - * - * These will not find and run ./prog or .\prog.exe, - * no matter how the current path is configured. - * - * Code that always wants to run a program from the current directory - * can be rewritten to say "./prog" instead of "prog". - * - * Code that insists on including results from relative path entries - * can instead override the error using an errors.Is check: - * - * ``` - * path, err := exec.LookPath("prog") - * if errors.Is(err, exec.ErrDot) { - * err = nil - * } - * if err != nil { - * log.Fatal(err) - * } - * use(path) - * ``` - * - * and - * - * ``` - * cmd := exec.Command("prog") - * if errors.Is(cmd.Err, exec.ErrDot) { - * cmd.Err = nil - * } - * if err := cmd.Run(); err != nil { - * log.Fatal(err) - * } - * ``` - * - * Setting the environment variable GODEBUG=execerrdot=0 - * disables generation of ErrDot entirely, temporarily restoring the pre-Go 1.19 - * behavior for programs that are unable to apply more targeted fixes. - * A future version of Go may remove support for this variable. - * - * Before adding such overrides, make sure you understand the - * security implications of doing so. - * See https://go.dev/blog/path-security for more information. - */ -namespace exec { - /** - * Cmd represents an external command being prepared or run. - * - * A Cmd cannot be reused after calling its Run, Output or CombinedOutput - * methods. - */ - interface Cmd { - /** - * Path is the path of the command to run. - * - * This is the only field that must be set to a non-zero - * value. If Path is relative, it is evaluated relative - * to Dir. - */ - path: string - /** - * Args holds command line arguments, including the command as Args[0]. - * If the Args field is empty or nil, Run uses {Path}. - * - * In typical use, both Path and Args are set by calling Command. - */ - args: Array - /** - * Env specifies the environment of the process. - * Each entry is of the form "key=value". - * If Env is nil, the new process uses the current process's - * environment. - * If Env contains duplicate environment keys, only the last - * value in the slice for each duplicate key is used. - * As a special case on Windows, SYSTEMROOT is always added if - * missing and not explicitly set to the empty string. - */ - env: Array - /** - * Dir specifies the working directory of the command. - * If Dir is the empty string, Run runs the command in the - * calling process's current directory. - */ - dir: string - /** - * Stdin specifies the process's standard input. - * - * If Stdin is nil, the process reads from the null device (os.DevNull). - * - * If Stdin is an *os.File, the process's standard input is connected - * directly to that file. - * - * Otherwise, during the execution of the command a separate - * goroutine reads from Stdin and delivers that data to the command - * over a pipe. In this case, Wait does not complete until the goroutine - * stops copying, either because it has reached the end of Stdin - * (EOF or a read error), or because writing to the pipe returned an error, - * or because a nonzero WaitDelay was set and expired. - */ - stdin: io.Reader - /** - * Stdout and Stderr specify the process's standard output and error. - * - * If either is nil, Run connects the corresponding file descriptor - * to the null device (os.DevNull). - * - * If either is an *os.File, the corresponding output from the process - * is connected directly to that file. - * - * Otherwise, during the execution of the command a separate goroutine - * reads from the process over a pipe and delivers that data to the - * corresponding Writer. In this case, Wait does not complete until the - * goroutine reaches EOF or encounters an error or a nonzero WaitDelay - * expires. - * - * If Stdout and Stderr are the same writer, and have a type that can - * be compared with ==, at most one goroutine at a time will call Write. - */ - stdout: io.Writer - stderr: io.Writer - /** - * ExtraFiles specifies additional open files to be inherited by the - * new process. It does not include standard input, standard output, or - * standard error. If non-nil, entry i becomes file descriptor 3+i. - * - * ExtraFiles is not supported on Windows. - */ - extraFiles: Array<(os.File | undefined)> - /** - * SysProcAttr holds optional, operating system-specific attributes. - * Run passes it to os.StartProcess as the os.ProcAttr's Sys field. - */ - sysProcAttr?: syscall.SysProcAttr - /** - * Process is the underlying process, once started. - */ - process?: os.Process - /** - * ProcessState contains information about an exited process. - * If the process was started successfully, Wait or Run will - * populate its ProcessState when the command completes. - */ - processState?: os.ProcessState - err: Error // LookPath error, if any. - /** - * If Cancel is non-nil, the command must have been created with - * CommandContext and Cancel will be called when the command's - * Context is done. By default, CommandContext sets Cancel to - * call the Kill method on the command's Process. - * - * Typically a custom Cancel will send a signal to the command's - * Process, but it may instead take other actions to initiate cancellation, - * such as closing a stdin or stdout pipe or sending a shutdown request on a - * network socket. - * - * If the command exits with a success status after Cancel is - * called, and Cancel does not return an error equivalent to - * os.ErrProcessDone, then Wait and similar methods will return a non-nil - * error: either an error wrapping the one returned by Cancel, - * or the error from the Context. - * (If the command exits with a non-success status, or Cancel - * returns an error that wraps os.ErrProcessDone, Wait and similar methods - * continue to return the command's usual exit status.) - * - * If Cancel is set to nil, nothing will happen immediately when the command's - * Context is done, but a nonzero WaitDelay will still take effect. That may - * be useful, for example, to work around deadlocks in commands that do not - * support shutdown signals but are expected to always finish quickly. - * - * Cancel will not be called if Start returns a non-nil error. - */ - cancel: () => void - /** - * If WaitDelay is non-zero, it bounds the time spent waiting on two sources - * of unexpected delay in Wait: a child process that fails to exit after the - * associated Context is canceled, and a child process that exits but leaves - * its I/O pipes unclosed. - * - * The WaitDelay timer starts when either the associated Context is done or a - * call to Wait observes that the child process has exited, whichever occurs - * first. When the delay has elapsed, the command shuts down the child process - * and/or its I/O pipes. - * - * If the child process has failed to exit — perhaps because it ignored or - * failed to receive a shutdown signal from a Cancel function, or because no - * Cancel function was set — then it will be terminated using os.Process.Kill. - * - * Then, if the I/O pipes communicating with the child process are still open, - * those pipes are closed in order to unblock any goroutines currently blocked - * on Read or Write calls. - * - * If pipes are closed due to WaitDelay, no Cancel call has occurred, - * and the command has otherwise exited with a successful status, Wait and - * similar methods will return ErrWaitDelay instead of nil. - * - * If WaitDelay is zero (the default), I/O pipes will be read until EOF, - * which might not occur until orphaned subprocesses of the command have - * also closed their descriptors for the pipes. - */ - waitDelay: time.Duration - } - interface Cmd { - /** - * String returns a human-readable description of c. - * It is intended only for debugging. - * In particular, it is not suitable for use as input to a shell. - * The output of String may vary across Go releases. - */ - string(): string - } - interface Cmd { - /** - * Run starts the specified command and waits for it to complete. - * - * The returned error is nil if the command runs, has no problems - * copying stdin, stdout, and stderr, and exits with a zero exit - * status. - * - * If the command starts but does not complete successfully, the error is of - * type *ExitError. Other error types may be returned for other situations. - * - * If the calling goroutine has locked the operating system thread - * with runtime.LockOSThread and modified any inheritable OS-level - * thread state (for example, Linux or Plan 9 name spaces), the new - * process will inherit the caller's thread state. - */ - run(): void - } - interface Cmd { - /** - * Start starts the specified command but does not wait for it to complete. - * - * If Start returns successfully, the c.Process field will be set. - * - * After a successful call to Start the Wait method must be called in - * order to release associated system resources. - */ - start(): void - } - interface Cmd { - /** - * Wait waits for the command to exit and waits for any copying to - * stdin or copying from stdout or stderr to complete. - * - * The command must have been started by Start. - * - * The returned error is nil if the command runs, has no problems - * copying stdin, stdout, and stderr, and exits with a zero exit - * status. - * - * If the command fails to run or doesn't complete successfully, the - * error is of type *ExitError. Other error types may be - * returned for I/O problems. - * - * If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits - * for the respective I/O loop copying to or from the process to complete. - * - * Wait releases any resources associated with the Cmd. - */ - wait(): void - } - interface Cmd { - /** - * Output runs the command and returns its standard output. - * Any returned error will usually be of type *ExitError. - * If c.Stderr was nil, Output populates ExitError.Stderr. - */ - output(): string|Array - } - interface Cmd { - /** - * CombinedOutput runs the command and returns its combined standard - * output and standard error. - */ - combinedOutput(): string|Array - } - interface Cmd { - /** - * StdinPipe returns a pipe that will be connected to the command's - * standard input when the command starts. - * The pipe will be closed automatically after Wait sees the command exit. - * A caller need only call Close to force the pipe to close sooner. - * For example, if the command being run will not exit until standard input - * is closed, the caller must close the pipe. - */ - stdinPipe(): io.WriteCloser - } - interface Cmd { - /** - * StdoutPipe returns a pipe that will be connected to the command's - * standard output when the command starts. - * - * Wait will close the pipe after seeing the command exit, so most callers - * need not close the pipe themselves. It is thus incorrect to call Wait - * before all reads from the pipe have completed. - * For the same reason, it is incorrect to call Run when using StdoutPipe. - * See the example for idiomatic usage. - */ - stdoutPipe(): io.ReadCloser - } - interface Cmd { - /** - * StderrPipe returns a pipe that will be connected to the command's - * standard error when the command starts. - * - * Wait will close the pipe after seeing the command exit, so most callers - * need not close the pipe themselves. It is thus incorrect to call Wait - * before all reads from the pipe have completed. - * For the same reason, it is incorrect to use Run when using StderrPipe. - * See the StdoutPipe example for idiomatic usage. - */ - stderrPipe(): io.ReadCloser - } - interface Cmd { - /** - * Environ returns a copy of the environment in which the command would be run - * as it is currently configured. - */ - environ(): Array - } -} - -/** - * Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html - * - * See README.md for more info. - */ -namespace jwt { - /** - * MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. - * This is the default claims type if you don't supply one - */ - interface MapClaims extends _TygojaDict{} - interface MapClaims { - /** - * VerifyAudience Compares the aud claim against cmp. - * If required is false, this method will return true if the value matches or is unset - */ - verifyAudience(cmp: string, req: boolean): boolean - } - interface MapClaims { - /** - * VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). - * If req is false, it will return true, if exp is unset. - */ - verifyExpiresAt(cmp: number, req: boolean): boolean - } - interface MapClaims { - /** - * VerifyIssuedAt compares the exp claim against cmp (cmp >= iat). - * If req is false, it will return true, if iat is unset. - */ - verifyIssuedAt(cmp: number, req: boolean): boolean - } - interface MapClaims { - /** - * VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). - * If req is false, it will return true, if nbf is unset. - */ - verifyNotBefore(cmp: number, req: boolean): boolean - } - interface MapClaims { - /** - * VerifyIssuer compares the iss claim against cmp. - * If required is false, this method will return true if the value matches or is unset - */ - verifyIssuer(cmp: string, req: boolean): boolean - } - interface MapClaims { - /** - * Valid validates time based claims "exp, iat, nbf". - * There is no accounting for clock skew. - * As well, if any of the above claims are not in the token, it will still - * be considered a valid claim. - */ - valid(): void - } } /** @@ -10450,360 +10334,1923 @@ namespace blob { */ namespace types { /** - * JsonArray defines a slice that is safe for json and db read/write. + * DateTime represents a [time.Time] instance in UTC that is wrapped + * and serialized using the app default date layout. */ - interface JsonArray extends Array{} - interface JsonArray { + interface DateTime { + } + interface DateTime { + /** + * Time returns the internal [time.Time] instance. + */ + time(): time.Time + } + interface DateTime { + /** + * Add returns a new DateTime based on the current DateTime + the specified duration. + */ + add(duration: time.Duration): DateTime + } + interface DateTime { + /** + * Sub returns a [time.Duration] by substracting the specified DateTime from the current one. + * + * If the result exceeds the maximum (or minimum) value that can be stored in a [time.Duration], + * the maximum (or minimum) duration will be returned. + */ + sub(u: DateTime): time.Duration + } + interface DateTime { + /** + * AddDate returns a new DateTime based on the current one + duration. + * + * It follows the same rules as [time.AddDate]. + */ + addDate(years: number, months: number, days: number): DateTime + } + interface DateTime { + /** + * After reports whether the current DateTime instance is after u. + */ + after(u: DateTime): boolean + } + interface DateTime { + /** + * Before reports whether the current DateTime instance is before u. + */ + before(u: DateTime): boolean + } + interface DateTime { + /** + * Compare compares the current DateTime instance with u. + * If the current instance is before u, it returns -1. + * If the current instance is after u, it returns +1. + * If they're the same, it returns 0. + */ + compare(u: DateTime): number + } + interface DateTime { + /** + * Equal reports whether the current DateTime and u represent the same time instant. + * Two DateTime can be equal even if they are in different locations. + * For example, 6:00 +0200 and 4:00 UTC are Equal. + */ + equal(u: DateTime): boolean + } + interface DateTime { + /** + * Unix returns the current DateTime as a Unix time, aka. + * the number of seconds elapsed since January 1, 1970 UTC. + */ + unix(): number + } + interface DateTime { + /** + * IsZero checks whether the current DateTime instance has zero time value. + */ + isZero(): boolean + } + interface DateTime { + /** + * String serializes the current DateTime instance into a formatted + * UTC date string. + * + * The zero value is serialized to an empty string. + */ + string(): string + } + interface DateTime { /** * MarshalJSON implements the [json.Marshaler] interface. */ marshalJSON(): string|Array } - interface JsonArray { - /** - * Value implements the [driver.Valuer] interface. - */ - value(): any - } - interface JsonArray { - /** - * Scan implements [sql.Scanner] interface to scan the provided value - * into the current JsonArray[T] instance. - */ - scan(value: any): void - } - /** - * JsonMap defines a map that is safe for json and db read/write. - */ - interface JsonMap extends _TygojaDict{} - interface JsonMap { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface JsonMap { - /** - * Get retrieves a single value from the current JsonMap. - * - * This helper was added primarily to assist the goja integration since custom map types - * don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). - */ - get(key: string): any - } - interface JsonMap { - /** - * Set sets a single value in the current JsonMap. - * - * This helper was added primarily to assist the goja integration since custom map types - * don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). - */ - set(key: string, value: any): void - } - interface JsonMap { - /** - * Value implements the [driver.Valuer] interface. - */ - value(): any - } - interface JsonMap { - /** - * Scan implements [sql.Scanner] interface to scan the provided value - * into the current `JsonMap` instance. - */ - scan(value: any): void - } -} - -/** - * Package schema implements custom Schema and SchemaField datatypes - * for handling the Collection schema definitions. - */ -namespace schema { - // @ts-ignore - import validation = ozzo_validation - /** - * Schema defines a dynamic db schema as a slice of `SchemaField`s. - */ - interface Schema { - } - interface Schema { - /** - * Fields returns the registered schema fields. - */ - fields(): Array<(SchemaField | undefined)> - } - interface Schema { - /** - * InitFieldsOptions calls `InitOptions()` for all schema fields. - */ - initFieldsOptions(): void - } - interface Schema { - /** - * Clone creates a deep clone of the current schema. - */ - clone(): (Schema) - } - interface Schema { - /** - * AsMap returns a map with all registered schema field. - * The returned map is indexed with each field name. - */ - asMap(): _TygojaDict - } - interface Schema { - /** - * GetFieldById returns a single field by its id. - */ - getFieldById(id: string): (SchemaField) - } - interface Schema { - /** - * GetFieldByName returns a single field by its name. - */ - getFieldByName(name: string): (SchemaField) - } - interface Schema { - /** - * RemoveField removes a single schema field by its id. - * - * This method does nothing if field with `id` doesn't exist. - */ - removeField(id: string): void - } - interface Schema { - /** - * AddField registers the provided newField to the current schema. - * - * If field with `newField.Id` already exist, the existing field is - * replaced with the new one. - * - * Otherwise the new field is appended to the other schema fields. - */ - addField(newField: SchemaField): void - } - interface Schema { - /** - * Validate makes Schema validatable by implementing [validation.Validatable] interface. - * - * Internally calls each individual field's validator and additionally - * checks for invalid renamed fields and field name duplications. - */ - validate(): void - } - interface Schema { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface Schema { + interface DateTime { /** * UnmarshalJSON implements the [json.Unmarshaler] interface. - * - * On success, all schema field options are auto initialized. */ - unmarshalJSON(data: string|Array): void + unmarshalJSON(b: string|Array): void } - interface Schema { + interface DateTime { /** * Value implements the [driver.Valuer] interface. */ value(): any } - interface Schema { + interface DateTime { /** * Scan implements [sql.Scanner] interface to scan the provided value - * into the current Schema instance. + * into the current DateTime instance. */ scan(value: any): void } } -/** - * Package models implements all PocketBase DB models and DTOs. - */ -namespace models { - type _subxyKhr = BaseModel - interface Admin extends _subxyKhr { - avatar: number - email: string - tokenKey: string - passwordHash: string - lastResetSentAt: types.DateTime - } - interface Admin { +namespace hook { + /** + * HandlerFunc defines a hook handler function. + */ + interface HandlerFunc {(e: T): void } + /** + * Handler defines a single Hook handler. + * Multiple handlers can share the same id. + * If Id is not explicitly set it will be autogenerated by Hook.Add and Hook.AddHandler. + */ + interface Handler { /** - * TableName returns the Admin model SQL table name. - */ - tableName(): string - } - interface Admin { - /** - * ValidatePassword validates a plain password against the model's password. - */ - validatePassword(password: string): boolean - } - interface Admin { - /** - * SetPassword sets cryptographically secure string to `model.Password`. + * Func defines the handler function to execute. * - * Additionally this method also resets the LastResetSentAt and the TokenKey fields. + * Note that users need to call e.Next() in order to proceed with + * the execution of the hook chain. */ - setPassword(password: string): void - } - interface Admin { + func: HandlerFunc /** - * RefreshTokenKey generates and sets new random token key. + * Id is the unique identifier of the handler. + * + * It could be used later to remove the handler from a hook via [Hook.Remove]. + * + * If missing, an autogenerated value will be assigned when adding + * the handler to a hook. */ - refreshTokenKey(): void + id: string + /** + * Priority allows changing the default exec priority of the handler + * withing a hook. + * + * If 0, the handler will be executed in the same order it was registered. + */ + priority: number + } +} + +namespace router { + // @ts-ignore + import validation = ozzo_validation + /** + * ApiError defines the struct for a basic api error response. + */ + interface ApiError { + data: _TygojaDict + message: string + status: number + } + interface ApiError { + /** + * Error makes it compatible with the `error` interface. + */ + error(): string + } + interface ApiError { + /** + * RawData returns the unformatted error data (could be an internal error, text, etc.) + */ + rawData(): any + } + interface ApiError { + /** + * Is reports whether the current ApiError wraps the target. + */ + is(target: Error): boolean + } + /** + * Router defines a thin wrapper around the standard Go [http.ServeMux] by + * adding support for routing sub-groups, middlewares and other common utils. + * + * Example: + * + * ``` + * r := NewRouter[*MyEvent](eventFactory) + * + * // middlewares + * r.BindFunc(m1, m2) + * + * // routes + * r.GET("/test", handler1) + * + * // sub-routers/groups + * api := r.Group("/api") + * api.GET("/admins", handler2) + * + * // generate a http.ServeMux instance based on the router configurations + * mux, _ := r.BuildMux() + * + * http.ListenAndServe("localhost:8090", mux) + * ``` + */ + type _subjGLEK = RouterGroup + interface Router extends _subjGLEK { + } + interface Router { + /** + * BuildMux constructs a new mux [http.Handler] instance from the current router configurations. + */ + buildMux(): http.Handler + } +} + +/** + * Package core is the backbone of PocketBase. + * + * It defines the main PocketBase App interface and its base implementation. + */ +namespace core { + /** + * App defines the main PocketBase app interface. + * + * Note that the interface is not intended to be implemented manually by users + * and instead they should use core.BaseApp (either directly or as embedded field in a custom struct). + * + * This interface exists to make testing easier and to allow users to + * create common and pluggable helpers and methods that doesn't rely + * on a specific wrapped app struct (hence the large interface size). + */ + interface App { + [key:string]: any; + /** + * 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. + */ + unsafeWithoutHooks(): App + /** + * Logger returns the default app logger. + * + * If the application is not bootstrapped yet, fallbacks to slog.Default(). + */ + logger(): (slog.Logger) + /** + * IsBootstrapped checks if the application was initialized + * (aka. whether Bootstrap() was called). + */ + isBootstrapped(): boolean + /** + * IsTransactional checks if the current app instance is part of a transaction. + */ + isTransactional(): boolean + /** + * Bootstrap initializes the application + * (aka. create data dir, open db connections, load settings, etc.). + * + * It will call ResetBootstrapState() if the application was already bootstrapped. + */ + bootstrap(): void + /** + * ResetBootstrapState releases the initialized core app resources + * (closing db connections, stopping cron ticker, etc.). + */ + resetBootstrapState(): void + /** + * DataDir returns the app data directory path. + */ + dataDir(): string + /** + * EncryptionEnv returns the name of the app secret env key + * (currently used primarily for optional settings encryption but this may change in the future). + */ + encryptionEnv(): string + /** + * IsDev returns whether the app is in dev mode. + * + * When enabled logs, executed sql statements, etc. are printed to the stderr. + */ + isDev(): boolean + /** + * Settings returns the loaded app settings. + */ + settings(): (Settings) + /** + * Store returns the app runtime store. + */ + store(): (store.Store) + /** + * Cron returns the app cron instance. + */ + cron(): (cron.Cron) + /** + * SubscriptionsBroker returns the app realtime subscriptions broker instance. + */ + subscriptionsBroker(): (subscriptions.Broker) + /** + * NewMailClient creates and returns a new SMTP or Sendmail client + * based on the current app settings. + */ + newMailClient(): mailer.Mailer + /** + * NewFilesystem creates a new local or S3 filesystem instance + * for managing regular app files (ex. record uploads) + * based on the current app settings. + * + * NB! Make sure to call Close() on the returned result + * after you are done working with it. + */ + newFilesystem(): (filesystem.System) + /** + * NewFilesystem creates a new local or S3 filesystem instance + * for managing app backups based on the current app settings. + * + * NB! Make sure to call Close() on the returned result + * after you are done working with it. + */ + newBackupsFilesystem(): (filesystem.System) + /** + * ReloadSettings reinitializes and reloads the stored application settings. + */ + reloadSettings(): void + /** + * CreateBackup creates a new backup of the current app pb_data directory. + * + * Backups can be stored on S3 if it is configured in app.Settings().Backups. + * + * Please refer to the godoc of the specific CoreApp implementation + * for details on the backup procedures. + */ + createBackup(ctx: context.Context, name: string): void + /** + * RestoreBackup restores the backup with the specified name and restarts + * the current running application process. + * + * The safely perform the restore it is recommended to have free disk space + * for at least 2x the size of the restored pb_data backup. + * + * Please refer to the godoc of the specific CoreApp implementation + * for details on the restore procedures. + * + * NB! This feature is experimental and currently is expected to work only on UNIX based systems. + */ + restoreBackup(ctx: context.Context, name: string): void + /** + * Restart restarts (aka. replaces) the current running application process. + * + * NB! It relies on execve which is supported only on UNIX based systems. + */ + restart(): void + /** + * RunSystemMigrations applies all new migrations registered in the [core.SystemMigrations] list. + */ + runSystemMigrations(): void + /** + * RunAppMigrations applies all new migrations registered in the [CoreAppMigrations] list. + */ + runAppMigrations(): void + /** + * RunAllMigrations applies all system and app migrations + * (aka. from both [core.SystemMigrations] and [CoreAppMigrations]). + */ + runAllMigrations(): void + /** + * DB returns the default app data db instance (pb_data/data.db). + */ + db(): dbx.Builder + /** + * NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db). + * + * The returned db instance is limited only to a single open connection, + * meaning that it can process only 1 db operation at a time (other operations will be queued up). + * + * This method is used mainly internally and in the tests to execute write + * (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. + * + * For the majority of cases you would want to use the regular DB() method + * since it allows concurrent db read operations. + * + * In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance. + */ + nonconcurrentDB(): dbx.Builder + /** + * AuxDB returns the default app auxiliary db instance (pb_data/aux.db). + */ + auxDB(): dbx.Builder + /** + * AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/aux.db).. + * + * The returned db instance is limited only to a single open connection, + * meaning that it can process only 1 db operation at a time (other operations will be queued up). + * + * This method is used mainly internally and in the tests to execute write + * (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors. + * + * For the majority of cases you would want to use the regular DB() method + * since it allows concurrent db read operations. + * + * In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance. + */ + auxNonconcurrentDB(): dbx.Builder + /** + * HasTable checks if a table (or view) with the provided name exists (case insensitive). + */ + hasTable(tableName: string): boolean + /** + * TableColumns returns all column names of a single table by its name. + */ + tableColumns(tableName: string): Array + /** + * TableInfo returns the "table_info" pragma result for the specified table. + */ + tableInfo(tableName: string): Array<(TableInfoRow | undefined)> + /** + * TableIndexes returns a name grouped map with all non empty index of the specified table. + * + * Note: This method doesn't return an error on nonexisting table. + */ + tableIndexes(tableName: string): _TygojaDict + /** + * DeleteTable drops the specified table. + * + * This method is a no-op if a table with the provided name doesn't exist. + * + * NB! Be aware that this method is vulnerable to SQL injection and the + * "tableName" argument must come only from trusted input! + */ + deleteTable(tableName: string): void + /** + * DeleteView drops the specified view name. + * + * This method is a no-op if a view with the provided name doesn't exist. + * + * NB! Be aware that this method is vulnerable to SQL injection and the + * "name" argument must come only from trusted input! + */ + deleteView(name: string): void + /** + * SaveView creates (or updates already existing) persistent SQL view. + * + * NB! Be aware that this method is vulnerable to SQL injection and the + * "selectQuery" argument must come only from trusted input! + */ + saveView(name: string, selectQuery: string): void + /** + * CreateViewFields creates a new FieldsList from the provided select query. + * + * There are some caveats: + * - The select query must have an "id" column. + * - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data. + */ + createViewFields(selectQuery: string): FieldsList + /** + * FindRecordByViewFile returns the original Record of the provided view collection file. + */ + findRecordByViewFile(viewCollectionModelOrIdentifier: any, fileFieldName: string, filename: string): (Record) + /** + * Vacuum executes VACUUM on the current app.DB() instance + * in order to reclaim unused data db disk space. + */ + vacuum(): void + /** + * AuxVacuum executes VACUUM on the current app.AuxDB() instance + * in order to reclaim unused auxiliary db disk space. + */ + auxVacuum(): void + /** + * ModelQuery creates a new preconfigured select app.DB() query with preset + * SELECT, FROM and other common fields based on the provided model. + */ + modelQuery(model: Model): (dbx.SelectQuery) + /** + * AuxModelQuery creates a new preconfigured select app.AuxDB() query with preset + * SELECT, FROM and other common fields based on the provided model. + */ + auxModelQuery(model: Model): (dbx.SelectQuery) + /** + * Delete deletes the specified model from the regular app database. + */ + delete(model: Model): void + /** + * Delete deletes the specified model from the regular app database + * (the context could be used to limit the query execution). + */ + deleteWithContext(ctx: context.Context, model: Model): void + /** + * AuxDelete deletes the specified model from the auxiliary database. + */ + auxDelete(model: Model): void + /** + * AuxDeleteWithContext deletes the specified model from the auxiliary database + * (the context could be used to limit the query execution). + */ + auxDeleteWithContext(ctx: context.Context, model: Model): void + /** + * Save validates and saves the specified model into the regular app database. + * + * If you don't want to run validations, use [App.SaveNoValidate()]. + */ + save(model: Model): void + /** + * SaveWithContext is the same as [App.Save()] but allows specifying a context to limit the db execution. + * + * If you don't want to run validations, use [App.SaveNoValidateWithContext()]. + */ + saveWithContext(ctx: context.Context, model: Model): void + /** + * SaveNoValidate saves the specified model into the regular app database without performing validations. + * + * If you want to also run validations before persisting, use [App.Save()]. + */ + saveNoValidate(model: Model): void + /** + * SaveNoValidateWithContext is the same as [App.SaveNoValidate()] + * but allows specifying a context to limit the db execution. + * + * If you want to also run validations before persisting, use [App.SaveWithContext()]. + */ + saveNoValidateWithContext(ctx: context.Context, model: Model): void + /** + * AuxSave validates and saves the specified model into the auxiliary app database. + * + * If you don't want to run validations, use [App.AuxSaveNoValidate()]. + */ + auxSave(model: Model): void + /** + * AuxSaveWithContext is the same as [App.AuxSave()] but allows specifying a context to limit the db execution. + * + * If you don't want to run validations, use [App.AuxSaveNoValidateWithContext()]. + */ + auxSaveWithContext(ctx: context.Context, model: Model): void + /** + * AuxSaveNoValidate saves the specified model into the auxiliary app database without performing validations. + * + * If you want to also run validations before persisting, use [App.AuxSave()]. + */ + auxSaveNoValidate(model: Model): void + /** + * AuxSaveNoValidateWithContext is the same as [App.AuxSaveNoValidate()] + * but allows specifying a context to limit the db execution. + * + * If you want to also run validations before persisting, use [App.AuxSaveWithContext()]. + */ + auxSaveNoValidateWithContext(ctx: context.Context, model: Model): void + /** + * Validate triggers the OnModelValidate hook for the specified model. + */ + validate(model: Model): void + /** + * ValidateWithContext is the same as Validate but allows specifying the ModelEvent context. + */ + validateWithContext(ctx: context.Context, model: Model): void + /** + * RunInTransaction wraps fn into a transaction for the regular app database. + * + * It is safe to nest RunInTransaction calls as long as you use the callback's txApp. + */ + runInTransaction(fn: (txApp: App) => void): void + /** + * AuxRunInTransaction wraps fn into a transaction for the auxiliary app database. + * + * It is safe to nest RunInTransaction calls as long as you use the callback's txApp. + */ + auxRunInTransaction(fn: (txApp: App) => void): void + /** + * LogQuery returns a new Log select query. + */ + logQuery(): (dbx.SelectQuery) + /** + * FindLogById finds a single Log entry by its id. + */ + findLogById(id: string): (Log) + /** + * LogsStatsItem defines the total number of logs for a specific time period. + */ + logsStats(expr: dbx.Expression): Array<(LogsStatsItem | undefined)> + /** + * DeleteOldLogs delete all requests that are created before createdBefore. + */ + deleteOldLogs(createdBefore: time.Time): void + /** + * CollectionQuery returns a new Collection select query. + */ + collectionQuery(): (dbx.SelectQuery) + /** + * FindCollections finds all collections by the given type(s). + * + * If collectionTypes is not set, it returns all collections. + * + * Example: + * + * ``` + * app.FindAllCollections() // all collections + * app.FindAllCollections("auth", "view") // only auth and view collections + * ``` + */ + findAllCollections(...collectionTypes: string[]): Array<(Collection | undefined)> + /** + * ReloadCachedCollections fetches all collections and caches them into the app store. + */ + reloadCachedCollections(): void + /** + * FindCollectionByNameOrId finds a single collection by its name (case insensitive) or id.s + */ + findCollectionByNameOrId(nameOrId: string): (Collection) + /** + * FindCachedCollectionByNameOrId is similar to [App.FindCollectionByNameOrId] + * but retrieves the Collection from the app cache instead of making a db call. + * + * NB! This method is suitable for read-only Collection operations. + * + * Returns [sql.ErrNoRows] if no Collection is found for consistency + * with the [App.FindCollectionByNameOrId] method. + * + * If you plan making changes to the returned Collection model, + * use [App.FindCollectionByNameOrId] instead. + * + * Caveats: + * + * ``` + * - The returned Collection should be used only for read-only operations. + * Avoid directly modifying the returned cached Collection as it will affect + * the global cached value even if you don't persist the changes in the database! + * - If you are updating a Collection in a transaction and then call this method before commit, + * it'll return the cached Collection state and not the one from the uncommited transaction. + * - The cache is automatically updated on collections db change (create/update/delete). + * To manually reload the cache you can call [App.ReloadCachedCollections()] + * ``` + */ + findCachedCollectionByNameOrId(nameOrId: string): (Collection) + /** + * IsCollectionNameUnique checks that there is no existing collection + * with the provided name (case insensitive!). + * + * Note: case insensitive check because the name is used also as + * table name for the records. + */ + isCollectionNameUnique(name: string, ...excludeIds: string[]): boolean + /** + * FindCollectionReferences returns information for all relation + * fields referencing the provided collection. + * + * If the provided collection has reference to itself then it will be + * also included in the result. To exclude it, pass the collection id + * as the excludeIds argument. + */ + findCollectionReferences(collection: Collection, ...excludeIds: string[]): _TygojaDict + /** + * TruncateCollection deletes all records associated with the provided collection. + * + * The truncate operation is executed in a single transaction, + * aka. either everything is deleted or none. + * + * Note that this method will also trigger the records related + * cascade and file delete actions. + */ + truncateCollection(collection: Collection): void + /** + * ImportCollections imports the provided collections data in a single transaction. + * + * For existing matching collections, the imported data is unmarshaled on top of the existing model. + * + * NB! If deleteMissing is true, ALL NON-SYSTEM COLLECTIONS AND SCHEMA FIELDS, + * that are not present in the imported configuration, WILL BE DELETED + * (this includes their related records data). + */ + importCollections(toImport: Array<_TygojaDict>, deleteMissing: boolean): void + /** + * ImportCollectionsByMarshaledJSON is the same as [ImportCollections] + * but accept marshaled json array as import data (usually used for the autogenerated snapshots). + */ + importCollectionsByMarshaledJSON(rawSliceOfMaps: string|Array, deleteMissing: boolean): void + /** + * SyncRecordTableSchema compares the two provided collections + * and applies the necessary related record table changes. + * + * If oldCollection is null, then only newCollection is used to create the record table. + * + * This method is automatically invoked as part of a collection create/update/delete operation. + */ + syncRecordTableSchema(newCollection: Collection, oldCollection: Collection): void + /** + * FindAllExternalAuthsByRecord returns all ExternalAuth models + * linked to the provided auth record. + */ + findAllExternalAuthsByRecord(authRecord: Record): Array<(ExternalAuth | undefined)> + /** + * FindAllExternalAuthsByCollection returns all ExternalAuth models + * linked to the provided auth collection. + */ + findAllExternalAuthsByCollection(collection: Collection): Array<(ExternalAuth | undefined)> + /** + * FindFirstExternalAuthByExpr returns the first available (the most recent created) + * ExternalAuth model that satisfies the non-nil expression. + */ + findFirstExternalAuthByExpr(expr: dbx.Expression): (ExternalAuth) + /** + * FindAllMFAsByRecord returns all MFA models linked to the provided auth record. + */ + findAllMFAsByRecord(authRecord: Record): Array<(MFA | undefined)> + /** + * FindAllMFAsByCollection returns all MFA models linked to the provided collection. + */ + findAllMFAsByCollection(collection: Collection): Array<(MFA | undefined)> + /** + * FindMFAById retuns a single MFA model by its id. + */ + findMFAById(id: string): (MFA) + /** + * DeleteAllMFAsByRecord deletes all MFA models associated with the provided record. + * + * Returns a combined error with the failed deletes. + */ + deleteAllMFAsByRecord(authRecord: Record): void + /** + * DeleteExpiredMFAs deletes the expired MFAs for all auth collections. + */ + deleteExpiredMFAs(): void + /** + * FindAllOTPsByRecord returns all OTP models linked to the provided auth record. + */ + findAllOTPsByRecord(authRecord: Record): Array<(OTP | undefined)> + /** + * FindAllOTPsByCollection returns all OTP models linked to the provided collection. + */ + findAllOTPsByCollection(collection: Collection): Array<(OTP | undefined)> + /** + * FindOTPById retuns a single OTP model by its id. + */ + findOTPById(id: string): (OTP) + /** + * DeleteAllOTPsByRecord deletes all OTP models associated with the provided record. + * + * Returns a combined error with the failed deletes. + */ + deleteAllOTPsByRecord(authRecord: Record): void + /** + * DeleteExpiredOTPs deletes the expired OTPs for all auth collections. + */ + deleteExpiredOTPs(): void + /** + * FindAllAuthOriginsByRecord returns all AuthOrigin models linked to the provided auth record (in DESC order). + */ + findAllAuthOriginsByRecord(authRecord: Record): Array<(AuthOrigin | undefined)> + /** + * FindAllAuthOriginsByCollection returns all AuthOrigin models linked to the provided collection (in DESC order). + */ + findAllAuthOriginsByCollection(collection: Collection): Array<(AuthOrigin | undefined)> + /** + * FindAuthOriginById returns a single AuthOrigin model by its id. + */ + findAuthOriginById(id: string): (AuthOrigin) + /** + * FindAuthOriginByRecordAndFingerprint returns a single AuthOrigin model + * by its authRecord relation and fingerprint. + */ + findAuthOriginByRecordAndFingerprint(authRecord: Record, fingerprint: string): (AuthOrigin) + /** + * DeleteAllAuthOriginsByRecord deletes all AuthOrigin models associated with the provided record. + * + * Returns a combined error with the failed deletes. + */ + deleteAllAuthOriginsByRecord(authRecord: Record): void + /** + * RecordQuery returns a new Record select query from a collection model, id or name. + * + * In case a collection id or name is provided and that collection doesn't + * actually exists, the generated query will be created with a cancelled context + * and will fail once an executor (Row(), One(), All(), etc.) is called. + */ + recordQuery(collectionModelOrIdentifier: any): (dbx.SelectQuery) + /** + * FindRecordById finds the Record model by its id. + */ + findRecordById(collectionModelOrIdentifier: any, recordId: string, ...optFilters: ((q: dbx.SelectQuery) => void)[]): (Record) + /** + * FindRecordsByIds finds all records by the specified ids. + * If no records are found, returns an empty slice. + */ + findRecordsByIds(collectionModelOrIdentifier: any, recordIds: Array, ...optFilters: ((q: dbx.SelectQuery) => void)[]): Array<(Record | undefined)> + /** + * FindAllRecords finds all records matching specified db expressions. + * + * Returns all collection records if no expression is provided. + * + * Returns an empty slice if no records are found. + * + * Example: + * + * ``` + * // no extra expressions + * app.FindAllRecords("example") + * + * // with extra expressions + * expr1 := dbx.HashExp{"email": "test@example.com"} + * expr2 := dbx.NewExp("LOWER(username) = {:username}", dbx.Params{"username": "test"}) + * app.FindAllRecords("example", expr1, expr2) + * ``` + */ + findAllRecords(collectionModelOrIdentifier: any, ...exprs: dbx.Expression[]): Array<(Record | undefined)> + /** + * FindFirstRecordByData returns the first found record matching + * the provided key-value pair. + */ + findFirstRecordByData(collectionModelOrIdentifier: any, key: string, value: any): (Record) + /** + * FindRecordsByFilter returns limit number of records matching the + * provided string filter. + * + * NB! Use the last "params" argument to bind untrusted user variables! + * + * The filter argument is optional and can be empty string to target + * all available records. + * + * The sort argument is optional and can be empty string OR the same format + * used in the web APIs, ex. "-created,title". + * + * If the limit argument is <= 0, no limit is applied to the query and + * all matching records are returned. + * + * Returns an empty slice if no records are found. + * + * Example: + * + * ``` + * app.FindRecordsByFilter( + * "posts", + * "title ~ {:title} && visible = {:visible}", + * "-created", + * 10, + * 0, + * dbx.Params{"title": "lorem ipsum", "visible": true} + * ) + * ``` + */ + findRecordsByFilter(collectionModelOrIdentifier: any, filter: string, sort: string, limit: number, offset: number, ...params: dbx.Params[]): Array<(Record | undefined)> + /** + * FindFirstRecordByFilter returns the first available record matching the provided filter (if any). + * + * NB! Use the last params argument to bind untrusted user variables! + * + * Returns sql.ErrNoRows if no record is found. + * + * Example: + * + * ``` + * app.FindFirstRecordByFilter("posts", "") + * app.FindFirstRecordByFilter("posts", "slug={:slug} && status='public'", dbx.Params{"slug": "test"}) + * ``` + */ + findFirstRecordByFilter(collectionModelOrIdentifier: any, filter: string, ...params: dbx.Params[]): (Record) + /** + * CountRecords returns the total number of records in a collection. + */ + countRecords(collectionModelOrIdentifier: any, ...exprs: dbx.Expression[]): number + /** + * FindAuthRecordByToken finds the auth record associated with the provided JWT + * (auth, file, verifyEmail, changeEmail, passwordReset types). + * + * Optionally specify a list of validTypes to check tokens only from those types. + * + * Returns an error if the JWT is invalid, expired or not associated to an auth collection record. + */ + findAuthRecordByToken(token: string, ...validTypes: string[]): (Record) + /** + * FindAuthRecordByEmail finds the auth record associated with the provided email. + * + * Returns an error if it is not an auth collection or the record is not found. + */ + findAuthRecordByEmail(collectionModelOrIdentifier: any, email: string): (Record) + /** + * 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. + * + * The returned error indicate that something unexpected happened during + * the check (eg. invalid rule or db query error). + * + * The method always return false on invalid rule or db query error. + * + * Example: + * + * ``` + * requestInfo, _ := e.RequestInfo() + * record, _ := app.FindRecordById("example", "RECORD_ID") + * rule := types.Pointer("@request.auth.id != '' || status = 'public'") + * // ... or use one of the record collection's rule, eg. record.Collection().ViewRule + * + * if ok, _ := app.CanAccessRecord(record, requestInfo, rule); ok { ... } + * ``` + */ + canAccessRecord(record: Record, requestInfo: RequestInfo, accessRule: string): boolean + /** + * ExpandRecord expands the relations of a single Record model. + * + * If optFetchFunc is not set, then a default function will be used + * that returns all relation records. + * + * Returns a map with the failed expand parameters and their errors. + */ + expandRecord(record: Record, expands: Array, optFetchFunc: ExpandFetchFunc): _TygojaDict + /** + * ExpandRecords expands the relations of the provided Record models list. + * + * If optFetchFunc is not set, then a default function will be used + * that returns all relation records. + * + * Returns a map with the failed expand parameters and their errors. + */ + expandRecords(records: Array<(Record | undefined)>, expands: Array, optFetchFunc: ExpandFetchFunc): _TygojaDict + /** + * OnBootstrap hook is triggered on initializing the main application + * resources (db, app settings, etc). + */ + onBootstrap(): (hook.Hook) + /** + * OnServe hook is triggered on 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) + /** + * OnTerminate hook is triggered when the app is in the process + * of being terminated (ex. on SIGTERM signal). + */ + onTerminate(): (hook.Hook) + /** + * OnBackupCreate hook is triggered on each [App.CreateBackup] call. + */ + onBackupCreate(): (hook.Hook) + /** + * OnBackupRestore hook is triggered before app backup restore (aka. [App.RestoreBackup] call). + * + * Note that by default on success the application is restarted and the after state of the hook is ignored. + */ + onBackupRestore(): (hook.Hook) + /** + * OnModelValidate is triggered every time when a model is being validated + * (e.g. triggered by App.Validate() or App.Save()). + * + * 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. + */ + onModelValidate(...tags: string[]): (hook.TaggedHook) + /** + * OnModelCreate is triggered every time when a new model is being created + * (e.g. triggered by App.Save()). + * + * Operations BEFORE the e.Next() execute before the model validation + * and the INSERT DB statement. + * + * Operations AFTER the e.Next() execute after the model validation + * and the INSERT DB statement. + * + * Note that succesful 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 + * bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks. + * + * 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. + */ + onModelCreate(...tags: string[]): (hook.TaggedHook) + /** + * OnModelCreateExecute is triggered after successful Model validation + * and right before the model INSERT DB statement execution. + * + * Usually it is triggered as part of the App.Save() in the following firing order: + * OnModelCreate { + * ``` + * -> OnModelValidate (skipped with App.SaveNoValidate()) + * -> OnModelCreateExecute + * ``` + * } + * + * Note that succesful 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, + * you can bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks. + * + * 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. + */ + onModelCreateExecute(...tags: string[]): (hook.TaggedHook) + /** + * OnModelAfterCreateSuccess is triggered after each successful + * 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 commited. + * This hook is NOT triggered in case the transaction rollbacks + * (aka. when the model wasn't persisted). + * + * 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. + */ + onModelAfterCreateSuccess(...tags: string[]): (hook.TaggedHook) + /** + * 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: + * ``` + * - immediatelly after App.Save() failure + * - 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. + * + * 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. + */ + onModelAfterCreateError(...tags: string[]): (hook.TaggedHook) + /** + * OnModelUpdate is triggered every time when a new model is being updated + * (e.g. triggered by App.Save()). + * + * Operations BEFORE the e.Next() execute before the model validation + * and the UPDATE DB statement. + * + * Operations AFTER the e.Next() execute after the model validation + * and the UPDATE DB statement. + * + * Note that succesful 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 + * bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks. + * + * 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. + */ + onModelUpdate(...tags: string[]): (hook.TaggedHook) + /** + * OnModelUpdateExecute is triggered after successful Model validation + * and right before the model UPDATE DB statement execution. + * + * Usually it is triggered as part of the App.Save() in the following firing order: + * OnModelUpdate { + * ``` + * -> OnModelValidate (skipped with App.SaveNoValidate()) + * -> OnModelUpdateExecute + * ``` + * } + * + * Note that succesful 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, + * you can bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks. + * + * 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. + */ + onModelUpdateExecute(...tags: string[]): (hook.TaggedHook) + /** + * OnModelAfterUpdateSuccess is triggered after each successful + * 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 commited. + * This hook is NOT triggered in case the transaction rollbacks + * (aka. when the model changes weren't persisted). + * + * 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. + */ + onModelAfterUpdateSuccess(...tags: string[]): (hook.TaggedHook) + /** + * 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: + * ``` + * - immediatelly after App.Save() failure + * - 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. + * + * 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. + */ + onModelAfterUpdateError(...tags: string[]): (hook.TaggedHook) + /** + * OnModelDelete is triggered every time when a new model is being deleted + * (e.g. triggered by App.Delete()). + * + * Note that succesful 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 + * bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks. + * + * 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. + */ + onModelDelete(...tags: string[]): (hook.TaggedHook) + /** + * OnModelUpdateExecute is triggered right before the model + * DELETE DB statement execution. + * + * Usually it is triggered as part of the App.Delete() in the following firing order: + * OnModelDelete { + * ``` + * -> (internal delete checks) + * -> OnModelDeleteExecute + * ``` + * } + * + * Note that succesful 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 + * bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks. + * + * 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. + */ + onModelDeleteExecute(...tags: string[]): (hook.TaggedHook) + /** + * OnModelAfterDeleteSuccess is triggered after each successful + * 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 commited. + * This hook is NOT triggered in case the transaction rollbacks + * (aka. when the model delete wasn't persisted). + * + * 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. + */ + onModelAfterDeleteSuccess(...tags: string[]): (hook.TaggedHook) + /** + * 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: + * ``` + * - immediatelly after App.Delete() failure + * - 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. + * + * 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. + */ + onModelAfterDeleteError(...tags: string[]): (hook.TaggedHook) + /** + * 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). + * + * It could be used for example to redact/hide or add computed temp + * Record model props only for the specific request info. For example: + * + * app.OnRecordEnrich("posts").BindFunc(func(e core.*RecordEnrichEvent) { + * ``` + * // hide one or more fields + * e.Record.Hide("role") + * + * // add new custom field for registered users + * if e.RequestInfo.Auth != nil && e.RequestInfo.Auth.Collection().Name == "users" { + * e.Record.WithCustomData(true) // for security requires explicitly allowing it + * e.Record.Set("computedScore", e.Record.GetInt("score") * e.RequestInfo.Auth.GetInt("baseScore")) + * } + * + * return e.Next() + * ``` + * }) + * + * 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. + */ + onRecordEnrich(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordValidate is a proxy Record model hook for [OnModelValidate]. + * + * 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. + */ + onRecordValidate(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordCreate is a proxy Record model hook for [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) + /** + * OnRecordCreateExecute is a proxy Record model hook for [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) + /** + * OnRecordAfterCreateSuccess is a proxy Record model hook for [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) + /** + * OnRecordAfterCreateError is a proxy Record model hook for [OnModelAfterCreateError]. + * + * 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. + */ + onRecordAfterCreateError(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordUpdate is a proxy Record model hook for [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) + /** + * OnRecordUpdateExecute is a proxy Record model hook for [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) + /** + * OnRecordAfterUpdateSuccess is a proxy Record model hook for [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) + /** + * OnRecordAfterUpdateError is a proxy Record model hook for [OnModelAfterUpdateError]. + * + * 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. + */ + onRecordAfterUpdateError(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordDelete is a proxy Record model hook for [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) + /** + * OnRecordDeleteExecute is a proxy Record model hook for [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) + /** + * OnRecordAfterDeleteSuccess is a proxy Record model hook for [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) + /** + * OnRecordAfterDeleteError is a proxy Record model hook for [OnModelAfterDeleteError]. + * + * 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. + */ + onRecordAfterDeleteError(...tags: string[]): (hook.TaggedHook) + /** + * OnCollectionValidate is a proxy Collection model hook for [OnModelValidate]. + * + * 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. + */ + onCollectionValidate(...tags: string[]): (hook.TaggedHook) + /** + * OnCollectionCreate is a proxy Collection model hook for [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) + /** + * OnCollectionCreateExecute is a proxy Collection model hook for [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) + /** + * OnCollectionAfterCreateSuccess is a proxy Collection model hook for [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) + /** + * OnCollectionAfterCreateError is a proxy Collection model hook for [OnModelAfterCreateError]. + * + * 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. + */ + onCollectionAfterCreateError(...tags: string[]): (hook.TaggedHook) + /** + * OnCollectionUpdate is a proxy Collection model hook for [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) + /** + * OnCollectionUpdateExecute is a proxy Collection model hook for [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) + /** + * OnCollectionAfterUpdateSuccess is a proxy Collection model hook for [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) + /** + * OnCollectionAfterUpdateError is a proxy Collection model hook for [OnModelAfterUpdateError]. + * + * 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. + */ + onCollectionAfterUpdateError(...tags: string[]): (hook.TaggedHook) + /** + * OnCollectionDelete is a proxy Collection model hook for [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) + /** + * OnCollectionDeleteExecute is a proxy Collection model hook for [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) + /** + * OnCollectionAfterDeleteSuccess is a proxy Collection model hook for [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) + /** + * OnCollectionAfterDeleteError is a proxy Collection model hook for [OnModelAfterDeleteError]. + * + * 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. + */ + onCollectionAfterDeleteError(...tags: string[]): (hook.TaggedHook) + /** + * OnMailerSend hook is triggered every time when a new email is + * being send using the App.NewMailClient() instance. + * + * It allows intercepting the email message or to use a custom mailer client. + */ + onMailerSend(): (hook.Hook) + /** + * OnMailerRecordAuthAlertSend hook is triggered when + * sending a new device login auth alert email, allowing you to + * intercept and customize the email message that is being sent. + * + * 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. + */ + onMailerRecordAuthAlertSend(...tags: string[]): (hook.TaggedHook) + /** + * OnMailerBeforeRecordResetPasswordSend hook is triggered when + * sending a password reset email to an auth record, allowing + * you to intercept and customize the email message that is being sent. + * + * 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. + */ + onMailerRecordPasswordResetSend(...tags: string[]): (hook.TaggedHook) + /** + * OnMailerBeforeRecordVerificationSend hook is triggered when + * sending a verification email to an auth record, allowing + * you to intercept and customize the email message that is being sent. + * + * 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. + */ + onMailerRecordVerificationSend(...tags: string[]): (hook.TaggedHook) + /** + * OnMailerRecordEmailChangeSend hook is triggered when sending a + * confirmation new address email to an auth record, allowing + * you to intercept and customize the email message that is being sent. + * + * 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. + */ + onMailerRecordEmailChangeSend(...tags: string[]): (hook.TaggedHook) + /** + * OnMailerRecordOTPSend hook is triggered when sending an OTP email + * to an auth record, allowing you to intercept and customize the + * email message that is being sent. + * + * 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. + */ + onMailerRecordOTPSend(...tags: string[]): (hook.TaggedHook) + /** + * OnRealtimeConnectRequest hook is triggered when establishing the SSE client connection. + * + * Any execution after [e.Next()] of a hook handler happens after the client disconnects. + */ + onRealtimeConnectRequest(): (hook.Hook) + /** + * OnRealtimeMessageSend hook is triggered when sending an SSE message to a client. + */ + onRealtimeMessageSend(): (hook.Hook) + /** + * OnRealtimeSubscribeRequest hook is triggered when updating the + * client subscriptions, allowing you to further validate and + * modify the submitted change. + */ + onRealtimeSubscribeRequest(): (hook.Hook) + /** + * OnSettingsListRequest hook is triggered on each API Settings list request. + * + * Could be used to validate or modify the response before returning it to the client. + */ + onSettingsListRequest(): (hook.Hook) + /** + * OnSettingsUpdateRequest hook is triggered on each API Settings update request. + * + * Could be used to additionally validate the request data or + * implement completely different persistence behavior. + */ + onSettingsUpdateRequest(): (hook.Hook) + /** + * 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. + */ + onSettingsReload(): (hook.Hook) + /** + * OnFileDownloadRequest hook is triggered before each API File download request. + * + * Could be used to validate or modify the file response before + * returning it to the client. + */ + onFileDownloadRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnFileBeforeTokenRequest hook is triggered on each file token API request. + */ + onFileTokenRequest(): (hook.Hook) + /** + * OnRecordAuthRequest hook is triggered on each successful API + * record authentication request (sign-in, token refresh, etc.). + * + * Could be used to additionally validate or modify the authenticated + * record data and token. + * + * 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. + */ + onRecordAuthRequest(...tags: string[]): (hook.TaggedHook) + /** + * 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]). + * + * 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. + */ + onRecordAuthWithPasswordRequest(...tags: string[]): (hook.TaggedHook) + /** + * 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 + * request will try to create a new auth Record. + * + * To assign or link a different existing record model you can + * change the [RecordAuthWithOAuth2RequestEvent.Record] field. + * + * 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. + */ + onRecordAuthWithOAuth2Request(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordAuthRefreshRequest hook is triggered on each Record + * auth refresh API request (right before generating a new auth token). + * + * Could be used to additionally validate the request data or implement + * completely different auth refresh behavior. + * + * 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. + */ + onRecordAuthRefreshRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordRequestPasswordResetRequest hook is triggered on + * each Record request password reset API request. + * + * Could be used to additionally validate the request data or implement + * completely different password reset behavior. + * + * 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. + */ + onRecordRequestPasswordResetRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordConfirmPasswordResetRequest hook is triggered on + * each Record confirm password reset API request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + * + * 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. + */ + onRecordConfirmPasswordResetRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordRequestVerificationRequest hook is triggered on + * each Record request verification API request. + * + * Could be used to additionally validate the loaded request data or implement + * completely different verification behavior. + * + * 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. + */ + onRecordRequestVerificationRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordConfirmVerificationRequest hook is triggered on each + * Record confirm verification API request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + * + * 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. + */ + onRecordConfirmVerificationRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordRequestEmailChangeRequest hook is triggered on each + * Record request email change API request. + * + * Could be used to additionally validate the request data or implement + * completely different request email change behavior. + * + * 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. + */ + onRecordRequestEmailChangeRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordConfirmEmailChangeRequest hook is triggered on each + * Record confirm email change API request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + * + * 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. + */ + onRecordConfirmEmailChangeRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordRequestOTPRequest hook is triggered on each Record + * request OTP 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. + */ + onRecordRequestOTPRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordAuthWithOTPRequest hook is triggered on each Record + * auth with OTP 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. + */ + onRecordAuthWithOTPRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordsListRequest hook is triggered on each API Records list request. + * + * Could be used to validate or modify the response before returning it to the client. + * + * 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. + */ + onRecordsListRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordViewRequest hook is triggered on each API Record view request. + * + * Could be used to validate or modify the response before returning it to the client. + * + * 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. + */ + onRecordViewRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordCreateRequest hook is triggered on each API Record create request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + * + * 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. + */ + onRecordCreateRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordUpdateRequest hook is triggered on each API Record update request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + * + * 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. + */ + onRecordUpdateRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnRecordDeleteRequest hook is triggered on each API Record delete request. + * + * Could be used to additionally validate the request data or implement + * completely different delete behavior. + * + * 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. + */ + onRecordDeleteRequest(...tags: string[]): (hook.TaggedHook) + /** + * OnCollectionsListRequest hook is triggered on each API Collections list request. + * + * Could be used to validate or modify the response before returning it to the client. + */ + onCollectionsListRequest(): (hook.Hook) + /** + * OnCollectionViewRequest hook is triggered on each API Collection view request. + * + * Could be used to validate or modify the response before returning it to the client. + */ + onCollectionViewRequest(): (hook.Hook) + /** + * OnCollectionCreateRequest hook is triggered on each API Collection create request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + */ + onCollectionCreateRequest(): (hook.Hook) + /** + * OnCollectionUpdateRequest hook is triggered on each API Collection update request. + * + * Could be used to additionally validate the request data or implement + * completely different persistence behavior. + */ + onCollectionUpdateRequest(): (hook.Hook) + /** + * OnCollectionDeleteRequest hook is triggered on each API Collection delete request. + * + * Could be used to additionally validate the request data or implement + * completely different delete behavior. + */ + onCollectionDeleteRequest(): (hook.Hook) + /** + * OnCollectionsBeforeImportRequest hook is triggered on each API + * collections import request. + * + * Could be used to additionally validate the imported collections or + * to implement completely different import behavior. + */ + onCollectionsImportRequest(): (hook.Hook) + /** + * OnBatchRequest hook is triggered on each API batch request. + * + * Could be used to additionally validate or modify the submitted batch requests. + */ + onBatchRequest(): (hook.Hook) } // @ts-ignore import validation = ozzo_validation - type _subUyFbk = BaseModel - interface Collection extends _subUyFbk { - name: string - type: string - system: boolean - schema: schema.Schema - indexes: types.JsonArray + /** + * DBConnectFunc defines a database connection initialization function. + */ + interface DBConnectFunc {(dbPath: string): (dbx.DB) } + /** + * RequestEvent defines the PocketBase router handler event. + */ + type _subwWwMU = router.Event + interface RequestEvent extends _subwWwMU { + app: App + auth?: Record + } + interface RequestEvent { /** - * rules + * RealIP returns the "real" IP address from the configured trusted proxy headers. + * + * If Settings.TrustedProxy is not configured or the found IP is empty, + * it fallbacks to e.RemoteIP(). + * + * NB! + * Be careful when used in a security critical context as it relies on + * the trusted proxy to be properly configured and your app to be accessible only through it. + * If you are not sure, use e.RemoteIP(). */ - listRule?: string - viewRule?: string - createRule?: string - updateRule?: string - deleteRule?: string - options: types.JsonMap + realIP(): string } - interface Collection { + interface RequestEvent { /** - * TableName returns the Collection model SQL table name. + * HasSuperuserAuth checks whether the current RequestEvent has superuser authentication loaded. */ - tableName(): string + hasSuperuserAuth(): boolean } - interface Collection { + interface RequestEvent { /** - * BaseFilesPath returns the storage dir path used by the collection. + * RequestInfo parses the current request into RequestInfo instance. + * + * Note that the returned result is cached to avoid copying the request data multiple times + * but the auth state and other common store items are always refreshed in case they were changed my another handler. */ - baseFilesPath(): string + requestInfo(): (RequestInfo) } - interface Collection { + interface InternalRequest { /** - * IsBase checks if the current collection has "base" type. + * note: for uploading files the value must be either *filesystem.File or []*filesystem.File */ - isBase(): boolean + body: _TygojaDict + headers: _TygojaDict + method: string + url: string } - interface Collection { - /** - * IsAuth checks if the current collection has "auth" type. - */ - isAuth(): boolean + interface InternalRequest { + validate(): void } - interface Collection { - /** - * IsView checks if the current collection has "view" type. - */ - isView(): boolean - } - interface Collection { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface Collection { - /** - * BaseOptions decodes the current collection options and returns them - * as new [CollectionBaseOptions] instance. - */ - baseOptions(): CollectionBaseOptions - } - interface Collection { - /** - * AuthOptions decodes the current collection options and returns them - * as new [CollectionAuthOptions] instance. - */ - authOptions(): CollectionAuthOptions - } - interface Collection { - /** - * ViewOptions decodes the current collection options and returns them - * as new [CollectionViewOptions] instance. - */ - viewOptions(): CollectionViewOptions - } - interface Collection { - /** - * NormalizeOptions updates the current collection options with a - * new normalized state based on the collection type. - */ - normalizeOptions(): void - } - interface Collection { - /** - * DecodeOptions decodes the current collection options into the - * provided "result" (must be a pointer). - */ - decodeOptions(result: any): void - } - interface Collection { - /** - * SetOptions normalizes and unmarshals the specified options into m.Options. - */ - setOptions(typedOptions: any): void - } - type _subeDKbD = BaseModel - interface ExternalAuth extends _subeDKbD { - collectionId: string - recordId: string - provider: string - providerId: string - } - interface ExternalAuth { - tableName(): string - } - type _subkdarp = BaseModel - interface Record extends _subkdarp { + type _subxDBbH = BaseModel + interface Record extends _subxDBbH { } interface Record { /** - * TableName returns the table name associated to the current Record model. - */ - tableName(): string - } - interface Record { - /** - * Collection returns the Collection model associated to the current Record model. + * Collection returns the Collection model associated with the current Record model. + * + * NB! The returned collection is only for read purposes and it shouldn't be modified + * because it could have unintended side-effects on other Record models from the same collection. */ collection(): (Collection) } interface Record { /** - * OriginalCopy returns a copy of the current record model populated - * with its ORIGINAL data state (aka. the initially loaded) and - * everything else reset to the defaults. + * TableName returns the table name associated with the current Record model. */ - originalCopy(): (Record) + tableName(): string } interface Record { /** - * CleanCopy returns a copy of the current record model populated only - * with its LATEST data state and everything else reset to the defaults. + * PostScan implements the [dbx.PostScanner] interface. + * + * It essentially refreshes/updates the current Record original state + * as if the model was fetched from the databases for the first time. + * + * Or in other words, it means that m.Original().FieldsData() will have + * the same values as m.Record().FieldsData(). */ - cleanCopy(): (Record) + postScan(): void } interface Record { /** - * Expand returns a shallow copy of the current Record model expand data. + * HookTags returns the hook tags associated with the current record. + */ + hookTags(): Array + } + interface Record { + /** + * BaseFilesPath returns the storage dir path used by the record. + */ + baseFilesPath(): string + } + interface Record { + /** + * Original returns a shallow copy of the current record model populated + * with its ORIGINAL db data state (aka. right after PostScan()) + * and everything else reset to the defaults. + * + * If record was created using NewRecord() the original will be always + * a blank record (until PostScan() is invoked). + */ + original(): (Record) + } + interface Record { + /** + * Fresh returns a shallow copy of the current record model populated + * with its LATEST data state and everything else reset to the defaults + * (aka. no expand, no unknown fields and with default visibility flags). + */ + fresh(): (Record) + } + interface Record { + /** + * Clone returns a shallow copy of the current record model with all of + * its collection and unknown fields data, expand and flags copied. + * + * use [Record.Fresh()] instead if you want a copy with only the latest + * collection fields data and everything else reset to the defaults. + */ + clone(): (Record) + } + interface Record { + /** + * Expand returns a shallow copy of the current Record model expand data (if any). */ expand(): _TygojaDict } interface Record { /** - * SetExpand shallow copies the provided data to the current Record model's expand. + * SetExpand replaces the current Record's expand with the provided expand arg data (shallow copied). */ setExpand(expand: _TygojaDict): void } @@ -10820,46 +12267,88 @@ namespace models { } interface Record { /** - * SchemaData returns a shallow copy ONLY of the defined record schema fields data. + * FieldsData returns a shallow copy ONLY of the collection's fields record's data. */ - schemaData(): _TygojaDict + fieldsData(): _TygojaDict } interface Record { /** - * UnknownData returns a shallow copy ONLY of the unknown record fields data, - * aka. fields that are neither one of the base and special system ones, - * nor defined by the collection schema. + * CustomData returns a shallow copy ONLY of the custom record fields data, + * aka. fields that are neither defined by the collection, nor special system ones. + * + * Note that custom fields prefixed with "@pbInternal" are always skipped. */ - unknownData(): _TygojaDict + customData(): _TygojaDict + } + interface Record { + /** + * WithCustomData toggles the export/serialization of custom data fields + * (false by default). + */ + withCustomData(state: boolean): (Record) } interface Record { /** * IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check. */ - ignoreEmailVisibility(state: boolean): void + ignoreEmailVisibility(state: boolean): (Record) } interface Record { /** - * WithUnknownData toggles the export/serialization of unknown data fields - * (false by default). + * IgnoreUnchangedFields toggles the flag to ignore the unchanged fields + * from the DB export for the UPDATE SQL query. + * + * This could be used if you want to save only the record fields that you've changed + * without overwrite other untouched fields in case of concurrent update. */ - withUnknownData(state: boolean): void + ignoreUnchangedFields(state: boolean): (Record) } interface Record { /** - * Set sets the provided key-value data pair for the current Record model. + * Set sets the provided key-value data pair into the current Record + * model directly as it is WITHOUT NORMALIZATIONS. + * + * See also [Record.Set]. + */ + setRaw(key: string, value: any): void + } + interface Record { + /** + * SetIfFieldExists sets the provided key-value data pair into the current Record model + * ONLY if key is existing Collection field name/modifier. + * + * This method does nothing if key is not a known Collection field name/modifier. + * + * On success returns the matched Field, otherwise - nil. + * + * To set any key-value, including custom/unknown fields, use the [Record.Set] method. + */ + setIfFieldExists(key: string, value: any): Field + } + interface Record { + /** + * Set sets the provided key-value data pair into the current Record model. * * If the record collection has field with name matching the provided "key", - * the value will be further normalized according to the field rules. + * the value will be further normalized according to the field setter(s). */ set(key: string, value: any): void } + interface Record { + getRaw(key: string): any + } interface Record { /** * Get returns a normalized single record model data value for "key". */ get(key: string): any } + interface Record { + /** + * Load bulk loads the provided data into the current Record model. + */ + load(data: _TygojaDict): void + } interface Record { /** * GetBool returns the data value for "key" as a bool. @@ -10884,12 +12373,6 @@ namespace models { */ getFloat(key: string): number } - interface Record { - /** - * GetTime returns the data value for "key" as a [time.Time] instance. - */ - getTime(key: string): time.Time - } interface Record { /** * GetDateTime returns the data value for "key" as a DateTime instance. @@ -10898,10 +12381,43 @@ namespace models { } interface Record { /** - * GetStringSlice returns the data value for "key" as a slice of unique strings. + * GetStringSlice returns the data value for "key" as a slice of non-zero unique strings. */ getStringSlice(key: string): Array } + interface Record { + /** + * GetUploadedFiles returns the uploaded files for the provided "file" field key, + * (aka. the current [*filesytem.File] values) so that you can apply further + * validations or modifications (including changing the file name or content before persisting). + * + * Example: + * + * ``` + * files := record.GetUploadedFiles("documents") + * for _, f := range files { + * f.Name = "doc_" + f.Name // add a prefix to each file name + * } + * app.Save(record) // the files are pointers so the applied changes will transparently reflect on the record value + * ``` + */ + getUploadedFiles(key: string): Array<(filesystem.File | undefined)> + } + interface Record { + /** + * Retrieves the "key" json field value and unmarshals it into "result". + * + * Example + * + * ``` + * result := struct { + * FirstName string `json:"first_name"` + * }{} + * err := m.UnmarshalJSONField("my_field_name", &result) + * ``` + */ + unmarshalJSONField(key: string, result: any): void + } interface Record { /** * ExpandedOne retrieves a single relation Record from the already @@ -10926,52 +12442,41 @@ namespace models { */ expandedAll(relField: string): Array<(Record | undefined)> } - interface Record { - /** - * Retrieves the "key" json field value and unmarshals it into "result". - * - * Example - * - * ``` - * result := struct { - * FirstName string `json:"first_name"` - * }{} - * err := m.UnmarshalJSONField("my_field_name", &result) - * ``` - */ - unmarshalJSONField(key: string, result: any): void - } - interface Record { - /** - * BaseFilesPath returns the storage dir path used by the record. - */ - baseFilesPath(): string - } interface Record { /** * FindFileFieldByFile returns the first file type field for which * any of the record's data contains the provided filename. */ - findFileFieldByFile(filename: string): (schema.SchemaField) + findFileFieldByFile(filename: string): (FileField) } interface Record { /** - * Load bulk loads the provided data into the current Record model. + * DBExport implements the [DBExporter] interface and returns a key-value + * map with the data to be persisted when saving the Record in the database. */ - load(data: _TygojaDict): void + dbExport(app: App): _TygojaDict } interface Record { /** - * ColumnValueMap implements [ColumnValueMapper] interface. + * Hide hides the specified fields from the public safe serialization of the record. */ - columnValueMap(): _TygojaDict + hide(...fieldNames: string[]): (Record) + } + interface Record { + /** + * Unhide forces to unhide the specified fields from the public safe serialization + * of the record (even when the collection field itself is marked as hidden). + */ + unhide(...fieldNames: string[]): (Record) } interface Record { /** * PublicExport exports only the record fields that are safe to be public. * + * To export unknown data fields you need to set record.WithCustomData(true). + * * For auth records, to force the export of the email field you need to set - * `m.IgnoreEmailVisibility(true)`. + * record.IgnoreEmailVisibility(true). */ publicExport(): _TygojaDict } @@ -10991,753 +12496,143 @@ namespace models { } interface Record { /** - * ReplaceModifers returns a new map with applied modifier + * ReplaceModifiers returns a new map with applied modifier * values based on the current record and the specified data. * * The resolved modifier keys will be removed. * * Multiple modifiers will be applied one after another, - * while reusing the previous base key value result (eg. 1; -5; +2 => -2). + * while reusing the previous base key value result (ex. 1; -5; +2 => -2). + * + * Note that because Go doesn't guaranteed the iteration order of maps, + * we would explicitly apply shorter keys first for a more consistent and reproducible behavior. * * Example usage: * * ``` - * newData := record.ReplaceModifers(data) - * // record: {"field": 10} - * // data: {"field+": 5} - * // newData: {"field": 15} + * newData := record.ReplaceModifiers(data) + * // record: {"field": 10} + * // data: {"field+": 5} + * // result: {"field": 15} * ``` */ - replaceModifers(data: _TygojaDict): _TygojaDict + replaceModifiers(data: _TygojaDict): _TygojaDict } interface Record { /** - * Username returns the "username" auth record data value. - */ - username(): string - } - interface Record { - /** - * SetUsername sets the "username" auth record data value. - * - * This method doesn't check whether the provided value is a valid username. - * - * Returns an error if the record is not from an auth collection. - */ - setUsername(username: string): void - } - interface Record { - /** - * Email returns the "email" auth record data value. + * Email returns the "email" record field value (usually available with Auth collections). */ email(): string } interface Record { /** - * SetEmail sets the "email" auth record data value. - * - * This method doesn't check whether the provided value is a valid email. - * - * Returns an error if the record is not from an auth collection. + * SetEmail sets the "email" record field value (usually available with Auth collections). */ setEmail(email: string): void } interface Record { /** - * Verified returns the "emailVisibility" auth record data value. + * Verified returns the "emailVisibility" record field value (usually available with Auth collections). */ emailVisibility(): boolean } interface Record { /** - * SetEmailVisibility sets the "emailVisibility" auth record data value. - * - * Returns an error if the record is not from an auth collection. + * SetEmailVisibility sets the "emailVisibility" record field value (usually available with Auth collections). */ setEmailVisibility(visible: boolean): void } interface Record { /** - * Verified returns the "verified" auth record data value. + * Verified returns the "verified" record field value (usually available with Auth collections). */ verified(): boolean } interface Record { /** - * SetVerified sets the "verified" auth record data value. - * - * Returns an error if the record is not from an auth collection. + * SetVerified sets the "verified" record field value (usually available with Auth collections). */ setVerified(verified: boolean): void } interface Record { /** - * TokenKey returns the "tokenKey" auth record data value. + * TokenKey returns the "tokenKey" record field value (usually available with Auth collections). */ tokenKey(): string } interface Record { /** - * SetTokenKey sets the "tokenKey" auth record data value. - * - * Returns an error if the record is not from an auth collection. + * SetTokenKey sets the "tokenKey" record field value (usually available with Auth collections). */ setTokenKey(key: string): void } interface Record { /** - * RefreshTokenKey generates and sets new random auth record "tokenKey". - * - * Returns an error if the record is not from an auth collection. + * RefreshTokenKey generates and sets a new random auth record "tokenKey". */ refreshTokenKey(): void } interface Record { /** - * LastResetSentAt returns the "lastResentSentAt" auth record data value. + * SetPassword sets the "password" record field value (usually available with Auth collections). */ - lastResetSentAt(): types.DateTime + setPassword(password: string): void } interface Record { /** - * SetLastResetSentAt sets the "lastResentSentAt" auth record data value. + * ValidatePassword validates a plain password against the "password" record field. * - * Returns an error if the record is not from an auth collection. - */ - setLastResetSentAt(dateTime: types.DateTime): void - } - interface Record { - /** - * LastVerificationSentAt returns the "lastVerificationSentAt" auth record data value. - */ - lastVerificationSentAt(): types.DateTime - } - interface Record { - /** - * SetLastVerificationSentAt sets an "lastVerificationSentAt" auth record data value. - * - * Returns an error if the record is not from an auth collection. - */ - setLastVerificationSentAt(dateTime: types.DateTime): void - } - interface Record { - /** - * PasswordHash returns the "passwordHash" auth record data value. - */ - passwordHash(): string - } - interface Record { - /** - * ValidatePassword validates a plain password against the auth record password. - * - * Returns false if the password is incorrect or record is not from an auth collection. + * Returns false if the password is incorrect. */ validatePassword(password: string): boolean } interface Record { /** - * SetPassword sets cryptographically secure string to the auth record "password" field. - * This method also resets the "lastResetSentAt" and the "tokenKey" fields. + * IsSuperuser returns whether the current record is a superuser, aka. + * whether the record is from the _superusers collection. + */ + isSuperuser(): boolean + } + interface Record { + /** + * NewStaticAuthToken generates and returns a new static record authentication token. * - * Returns an error if the record is not from an auth collection or - * an empty password is provided. - */ - setPassword(password: string): void - } - /** - * RequestInfo defines a HTTP request data struct, usually used - * as part of the `@request.*` filter resolver. - */ - interface RequestInfo { - context: string - query: _TygojaDict - data: _TygojaDict - headers: _TygojaDict - authRecord?: Record - admin?: Admin - method: string - } - interface RequestInfo { - /** - * HasModifierDataKeys loosely checks if the current struct has any modifier Data keys. - */ - hasModifierDataKeys(): boolean - } -} - -/** - * Package echo implements high performance, minimalist Go web framework. - * - * Example: - * - * ``` - * package main - * - * import ( - * "github.com/labstack/echo/v5" - * "github.com/labstack/echo/v5/middleware" - * "log" - * "net/http" - * ) - * - * // Handler - * func hello(c echo.Context) error { - * return c.String(http.StatusOK, "Hello, World!") - * } - * - * func main() { - * // Echo instance - * e := echo.New() - * - * // Middleware - * e.Use(middleware.Logger()) - * e.Use(middleware.Recover()) - * - * // Routes - * e.GET("/", hello) - * - * // Start server - * if err := e.Start(":8080"); err != http.ErrServerClosed { - * log.Fatal(err) - * } - * } - * ``` - * - * Learn more at https://echo.labstack.com - */ -namespace echo { - /** - * Context represents the context of the current HTTP request. It holds request and - * response objects, path, path parameters, data and registered handler. - */ - interface Context { - [key:string]: any; - /** - * Request returns `*http.Request`. - */ - request(): (http.Request) - /** - * SetRequest sets `*http.Request`. - */ - setRequest(r: http.Request): void - /** - * SetResponse sets `*Response`. - */ - setResponse(r: Response): void - /** - * Response returns `*Response`. - */ - response(): (Response) - /** - * IsTLS returns true if HTTP connection is TLS otherwise false. - */ - isTLS(): boolean - /** - * IsWebSocket returns true if HTTP connection is WebSocket otherwise false. - */ - isWebSocket(): boolean - /** - * Scheme returns the HTTP protocol scheme, `http` or `https`. - */ - scheme(): string - /** - * RealIP returns the client's network address based on `X-Forwarded-For` - * or `X-Real-IP` request header. - * The behavior can be configured using `Echo#IPExtractor`. - */ - realIP(): string - /** - * RouteInfo returns current request route information. Method, Path, Name and params if they exist for matched route. - * In case of 404 (route not found) and 405 (method not allowed) RouteInfo returns generic struct for these cases. - */ - routeInfo(): RouteInfo - /** - * Path returns the registered path for the handler. - */ - path(): string - /** - * PathParam returns path parameter by name. - */ - pathParam(name: string): string - /** - * PathParamDefault returns the path parameter or default value for the provided name. + * Static auth tokens are similar to the regular auth tokens, but are + * non-refreshable and support custom duration. * - * Notes for DefaultRouter implementation: - * Path parameter could be empty for cases like that: - * * route `/release-:version/bin` and request URL is `/release-/bin` - * * route `/api/:version/image.jpg` and request URL is `/api//image.jpg` - * but not when path parameter is last part of route path - * * route `/download/file.:ext` will not match request `/download/file.` + * Zero or negative duration will fallback to the duration from the auth collection settings. */ - pathParamDefault(name: string, defaultValue: string): string - /** - * PathParams returns path parameter values. - */ - pathParams(): PathParams - /** - * SetPathParams sets path parameters for current request. - */ - setPathParams(params: PathParams): void - /** - * QueryParam returns the query param for the provided name. - */ - queryParam(name: string): string - /** - * QueryParamDefault returns the query param or default value for the provided name. - */ - queryParamDefault(name: string, defaultValue: string): string - /** - * QueryParams returns the query parameters as `url.Values`. - */ - queryParams(): url.Values - /** - * QueryString returns the URL query string. - */ - queryString(): string - /** - * FormValue returns the form field value for the provided name. - */ - formValue(name: string): string - /** - * FormValueDefault returns the form field value or default value for the provided name. - */ - formValueDefault(name: string, defaultValue: string): string - /** - * FormValues returns the form field values as `url.Values`. - */ - formValues(): url.Values - /** - * FormFile returns the multipart form file for the provided name. - */ - formFile(name: string): (multipart.FileHeader) - /** - * MultipartForm returns the multipart form. - */ - multipartForm(): (multipart.Form) - /** - * Cookie returns the named cookie provided in the request. - */ - cookie(name: string): (http.Cookie) - /** - * SetCookie adds a `Set-Cookie` header in HTTP response. - */ - setCookie(cookie: http.Cookie): void - /** - * Cookies returns the HTTP cookies sent with the request. - */ - cookies(): Array<(http.Cookie | undefined)> - /** - * Get retrieves data from the context. - */ - get(key: string): { + newStaticAuthToken(duration: time.Duration): string } + interface Record { /** - * Set saves data in the context. + * NewAuthToken generates and returns a new record authentication token. */ - set(key: string, val: { - }): void - /** - * Bind binds path params, query params and the request body into provided type `i`. The default binder - * binds body based on Content-Type header. - */ - bind(i: { - }): void - /** - * Validate validates provided `i`. It is usually called after `Context#Bind()`. - * Validator must be registered using `Echo#Validator`. - */ - validate(i: { - }): void - /** - * Render renders a template with data and sends a text/html response with status - * code. Renderer must be registered using `Echo.Renderer`. - */ - render(code: number, name: string, data: { - }): void - /** - * HTML sends an HTTP response with status code. - */ - html(code: number, html: string): void - /** - * HTMLBlob sends an HTTP blob response with status code. - */ - htmlBlob(code: number, b: string|Array): void - /** - * String sends a string response with status code. - */ - string(code: number, s: string): void - /** - * JSON sends a JSON response with status code. - */ - json(code: number, i: { - }): void - /** - * JSONPretty sends a pretty-print JSON with status code. - */ - jsonPretty(code: number, i: { - }, indent: string): void - /** - * JSONBlob sends a JSON blob response with status code. - */ - jsonBlob(code: number, b: string|Array): void - /** - * JSONP sends a JSONP response with status code. It uses `callback` to construct - * the JSONP payload. - */ - jsonp(code: number, callback: string, i: { - }): void - /** - * JSONPBlob sends a JSONP blob response with status code. It uses `callback` - * to construct the JSONP payload. - */ - jsonpBlob(code: number, callback: string, b: string|Array): void - /** - * XML sends an XML response with status code. - */ - xml(code: number, i: { - }): void - /** - * XMLPretty sends a pretty-print XML with status code. - */ - xmlPretty(code: number, i: { - }, indent: string): void - /** - * XMLBlob sends an XML blob response with status code. - */ - xmlBlob(code: number, b: string|Array): void - /** - * Blob sends a blob response with status code and content type. - */ - blob(code: number, contentType: string, b: string|Array): void - /** - * Stream sends a streaming response with status code and content type. - */ - stream(code: number, contentType: string, r: io.Reader): void - /** - * File sends a response with the content of the file. - */ - file(file: string): void - /** - * FileFS sends a response with the content of the file from given filesystem. - */ - fileFS(file: string, filesystem: fs.FS): void - /** - * Attachment sends a response as attachment, prompting client to save the - * file. - */ - attachment(file: string, name: string): void - /** - * Inline sends a response as inline, opening the file in the browser. - */ - inline(file: string, name: string): void - /** - * NoContent sends a response with no body and a status code. - */ - noContent(code: number): void - /** - * Redirect redirects the request to a provided URL with status code. - */ - redirect(code: number, url: string): void - /** - * Error invokes the registered global HTTP error handler. Generally used by middleware. - * A side-effect of calling global error handler is that now Response has been committed (sent to the client) and - * middlewares up in chain can not change Response status code or Response body anymore. - * - * Avoid using this method in handlers as no middleware will be able to effectively handle errors after that. - * Instead of calling this method in handler return your error and let it be handled by middlewares or global error handler. - */ - error(err: Error): void - /** - * Echo returns the `Echo` instance. - * - * WARNING: Remember that Echo public fields and methods are coroutine safe ONLY when you are NOT mutating them - * anywhere in your code after Echo server has started. - */ - echo(): (Echo) + newAuthToken(): string } - // @ts-ignore - import stdContext = context - /** - * Echo is the top-level framework instance. - * - * Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these - * fields from handlers/middlewares and changing field values at the same time leads to data-races. - * Same rule applies to adding new routes after server has been started - Adding a route is not Goroutine safe action. - */ - interface Echo { + interface Record { /** - * NewContextFunc allows using custom context implementations, instead of default *echo.context + * NewVerificationToken generates and returns a new record verification token. */ - newContextFunc: (e: Echo, pathParamAllocSize: number) => ServableContext - debug: boolean - httpErrorHandler: HTTPErrorHandler - binder: Binder - jsonSerializer: JSONSerializer - validator: Validator - renderer: Renderer - logger: Logger - ipExtractor: IPExtractor - /** - * Filesystem is file system used by Static and File handlers to access files. - * Defaults to os.DirFS(".") - * - * When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary - * prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths - * including `assets/images` as their prefix. - */ - filesystem: fs.FS - /** - * OnAddRoute is called when Echo adds new route to specific host router. Handler is called for every router - * and before route is added to the host router. - */ - onAddRoute: (host: string, route: Routable) => void + newVerificationToken(): string } - /** - * HandlerFunc defines a function to serve HTTP requests. - */ - interface HandlerFunc {(c: Context): void } - /** - * MiddlewareFunc defines a function to process middleware. - */ - interface MiddlewareFunc {(next: HandlerFunc): HandlerFunc } - interface Echo { + interface Record { /** - * NewContext returns a new Context instance. - * - * Note: both request and response can be left to nil as Echo.ServeHTTP will call c.Reset(req,resp) anyway - * these arguments are useful when creating context for tests and cases like that. + * NewPasswordResetToken generates and returns a new auth record password reset request token. */ - newContext(r: http.Request, w: http.ResponseWriter): Context + newPasswordResetToken(): string } - interface Echo { + interface Record { /** - * Router returns the default router. + * NewEmailChangeToken generates and returns a new auth record change email request token. */ - router(): Router + newEmailChangeToken(newEmail: string): string } - interface Echo { + interface Record { /** - * Routers returns the new map of host => router. + * NewFileToken generates and returns a new record private file access token. */ - routers(): _TygojaDict - } - interface Echo { - /** - * RouterFor returns Router for given host. When host is left empty the default router is returned. - */ - routerFor(host: string): [Router, boolean] - } - interface Echo { - /** - * ResetRouterCreator resets callback for creating new router instances. - * Note: current (default) router is immediately replaced with router created with creator func and vhost routers are cleared. - */ - resetRouterCreator(creator: (e: Echo) => Router): void - } - interface Echo { - /** - * Pre adds middleware to the chain which is run before router tries to find matching route. - * Meaning middleware is executed even for 404 (not found) cases. - */ - pre(...middleware: MiddlewareFunc[]): void - } - interface Echo { - /** - * Use adds middleware to the chain which is run after router has found matching route and before route/request handler method is executed. - */ - use(...middleware: MiddlewareFunc[]): void - } - interface Echo { - /** - * CONNECT registers a new CONNECT route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - connect(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * DELETE registers a new DELETE route for a path with matching handler in the router - * with optional route-level middleware. Panics on error. - */ - delete(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * GET registers a new GET route for a path with matching handler in the router - * with optional route-level middleware. Panics on error. - */ - get(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * HEAD registers a new HEAD route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - head(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * OPTIONS registers a new OPTIONS route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - options(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * PATCH registers a new PATCH route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - patch(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * POST registers a new POST route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - post(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * PUT registers a new PUT route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - put(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * TRACE registers a new TRACE route for a path with matching handler in the - * router with optional route-level middleware. Panics on error. - */ - trace(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases) - * for current request URL. - * Path supports static and named/any parameters just like other http method is defined. Generally path is ended with - * wildcard/match-any character (`/*`, `/download/*` etc). - * - * Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` - */ - routeNotFound(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * Any registers a new route for all HTTP methods (supported by Echo) and path with matching handler - * in the router with optional route-level middleware. - * - * Note: this method only adds specific set of supported HTTP methods as handler and is not true - * "catch-any-arbitrary-method" way of matching requests. - */ - any(path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): Routes - } - interface Echo { - /** - * Match registers a new route for multiple HTTP methods and path with matching - * handler in the router with optional route-level middleware. Panics on error. - */ - match(methods: Array, path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): Routes - } - interface Echo { - /** - * Static registers a new route with path prefix to serve static files from the provided root directory. - */ - static(pathPrefix: string, fsRoot: string): RouteInfo - } - interface Echo { - /** - * StaticFS registers a new route with path prefix to serve static files from the provided file system. - * - * When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary - * prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths - * including `assets/images` as their prefix. - */ - staticFS(pathPrefix: string, filesystem: fs.FS): RouteInfo - } - interface Echo { - /** - * FileFS registers a new route with path to serve file from the provided file system. - */ - fileFS(path: string, file: string, filesystem: fs.FS, ...m: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * File registers a new route with path to serve a static file with optional route-level middleware. Panics on error. - */ - file(path: string, file: string, ...middleware: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * AddRoute registers a new Route with default host Router - */ - addRoute(route: Routable): RouteInfo - } - interface Echo { - /** - * Add registers a new route for an HTTP method and path with matching handler - * in the router with optional route-level middleware. - */ - add(method: string, path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): RouteInfo - } - interface Echo { - /** - * Host creates a new router group for the provided host and optional host-level middleware. - */ - host(name: string, ...m: MiddlewareFunc[]): (Group) - } - interface Echo { - /** - * Group creates a new router group with prefix and optional group-level middleware. - */ - group(prefix: string, ...m: MiddlewareFunc[]): (Group) - } - interface Echo { - /** - * AcquireContext returns an empty `Context` instance from the pool. - * You must return the context by calling `ReleaseContext()`. - */ - acquireContext(): Context - } - interface Echo { - /** - * ReleaseContext returns the `Context` instance back to the pool. - * You must call it after `AcquireContext()`. - */ - releaseContext(c: Context): void - } - interface Echo { - /** - * ServeHTTP implements `http.Handler` interface, which serves HTTP requests. - */ - serveHTTP(w: http.ResponseWriter, r: http.Request): void - } - interface Echo { - /** - * Start stars HTTP server on given address with Echo as a handler serving requests. The server can be shutdown by - * sending os.Interrupt signal with `ctrl+c`. - * - * Note: this method is created for use in examples/demos and is deliberately simple without providing configuration - * options. - * - * In need of customization use: - * - * ``` - * sc := echo.StartConfig{Address: ":8080"} - * if err := sc.Start(e); err != http.ErrServerClosed { - * log.Fatal(err) - * } - * ``` - * - * // or standard library `http.Server` - * - * ``` - * s := http.Server{Addr: ":8080", Handler: e} - * if err := s.ListenAndServe(); err != http.ErrServerClosed { - * log.Fatal(err) - * } - * ``` - */ - start(address: string): void + newFileToken(): string } } @@ -12404,7 +13299,6 @@ namespace cobra { /** * DebugFlags used to determine which flags have been assigned to which commands * and which persist. - * nolint:goconst */ debugFlags(): void } @@ -12505,24 +13399,28 @@ namespace cobra { interface Command { /** * LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands. + * This function does not modify the flags of the current command, it's purpose is to return the current state. */ localNonPersistentFlags(): (any) } interface Command { /** * LocalFlags returns the local FlagSet specifically set in the current command. + * This function does not modify the flags of the current command, it's purpose is to return the current state. */ localFlags(): (any) } interface Command { /** * InheritedFlags returns all flags which were inherited from parent commands. + * This function does not modify the flags of the current command, it's purpose is to return the current state. */ inheritedFlags(): (any) } interface Command { /** * NonInheritedFlags returns all flags which were not inherited from parent commands. + * This function does not modify the flags of the current command, it's purpose is to return the current state. */ nonInheritedFlags(): (any) } @@ -12806,1798 +13704,22 @@ namespace cobra { } } -namespace auth { - /** - * AuthUser defines a standardized oauth2 user data structure. - */ - interface AuthUser { - id: string - name: string - username: string - email: string - avatarUrl: string - accessToken: string - refreshToken: string - expiry: types.DateTime - rawUser: _TygojaDict - } - /** - * Provider defines a common interface for an OAuth2 client. - */ - interface Provider { - [key:string]: any; - /** - * Context returns the context associated with the provider (if any). - */ - context(): context.Context - /** - * SetContext assigns the specified context to the current provider. - */ - setContext(ctx: context.Context): void - /** - * PKCE indicates whether the provider can use the PKCE flow. - */ - pkce(): boolean - /** - * SetPKCE toggles the state whether the provider can use the PKCE flow or not. - */ - setPKCE(enable: boolean): void - /** - * DisplayName usually returns provider name as it is officially written - * and it could be used directly in the UI. - */ - displayName(): string - /** - * SetDisplayName sets the provider's display name. - */ - setDisplayName(displayName: string): void - /** - * Scopes returns the provider access permissions that will be requested. - */ - scopes(): Array - /** - * SetScopes sets the provider access permissions that will be requested later. - */ - setScopes(scopes: Array): void - /** - * ClientId returns the provider client's app ID. - */ - clientId(): string - /** - * SetClientId sets the provider client's ID. - */ - setClientId(clientId: string): void - /** - * ClientSecret returns the provider client's app secret. - */ - clientSecret(): string - /** - * SetClientSecret sets the provider client's app secret. - */ - setClientSecret(secret: string): void - /** - * RedirectUrl returns the end address to redirect the user - * going through the OAuth flow. - */ - redirectUrl(): string - /** - * SetRedirectUrl sets the provider's RedirectUrl. - */ - setRedirectUrl(url: string): void - /** - * AuthUrl returns the provider's authorization service url. - */ - authUrl(): string - /** - * SetAuthUrl sets the provider's AuthUrl. - */ - setAuthUrl(url: string): void - /** - * TokenUrl returns the provider's token exchange service url. - */ - tokenUrl(): string - /** - * SetTokenUrl sets the provider's TokenUrl. - */ - setTokenUrl(url: string): void - /** - * UserApiUrl returns the provider's user info api url. - */ - userApiUrl(): string - /** - * SetUserApiUrl sets the provider's UserApiUrl. - */ - setUserApiUrl(url: string): void - /** - * Client returns an http client using the provided token. - */ - client(token: oauth2.Token): (any) - /** - * BuildAuthUrl returns a URL to the provider's consent page - * that asks for permissions for the required scopes explicitly. - */ - buildAuthUrl(state: string, ...opts: oauth2.AuthCodeOption[]): string - /** - * FetchToken converts an authorization code to token. - */ - fetchToken(code: string, ...opts: oauth2.AuthCodeOption[]): (oauth2.Token) - /** - * FetchRawUserData requests and marshalizes into `result` the - * the OAuth user api response. - */ - fetchRawUserData(token: oauth2.Token): string|Array - /** - * FetchAuthUser is similar to FetchRawUserData, but normalizes and - * marshalizes the user api response into a standardized AuthUser struct. - */ - fetchAuthUser(token: oauth2.Token): (AuthUser) - } -} - -namespace settings { - // @ts-ignore - import validation = ozzo_validation - /** - * Settings defines common app configuration options. - */ - interface Settings { - meta: MetaConfig - logs: LogsConfig - smtp: SmtpConfig - s3: S3Config - backups: BackupsConfig - adminAuthToken: TokenConfig - adminPasswordResetToken: TokenConfig - adminFileToken: TokenConfig - recordAuthToken: TokenConfig - recordPasswordResetToken: TokenConfig - recordEmailChangeToken: TokenConfig - recordVerificationToken: TokenConfig - recordFileToken: TokenConfig - /** - * Deprecated: Will be removed in v0.9+ - */ - emailAuth: EmailAuthConfig - googleAuth: AuthProviderConfig - facebookAuth: AuthProviderConfig - githubAuth: AuthProviderConfig - gitlabAuth: AuthProviderConfig - discordAuth: AuthProviderConfig - twitterAuth: AuthProviderConfig - microsoftAuth: AuthProviderConfig - spotifyAuth: AuthProviderConfig - kakaoAuth: AuthProviderConfig - twitchAuth: AuthProviderConfig - stravaAuth: AuthProviderConfig - giteeAuth: AuthProviderConfig - livechatAuth: AuthProviderConfig - giteaAuth: AuthProviderConfig - oidcAuth: AuthProviderConfig - oidc2Auth: AuthProviderConfig - oidc3Auth: AuthProviderConfig - appleAuth: AuthProviderConfig - instagramAuth: AuthProviderConfig - vkAuth: AuthProviderConfig - yandexAuth: AuthProviderConfig - patreonAuth: AuthProviderConfig - mailcowAuth: AuthProviderConfig - bitbucketAuth: AuthProviderConfig - planningcenterAuth: AuthProviderConfig - } - interface Settings { - /** - * Validate makes Settings validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface Settings { - /** - * Merge merges `other` settings into the current one. - */ - merge(other: Settings): void - } - interface Settings { - /** - * Clone creates a new deep copy of the current settings. - */ - clone(): (Settings) - } - interface Settings { - /** - * RedactClone creates a new deep copy of the current settings, - * while replacing the secret values with `******`. - */ - redactClone(): (Settings) - } - interface Settings { - /** - * NamedAuthProviderConfigs returns a map with all registered OAuth2 - * provider configurations (indexed by their name identifier). - */ - namedAuthProviderConfigs(): _TygojaDict - } -} - /** - * Package daos handles common PocketBase DB model manipulations. + * Package sync provides basic synchronization primitives such as mutual + * exclusion locks. Other than the [Once] and [WaitGroup] types, most are intended + * for use by low-level library routines. Higher-level synchronization is + * better done via channels and communication. * - * Think of daos as DB repository and service layer in one. + * Values containing the types defined in this package should not be copied. */ -namespace daos { - interface Dao { - /** - * AdminQuery returns a new Admin select query. - */ - adminQuery(): (dbx.SelectQuery) - } - interface Dao { - /** - * FindAdminById finds the admin with the provided id. - */ - findAdminById(id: string): (models.Admin) - } - interface Dao { - /** - * FindAdminByEmail finds the admin with the provided email address. - */ - findAdminByEmail(email: string): (models.Admin) - } - interface Dao { - /** - * FindAdminByToken finds the admin associated with the provided JWT. - * - * Returns an error if the JWT is invalid or expired. - */ - findAdminByToken(token: string, baseTokenKey: string): (models.Admin) - } - interface Dao { - /** - * TotalAdmins returns the number of existing admin records. - */ - totalAdmins(): number - } - interface Dao { - /** - * IsAdminEmailUnique checks if the provided email address is not - * already in use by other admins. - */ - isAdminEmailUnique(email: string, ...excludeIds: string[]): boolean - } - interface Dao { - /** - * DeleteAdmin deletes the provided Admin model. - * - * Returns an error if there is only 1 admin. - */ - deleteAdmin(admin: models.Admin): void - } - interface Dao { - /** - * SaveAdmin upserts the provided Admin model. - */ - saveAdmin(admin: models.Admin): void - } +namespace sync { /** - * Dao handles various db operations. - * - * You can think of Dao as a repository and service layer in one. + * A Locker represents an object that can be locked and unlocked. */ - interface Dao { - /** - * MaxLockRetries specifies the default max "database is locked" auto retry attempts. - */ - maxLockRetries: number - /** - * ModelQueryTimeout is the default max duration of a running ModelQuery(). - * - * This field has no effect if an explicit query context is already specified. - */ - modelQueryTimeout: time.Duration - /** - * write hooks - */ - beforeCreateFunc: (eventDao: Dao, m: models.Model, action: () => void) => void - afterCreateFunc: (eventDao: Dao, m: models.Model) => void - beforeUpdateFunc: (eventDao: Dao, m: models.Model, action: () => void) => void - afterUpdateFunc: (eventDao: Dao, m: models.Model) => void - beforeDeleteFunc: (eventDao: Dao, m: models.Model, action: () => void) => void - afterDeleteFunc: (eventDao: Dao, m: models.Model) => void - } - interface Dao { - /** - * DB returns the default dao db builder (*dbx.DB or *dbx.TX). - * - * Currently the default db builder is dao.concurrentDB but that may change in the future. - */ - db(): dbx.Builder - } - interface Dao { - /** - * ConcurrentDB returns the dao concurrent (aka. multiple open connections) - * db builder (*dbx.DB or *dbx.TX). - * - * In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance. - */ - concurrentDB(): dbx.Builder - } - interface Dao { - /** - * NonconcurrentDB returns the dao nonconcurrent (aka. single open connection) - * db builder (*dbx.DB or *dbx.TX). - * - * In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance. - */ - nonconcurrentDB(): dbx.Builder - } - interface Dao { - /** - * Clone returns a new Dao with the same configuration options as the current one. - */ - clone(): (Dao) - } - interface Dao { - /** - * WithoutHooks returns a new Dao with the same configuration options - * as the current one, but without create/update/delete hooks. - */ - withoutHooks(): (Dao) - } - interface Dao { - /** - * ModelQuery creates a new preconfigured select query with preset - * SELECT, FROM and other common fields based on the provided model. - */ - modelQuery(m: models.Model): (dbx.SelectQuery) - } - interface Dao { - /** - * FindById finds a single db record with the specified id and - * scans the result into m. - */ - findById(m: models.Model, id: string): void - } - interface Dao { - /** - * RunInTransaction wraps fn into a transaction. - * - * It is safe to nest RunInTransaction calls as long as you use the txDao. - */ - runInTransaction(fn: (txDao: Dao) => void): void - } - interface Dao { - /** - * Delete deletes the provided model. - */ - delete(m: models.Model): void - } - interface Dao { - /** - * Save persists the provided model in the database. - * - * If m.IsNew() is true, the method will perform a create, otherwise an update. - * To explicitly mark a model for update you can use m.MarkAsNotNew(). - */ - save(m: models.Model): void - } - interface Dao { - /** - * CollectionQuery returns a new Collection select query. - */ - collectionQuery(): (dbx.SelectQuery) - } - interface Dao { - /** - * FindCollectionsByType finds all collections by the given type. - */ - findCollectionsByType(collectionType: string): Array<(models.Collection | undefined)> - } - interface Dao { - /** - * FindCollectionByNameOrId finds a single collection by its name (case insensitive) or id. - */ - findCollectionByNameOrId(nameOrId: string): (models.Collection) - } - interface Dao { - /** - * IsCollectionNameUnique checks that there is no existing collection - * with the provided name (case insensitive!). - * - * Note: case insensitive check because the name is used also as a table name for the records. - */ - isCollectionNameUnique(name: string, ...excludeIds: string[]): boolean - } - interface Dao { - /** - * FindCollectionReferences returns information for all - * relation schema fields referencing the provided collection. - * - * If the provided collection has reference to itself then it will be - * also included in the result. To exclude it, pass the collection id - * as the excludeId argument. - */ - findCollectionReferences(collection: models.Collection, ...excludeIds: string[]): _TygojaDict - } - interface Dao { - /** - * DeleteCollection deletes the provided Collection model. - * This method automatically deletes the related collection records table. - * - * NB! The collection cannot be deleted, if: - * - is system collection (aka. collection.System is true) - * - is referenced as part of a relation field in another collection - */ - deleteCollection(collection: models.Collection): void - } - interface Dao { - /** - * SaveCollection persists the provided Collection model and updates - * its related records table schema. - * - * If collection.IsNew() is true, the method will perform a create, otherwise an update. - * To explicitly mark a collection for update you can use collection.MarkAsNotNew(). - */ - saveCollection(collection: models.Collection): void - } - interface Dao { - /** - * ImportCollections imports the provided collections list within a single transaction. - * - * NB1! If deleteMissing is set, all local collections and schema fields, that are not present - * in the imported configuration, WILL BE DELETED (including their related records data). - * - * NB2! This method doesn't perform validations on the imported collections data! - * If you need validations, use [forms.CollectionsImport]. - */ - importCollections(importedCollections: Array<(models.Collection | undefined)>, deleteMissing: boolean, afterSync: (txDao: Dao, mappedImported: _TygojaDict, mappedExisting: _TygojaDict) => void): void - } - interface Dao { - /** - * ExternalAuthQuery returns a new ExternalAuth select query. - */ - externalAuthQuery(): (dbx.SelectQuery) - } - interface Dao { - /** - * FindAllExternalAuthsByRecord returns all ExternalAuth models - * linked to the provided auth record. - */ - findAllExternalAuthsByRecord(authRecord: models.Record): Array<(models.ExternalAuth | undefined)> - } - interface Dao { - /** - * FindExternalAuthByRecordAndProvider returns the first available - * ExternalAuth model for the specified record data and provider. - */ - findExternalAuthByRecordAndProvider(authRecord: models.Record, provider: string): (models.ExternalAuth) - } - interface Dao { - /** - * FindFirstExternalAuthByExpr returns the first available - * ExternalAuth model that satisfies the non-nil expression. - */ - findFirstExternalAuthByExpr(expr: dbx.Expression): (models.ExternalAuth) - } - interface Dao { - /** - * SaveExternalAuth upserts the provided ExternalAuth model. - */ - saveExternalAuth(model: models.ExternalAuth): void - } - interface Dao { - /** - * DeleteExternalAuth deletes the provided ExternalAuth model. - */ - deleteExternalAuth(model: models.ExternalAuth): void - } - interface Dao { - /** - * LogQuery returns a new Log select query. - */ - logQuery(): (dbx.SelectQuery) - } - interface Dao { - /** - * FindLogById finds a single Log entry by its id. - */ - findLogById(id: string): (models.Log) - } - interface Dao { - /** - * LogsStats returns hourly grouped requests logs statistics. - */ - logsStats(expr: dbx.Expression): Array<(LogsStatsItem | undefined)> - } - interface Dao { - /** - * DeleteOldLogs delete all requests that are created before createdBefore. - */ - deleteOldLogs(createdBefore: time.Time): void - } - interface Dao { - /** - * SaveLog upserts the provided Log model. - */ - saveLog(log: models.Log): void - } - interface Dao { - /** - * ParamQuery returns a new Param select query. - */ - paramQuery(): (dbx.SelectQuery) - } - interface Dao { - /** - * FindParamByKey finds the first Param model with the provided key. - */ - findParamByKey(key: string): (models.Param) - } - interface Dao { - /** - * SaveParam creates or updates a Param model by the provided key-value pair. - * The value argument will be encoded as json string. - * - * If `optEncryptionKey` is provided it will encrypt the value before storing it. - */ - saveParam(key: string, value: any, ...optEncryptionKey: string[]): void - } - interface Dao { - /** - * DeleteParam deletes the provided Param model. - */ - deleteParam(param: models.Param): void - } - interface Dao { - /** - * RecordQuery returns a new Record select query from a collection model, id or name. - * - * In case a collection id or name is provided and that collection doesn't - * actually exists, the generated query will be created with a cancelled context - * and will fail once an executor (Row(), One(), All(), etc.) is called. - */ - recordQuery(collectionModelOrIdentifier: any): (dbx.SelectQuery) - } - interface Dao { - /** - * FindRecordById finds the Record model by its id. - */ - findRecordById(collectionNameOrId: string, recordId: string, ...optFilters: ((q: dbx.SelectQuery) => void)[]): (models.Record) - } - interface Dao { - /** - * FindRecordsByIds finds all Record models by the provided ids. - * If no records are found, returns an empty slice. - */ - findRecordsByIds(collectionNameOrId: string, recordIds: Array, ...optFilters: ((q: dbx.SelectQuery) => void)[]): Array<(models.Record | undefined)> - } - interface Dao { - /** - * FindRecordsByExpr finds all records by the specified db expression. - * - * Returns all collection records if no expressions are provided. - * - * Returns an empty slice if no records are found. - * - * Example: - * - * ``` - * expr1 := dbx.HashExp{"email": "test@example.com"} - * expr2 := dbx.NewExp("LOWER(username) = {:username}", dbx.Params{"username": "test"}) - * dao.FindRecordsByExpr("example", expr1, expr2) - * ``` - */ - findRecordsByExpr(collectionNameOrId: string, ...exprs: dbx.Expression[]): Array<(models.Record | undefined)> - } - interface Dao { - /** - * FindFirstRecordByData returns the first found record matching - * the provided key-value pair. - */ - findFirstRecordByData(collectionNameOrId: string, key: string, value: any): (models.Record) - } - interface Dao { - /** - * FindRecordsByFilter returns limit number of records matching the - * provided string filter. - * - * NB! Use the last "params" argument to bind untrusted user variables! - * - * The sort argument is optional and can be empty string OR the same format - * used in the web APIs, eg. "-created,title". - * - * If the limit argument is <= 0, no limit is applied to the query and - * all matching records are returned. - * - * Example: - * - * ``` - * dao.FindRecordsByFilter( - * "posts", - * "title ~ {:title} && visible = {:visible}", - * "-created", - * 10, - * 0, - * dbx.Params{"title": "lorem ipsum", "visible": true} - * ) - * ``` - */ - findRecordsByFilter(collectionNameOrId: string, filter: string, sort: string, limit: number, offset: number, ...params: dbx.Params[]): Array<(models.Record | undefined)> - } - interface Dao { - /** - * FindFirstRecordByFilter returns the first available record matching the provided filter. - * - * NB! Use the last params argument to bind untrusted user variables! - * - * Example: - * - * ``` - * dao.FindFirstRecordByFilter("posts", "slug={:slug} && status='public'", dbx.Params{"slug": "test"}) - * ``` - */ - findFirstRecordByFilter(collectionNameOrId: string, filter: string, ...params: dbx.Params[]): (models.Record) - } - interface Dao { - /** - * IsRecordValueUnique checks if the provided key-value pair is a unique Record value. - * - * For correctness, if the collection is "auth" and the key is "username", - * the unique check will be case insensitive. - * - * NB! Array values (eg. from multiple select fields) are matched - * as a serialized json strings (eg. `["a","b"]`), so the value uniqueness - * depends on the elements order. Or in other words the following values - * are considered different: `[]string{"a","b"}` and `[]string{"b","a"}` - */ - isRecordValueUnique(collectionNameOrId: string, key: string, value: any, ...excludeIds: string[]): boolean - } - interface Dao { - /** - * FindAuthRecordByToken finds the auth record associated with the provided JWT. - * - * Returns an error if the JWT is invalid, expired or not associated to an auth collection record. - */ - findAuthRecordByToken(token: string, baseTokenKey: string): (models.Record) - } - interface Dao { - /** - * FindAuthRecordByEmail finds the auth record associated with the provided email. - * - * Returns an error if it is not an auth collection or the record is not found. - */ - findAuthRecordByEmail(collectionNameOrId: string, email: string): (models.Record) - } - interface Dao { - /** - * FindAuthRecordByUsername finds the auth record associated with the provided username (case insensitive). - * - * Returns an error if it is not an auth collection or the record is not found. - */ - findAuthRecordByUsername(collectionNameOrId: string, username: string): (models.Record) - } - interface Dao { - /** - * SuggestUniqueAuthRecordUsername checks if the provided username is unique - * and return a new "unique" username with appended random numeric part - * (eg. "existingName" -> "existingName583"). - * - * The same username will be returned if the provided string is already unique. - */ - suggestUniqueAuthRecordUsername(collectionNameOrId: string, baseUsername: string, ...excludeIds: string[]): string - } - interface Dao { - /** - * 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.Admin is set. - * - * The returned error indicate that something unexpected happened during - * the check (eg. invalid rule or db error). - * - * The method always return false on invalid access rule or db error. - * - * Example: - * - * ``` - * requestInfo := apis.RequestInfo(c /* echo.Context *\/) - * record, _ := dao.FindRecordById("example", "RECORD_ID") - * rule := types.Pointer("@request.auth.id != '' || status = 'public'") - * // ... or use one of the record collection's rule, eg. record.Collection().ViewRule - * - * if ok, _ := dao.CanAccessRecord(record, requestInfo, rule); ok { ... } - * ``` - */ - canAccessRecord(record: models.Record, requestInfo: models.RequestInfo, accessRule: string): boolean - } - interface Dao { - /** - * SaveRecord persists the provided Record model in the database. - * - * If record.IsNew() is true, the method will perform a create, otherwise an update. - * To explicitly mark a record for update you can use record.MarkAsNotNew(). - */ - saveRecord(record: models.Record): void - } - interface Dao { - /** - * DeleteRecord deletes the provided Record model. - * - * This method will also cascade the delete operation to all linked - * relational records (delete or unset, depending on the rel settings). - * - * The delete operation may fail if the record is part of a required - * reference in another record (aka. cannot be deleted or unset). - */ - deleteRecord(record: models.Record): void - } - interface Dao { - /** - * ExpandRecord expands the relations of a single Record model. - * - * If optFetchFunc is not set, then a default function will be used - * that returns all relation records. - * - * Returns a map with the failed expand parameters and their errors. - */ - expandRecord(record: models.Record, expands: Array, optFetchFunc: ExpandFetchFunc): _TygojaDict - } - interface Dao { - /** - * ExpandRecords expands the relations of the provided Record models list. - * - * If optFetchFunc is not set, then a default function will be used - * that returns all relation records. - * - * Returns a map with the failed expand parameters and their errors. - */ - expandRecords(records: Array<(models.Record | undefined)>, expands: Array, optFetchFunc: ExpandFetchFunc): _TygojaDict - } - // @ts-ignore - import validation = ozzo_validation - interface Dao { - /** - * SyncRecordTableSchema compares the two provided collections - * and applies the necessary related record table changes. - * - * If `oldCollection` is null, then only `newCollection` is used to create the record table. - */ - syncRecordTableSchema(newCollection: models.Collection, oldCollection: models.Collection): void - } - interface Dao { - /** - * FindSettings returns and decode the serialized app settings param value. - * - * The method will first try to decode the param value without decryption. - * If it fails and optEncryptionKey is set, it will try again by first - * decrypting the value and then decode it again. - * - * Returns an error if it fails to decode the stored serialized param value. - */ - findSettings(...optEncryptionKey: string[]): (settings.Settings) - } - interface Dao { - /** - * SaveSettings persists the specified settings configuration. - * - * If optEncryptionKey is set, then the stored serialized value will be encrypted with it. - */ - saveSettings(newSettings: settings.Settings, ...optEncryptionKey: string[]): void - } - interface Dao { - /** - * HasTable checks if a table (or view) with the provided name exists (case insensitive). - */ - hasTable(tableName: string): boolean - } - interface Dao { - /** - * TableColumns returns all column names of a single table by its name. - */ - tableColumns(tableName: string): Array - } - interface Dao { - /** - * TableInfo returns the `table_info` pragma result for the specified table. - */ - tableInfo(tableName: string): Array<(models.TableInfoRow | undefined)> - } - interface Dao { - /** - * TableIndexes returns a name grouped map with all non empty index of the specified table. - * - * Note: This method doesn't return an error on nonexisting table. - */ - tableIndexes(tableName: string): _TygojaDict - } - interface Dao { - /** - * DeleteTable drops the specified table. - * - * This method is a no-op if a table with the provided name doesn't exist. - * - * Be aware that this method is vulnerable to SQL injection and the - * "tableName" argument must come only from trusted input! - */ - deleteTable(tableName: string): void - } - interface Dao { - /** - * Vacuum executes VACUUM on the current dao.DB() instance in order to - * reclaim unused db disk space. - */ - vacuum(): void - } - interface Dao { - /** - * DeleteView drops the specified view name. - * - * This method is a no-op if a view with the provided name doesn't exist. - * - * Be aware that this method is vulnerable to SQL injection and the - * "name" argument must come only from trusted input! - */ - deleteView(name: string): void - } - interface Dao { - /** - * SaveView creates (or updates already existing) persistent SQL view. - * - * Be aware that this method is vulnerable to SQL injection and the - * "selectQuery" argument must come only from trusted input! - */ - saveView(name: string, selectQuery: string): void - } - interface Dao { - /** - * CreateViewSchema creates a new view schema from the provided select query. - * - * There are some caveats: - * - The select query must have an "id" column. - * - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data. - */ - createViewSchema(selectQuery: string): schema.Schema - } - interface Dao { - /** - * FindRecordByViewFile returns the original models.Record of the - * provided view collection file. - */ - findRecordByViewFile(viewCollectionNameOrId: string, fileFieldName: string, filename: string): (models.Record) - } -} - -/** - * Package core is the backbone of PocketBase. - * - * It defines the main PocketBase App interface and its base implementation. - */ -namespace core { - /** - * App defines the main PocketBase app interface. - */ - interface App { - [key:string]: any; - /** - * Deprecated: - * This method may get removed in the near future. - * It is recommended to access the app db instance from app.Dao().DB() or - * if you want more flexibility - app.Dao().ConcurrentDB() and app.Dao().NonconcurrentDB(). - * - * DB returns the default app database instance. - */ - db(): (dbx.DB) - /** - * Dao returns the default app Dao instance. - * - * This Dao could operate only on the tables and models - * associated with the default app database. For example, - * trying to access the request logs table will result in error. - */ - dao(): (daos.Dao) - /** - * Deprecated: - * This method may get removed in the near future. - * It is recommended to access the logs db instance from app.LogsDao().DB() or - * if you want more flexibility - app.LogsDao().ConcurrentDB() and app.LogsDao().NonconcurrentDB(). - * - * LogsDB returns the app logs database instance. - */ - logsDB(): (dbx.DB) - /** - * LogsDao returns the app logs Dao instance. - * - * This Dao could operate only on the tables and models - * associated with the logs database. For example, trying to access - * the users table from LogsDao will result in error. - */ - logsDao(): (daos.Dao) - /** - * Logger returns the active app logger. - */ - logger(): (slog.Logger) - /** - * DataDir returns the app data directory path. - */ - dataDir(): string - /** - * EncryptionEnv returns the name of the app secret env key - * (used for settings encryption). - */ - encryptionEnv(): string - /** - * IsDev returns whether the app is in dev mode. - */ - isDev(): boolean - /** - * Settings returns the loaded app settings. - */ - settings(): (settings.Settings) - /** - * Deprecated: Use app.Store() instead. - */ - cache(): (store.Store) - /** - * Store returns the app runtime store. - */ - store(): (store.Store) - /** - * SubscriptionsBroker returns the app realtime subscriptions broker instance. - */ - subscriptionsBroker(): (subscriptions.Broker) - /** - * NewMailClient creates and returns a configured app mail client. - */ - newMailClient(): mailer.Mailer - /** - * NewFilesystem creates and returns a configured filesystem.System instance - * for managing regular app files (eg. collection uploads). - * - * NB! Make sure to call Close() on the returned result - * after you are done working with it. - */ - newFilesystem(): (filesystem.System) - /** - * NewBackupsFilesystem creates and returns a configured filesystem.System instance - * for managing app backups. - * - * NB! Make sure to call Close() on the returned result - * after you are done working with it. - */ - newBackupsFilesystem(): (filesystem.System) - /** - * RefreshSettings reinitializes and reloads the stored application settings. - */ - refreshSettings(): void - /** - * IsBootstrapped checks if the application was initialized - * (aka. whether Bootstrap() was called). - */ - isBootstrapped(): boolean - /** - * Bootstrap takes care for initializing the application - * (open db connections, load settings, etc.). - * - * It will call ResetBootstrapState() if the application was already bootstrapped. - */ - bootstrap(): void - /** - * ResetBootstrapState takes care for releasing initialized app resources - * (eg. closing db connections). - */ - resetBootstrapState(): void - /** - * CreateBackup creates a new backup of the current app pb_data directory. - * - * Backups can be stored on S3 if it is configured in app.Settings().Backups. - * - * Please refer to the godoc of the specific CoreApp implementation - * for details on the backup procedures. - */ - createBackup(ctx: context.Context, name: string): void - /** - * RestoreBackup restores the backup with the specified name and restarts - * the current running application process. - * - * The safely perform the restore it is recommended to have free disk space - * for at least 2x the size of the restored pb_data backup. - * - * Please refer to the godoc of the specific CoreApp implementation - * for details on the restore procedures. - * - * NB! This feature is experimental and currently is expected to work only on UNIX based systems. - */ - restoreBackup(ctx: context.Context, name: string): void - /** - * Restart restarts the current running application process. - * - * Currently it is relying on execve so it is supported only on UNIX based systems. - */ - restart(): void - /** - * OnBeforeBootstrap hook is triggered before initializing the main - * application resources (eg. before db open and initial settings load). - */ - onBeforeBootstrap(): (hook.Hook) - /** - * OnAfterBootstrap hook is triggered after initializing the main - * application resources (eg. after db open and initial settings load). - */ - onAfterBootstrap(): (hook.Hook) - /** - * OnBeforeServe hook is triggered before serving the internal router (echo), - * allowing you to adjust its options and attach new routes or middlewares. - */ - onBeforeServe(): (hook.Hook) - /** - * OnBeforeApiError hook is triggered right before sending an error API - * response to the client, allowing you to further modify the error data - * or to return a completely different API response. - */ - onBeforeApiError(): (hook.Hook) - /** - * OnAfterApiError hook is triggered right after sending an error API - * response to the client. - * It could be used to log the final API error in external services. - */ - onAfterApiError(): (hook.Hook) - /** - * OnTerminate hook is triggered when the app is in the process - * of being terminated (eg. on SIGTERM signal). - */ - onTerminate(): (hook.Hook) - /** - * OnModelBeforeCreate hook is triggered before inserting a new - * model in the DB, allowing you to modify or validate the stored data. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelBeforeCreate(...tags: string[]): (hook.TaggedHook) - /** - * OnModelAfterCreate hook is triggered after successfully - * inserting a new model in the DB. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelAfterCreate(...tags: string[]): (hook.TaggedHook) - /** - * OnModelBeforeUpdate hook is triggered before updating existing - * model in the DB, allowing you to modify or validate the stored data. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelBeforeUpdate(...tags: string[]): (hook.TaggedHook) - /** - * OnModelAfterUpdate hook is triggered after successfully updating - * existing model in the DB. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelAfterUpdate(...tags: string[]): (hook.TaggedHook) - /** - * OnModelBeforeDelete hook is triggered before deleting an - * existing model from the DB. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelBeforeDelete(...tags: string[]): (hook.TaggedHook) - /** - * OnModelAfterDelete hook is triggered after successfully deleting an - * existing model from the DB. - * - * If the optional "tags" list (table names and/or the Collection id for Record models) - * 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. - */ - onModelAfterDelete(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerBeforeAdminResetPasswordSend hook is triggered right - * before sending a password reset email to an admin, allowing you - * to inspect and customize the email message that is being sent. - */ - onMailerBeforeAdminResetPasswordSend(): (hook.Hook) - /** - * OnMailerAfterAdminResetPasswordSend hook is triggered after - * admin password reset email was successfully sent. - */ - onMailerAfterAdminResetPasswordSend(): (hook.Hook) - /** - * OnMailerBeforeRecordResetPasswordSend hook is triggered right - * before sending a password reset email to an auth record, allowing - * you to inspect and customize the email message that is being sent. - * - * 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. - */ - onMailerBeforeRecordResetPasswordSend(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerAfterRecordResetPasswordSend hook is triggered after - * an auth record password reset email was successfully sent. - * - * 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. - */ - onMailerAfterRecordResetPasswordSend(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerBeforeRecordVerificationSend hook is triggered right - * before sending a verification email to an auth record, allowing - * you to inspect and customize the email message that is being sent. - * - * 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. - */ - onMailerBeforeRecordVerificationSend(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerAfterRecordVerificationSend hook is triggered after a - * verification email was successfully sent to an auth record. - * - * 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. - */ - onMailerAfterRecordVerificationSend(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerBeforeRecordChangeEmailSend hook is triggered right before - * sending a confirmation new address email to an auth record, allowing - * you to inspect and customize the email message that is being sent. - * - * 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. - */ - onMailerBeforeRecordChangeEmailSend(...tags: string[]): (hook.TaggedHook) - /** - * OnMailerAfterRecordChangeEmailSend hook is triggered after a - * verification email was successfully sent to an auth record. - * - * 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. - */ - onMailerAfterRecordChangeEmailSend(...tags: string[]): (hook.TaggedHook) - /** - * OnRealtimeConnectRequest hook is triggered right before establishing - * the SSE client connection. - */ - onRealtimeConnectRequest(): (hook.Hook) - /** - * OnRealtimeDisconnectRequest hook is triggered on disconnected/interrupted - * SSE client connection. - */ - onRealtimeDisconnectRequest(): (hook.Hook) - /** - * OnRealtimeBeforeMessageSend hook is triggered right before sending - * an SSE message to a client. - * - * Returning [hook.StopPropagation] will prevent sending the message. - * Returning any other non-nil error will close the realtime connection. - */ - onRealtimeBeforeMessageSend(): (hook.Hook) - /** - * OnRealtimeAfterMessageSend hook is triggered right after sending - * an SSE message to a client. - */ - onRealtimeAfterMessageSend(): (hook.Hook) - /** - * OnRealtimeBeforeSubscribeRequest hook is triggered before changing - * the client subscriptions, allowing you to further validate and - * modify the submitted change. - */ - onRealtimeBeforeSubscribeRequest(): (hook.Hook) - /** - * OnRealtimeAfterSubscribeRequest hook is triggered after the client - * subscriptions were successfully changed. - */ - onRealtimeAfterSubscribeRequest(): (hook.Hook) - /** - * OnSettingsListRequest hook is triggered on each successful - * API Settings list request. - * - * Could be used to validate or modify the response before - * returning it to the client. - */ - onSettingsListRequest(): (hook.Hook) - /** - * OnSettingsBeforeUpdateRequest hook is triggered before each API - * Settings update request (after request data load and before settings persistence). - * - * Could be used to additionally validate the request data or - * implement completely different persistence behavior. - */ - onSettingsBeforeUpdateRequest(): (hook.Hook) - /** - * OnSettingsAfterUpdateRequest hook is triggered after each - * successful API Settings update request. - */ - onSettingsAfterUpdateRequest(): (hook.Hook) - /** - * OnFileDownloadRequest hook is triggered before each API File download request. - * - * Could be used to validate or modify the file response before - * returning it to the client. - */ - onFileDownloadRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnFileBeforeTokenRequest hook is triggered before each file - * token API request. - * - * If no token or model was submitted, e.Model and e.Token will be empty, - * allowing you to implement your own custom model file auth implementation. - * - * 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. - */ - onFileBeforeTokenRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnFileAfterTokenRequest hook is triggered after each - * successful 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. - */ - onFileAfterTokenRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnAdminsListRequest hook is triggered on each API Admins list request. - * - * Could be used to validate or modify the response before returning it to the client. - */ - onAdminsListRequest(): (hook.Hook) - /** - * OnAdminViewRequest hook is triggered on each API Admin view request. - * - * Could be used to validate or modify the response before returning it to the client. - */ - onAdminViewRequest(): (hook.Hook) - /** - * OnAdminBeforeCreateRequest hook is triggered before each API - * Admin create request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - */ - onAdminBeforeCreateRequest(): (hook.Hook) - /** - * OnAdminAfterCreateRequest hook is triggered after each - * successful API Admin create request. - */ - onAdminAfterCreateRequest(): (hook.Hook) - /** - * OnAdminBeforeUpdateRequest hook is triggered before each API - * Admin update request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - */ - onAdminBeforeUpdateRequest(): (hook.Hook) - /** - * OnAdminAfterUpdateRequest hook is triggered after each - * successful API Admin update request. - */ - onAdminAfterUpdateRequest(): (hook.Hook) - /** - * OnAdminBeforeDeleteRequest hook is triggered before each API - * Admin delete request (after model load and before actual deletion). - * - * Could be used to additionally validate the request data or implement - * completely different delete behavior. - */ - onAdminBeforeDeleteRequest(): (hook.Hook) - /** - * OnAdminAfterDeleteRequest hook is triggered after each - * successful API Admin delete request. - */ - onAdminAfterDeleteRequest(): (hook.Hook) - /** - * OnAdminAuthRequest hook is triggered on each successful API Admin - * authentication request (sign-in, token refresh, etc.). - * - * Could be used to additionally validate or modify the - * authenticated admin data and token. - */ - onAdminAuthRequest(): (hook.Hook) - /** - * OnAdminBeforeAuthWithPasswordRequest hook is triggered before each Admin - * auth with password API request (after request data load and before password validation). - * - * Could be used to implement for example a custom password validation - * or to locate a different Admin identity (by assigning [AdminAuthWithPasswordEvent.Admin]). - */ - onAdminBeforeAuthWithPasswordRequest(): (hook.Hook) - /** - * OnAdminAfterAuthWithPasswordRequest hook is triggered after each - * successful Admin auth with password API request. - */ - onAdminAfterAuthWithPasswordRequest(): (hook.Hook) - /** - * OnAdminBeforeAuthRefreshRequest hook is triggered before each Admin - * auth refresh API request (right before generating a new auth token). - * - * Could be used to additionally validate the request data or implement - * completely different auth refresh behavior. - */ - onAdminBeforeAuthRefreshRequest(): (hook.Hook) - /** - * OnAdminAfterAuthRefreshRequest hook is triggered after each - * successful auth refresh API request (right after generating a new auth token). - */ - onAdminAfterAuthRefreshRequest(): (hook.Hook) - /** - * OnAdminBeforeRequestPasswordResetRequest hook is triggered before each Admin - * request password reset API request (after request data load and before sending the reset email). - * - * Could be used to additionally validate the request data or implement - * completely different password reset behavior. - */ - onAdminBeforeRequestPasswordResetRequest(): (hook.Hook) - /** - * OnAdminAfterRequestPasswordResetRequest hook is triggered after each - * successful request password reset API request. - */ - onAdminAfterRequestPasswordResetRequest(): (hook.Hook) - /** - * OnAdminBeforeConfirmPasswordResetRequest hook is triggered before each Admin - * confirm password reset API request (after request data load and before persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - */ - onAdminBeforeConfirmPasswordResetRequest(): (hook.Hook) - /** - * OnAdminAfterConfirmPasswordResetRequest hook is triggered after each - * successful confirm password reset API request. - */ - onAdminAfterConfirmPasswordResetRequest(): (hook.Hook) - /** - * OnRecordAuthRequest hook is triggered on each successful API - * record authentication request (sign-in, token refresh, etc.). - * - * Could be used to additionally validate or modify the authenticated - * record data and token. - * - * 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. - */ - onRecordAuthRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeAuthWithPasswordRequest hook is triggered before each Record - * auth with password API request (after request data load and before password validation). - * - * Could be used to implement for example a custom password validation - * or to locate a different Record model (by reassigning [RecordAuthWithPasswordEvent.Record]). - * - * 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. - */ - onRecordBeforeAuthWithPasswordRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterAuthWithPasswordRequest hook is triggered after each - * successful Record auth with password 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. - */ - onRecordAfterAuthWithPasswordRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeAuthWithOAuth2Request hook is triggered before each Record - * OAuth2 sign-in/sign-up API request (after token exchange and before external provider linking). - * - * If the [RecordAuthWithOAuth2Event.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 - * change the [RecordAuthWithOAuth2Event.Record] field. - * - * 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. - */ - onRecordBeforeAuthWithOAuth2Request(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterAuthWithOAuth2Request hook is triggered after each - * successful Record OAuth2 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. - */ - onRecordAfterAuthWithOAuth2Request(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeAuthRefreshRequest hook is triggered before each Record - * auth refresh API request (right before generating a new auth token). - * - * Could be used to additionally validate the request data or implement - * completely different auth refresh behavior. - * - * 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. - */ - onRecordBeforeAuthRefreshRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterAuthRefreshRequest hook is triggered after each - * successful auth refresh API request (right after generating a new auth token). - * - * 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. - */ - onRecordAfterAuthRefreshRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordListExternalAuthsRequest hook is triggered on each API record external auths list request. - * - * Could be used to validate or modify the response before returning it to the client. - * - * 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. - */ - onRecordListExternalAuthsRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeUnlinkExternalAuthRequest hook is triggered before each API record - * external auth unlink request (after models load and before the actual relation deletion). - * - * Could be used to additionally validate the request data or implement - * completely different delete behavior. - * - * 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. - */ - onRecordBeforeUnlinkExternalAuthRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterUnlinkExternalAuthRequest hook is triggered after each - * successful API record external auth unlink 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. - */ - onRecordAfterUnlinkExternalAuthRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeRequestPasswordResetRequest hook is triggered before each Record - * request password reset API request (after request data load and before sending the reset email). - * - * Could be used to additionally validate the request data or implement - * completely different password reset behavior. - * - * 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. - */ - onRecordBeforeRequestPasswordResetRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterRequestPasswordResetRequest hook is triggered after each - * successful request password reset 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. - */ - onRecordAfterRequestPasswordResetRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeConfirmPasswordResetRequest hook is triggered before each Record - * confirm password reset API request (after request data load and before persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - * - * 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. - */ - onRecordBeforeConfirmPasswordResetRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterConfirmPasswordResetRequest hook is triggered after each - * successful confirm password reset 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. - */ - onRecordAfterConfirmPasswordResetRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeRequestVerificationRequest hook is triggered before each Record - * request verification API request (after request data load and before sending the verification email). - * - * Could be used to additionally validate the loaded request data or implement - * completely different verification behavior. - * - * 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. - */ - onRecordBeforeRequestVerificationRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterRequestVerificationRequest hook is triggered after each - * successful request verification 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. - */ - onRecordAfterRequestVerificationRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeConfirmVerificationRequest hook is triggered before each Record - * confirm verification API request (after request data load and before persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - * - * 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. - */ - onRecordBeforeConfirmVerificationRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterConfirmVerificationRequest hook is triggered after each - * successful confirm verification 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. - */ - onRecordAfterConfirmVerificationRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeRequestEmailChangeRequest hook is triggered before each Record request email change API request - * (after request data load and before sending the email link to confirm the change). - * - * Could be used to additionally validate the request data or implement - * completely different request email change behavior. - * - * 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. - */ - onRecordBeforeRequestEmailChangeRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterRequestEmailChangeRequest hook is triggered after each - * successful request email change 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. - */ - onRecordAfterRequestEmailChangeRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeConfirmEmailChangeRequest hook is triggered before each Record - * confirm email change API request (after request data load and before persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - * - * 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. - */ - onRecordBeforeConfirmEmailChangeRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterConfirmEmailChangeRequest hook is triggered after each - * successful confirm email change 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. - */ - onRecordAfterConfirmEmailChangeRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordsListRequest hook is triggered on each API Records list request. - * - * Could be used to validate or modify the response before returning it to the client. - * - * 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. - */ - onRecordsListRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordViewRequest hook is triggered on each API Record view request. - * - * Could be used to validate or modify the response before returning it to the client. - * - * 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. - */ - onRecordViewRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeCreateRequest hook is triggered before each API Record - * create request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - * - * 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. - */ - onRecordBeforeCreateRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterCreateRequest hook is triggered after each - * successful API Record create 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. - */ - onRecordAfterCreateRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeUpdateRequest hook is triggered before each API Record - * update request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - * - * 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. - */ - onRecordBeforeUpdateRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterUpdateRequest hook is triggered after each - * successful API Record update 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. - */ - onRecordAfterUpdateRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordBeforeDeleteRequest hook is triggered before each API Record - * delete request (after model load and before actual deletion). - * - * Could be used to additionally validate the request data or implement - * completely different delete behavior. - * - * 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. - */ - onRecordBeforeDeleteRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnRecordAfterDeleteRequest hook is triggered after each - * successful API Record delete 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. - */ - onRecordAfterDeleteRequest(...tags: string[]): (hook.TaggedHook) - /** - * OnCollectionsListRequest hook is triggered on each API Collections list request. - * - * Could be used to validate or modify the response before returning it to the client. - */ - onCollectionsListRequest(): (hook.Hook) - /** - * OnCollectionViewRequest hook is triggered on each API Collection view request. - * - * Could be used to validate or modify the response before returning it to the client. - */ - onCollectionViewRequest(): (hook.Hook) - /** - * OnCollectionBeforeCreateRequest hook is triggered before each API Collection - * create request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - */ - onCollectionBeforeCreateRequest(): (hook.Hook) - /** - * OnCollectionAfterCreateRequest hook is triggered after each - * successful API Collection create request. - */ - onCollectionAfterCreateRequest(): (hook.Hook) - /** - * OnCollectionBeforeUpdateRequest hook is triggered before each API Collection - * update request (after request data load and before model persistence). - * - * Could be used to additionally validate the request data or implement - * completely different persistence behavior. - */ - onCollectionBeforeUpdateRequest(): (hook.Hook) - /** - * OnCollectionAfterUpdateRequest hook is triggered after each - * successful API Collection update request. - */ - onCollectionAfterUpdateRequest(): (hook.Hook) - /** - * OnCollectionBeforeDeleteRequest hook is triggered before each API - * Collection delete request (after model load and before actual deletion). - * - * Could be used to additionally validate the request data or implement - * completely different delete behavior. - */ - onCollectionBeforeDeleteRequest(): (hook.Hook) - /** - * OnCollectionAfterDeleteRequest hook is triggered after each - * successful API Collection delete request. - */ - onCollectionAfterDeleteRequest(): (hook.Hook) - /** - * OnCollectionsBeforeImportRequest hook is triggered before each API - * collections import request (after request data load and before the actual import). - * - * Could be used to additionally validate the imported collections or - * to implement completely different import behavior. - */ - onCollectionsBeforeImportRequest(): (hook.Hook) - /** - * OnCollectionsAfterImportRequest hook is triggered after each - * successful API collections import request. - */ - onCollectionsAfterImportRequest(): (hook.Hook) - } -} - -namespace migrate { - /** - * MigrationsList defines a list with migration definitions - */ - interface MigrationsList { - } - interface MigrationsList { - /** - * Item returns a single migration from the list by its index. - */ - item(index: number): (Migration) - } - interface MigrationsList { - /** - * Items returns the internal migrations list slice. - */ - items(): Array<(Migration | undefined)> - } - interface MigrationsList { - /** - * Register adds new migration definition to the list. - * - * If `optFilename` is not provided, it will try to get the name from its .go file. - * - * The list will be sorted automatically based on the migrations file name. - */ - register(up: (db: dbx.Builder) => void, down: (db: dbx.Builder) => void, ...optFilename: string[]): void - } -} - -/** - * Package io provides basic interfaces to I/O primitives. - * Its primary job is to wrap existing implementations of such primitives, - * such as those in package os, into shared public interfaces that - * abstract the functionality, plus some other related primitives. - * - * Because these interfaces and primitives wrap lower-level operations with - * various implementations, unless otherwise informed clients should not - * assume they are safe for parallel execution. - */ -namespace io { - /** - * ReadCloser is the interface that groups the basic Read and Close methods. - */ - interface ReadCloser { - [key:string]: any; - } - /** - * WriteCloser is the interface that groups the basic Write and Close methods. - */ - interface WriteCloser { + interface Locker { [key:string]: any; + lock(): void + unlock(): void } } @@ -14616,7 +13738,7 @@ namespace io { * the manuals for the appropriate operating system. * These calls return err == nil to indicate success; otherwise * err is an operating system error describing the failure. - * On most systems, that error has type syscall.Errno. + * On most systems, that error has type [Errno]. * * NOTE: Most of the functions, types, and constants defined in * this package are also available in the [golang.org/x/sys] package. @@ -14628,6 +13750,10 @@ namespace syscall { /** * SysProcIDMap holds Container ID to Host ID mappings used for User Namespaces in Linux. * See user_namespaces(7). + * + * Note that User Namespaces are not available on a number of popular Linux + * versions (due to security issues), or are available but subject to AppArmor + * restrictions like in Ubuntu 24.04. */ interface SysProcIDMap { containerID: number // Container ID. @@ -14638,7 +13764,7 @@ namespace syscall { import errorspkg = errors /** * Credential holds user and group identities to be assumed - * by a child process started by StartProcess. + * by a child process started by [StartProcess]. */ interface Credential { uid: number // User ID. @@ -14646,9 +13772,11 @@ namespace syscall { groups: Array // Supplementary group IDs. noSetGroups: boolean // If true, don't set supplementary groups } + // @ts-ignore + import runtimesyscall = syscall /** * A Signal is a number describing a process signal. - * It implements the os.Signal interface. + * It implements the [os.Signal] interface. */ interface Signal extends Number{} interface Signal { @@ -14671,7 +13799,7 @@ namespace syscall { * changes for clock synchronization, and a “monotonic clock,” which is * not. The general rule is that the wall clock is for telling time and * the monotonic clock is for measuring time. Rather than split the API, - * in this package the Time returned by time.Now contains both a wall + * in this package the Time returned by [time.Now] contains both a wall * clock reading and a monotonic clock reading; later time-telling * operations use the wall clock reading, but later time-measuring * operations, specifically comparisons and subtractions, use the @@ -14688,7 +13816,7 @@ namespace syscall { * elapsed := t.Sub(start) * ``` * - * Other idioms, such as time.Since(start), time.Until(deadline), and + * Other idioms, such as [time.Since](start), [time.Until](deadline), and * time.Now().Before(deadline), are similarly robust against wall clock * resets. * @@ -14713,23 +13841,26 @@ namespace syscall { * * On some systems the monotonic clock will stop if the computer goes to sleep. * On such a system, t.Sub(u) may not accurately reflect the actual - * time that passed between t and u. + * time that passed between t and u. The same applies to other functions and + * methods that subtract times, such as [Since], [Until], [Before], [After], + * [Add], [Sub], [Equal] and [Compare]. In some cases, you may need to strip + * the monotonic clock to get accurate results. * * Because the monotonic clock reading has no meaning outside * the current process, the serialized forms generated by t.GobEncode, * t.MarshalBinary, t.MarshalJSON, and t.MarshalText omit the monotonic * clock reading, and t.Format provides no format for it. Similarly, the - * constructors time.Date, time.Parse, time.ParseInLocation, and time.Unix, + * constructors [time.Date], [time.Parse], [time.ParseInLocation], and [time.Unix], * as well as the unmarshalers t.GobDecode, t.UnmarshalBinary. * t.UnmarshalJSON, and t.UnmarshalText always create times with * no monotonic clock reading. * - * The monotonic clock reading exists only in Time values. It is not - * a part of Duration values or the Unix times returned by t.Unix and + * The monotonic clock reading exists only in [Time] values. It is not + * a part of [Duration] values or the Unix times returned by t.Unix and * friends. * * Note that the Go == operator compares not just the time instant but - * also the Location and the monotonic clock reading. See the + * also the [Location] and the monotonic clock reading. See the * documentation for the Time type for a discussion of equality * testing for Time values. * @@ -14739,10 +13870,11 @@ namespace syscall { * * # Timer Resolution * - * Timer resolution varies depending on the Go runtime, the operating system + * [Timer] resolution varies depending on the Go runtime, the operating system * and the underlying hardware. - * On Unix, the resolution is approximately 1ms. - * On Windows, the default resolution is approximately 16ms, but + * On Unix, the resolution is ~1ms. + * On Windows version 1803 and newer, the resolution is ~0.5ms. + * On older Windows versions, the default resolution is ~16ms, but * a higher resolution may be requested using [golang.org/x/sys/windows.TimeBeginPeriod]. */ namespace time { @@ -14781,23 +13913,12 @@ namespace time { interface Location { /** * String returns a descriptive name for the time zone information, - * corresponding to the name argument to LoadLocation or FixedZone. + * corresponding to the name argument to [LoadLocation] or [FixedZone]. */ string(): string } } -/** - * Package fs defines basic interfaces to a file system. - * A file system can be provided by the host operating system - * but also by other packages. - * - * See the [testing/fstest] package for support with testing - * implementations of file systems. - */ -namespace fs { -} - /** * Package context defines the Context type, which carries deadlines, * cancellation signals, and other request-scoped values across API boundaries @@ -14854,6 +13975,36 @@ namespace fs { namespace context { } +/** + * Package io provides basic interfaces to I/O primitives. + * Its primary job is to wrap existing implementations of such primitives, + * such as those in package os, into shared public interfaces that + * abstract the functionality, plus some other related primitives. + * + * Because these interfaces and primitives wrap lower-level operations with + * various implementations, unless otherwise informed clients should not + * assume they are safe for parallel execution. + */ +namespace io { + /** + * WriteCloser is the interface that groups the basic Write and Close methods. + */ + interface WriteCloser { + [key:string]: any; + } +} + +/** + * Package fs defines basic interfaces to a file system. + * A file system can be provided by the host operating system + * but also by other packages. + * + * See the [testing/fstest] package for support with testing + * implementations of file systems. + */ +namespace fs { +} + /** * Package url parses URLs and implements query escaping. */ @@ -15093,182 +14244,10 @@ namespace url { } /** - * Package net provides a portable interface for network I/O, including - * TCP/IP, UDP, domain name resolution, and Unix domain sockets. - * - * Although the package provides access to low-level networking - * primitives, most clients will need only the basic interface provided - * by the [Dial], [Listen], and Accept functions and the associated - * [Conn] and [Listener] interfaces. The crypto/tls package uses - * the same interfaces and similar Dial and Listen functions. - * - * The Dial function connects to a server: - * - * ``` - * conn, err := net.Dial("tcp", "golang.org:80") - * if err != nil { - * // handle error - * } - * fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") - * status, err := bufio.NewReader(conn).ReadString('\n') - * // ... - * ``` - * - * The Listen function creates servers: - * - * ``` - * ln, err := net.Listen("tcp", ":8080") - * if err != nil { - * // handle error - * } - * for { - * conn, err := ln.Accept() - * if err != nil { - * // handle error - * } - * go handleConnection(conn) - * } - * ``` - * - * # Name Resolution - * - * The method for resolving domain names, whether indirectly with functions like Dial - * or directly with functions like [LookupHost] and [LookupAddr], varies by operating system. - * - * On Unix systems, the resolver has two options for resolving names. - * It can use a pure Go resolver that sends DNS requests directly to the servers - * listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C - * library routines such as getaddrinfo and getnameinfo. - * - * By default the pure Go resolver is used, because a blocked DNS request consumes - * only a goroutine, while a blocked C call consumes an operating system thread. - * When cgo is available, the cgo-based resolver is used instead under a variety of - * conditions: on systems that do not let programs make direct DNS requests (OS X), - * when the LOCALDOMAIN environment variable is present (even if empty), - * when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, - * when the ASR_CONFIG environment variable is non-empty (OpenBSD only), - * when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the - * Go resolver does not implement, and when the name being looked up ends in .local - * or is an mDNS name. - * - * The resolver decision can be overridden by setting the netdns value of the - * GODEBUG environment variable (see package runtime) to go or cgo, as in: - * - * ``` - * export GODEBUG=netdns=go # force pure Go resolver - * export GODEBUG=netdns=cgo # force native resolver (cgo, win32) - * ``` - * - * The decision can also be forced while building the Go source tree - * by setting the netgo or netcgo build tag. - * - * A numeric netdns setting, as in GODEBUG=netdns=1, causes the resolver - * to print debugging information about its decisions. - * To force a particular resolver while also printing debugging information, - * join the two settings by a plus sign, as in GODEBUG=netdns=go+1. - * - * On macOS, if Go code that uses the net package is built with - * -buildmode=c-archive, linking the resulting archive into a C program - * requires passing -lresolv when linking the C code. - * - * On Plan 9, the resolver always accesses /net/cs and /net/dns. - * - * On Windows, in Go 1.18.x and earlier, the resolver always used C - * library functions, such as GetAddrInfo and DnsQuery. + * Package types implements some commonly used db serializable types + * like datetime, json, etc. */ -namespace net { - /** - * Conn is a generic stream-oriented network connection. - * - * Multiple goroutines may invoke methods on a Conn simultaneously. - */ - interface Conn { - [key:string]: any; - /** - * Read reads data from the connection. - * Read can be made to time out and return an error after a fixed - * time limit; see SetDeadline and SetReadDeadline. - */ - read(b: string|Array): number - /** - * Write writes data to the connection. - * Write can be made to time out and return an error after a fixed - * time limit; see SetDeadline and SetWriteDeadline. - */ - write(b: string|Array): number - /** - * Close closes the connection. - * Any blocked Read or Write operations will be unblocked and return errors. - */ - close(): void - /** - * LocalAddr returns the local network address, if known. - */ - localAddr(): Addr - /** - * RemoteAddr returns the remote network address, if known. - */ - remoteAddr(): Addr - /** - * SetDeadline sets the read and write deadlines associated - * with the connection. It is equivalent to calling both - * SetReadDeadline and SetWriteDeadline. - * - * A deadline is an absolute time after which I/O operations - * fail instead of blocking. The deadline applies to all future - * and pending I/O, not just the immediately following call to - * Read or Write. After a deadline has been exceeded, the - * connection can be refreshed by setting a deadline in the future. - * - * If the deadline is exceeded a call to Read or Write or to other - * I/O methods will return an error that wraps os.ErrDeadlineExceeded. - * This can be tested using errors.Is(err, os.ErrDeadlineExceeded). - * The error's Timeout method will return true, but note that there - * are other possible errors for which the Timeout method will - * return true even if the deadline has not been exceeded. - * - * An idle timeout can be implemented by repeatedly extending - * the deadline after successful Read or Write calls. - * - * A zero value for t means I/O operations will not time out. - */ - setDeadline(t: time.Time): void - /** - * SetReadDeadline sets the deadline for future Read calls - * and any currently-blocked Read call. - * A zero value for t means Read will not time out. - */ - setReadDeadline(t: time.Time): void - /** - * SetWriteDeadline sets the deadline for future Write calls - * and any currently-blocked Write call. - * Even if write times out, it may return n > 0, indicating that - * some of the data was successfully written. - * A zero value for t means Write will not time out. - */ - setWriteDeadline(t: time.Time): void - } - /** - * A Listener is a generic network listener for stream-oriented protocols. - * - * Multiple goroutines may invoke methods on a Listener simultaneously. - */ - interface Listener { - [key:string]: any; - /** - * Accept waits for and returns the next connection to the listener. - */ - accept(): Conn - /** - * Close closes the listener. - * Any blocked Accept operations will be unblocked and return errors. - */ - close(): void - /** - * Addr returns the listener's network address. - */ - addr(): Addr - } +namespace types { } /** @@ -15487,3853 +14466,6 @@ namespace sql { } } -/** - * Package textproto implements generic support for text-based request/response - * protocols in the style of HTTP, NNTP, and SMTP. - * - * The package provides: - * - * [Error], which represents a numeric error response from - * a server. - * - * [Pipeline], to manage pipelined requests and responses - * in a client. - * - * [Reader], to read numeric response code lines, - * key: value headers, lines wrapped with leading spaces - * on continuation lines, and whole text blocks ending - * with a dot on a line by itself. - * - * [Writer], to write dot-encoded text blocks. - * - * [Conn], a convenient packaging of [Reader], [Writer], and [Pipeline] for use - * with a single network connection. - */ -namespace textproto { - /** - * A MIMEHeader represents a MIME-style header mapping - * keys to sets of values. - */ - interface MIMEHeader extends _TygojaDict{} - interface MIMEHeader { - /** - * Add adds the key, value pair to the header. - * It appends to any existing values associated with key. - */ - add(key: string, value: string): void - } - interface MIMEHeader { - /** - * Set sets the header entries associated with key to - * the single element value. It replaces any existing - * values associated with key. - */ - set(key: string, value: string): void - } - interface MIMEHeader { - /** - * Get gets the first value associated with the given key. - * It is case insensitive; [CanonicalMIMEHeaderKey] is used - * to canonicalize the provided key. - * If there are no values associated with the key, Get returns "". - * To use non-canonical keys, access the map directly. - */ - get(key: string): string - } - interface MIMEHeader { - /** - * Values returns all values associated with the given key. - * It is case insensitive; [CanonicalMIMEHeaderKey] is - * used to canonicalize the provided key. To use non-canonical - * keys, access the map directly. - * The returned slice is not a copy. - */ - values(key: string): Array - } - interface MIMEHeader { - /** - * Del deletes the values associated with key. - */ - del(key: string): void - } -} - -/** - * Package multipart implements MIME multipart parsing, as defined in RFC - * 2046. - * - * The implementation is sufficient for HTTP (RFC 2388) and the multipart - * bodies generated by popular browsers. - * - * # Limits - * - * To protect against malicious inputs, this package sets limits on the size - * of the MIME data it processes. - * - * Reader.NextPart and Reader.NextRawPart limit the number of headers in a - * part to 10000 and Reader.ReadForm limits the total number of headers in all - * FileHeaders to 10000. - * These limits may be adjusted with the GODEBUG=multipartmaxheaders= - * setting. - * - * Reader.ReadForm further limits the number of parts in a form to 1000. - * This limit may be adjusted with the GODEBUG=multipartmaxparts= - * setting. - */ -/** - * Copyright 2023 The Go Authors. All rights reserved. - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ -namespace multipart { - interface Reader { - /** - * ReadForm parses an entire multipart message whose parts have - * a Content-Disposition of "form-data". - * It stores up to maxMemory bytes + 10MB (reserved for non-file parts) - * in memory. File parts which can't be stored in memory will be stored on - * disk in temporary files. - * It returns ErrMessageTooLarge if all non-file parts can't be stored in - * memory. - */ - readForm(maxMemory: number): (Form) - } - /** - * Form is a parsed multipart form. - * Its File parts are stored either in memory or on disk, - * and are accessible via the *FileHeader's Open method. - * Its Value parts are stored as strings. - * Both are keyed by field name. - */ - interface Form { - value: _TygojaDict - file: _TygojaDict - } - interface Form { - /** - * RemoveAll removes any temporary files associated with a Form. - */ - removeAll(): void - } - /** - * File is an interface to access the file part of a multipart message. - * Its contents may be either stored in memory or on disk. - * If stored on disk, the File's underlying concrete type will be an *os.File. - */ - interface File { - [key:string]: any; - } - /** - * Reader is an iterator over parts in a MIME multipart body. - * Reader's underlying parser consumes its input as needed. Seeking - * isn't supported. - */ - interface Reader { - } - interface Reader { - /** - * NextPart returns the next part in the multipart or an error. - * When there are no more parts, the error io.EOF is returned. - * - * As a special case, if the "Content-Transfer-Encoding" header - * has a value of "quoted-printable", that header is instead - * hidden and the body is transparently decoded during Read calls. - */ - nextPart(): (Part) - } - interface Reader { - /** - * NextRawPart returns the next part in the multipart or an error. - * When there are no more parts, the error io.EOF is returned. - * - * Unlike NextPart, it does not have special handling for - * "Content-Transfer-Encoding: quoted-printable". - */ - nextRawPart(): (Part) - } -} - -/** - * Package http provides HTTP client and server implementations. - * - * [Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests: - * - * ``` - * resp, err := http.Get("http://example.com/") - * ... - * resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) - * ... - * resp, err := http.PostForm("http://example.com/form", - * url.Values{"key": {"Value"}, "id": {"123"}}) - * ``` - * - * The caller must close the response body when finished with it: - * - * ``` - * resp, err := http.Get("http://example.com/") - * if err != nil { - * // handle error - * } - * defer resp.Body.Close() - * body, err := io.ReadAll(resp.Body) - * // ... - * ``` - * - * # Clients and Transports - * - * For control over HTTP client headers, redirect policy, and other - * settings, create a [Client]: - * - * ``` - * client := &http.Client{ - * CheckRedirect: redirectPolicyFunc, - * } - * - * resp, err := client.Get("http://example.com") - * // ... - * - * req, err := http.NewRequest("GET", "http://example.com", nil) - * // ... - * req.Header.Add("If-None-Match", `W/"wyzzy"`) - * resp, err := client.Do(req) - * // ... - * ``` - * - * For control over proxies, TLS configuration, keep-alives, - * compression, and other settings, create a [Transport]: - * - * ``` - * tr := &http.Transport{ - * MaxIdleConns: 10, - * IdleConnTimeout: 30 * time.Second, - * DisableCompression: true, - * } - * client := &http.Client{Transport: tr} - * resp, err := client.Get("https://example.com") - * ``` - * - * Clients and Transports are safe for concurrent use by multiple - * goroutines and for efficiency should only be created once and re-used. - * - * # Servers - * - * ListenAndServe starts an HTTP server with a given address and handler. - * The handler is usually nil, which means to use [DefaultServeMux]. - * [Handle] and [HandleFunc] add handlers to [DefaultServeMux]: - * - * ``` - * http.Handle("/foo", fooHandler) - * - * http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { - * fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - * }) - * - * log.Fatal(http.ListenAndServe(":8080", nil)) - * ``` - * - * More control over the server's behavior is available by creating a - * custom Server: - * - * ``` - * s := &http.Server{ - * Addr: ":8080", - * Handler: myHandler, - * ReadTimeout: 10 * time.Second, - * WriteTimeout: 10 * time.Second, - * MaxHeaderBytes: 1 << 20, - * } - * log.Fatal(s.ListenAndServe()) - * ``` - * - * # HTTP/2 - * - * Starting with Go 1.6, the http package has transparent support for the - * HTTP/2 protocol when using HTTPS. Programs that must disable HTTP/2 - * can do so by setting [Transport.TLSNextProto] (for clients) or - * [Server.TLSNextProto] (for servers) to a non-nil, empty - * map. Alternatively, the following GODEBUG settings are - * currently supported: - * - * ``` - * GODEBUG=http2client=0 # disable HTTP/2 client support - * GODEBUG=http2server=0 # disable HTTP/2 server support - * GODEBUG=http2debug=1 # enable verbose HTTP/2 debug logs - * GODEBUG=http2debug=2 # ... even more verbose, with frame dumps - * ``` - * - * Please report any issues before disabling HTTP/2 support: https://golang.org/s/http2bug - * - * The http package's [Transport] and [Server] both automatically enable - * HTTP/2 support for simple configurations. To enable HTTP/2 for more - * complex configurations, to use lower-level HTTP/2 features, or to use - * a newer version of Go's http2 package, import "golang.org/x/net/http2" - * directly and use its ConfigureTransport and/or ConfigureServer - * functions. Manually configuring HTTP/2 via the golang.org/x/net/http2 - * package takes precedence over the net/http package's built-in HTTP/2 - * support. - */ -namespace http { - /** - * A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an - * HTTP response or the Cookie header of an HTTP request. - * - * See https://tools.ietf.org/html/rfc6265 for details. - */ - interface Cookie { - name: string - value: string - path: string // optional - domain: string // optional - expires: time.Time // optional - rawExpires: string // for reading cookies only - /** - * MaxAge=0 means no 'Max-Age' attribute specified. - * MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' - * MaxAge>0 means Max-Age attribute present and given in seconds - */ - maxAge: number - secure: boolean - httpOnly: boolean - sameSite: SameSite - raw: string - unparsed: Array // Raw text of unparsed attribute-value pairs - } - interface Cookie { - /** - * String returns the serialization of the cookie for use in a [Cookie] - * header (if only Name and Value are set) or a Set-Cookie response - * header (if other fields are set). - * If c is nil or c.Name is invalid, the empty string is returned. - */ - string(): string - } - interface Cookie { - /** - * Valid reports whether the cookie is valid. - */ - valid(): void - } - // @ts-ignore - import mathrand = rand - /** - * A Header represents the key-value pairs in an HTTP header. - * - * The keys should be in canonical form, as returned by - * [CanonicalHeaderKey]. - */ - interface Header extends _TygojaDict{} - interface Header { - /** - * Add adds the key, value pair to the header. - * It appends to any existing values associated with key. - * The key is case insensitive; it is canonicalized by - * [CanonicalHeaderKey]. - */ - add(key: string, value: string): void - } - interface Header { - /** - * Set sets the header entries associated with key to the - * single element value. It replaces any existing values - * associated with key. The key is case insensitive; it is - * canonicalized by [textproto.CanonicalMIMEHeaderKey]. - * To use non-canonical keys, assign to the map directly. - */ - set(key: string, value: string): void - } - interface Header { - /** - * Get gets the first value associated with the given key. If - * there are no values associated with the key, Get returns "". - * It is case insensitive; [textproto.CanonicalMIMEHeaderKey] is - * used to canonicalize the provided key. Get assumes that all - * keys are stored in canonical form. To use non-canonical keys, - * access the map directly. - */ - get(key: string): string - } - interface Header { - /** - * Values returns all values associated with the given key. - * It is case insensitive; [textproto.CanonicalMIMEHeaderKey] is - * used to canonicalize the provided key. To use non-canonical - * keys, access the map directly. - * The returned slice is not a copy. - */ - values(key: string): Array - } - interface Header { - /** - * Del deletes the values associated with key. - * The key is case insensitive; it is canonicalized by - * [CanonicalHeaderKey]. - */ - del(key: string): void - } - interface Header { - /** - * Write writes a header in wire format. - */ - write(w: io.Writer): void - } - interface Header { - /** - * Clone returns a copy of h or nil if h is nil. - */ - clone(): Header - } - interface Header { - /** - * WriteSubset writes a header in wire format. - * If exclude is not nil, keys where exclude[key] == true are not written. - * Keys are not canonicalized before checking the exclude map. - */ - writeSubset(w: io.Writer, exclude: _TygojaDict): void - } - // @ts-ignore - import urlpkg = url - /** - * Response represents the response from an HTTP request. - * - * The [Client] and [Transport] return Responses from servers once - * the response headers have been received. The response body - * is streamed on demand as the Body field is read. - */ - interface Response { - status: string // e.g. "200 OK" - statusCode: number // e.g. 200 - proto: string // e.g. "HTTP/1.0" - protoMajor: number // e.g. 1 - protoMinor: number // e.g. 0 - /** - * Header maps header keys to values. If the response had multiple - * headers with the same key, they may be concatenated, with comma - * delimiters. (RFC 7230, section 3.2.2 requires that multiple headers - * be semantically equivalent to a comma-delimited sequence.) When - * Header values are duplicated by other fields in this struct (e.g., - * ContentLength, TransferEncoding, Trailer), the field values are - * authoritative. - * - * Keys in the map are canonicalized (see CanonicalHeaderKey). - */ - header: Header - /** - * Body represents the response body. - * - * The response body is streamed on demand as the Body field - * is read. If the network connection fails or the server - * terminates the response, Body.Read calls return an error. - * - * The http Client and Transport guarantee that Body is always - * non-nil, even on responses without a body or responses with - * a zero-length body. It is the caller's responsibility to - * close Body. The default HTTP client's Transport may not - * reuse HTTP/1.x "keep-alive" TCP connections if the Body is - * not read to completion and closed. - * - * The Body is automatically dechunked if the server replied - * with a "chunked" Transfer-Encoding. - * - * As of Go 1.12, the Body will also implement io.Writer - * on a successful "101 Switching Protocols" response, - * as used by WebSockets and HTTP/2's "h2c" mode. - */ - body: io.ReadCloser - /** - * ContentLength records the length of the associated content. The - * value -1 indicates that the length is unknown. Unless Request.Method - * is "HEAD", values >= 0 indicate that the given number of bytes may - * be read from Body. - */ - contentLength: number - /** - * Contains transfer encodings from outer-most to inner-most. Value is - * nil, means that "identity" encoding is used. - */ - transferEncoding: Array - /** - * Close records whether the header directed that the connection be - * closed after reading Body. The value is advice for clients: neither - * ReadResponse nor Response.Write ever closes a connection. - */ - close: boolean - /** - * Uncompressed reports whether the response was sent compressed but - * was decompressed by the http package. When true, reading from - * Body yields the uncompressed content instead of the compressed - * content actually set from the server, ContentLength is set to -1, - * and the "Content-Length" and "Content-Encoding" fields are deleted - * from the responseHeader. To get the original response from - * the server, set Transport.DisableCompression to true. - */ - uncompressed: boolean - /** - * Trailer maps trailer keys to values in the same - * format as Header. - * - * The Trailer initially contains only nil values, one for - * each key specified in the server's "Trailer" header - * value. Those values are not added to Header. - * - * Trailer must not be accessed concurrently with Read calls - * on the Body. - * - * After Body.Read has returned io.EOF, Trailer will contain - * any trailer values sent by the server. - */ - trailer: Header - /** - * Request is the request that was sent to obtain this Response. - * Request's Body is nil (having already been consumed). - * This is only populated for Client requests. - */ - request?: Request - /** - * TLS contains information about the TLS connection on which the - * response was received. It is nil for unencrypted responses. - * The pointer is shared between responses and should not be - * modified. - */ - tls?: any - } - interface Response { - /** - * Cookies parses and returns the cookies set in the Set-Cookie headers. - */ - cookies(): Array<(Cookie | undefined)> - } - interface Response { - /** - * Location returns the URL of the response's "Location" header, - * if present. Relative redirects are resolved relative to - * [Response.Request]. [ErrNoLocation] is returned if no - * Location header is present. - */ - location(): (url.URL) - } - interface Response { - /** - * ProtoAtLeast reports whether the HTTP protocol used - * in the response is at least major.minor. - */ - protoAtLeast(major: number, minor: number): boolean - } - interface Response { - /** - * Write writes r to w in the HTTP/1.x server response format, - * including the status line, headers, body, and optional trailer. - * - * This method consults the following fields of the response r: - * - * ``` - * StatusCode - * ProtoMajor - * ProtoMinor - * Request.Method - * TransferEncoding - * Trailer - * Body - * ContentLength - * Header, values for non-canonical keys will have unpredictable behavior - * ``` - * - * The Response Body is closed after it is sent. - */ - write(w: io.Writer): void - } - /** - * A Handler responds to an HTTP request. - * - * [Handler.ServeHTTP] should write reply headers and data to the [ResponseWriter] - * and then return. Returning signals that the request is finished; it - * is not valid to use the [ResponseWriter] or read from the - * [Request.Body] after or concurrently with the completion of the - * ServeHTTP call. - * - * Depending on the HTTP client software, HTTP protocol version, and - * any intermediaries between the client and the Go server, it may not - * be possible to read from the [Request.Body] after writing to the - * [ResponseWriter]. Cautious handlers should read the [Request.Body] - * first, and then reply. - * - * Except for reading the body, handlers should not modify the - * provided Request. - * - * If ServeHTTP panics, the server (the caller of ServeHTTP) assumes - * that the effect of the panic was isolated to the active request. - * It recovers the panic, logs a stack trace to the server error log, - * and either closes the network connection or sends an HTTP/2 - * RST_STREAM, depending on the HTTP protocol. To abort a handler so - * the client sees an interrupted response but the server doesn't log - * an error, panic with the value [ErrAbortHandler]. - */ - interface Handler { - [key:string]: any; - serveHTTP(_arg0: ResponseWriter, _arg1: Request): void - } - /** - * A ConnState represents the state of a client connection to a server. - * It's used by the optional [Server.ConnState] hook. - */ - interface ConnState extends Number{} - interface ConnState { - string(): string - } -} - -namespace store { - /** - * Store defines a concurrent safe in memory key-value data store. - */ - interface Store { - } - interface Store { - /** - * Reset clears the store and replaces the store data with a - * shallow copy of the provided newData. - */ - reset(newData: _TygojaDict): void - } - interface Store { - /** - * Length returns the current number of elements in the store. - */ - length(): number - } - interface Store { - /** - * RemoveAll removes all the existing store entries. - */ - removeAll(): void - } - interface Store { - /** - * Remove removes a single entry from the store. - * - * Remove does nothing if key doesn't exist in the store. - */ - remove(key: string): void - } - interface Store { - /** - * Has checks if element with the specified key exist or not. - */ - has(key: string): boolean - } - interface Store { - /** - * Get returns a single element value from the store. - * - * If key is not set, the zero T value is returned. - */ - get(key: string): T - } - interface Store { - /** - * GetAll returns a shallow copy of the current store data. - */ - getAll(): _TygojaDict - } - interface Store { - /** - * Set sets (or overwrite if already exist) a new value for key. - */ - set(key: string, value: T): void - } - interface Store { - /** - * SetIfLessThanLimit sets (or overwrite if already exist) a new value for key. - * - * This method is similar to Set() but **it will skip adding new elements** - * to the store if the store length has reached the specified limit. - * false is returned if maxAllowedElements limit is reached. - */ - setIfLessThanLimit(key: string, value: T, maxAllowedElements: number): boolean - } -} - -/** - * Package types implements some commonly used db serializable types - * like datetime, json, etc. - */ -namespace types { - /** - * DateTime represents a [time.Time] instance in UTC that is wrapped - * and serialized using the app default date layout. - */ - interface DateTime { - } - interface DateTime { - /** - * Time returns the internal [time.Time] instance. - */ - time(): time.Time - } - interface DateTime { - /** - * IsZero checks whether the current DateTime instance has zero time value. - */ - isZero(): boolean - } - interface DateTime { - /** - * String serializes the current DateTime instance into a formatted - * UTC date string. - * - * The zero value is serialized to an empty string. - */ - string(): string - } - interface DateTime { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface DateTime { - /** - * UnmarshalJSON implements the [json.Unmarshaler] interface. - */ - unmarshalJSON(b: string|Array): void - } - interface DateTime { - /** - * Value implements the [driver.Valuer] interface. - */ - value(): any - } - interface DateTime { - /** - * Scan implements [sql.Scanner] interface to scan the provided value - * into the current DateTime instance. - */ - scan(value: any): void - } -} - -/** - * Package schema implements custom Schema and SchemaField datatypes - * for handling the Collection schema definitions. - */ -namespace schema { - // @ts-ignore - import validation = ozzo_validation - /** - * SchemaField defines a single schema field structure. - */ - interface SchemaField { - system: boolean - id: string - name: string - type: string - required: boolean - /** - * Presentable indicates whether the field is suitable for - * visualization purposes (eg. in the Admin UI relation views). - */ - presentable: boolean - /** - * Deprecated: This field is no-op and will be removed in future versions. - * Please use the collection.Indexes field to define a unique constraint. - */ - unique: boolean - options: any - } - interface SchemaField { - /** - * ColDefinition returns the field db column type definition as string. - */ - colDefinition(): string - } - interface SchemaField { - /** - * String serializes and returns the current field as string. - */ - string(): string - } - interface SchemaField { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface SchemaField { - /** - * UnmarshalJSON implements the [json.Unmarshaler] interface. - * - * The schema field options are auto initialized on success. - */ - unmarshalJSON(data: string|Array): void - } - interface SchemaField { - /** - * Validate makes `SchemaField` validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface SchemaField { - /** - * InitOptions initializes the current field options based on its type. - * - * Returns error on unknown field type. - */ - initOptions(): void - } - interface SchemaField { - /** - * PrepareValue returns normalized and properly formatted field value. - */ - prepareValue(value: any): any - } - interface SchemaField { - /** - * PrepareValueWithModifier returns normalized and properly formatted field value - * by "merging" baseValue with the modifierValue based on the specified modifier (+ or -). - */ - prepareValueWithModifier(baseValue: any, modifier: string, modifierValue: any): any - } -} - -/** - * Package models implements all PocketBase DB models and DTOs. - */ -namespace models { - /** - * Model defines an interface with common methods that all db models should have. - */ - interface Model { - [key:string]: any; - tableName(): string - isNew(): boolean - markAsNew(): void - markAsNotNew(): void - hasId(): boolean - getId(): string - setId(id: string): void - getCreated(): types.DateTime - getUpdated(): types.DateTime - refreshId(): void - refreshCreated(): void - refreshUpdated(): void - } - /** - * BaseModel defines common fields and methods used by all other models. - */ - interface BaseModel { - id: string - created: types.DateTime - updated: types.DateTime - } - interface BaseModel { - /** - * HasId returns whether the model has a nonzero id. - */ - hasId(): boolean - } - interface BaseModel { - /** - * GetId returns the model id. - */ - getId(): string - } - interface BaseModel { - /** - * SetId sets the model id to the provided string value. - */ - setId(id: string): void - } - interface BaseModel { - /** - * MarkAsNew marks the model as "new" (aka. enforces m.IsNew() to be true). - */ - markAsNew(): void - } - interface BaseModel { - /** - * MarkAsNotNew marks the model as "not new" (aka. enforces m.IsNew() to be false) - */ - markAsNotNew(): void - } - interface BaseModel { - /** - * IsNew indicates what type of db query (insert or update) - * should be used with the model instance. - */ - isNew(): boolean - } - interface BaseModel { - /** - * GetCreated returns the model Created datetime. - */ - getCreated(): types.DateTime - } - interface BaseModel { - /** - * GetUpdated returns the model Updated datetime. - */ - getUpdated(): types.DateTime - } - interface BaseModel { - /** - * RefreshId generates and sets a new model id. - * - * The generated id is a cryptographically random 15 characters length string. - */ - refreshId(): void - } - interface BaseModel { - /** - * RefreshCreated updates the model Created field with the current datetime. - */ - refreshCreated(): void - } - interface BaseModel { - /** - * RefreshUpdated updates the model Updated field with the current datetime. - */ - refreshUpdated(): void - } - interface BaseModel { - /** - * PostScan implements the [dbx.PostScanner] interface. - * - * It is executed right after the model was populated with the db row values. - */ - postScan(): void - } - // @ts-ignore - import validation = ozzo_validation - /** - * CollectionBaseOptions defines the "base" Collection.Options fields. - */ - interface CollectionBaseOptions { - } - interface CollectionBaseOptions { - /** - * Validate implements [validation.Validatable] interface. - */ - validate(): void - } - /** - * CollectionAuthOptions defines the "auth" Collection.Options fields. - */ - interface CollectionAuthOptions { - manageRule?: string - allowOAuth2Auth: boolean - allowUsernameAuth: boolean - allowEmailAuth: boolean - requireEmail: boolean - exceptEmailDomains: Array - onlyVerified: boolean - onlyEmailDomains: Array - minPasswordLength: number - } - interface CollectionAuthOptions { - /** - * Validate implements [validation.Validatable] interface. - */ - validate(): void - } - /** - * CollectionViewOptions defines the "view" Collection.Options fields. - */ - interface CollectionViewOptions { - query: string - } - interface CollectionViewOptions { - /** - * Validate implements [validation.Validatable] interface. - */ - validate(): void - } - type _subWyIIM = BaseModel - interface Log extends _subWyIIM { - data: types.JsonMap - message: string - level: number - } - interface Log { - tableName(): string - } - type _subXUePJ = BaseModel - interface Param extends _subXUePJ { - key: string - value: types.JsonRaw - } - interface Param { - tableName(): string - } - interface TableInfoRow { - /** - * the `db:"pk"` tag has special semantic so we cannot rename - * the original field without specifying a custom mapper - */ - pk: number - index: number - name: string - type: string - notNull: boolean - defaultValue: types.JsonRaw - } -} - -/** - * Package echo implements high performance, minimalist Go web framework. - * - * Example: - * - * ``` - * package main - * - * import ( - * "github.com/labstack/echo/v5" - * "github.com/labstack/echo/v5/middleware" - * "log" - * "net/http" - * ) - * - * // Handler - * func hello(c echo.Context) error { - * return c.String(http.StatusOK, "Hello, World!") - * } - * - * func main() { - * // Echo instance - * e := echo.New() - * - * // Middleware - * e.Use(middleware.Logger()) - * e.Use(middleware.Recover()) - * - * // Routes - * e.GET("/", hello) - * - * // Start server - * if err := e.Start(":8080"); err != http.ErrServerClosed { - * log.Fatal(err) - * } - * } - * ``` - * - * Learn more at https://echo.labstack.com - */ -namespace echo { - /** - * Binder is the interface that wraps the Bind method. - */ - interface Binder { - [key:string]: any; - bind(c: Context, i: { - }): void - } - /** - * ServableContext is interface that Echo context implementation must implement to be usable in middleware/handlers and - * be able to be routed by Router. - */ - interface ServableContext { - [key:string]: any; - /** - * Reset resets the context after request completes. It must be called along - * with `Echo#AcquireContext()` and `Echo#ReleaseContext()`. - * See `Echo#ServeHTTP()` - */ - reset(r: http.Request, w: http.ResponseWriter): void - } - // @ts-ignore - import stdContext = context - /** - * JSONSerializer is the interface that encodes and decodes JSON to and from interfaces. - */ - interface JSONSerializer { - [key:string]: any; - serialize(c: Context, i: { - }, indent: string): void - deserialize(c: Context, i: { - }): void - } - /** - * HTTPErrorHandler is a centralized HTTP error handler. - */ - interface HTTPErrorHandler {(c: Context, err: Error): void } - /** - * Validator is the interface that wraps the Validate function. - */ - interface Validator { - [key:string]: any; - validate(i: { - }): void - } - /** - * Renderer is the interface that wraps the Render function. - */ - interface Renderer { - [key:string]: any; - render(_arg0: io.Writer, _arg1: string, _arg2: { - }, _arg3: Context): void - } - /** - * Group is a set of sub-routes for a specified route. It can be used for inner - * routes that share a common middleware or functionality that should be separate - * from the parent echo instance while still inheriting from it. - */ - interface Group { - } - interface Group { - /** - * Use implements `Echo#Use()` for sub-routes within the Group. - * Group middlewares are not executed on request when there is no matching route found. - */ - use(...middleware: MiddlewareFunc[]): void - } - interface Group { - /** - * CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. Panics on error. - */ - connect(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * DELETE implements `Echo#DELETE()` for sub-routes within the Group. Panics on error. - */ - delete(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * GET implements `Echo#GET()` for sub-routes within the Group. Panics on error. - */ - get(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * HEAD implements `Echo#HEAD()` for sub-routes within the Group. Panics on error. - */ - head(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. Panics on error. - */ - options(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * PATCH implements `Echo#PATCH()` for sub-routes within the Group. Panics on error. - */ - patch(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * POST implements `Echo#POST()` for sub-routes within the Group. Panics on error. - */ - post(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * PUT implements `Echo#PUT()` for sub-routes within the Group. Panics on error. - */ - put(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * TRACE implements `Echo#TRACE()` for sub-routes within the Group. Panics on error. - */ - trace(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * Any implements `Echo#Any()` for sub-routes within the Group. Panics on error. - */ - any(path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): Routes - } - interface Group { - /** - * Match implements `Echo#Match()` for sub-routes within the Group. Panics on error. - */ - match(methods: Array, path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): Routes - } - interface Group { - /** - * Group creates a new sub-group with prefix and optional sub-group-level middleware. - * Important! Group middlewares are only executed in case there was exact route match and not - * for 404 (not found) or 405 (method not allowed) cases. If this kind of behaviour is needed then add - * a catch-all route `/*` for the group which handler returns always 404 - */ - group(prefix: string, ...middleware: MiddlewareFunc[]): (Group) - } - interface Group { - /** - * Static implements `Echo#Static()` for sub-routes within the Group. - */ - static(pathPrefix: string, fsRoot: string): RouteInfo - } - interface Group { - /** - * StaticFS implements `Echo#StaticFS()` for sub-routes within the Group. - * - * When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, "rootDirectory") to create sub fs which uses necessary - * prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths - * including `assets/images` as their prefix. - */ - staticFS(pathPrefix: string, filesystem: fs.FS): RouteInfo - } - interface Group { - /** - * FileFS implements `Echo#FileFS()` for sub-routes within the Group. - */ - fileFS(path: string, file: string, filesystem: fs.FS, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * File implements `Echo#File()` for sub-routes within the Group. Panics on error. - */ - file(path: string, file: string, ...middleware: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group. - * - * Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` - */ - routeNotFound(path: string, h: HandlerFunc, ...m: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * Add implements `Echo#Add()` for sub-routes within the Group. Panics on error. - */ - add(method: string, path: string, handler: HandlerFunc, ...middleware: MiddlewareFunc[]): RouteInfo - } - interface Group { - /** - * AddRoute registers a new Routable with Router - */ - addRoute(route: Routable): RouteInfo - } - /** - * IPExtractor is a function to extract IP addr from http.Request. - * Set appropriate one to Echo#IPExtractor. - * See https://echo.labstack.com/guide/ip-address for more details. - */ - interface IPExtractor {(_arg0: http.Request): string } - /** - * Logger defines the logging interface that Echo uses internally in few places. - * For logging in handlers use your own logger instance (dependency injected or package/public variable) from logging framework of your choice. - */ - interface Logger { - [key:string]: any; - /** - * Write provides writer interface for http.Server `ErrorLog` and for logging startup messages. - * `http.Server.ErrorLog` logs errors from accepting connections, unexpected behavior from handlers, - * and underlying FileSystem errors. - * `logger` middleware will use this method to write its JSON payload. - */ - write(p: string|Array): number - /** - * Error logs the error - */ - error(err: Error): void - } - /** - * Response wraps an http.ResponseWriter and implements its interface to be used - * by an HTTP handler to construct an HTTP response. - * See: https://golang.org/pkg/net/http/#ResponseWriter - */ - interface Response { - writer: http.ResponseWriter - status: number - size: number - committed: boolean - } - interface Response { - /** - * Header returns the header map for the writer that will be sent by - * WriteHeader. Changing the header after a call to WriteHeader (or Write) has - * no effect unless the modified headers were declared as trailers by setting - * the "Trailer" header before the call to WriteHeader (see example) - * To suppress implicit response headers, set their value to nil. - * Example: https://golang.org/pkg/net/http/#example_ResponseWriter_trailers - */ - header(): http.Header - } - interface Response { - /** - * Before registers a function which is called just before the response is written. - */ - before(fn: () => void): void - } - interface Response { - /** - * After registers a function which is called just after the response is written. - * If the `Content-Length` is unknown, none of the after function is executed. - */ - after(fn: () => void): void - } - interface Response { - /** - * WriteHeader sends an HTTP response header with status code. If WriteHeader is - * not called explicitly, the first call to Write will trigger an implicit - * WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly - * used to send error codes. - */ - writeHeader(code: number): void - } - interface Response { - /** - * Write writes the data to the connection as part of an HTTP reply. - */ - write(b: string|Array): number - } - interface Response { - /** - * Flush implements the http.Flusher interface to allow an HTTP handler to flush - * buffered data to the client. - * See [http.Flusher](https://golang.org/pkg/net/http/#Flusher) - */ - flush(): void - } - interface Response { - /** - * Hijack implements the http.Hijacker interface to allow an HTTP handler to - * take over the connection. - * See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker) - */ - hijack(): [net.Conn, (bufio.ReadWriter)] - } - interface Response { - /** - * Unwrap returns the original http.ResponseWriter. - * ResponseController can be used to access the original http.ResponseWriter. - * See [https://go.dev/blog/go1.20] - */ - unwrap(): http.ResponseWriter - } - interface Routes { - /** - * Reverse reverses route to URL string by replacing path parameters with given params values. - */ - reverse(name: string, ...params: { - }[]): string - } - interface Routes { - /** - * FindByMethodPath searched for matching route info by method and path - */ - findByMethodPath(method: string, path: string): RouteInfo - } - interface Routes { - /** - * FilterByMethod searched for matching route info by method - */ - filterByMethod(method: string): Routes - } - interface Routes { - /** - * FilterByPath searched for matching route info by path - */ - filterByPath(path: string): Routes - } - interface Routes { - /** - * FilterByName searched for matching route info by name - */ - filterByName(name: string): Routes - } - /** - * Router is interface for routing request contexts to registered routes. - * - * Contract between Echo/Context instance and the router: - * ``` - * - all routes must be added through methods on echo.Echo instance. - * Reason: Echo instance uses RouteInfo.Params() length to allocate slice for paths parameters (see `Echo.contextPathParamAllocSize`). - * - Router must populate Context during Router.Route call with: - * - RoutableContext.SetPath - * - RoutableContext.SetRawPathParams (IMPORTANT! with same slice pointer that c.RawPathParams() returns) - * - RoutableContext.SetRouteInfo - * And optionally can set additional information to Context with RoutableContext.Set - * ``` - */ - interface Router { - [key:string]: any; - /** - * Add registers Routable with the Router and returns registered RouteInfo - */ - add(routable: Routable): RouteInfo - /** - * Remove removes route from the Router - */ - remove(method: string, path: string): void - /** - * Routes returns information about all registered routes - */ - routes(): Routes - /** - * Route searches Router for matching route and applies it to the given context. In case when no matching method - * was not found (405) or no matching route exists for path (404), router will return its implementation of 405/404 - * handler function. - */ - route(c: RoutableContext): HandlerFunc - } - /** - * Routable is interface for registering Route with Router. During route registration process the Router will - * convert Routable to RouteInfo with ToRouteInfo method. By creating custom implementation of Routable additional - * information about registered route can be stored in Routes (i.e. privileges used with route etc.) - */ - interface Routable { - [key:string]: any; - /** - * ToRouteInfo converts Routable to RouteInfo - * - * This method is meant to be used by Router after it parses url for path parameters, to store information about - * route just added. - */ - toRouteInfo(params: Array): RouteInfo - /** - * ToRoute converts Routable to Route which Router uses to register the method handler for path. - * - * This method is meant to be used by Router to get fields (including handler and middleware functions) needed to - * add Route to Router. - */ - toRoute(): Route - /** - * ForGroup recreates routable with added group prefix and group middlewares it is grouped to. - * - * Is necessary for Echo.Group to be able to add/register Routable with Router and having group prefix and group - * middlewares included in actually registered Route. - */ - forGroup(pathPrefix: string, middlewares: Array): Routable - } - /** - * Routes is collection of RouteInfo instances with various helper methods. - */ - interface Routes extends Array{} - /** - * RouteInfo describes registered route base fields. - * Method+Path pair uniquely identifies the Route. Name can have duplicates. - */ - interface RouteInfo { - [key:string]: any; - method(): string - path(): string - name(): string - params(): Array - /** - * Reverse reverses route to URL string by replacing path parameters with given params values. - */ - reverse(...params: { - }[]): string - } - /** - * PathParams is collections of PathParam instances with various helper methods - */ - interface PathParams extends Array{} - interface PathParams { - /** - * Get returns path parameter value for given name or default value. - */ - get(name: string, defaultValue: string): string - } -} - -/** - * Package oauth2 provides support for making - * OAuth2 authorized and authenticated HTTP requests, - * as specified in RFC 6749. - * It can additionally grant authorization with Bearer JWT. - */ -/** - * Copyright 2023 The Go Authors. All rights reserved. - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ -namespace oauth2 { - /** - * An AuthCodeOption is passed to Config.AuthCodeURL. - */ - interface AuthCodeOption { - [key:string]: any; - } - /** - * Token represents the credentials used to authorize - * the requests to access protected resources on the OAuth 2.0 - * provider's backend. - * - * Most users of this package should not access fields of Token - * directly. They're exported mostly for use by related packages - * implementing derivative OAuth2 flows. - */ - interface Token { - /** - * AccessToken is the token that authorizes and authenticates - * the requests. - */ - accessToken: string - /** - * TokenType is the type of token. - * The Type method returns either this or "Bearer", the default. - */ - tokenType: string - /** - * RefreshToken is a token that's used by the application - * (as opposed to the user) to refresh the access token - * if it expires. - */ - refreshToken: string - /** - * Expiry is the optional expiration time of the access token. - * - * If zero, TokenSource implementations will reuse the same - * token forever and RefreshToken or equivalent - * mechanisms for that TokenSource will not be used. - */ - expiry: time.Time - } - interface Token { - /** - * Type returns t.TokenType if non-empty, else "Bearer". - */ - type(): string - } - interface Token { - /** - * SetAuthHeader sets the Authorization header to r using the access - * token in t. - * - * This method is unnecessary when using Transport or an HTTP Client - * returned by this package. - */ - setAuthHeader(r: http.Request): void - } - interface Token { - /** - * WithExtra returns a new Token that's a clone of t, but using the - * provided raw extra map. This is only intended for use by packages - * implementing derivative OAuth2 flows. - */ - withExtra(extra: { - }): (Token) - } - interface Token { - /** - * Extra returns an extra field. - * Extra fields are key-value pairs returned by the server as a - * part of the token retrieval response. - */ - extra(key: string): { - } - } - interface Token { - /** - * Valid reports whether t is non-nil, has an AccessToken, and is not expired. - */ - valid(): boolean - } -} - -namespace mailer { - /** - * Mailer defines a base mail client interface. - */ - interface Mailer { - [key:string]: any; - /** - * Send sends an email with the provided Message. - */ - send(message: Message): void - } -} - -namespace settings { - // @ts-ignore - import validation = ozzo_validation - interface TokenConfig { - secret: string - duration: number - } - interface TokenConfig { - /** - * Validate makes TokenConfig validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface SmtpConfig { - enabled: boolean - host: string - port: number - username: string - password: string - /** - * SMTP AUTH - PLAIN (default) or LOGIN - */ - authMethod: string - /** - * Whether to enforce TLS encryption for the mail server connection. - * - * When set to false StartTLS command is send, leaving the server - * to decide whether to upgrade the connection or not. - */ - tls: boolean - /** - * LocalName is optional domain name or IP address used for the - * EHLO/HELO exchange (if not explicitly set, defaults to "localhost"). - * - * This is required only by some SMTP servers, such as Gmail SMTP-relay. - */ - localName: string - } - interface SmtpConfig { - /** - * Validate makes SmtpConfig validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface S3Config { - enabled: boolean - bucket: string - region: string - endpoint: string - accessKey: string - secret: string - forcePathStyle: boolean - } - interface S3Config { - /** - * Validate makes S3Config validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface BackupsConfig { - /** - * Cron is a cron expression to schedule auto backups, eg. "* * * * *". - * - * Leave it empty to disable the auto backups functionality. - */ - cron: string - /** - * CronMaxKeep is the max number of cron generated backups to - * keep before removing older entries. - * - * This field works only when the cron config has valid cron expression. - */ - cronMaxKeep: number - /** - * S3 is an optional S3 storage config specifying where to store the app backups. - */ - s3: S3Config - } - interface BackupsConfig { - /** - * Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface MetaConfig { - appName: string - appUrl: string - hideControls: boolean - senderName: string - senderAddress: string - verificationTemplate: EmailTemplate - resetPasswordTemplate: EmailTemplate - confirmEmailChangeTemplate: EmailTemplate - } - interface MetaConfig { - /** - * Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface LogsConfig { - maxDays: number - minLevel: number - logIp: boolean - } - interface LogsConfig { - /** - * Validate makes LogsConfig validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface AuthProviderConfig { - enabled: boolean - clientId: string - clientSecret: string - authUrl: string - tokenUrl: string - userApiUrl: string - displayName: string - pkce?: boolean - } - interface AuthProviderConfig { - /** - * Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface AuthProviderConfig { - /** - * SetupProvider loads the current AuthProviderConfig into the specified provider. - */ - setupProvider(provider: auth.Provider): void - } - /** - * Deprecated: Will be removed in v0.9+ - */ - interface EmailAuthConfig { - enabled: boolean - exceptDomains: Array - onlyDomains: Array - minPasswordLength: number - } - interface EmailAuthConfig { - /** - * Deprecated: Will be removed in v0.9+ - */ - validate(): void - } -} - -/** - * Package daos handles common PocketBase DB model manipulations. - * - * Think of daos as DB repository and service layer in one. - */ -namespace daos { - interface LogsStatsItem { - total: number - date: types.DateTime - } - /** - * ExpandFetchFunc defines the function that is used to fetch the expanded relation records. - */ - interface ExpandFetchFunc {(relCollection: models.Collection, relIds: Array): Array<(models.Record | undefined)> } - // @ts-ignore - import validation = ozzo_validation -} - -namespace hook { - /** - * Hook defines a concurrent safe structure for handling event hooks - * (aka. callbacks propagation). - */ - interface Hook { - } - interface Hook { - /** - * PreAdd registers a new handler to the hook by prepending it to the existing queue. - * - * Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id). - */ - preAdd(fn: Handler): string - } - interface Hook { - /** - * Add registers a new handler to the hook by appending it to the existing queue. - * - * Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id). - */ - add(fn: Handler): string - } - interface Hook { - /** - * Remove removes a single hook handler by its id. - */ - remove(id: string): void - } - interface Hook { - /** - * RemoveAll removes all registered handlers. - */ - removeAll(): void - } - interface Hook { - /** - * Trigger executes all registered hook handlers one by one - * with the specified `data` as an argument. - * - * Optionally, this method allows also to register additional one off - * handlers that will be temporary appended to the handlers queue. - * - * The execution stops when: - * - hook.StopPropagation is returned in one of the handlers - * - any non-nil error is returned in one of the handlers - */ - trigger(data: T, ...oneOffHandlers: Handler[]): void - } - /** - * TaggedHook defines a proxy hook which register handlers that are triggered only - * if the TaggedHook.tags are empty or includes at least one of the event data tag(s). - */ - type _subhfFNE = mainHook - interface TaggedHook extends _subhfFNE { - } - interface TaggedHook { - /** - * CanTriggerOn checks if the current TaggedHook can be triggered with - * the provided event data tags. - */ - canTriggerOn(tags: Array): boolean - } - interface TaggedHook { - /** - * PreAdd registers a new handler to the hook by prepending it to the existing queue. - * - * The fn handler will be called only if the event data tags satisfy h.CanTriggerOn. - */ - preAdd(fn: Handler): string - } - interface TaggedHook { - /** - * Add registers a new handler to the hook by appending it to the existing queue. - * - * The fn handler will be called only if the event data tags satisfy h.CanTriggerOn. - */ - add(fn: Handler): string - } -} - -/** - * Package slog provides structured logging, - * in which log records include a message, - * a severity level, and various other attributes - * expressed as key-value pairs. - * - * It defines a type, [Logger], - * which provides several methods (such as [Logger.Info] and [Logger.Error]) - * for reporting events of interest. - * - * Each Logger is associated with a [Handler]. - * A Logger output method creates a [Record] from the method arguments - * and passes it to the Handler, which decides how to handle it. - * There is a default Logger accessible through top-level functions - * (such as [Info] and [Error]) that call the corresponding Logger methods. - * - * A log record consists of a time, a level, a message, and a set of key-value - * pairs, where the keys are strings and the values may be of any type. - * As an example, - * - * ``` - * slog.Info("hello", "count", 3) - * ``` - * - * creates a record containing the time of the call, - * a level of Info, the message "hello", and a single - * pair with key "count" and value 3. - * - * The [Info] top-level function calls the [Logger.Info] method on the default Logger. - * In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. - * Besides these convenience methods for common levels, - * there is also a [Logger.Log] method which takes the level as an argument. - * Each of these methods has a corresponding top-level function that uses the - * default logger. - * - * The default handler formats the log record's message, time, level, and attributes - * as a string and passes it to the [log] package. - * - * ``` - * 2022/11/08 15:28:26 INFO hello count=3 - * ``` - * - * For more control over the output format, create a logger with a different handler. - * This statement uses [New] to create a new logger with a [TextHandler] - * that writes structured records in text form to standard error: - * - * ``` - * logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) - * ``` - * - * [TextHandler] output is a sequence of key=value pairs, easily and unambiguously - * parsed by machine. This statement: - * - * ``` - * logger.Info("hello", "count", 3) - * ``` - * - * produces this output: - * - * ``` - * time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 - * ``` - * - * The package also provides [JSONHandler], whose output is line-delimited JSON: - * - * ``` - * logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - * logger.Info("hello", "count", 3) - * ``` - * - * produces this output: - * - * ``` - * {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} - * ``` - * - * Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. - * There are options for setting the minimum level (see Levels, below), - * displaying the source file and line of the log call, and - * modifying attributes before they are logged. - * - * Setting a logger as the default with - * - * ``` - * slog.SetDefault(logger) - * ``` - * - * will cause the top-level functions like [Info] to use it. - * [SetDefault] also updates the default logger used by the [log] package, - * so that existing applications that use [log.Printf] and related functions - * will send log records to the logger's handler without needing to be rewritten. - * - * Some attributes are common to many log calls. - * For example, you may wish to include the URL or trace identifier of a server request - * with all log events arising from the request. - * Rather than repeat the attribute with every log call, you can use [Logger.With] - * to construct a new Logger containing the attributes: - * - * ``` - * logger2 := logger.With("url", r.URL) - * ``` - * - * The arguments to With are the same key-value pairs used in [Logger.Info]. - * The result is a new Logger with the same handler as the original, but additional - * attributes that will appear in the output of every call. - * - * # Levels - * - * A [Level] is an integer representing the importance or severity of a log event. - * The higher the level, the more severe the event. - * This package defines constants for the most common levels, - * but any int can be used as a level. - * - * In an application, you may wish to log messages only at a certain level or greater. - * One common configuration is to log messages at Info or higher levels, - * suppressing debug logging until it is needed. - * The built-in handlers can be configured with the minimum level to output by - * setting [HandlerOptions.Level]. - * The program's `main` function typically does this. - * The default value is LevelInfo. - * - * Setting the [HandlerOptions.Level] field to a [Level] value - * fixes the handler's minimum level throughout its lifetime. - * Setting it to a [LevelVar] allows the level to be varied dynamically. - * A LevelVar holds a Level and is safe to read or write from multiple - * goroutines. - * To vary the level dynamically for an entire program, first initialize - * a global LevelVar: - * - * ``` - * var programLevel = new(slog.LevelVar) // Info by default - * ``` - * - * Then use the LevelVar to construct a handler, and make it the default: - * - * ``` - * h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) - * slog.SetDefault(slog.New(h)) - * ``` - * - * Now the program can change its logging level with a single statement: - * - * ``` - * programLevel.Set(slog.LevelDebug) - * ``` - * - * # Groups - * - * Attributes can be collected into groups. - * A group has a name that is used to qualify the names of its attributes. - * How this qualification is displayed depends on the handler. - * [TextHandler] separates the group and attribute names with a dot. - * [JSONHandler] treats each group as a separate JSON object, with the group name as the key. - * - * Use [Group] to create a Group attribute from a name and a list of key-value pairs: - * - * ``` - * slog.Group("request", - * "method", r.Method, - * "url", r.URL) - * ``` - * - * TextHandler would display this group as - * - * ``` - * request.method=GET request.url=http://example.com - * ``` - * - * JSONHandler would display it as - * - * ``` - * "request":{"method":"GET","url":"http://example.com"} - * ``` - * - * Use [Logger.WithGroup] to qualify all of a Logger's output - * with a group name. Calling WithGroup on a Logger results in a - * new Logger with the same Handler as the original, but with all - * its attributes qualified by the group name. - * - * This can help prevent duplicate attribute keys in large systems, - * where subsystems might use the same keys. - * Pass each subsystem a different Logger with its own group name so that - * potential duplicates are qualified: - * - * ``` - * logger := slog.Default().With("id", systemID) - * parserLogger := logger.WithGroup("parser") - * parseInput(input, parserLogger) - * ``` - * - * When parseInput logs with parserLogger, its keys will be qualified with "parser", - * so even if it uses the common key "id", the log line will have distinct keys. - * - * # Contexts - * - * Some handlers may wish to include information from the [context.Context] that is - * available at the call site. One example of such information - * is the identifier for the current span when tracing is enabled. - * - * The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first - * argument, as do their corresponding top-level functions. - * - * Although the convenience methods on Logger (Info and so on) and the - * corresponding top-level functions do not take a context, the alternatives ending - * in "Context" do. For example, - * - * ``` - * slog.InfoContext(ctx, "message") - * ``` - * - * It is recommended to pass a context to an output method if one is available. - * - * # Attrs and Values - * - * An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as - * alternating keys and values. The statement - * - * ``` - * slog.Info("hello", slog.Int("count", 3)) - * ``` - * - * behaves the same as - * - * ``` - * slog.Info("hello", "count", 3) - * ``` - * - * There are convenience constructors for [Attr] such as [Int], [String], and [Bool] - * for common types, as well as the function [Any] for constructing Attrs of any - * type. - * - * The value part of an Attr is a type called [Value]. - * Like an [any], a Value can hold any Go value, - * but it can represent typical values, including all numbers and strings, - * without an allocation. - * - * For the most efficient log output, use [Logger.LogAttrs]. - * It is similar to [Logger.Log] but accepts only Attrs, not alternating - * keys and values; this allows it, too, to avoid allocation. - * - * The call - * - * ``` - * logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3)) - * ``` - * - * is the most efficient way to achieve the same output as - * - * ``` - * slog.InfoContext(ctx, "hello", "count", 3) - * ``` - * - * # Customizing a type's logging behavior - * - * If a type implements the [LogValuer] interface, the [Value] returned from its LogValue - * method is used for logging. You can use this to control how values of the type - * appear in logs. For example, you can redact secret information like passwords, - * or gather a struct's fields in a Group. See the examples under [LogValuer] for - * details. - * - * A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] - * method handles these cases carefully, avoiding infinite loops and unbounded recursion. - * Handler authors and others may wish to use [Value.Resolve] instead of calling LogValue directly. - * - * # Wrapping output methods - * - * The logger functions use reflection over the call stack to find the file name - * and line number of the logging call within the application. This can produce - * incorrect source information for functions that wrap slog. For instance, if you - * define this function in file mylog.go: - * - * ``` - * func Infof(logger *slog.Logger, format string, args ...any) { - * logger.Info(fmt.Sprintf(format, args...)) - * } - * ``` - * - * and you call it like this in main.go: - * - * ``` - * Infof(slog.Default(), "hello, %s", "world") - * ``` - * - * then slog will report the source file as mylog.go, not main.go. - * - * A correct implementation of Infof will obtain the source location - * (pc) and pass it to NewRecord. - * The Infof function in the package-level example called "wrapping" - * demonstrates how to do this. - * - * # Working with Records - * - * Sometimes a Handler will need to modify a Record - * before passing it on to another Handler or backend. - * A Record contains a mixture of simple public fields (e.g. Time, Level, Message) - * and hidden fields that refer to state (such as attributes) indirectly. This - * means that modifying a simple copy of a Record (e.g. by calling - * [Record.Add] or [Record.AddAttrs] to add attributes) - * may have unexpected effects on the original. - * Before modifying a Record, use [Record.Clone] to - * create a copy that shares no state with the original, - * or create a new Record with [NewRecord] - * and build up its Attrs by traversing the old ones with [Record.Attrs]. - * - * # Performance considerations - * - * If profiling your application demonstrates that logging is taking significant time, - * the following suggestions may help. - * - * If many log lines have a common attribute, use [Logger.With] to create a Logger with - * that attribute. The built-in handlers will format that attribute only once, at the - * call to [Logger.With]. The [Handler] interface is designed to allow that optimization, - * and a well-written Handler should take advantage of it. - * - * The arguments to a log call are always evaluated, even if the log event is discarded. - * If possible, defer computation so that it happens only if the value is actually logged. - * For example, consider the call - * - * ``` - * slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily - * ``` - * - * The URL.String method will be called even if the logger discards Info-level events. - * Instead, pass the URL directly: - * - * ``` - * slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed - * ``` - * - * The built-in [TextHandler] will call its String method, but only - * if the log event is enabled. - * Avoiding the call to String also preserves the structure of the underlying value. - * For example [JSONHandler] emits the components of the parsed URL as a JSON object. - * If you want to avoid eagerly paying the cost of the String call - * without causing the handler to potentially inspect the structure of the value, - * wrap the value in a fmt.Stringer implementation that hides its Marshal methods. - * - * You can also use the [LogValuer] interface to avoid unnecessary work in disabled log - * calls. Say you need to log some expensive value: - * - * ``` - * slog.Debug("frobbing", "value", computeExpensiveValue(arg)) - * ``` - * - * Even if this line is disabled, computeExpensiveValue will be called. - * To avoid that, define a type implementing LogValuer: - * - * ``` - * type expensive struct { arg int } - * - * func (e expensive) LogValue() slog.Value { - * return slog.AnyValue(computeExpensiveValue(e.arg)) - * } - * ``` - * - * Then use a value of that type in log calls: - * - * ``` - * slog.Debug("frobbing", "value", expensive{arg}) - * ``` - * - * Now computeExpensiveValue will only be called when the line is enabled. - * - * The built-in handlers acquire a lock before calling [io.Writer.Write] - * to ensure that each record is written in one piece. User-defined - * handlers are responsible for their own locking. - * - * # Writing a handler - * - * For a guide to writing a custom handler, see https://golang.org/s/slog-handler-guide. - */ -namespace slog { - // @ts-ignore - import loginternal = internal - /** - * A Logger records structured information about each call to its - * Log, Debug, Info, Warn, and Error methods. - * For each call, it creates a [Record] and passes it to a [Handler]. - * - * To create a new Logger, call [New] or a Logger method - * that begins "With". - */ - interface Logger { - } - interface Logger { - /** - * Handler returns l's Handler. - */ - handler(): Handler - } - interface Logger { - /** - * With returns a Logger that includes the given attributes - * in each output operation. Arguments are converted to - * attributes as if by [Logger.Log]. - */ - with(...args: any[]): (Logger) - } - interface Logger { - /** - * WithGroup returns a Logger that starts a group, if name is non-empty. - * The keys of all attributes added to the Logger will be qualified by the given - * name. (How that qualification happens depends on the [Handler.WithGroup] - * method of the Logger's Handler.) - * - * If name is empty, WithGroup returns the receiver. - */ - withGroup(name: string): (Logger) - } - interface Logger { - /** - * Enabled reports whether l emits log records at the given context and level. - */ - enabled(ctx: context.Context, level: Level): boolean - } - interface Logger { - /** - * Log emits a log record with the current time and the given level and message. - * The Record's Attrs consist of the Logger's attributes followed by - * the Attrs specified by args. - * - * The attribute arguments are processed as follows: - * ``` - * - If an argument is an Attr, it is used as is. - * - If an argument is a string and this is not the last argument, - * the following argument is treated as the value and the two are combined - * into an Attr. - * - Otherwise, the argument is treated as a value with key "!BADKEY". - * ``` - */ - log(ctx: context.Context, level: Level, msg: string, ...args: any[]): void - } - interface Logger { - /** - * LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs. - */ - logAttrs(ctx: context.Context, level: Level, msg: string, ...attrs: Attr[]): void - } - interface Logger { - /** - * Debug logs at [LevelDebug]. - */ - debug(msg: string, ...args: any[]): void - } - interface Logger { - /** - * DebugContext logs at [LevelDebug] with the given context. - */ - debugContext(ctx: context.Context, msg: string, ...args: any[]): void - } - interface Logger { - /** - * Info logs at [LevelInfo]. - */ - info(msg: string, ...args: any[]): void - } - interface Logger { - /** - * InfoContext logs at [LevelInfo] with the given context. - */ - infoContext(ctx: context.Context, msg: string, ...args: any[]): void - } - interface Logger { - /** - * Warn logs at [LevelWarn]. - */ - warn(msg: string, ...args: any[]): void - } - interface Logger { - /** - * WarnContext logs at [LevelWarn] with the given context. - */ - warnContext(ctx: context.Context, msg: string, ...args: any[]): void - } - interface Logger { - /** - * Error logs at [LevelError]. - */ - error(msg: string, ...args: any[]): void - } - interface Logger { - /** - * ErrorContext logs at [LevelError] with the given context. - */ - errorContext(ctx: context.Context, msg: string, ...args: any[]): void - } -} - -namespace subscriptions { - /** - * Broker defines a struct for managing subscriptions clients. - */ - interface Broker { - } - interface Broker { - /** - * Clients returns a shallow copy of all registered clients indexed - * with their connection id. - */ - clients(): _TygojaDict - } - interface Broker { - /** - * ClientById finds a registered client by its id. - * - * Returns non-nil error when client with clientId is not registered. - */ - clientById(clientId: string): Client - } - interface Broker { - /** - * Register adds a new client to the broker instance. - */ - register(client: Client): void - } - interface Broker { - /** - * Unregister removes a single client by its id. - * - * If client with clientId doesn't exist, this method does nothing. - */ - unregister(clientId: string): void - } -} - -/** - * Package core is the backbone of PocketBase. - * - * It defines the main PocketBase App interface and its base implementation. - */ -namespace core { - interface BootstrapEvent { - app: App - } - interface TerminateEvent { - app: App - isRestart: boolean - } - interface ServeEvent { - app: App - router?: echo.Echo - server?: http.Server - certManager?: any - } - interface ApiErrorEvent { - httpContext: echo.Context - error: Error - } - type _subIqXTj = BaseModelEvent - interface ModelEvent extends _subIqXTj { - dao?: daos.Dao - } - type _subYBsYP = BaseCollectionEvent - interface MailerRecordEvent extends _subYBsYP { - mailClient: mailer.Mailer - message?: mailer.Message - record?: models.Record - meta: _TygojaDict - } - interface MailerAdminEvent { - mailClient: mailer.Mailer - message?: mailer.Message - admin?: models.Admin - meta: _TygojaDict - } - interface RealtimeConnectEvent { - httpContext: echo.Context - client: subscriptions.Client - idleTimeout: time.Duration - } - interface RealtimeDisconnectEvent { - httpContext: echo.Context - client: subscriptions.Client - } - interface RealtimeMessageEvent { - httpContext: echo.Context - client: subscriptions.Client - message?: subscriptions.Message - } - interface RealtimeSubscribeEvent { - httpContext: echo.Context - client: subscriptions.Client - subscriptions: Array - } - interface SettingsListEvent { - httpContext: echo.Context - redactedSettings?: settings.Settings - } - interface SettingsUpdateEvent { - httpContext: echo.Context - oldSettings?: settings.Settings - newSettings?: settings.Settings - } - type _subLkmRK = BaseCollectionEvent - interface RecordsListEvent extends _subLkmRK { - httpContext: echo.Context - records: Array<(models.Record | undefined)> - result?: search.Result - } - type _subrYldn = BaseCollectionEvent - interface RecordViewEvent extends _subrYldn { - httpContext: echo.Context - record?: models.Record - } - type _subShsfQ = BaseCollectionEvent - interface RecordCreateEvent extends _subShsfQ { - httpContext: echo.Context - record?: models.Record - uploadedFiles: _TygojaDict - } - type _subBQgiv = BaseCollectionEvent - interface RecordUpdateEvent extends _subBQgiv { - httpContext: echo.Context - record?: models.Record - uploadedFiles: _TygojaDict - } - type _subwKNJA = BaseCollectionEvent - interface RecordDeleteEvent extends _subwKNJA { - httpContext: echo.Context - record?: models.Record - } - type _subNVvZz = BaseCollectionEvent - interface RecordAuthEvent extends _subNVvZz { - httpContext: echo.Context - record?: models.Record - token: string - meta: any - } - type _subxBJeP = BaseCollectionEvent - interface RecordAuthWithPasswordEvent extends _subxBJeP { - httpContext: echo.Context - record?: models.Record - identity: string - password: string - } - type _subUSiim = BaseCollectionEvent - interface RecordAuthWithOAuth2Event extends _subUSiim { - httpContext: echo.Context - providerName: string - providerClient: auth.Provider - record?: models.Record - oAuth2User?: auth.AuthUser - isNewRecord: boolean - } - type _subdXIjq = BaseCollectionEvent - interface RecordAuthRefreshEvent extends _subdXIjq { - httpContext: echo.Context - record?: models.Record - } - type _subvFage = BaseCollectionEvent - interface RecordRequestPasswordResetEvent extends _subvFage { - httpContext: echo.Context - record?: models.Record - } - type _subojVuh = BaseCollectionEvent - interface RecordConfirmPasswordResetEvent extends _subojVuh { - httpContext: echo.Context - record?: models.Record - } - type _subhRgnz = BaseCollectionEvent - interface RecordRequestVerificationEvent extends _subhRgnz { - httpContext: echo.Context - record?: models.Record - } - type _subOEyNW = BaseCollectionEvent - interface RecordConfirmVerificationEvent extends _subOEyNW { - httpContext: echo.Context - record?: models.Record - } - type _subJiOHg = BaseCollectionEvent - interface RecordRequestEmailChangeEvent extends _subJiOHg { - httpContext: echo.Context - record?: models.Record - } - type _suboVOGS = BaseCollectionEvent - interface RecordConfirmEmailChangeEvent extends _suboVOGS { - httpContext: echo.Context - record?: models.Record - } - type _subQMnYt = BaseCollectionEvent - interface RecordListExternalAuthsEvent extends _subQMnYt { - httpContext: echo.Context - record?: models.Record - externalAuths: Array<(models.ExternalAuth | undefined)> - } - type _subAmSHR = BaseCollectionEvent - interface RecordUnlinkExternalAuthEvent extends _subAmSHR { - httpContext: echo.Context - record?: models.Record - externalAuth?: models.ExternalAuth - } - interface AdminsListEvent { - httpContext: echo.Context - admins: Array<(models.Admin | undefined)> - result?: search.Result - } - interface AdminViewEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminCreateEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminUpdateEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminDeleteEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminAuthEvent { - httpContext: echo.Context - admin?: models.Admin - token: string - } - interface AdminAuthWithPasswordEvent { - httpContext: echo.Context - admin?: models.Admin - identity: string - password: string - } - interface AdminAuthRefreshEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminRequestPasswordResetEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface AdminConfirmPasswordResetEvent { - httpContext: echo.Context - admin?: models.Admin - } - interface CollectionsListEvent { - httpContext: echo.Context - collections: Array<(models.Collection | undefined)> - result?: search.Result - } - type _subsmzBX = BaseCollectionEvent - interface CollectionViewEvent extends _subsmzBX { - httpContext: echo.Context - } - type _subErkii = BaseCollectionEvent - interface CollectionCreateEvent extends _subErkii { - httpContext: echo.Context - } - type _subwhxGp = BaseCollectionEvent - interface CollectionUpdateEvent extends _subwhxGp { - httpContext: echo.Context - } - type _subLGwgi = BaseCollectionEvent - interface CollectionDeleteEvent extends _subLGwgi { - httpContext: echo.Context - } - interface CollectionsImportEvent { - httpContext: echo.Context - collections: Array<(models.Collection | undefined)> - } - type _subJwbPo = BaseModelEvent - interface FileTokenEvent extends _subJwbPo { - httpContext: echo.Context - token: string - } - type _subcppDF = BaseCollectionEvent - interface FileDownloadEvent extends _subcppDF { - httpContext: echo.Context - record?: models.Record - fileField?: schema.SchemaField - servedPath: string - servedName: string - } -} - -/** - * Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. - * In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code. - */ -namespace cobra { - interface PositionalArgs {(cmd: Command, args: Array): void } - // @ts-ignore - import flag = pflag - /** - * FParseErrWhitelist configures Flag parse errors to be ignored - */ - interface FParseErrWhitelist extends _TygojaAny{} - /** - * Group Structure to manage groups for commands - */ - interface Group { - id: string - title: string - } - /** - * ShellCompDirective is a bit map representing the different behaviors the shell - * can be instructed to have once completions have been provided. - */ - interface ShellCompDirective extends Number{} - /** - * CompletionOptions are the options to control shell completion - */ - interface CompletionOptions { - /** - * DisableDefaultCmd prevents Cobra from creating a default 'completion' command - */ - disableDefaultCmd: boolean - /** - * DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag - * for shells that support completion descriptions - */ - disableNoDescFlag: boolean - /** - * DisableDescriptions turns off all completion descriptions for shells - * that support them - */ - disableDescriptions: boolean - /** - * HiddenDefaultCmd makes the default 'completion' command hidden - */ - hiddenDefaultCmd: boolean - } -} - -namespace migrate { - interface Migration { - file: string - up: (db: dbx.Builder) => void - down: (db: dbx.Builder) => void - } -} - -/** - * Package url parses URLs and implements query escaping. - */ -namespace url { - /** - * The Userinfo type is an immutable encapsulation of username and - * password details for a [URL]. An existing Userinfo value is guaranteed - * to have a username set (potentially empty, as allowed by RFC 2396), - * and optionally a password. - */ - interface Userinfo { - } - interface Userinfo { - /** - * Username returns the username. - */ - username(): string - } - interface Userinfo { - /** - * Password returns the password in case it is set, and whether it is set. - */ - password(): [string, boolean] - } - interface Userinfo { - /** - * String returns the encoded userinfo information in the standard form - * of "username[:password]". - */ - string(): string - } -} - -/** - * Package net provides a portable interface for network I/O, including - * TCP/IP, UDP, domain name resolution, and Unix domain sockets. - * - * Although the package provides access to low-level networking - * primitives, most clients will need only the basic interface provided - * by the [Dial], [Listen], and Accept functions and the associated - * [Conn] and [Listener] interfaces. The crypto/tls package uses - * the same interfaces and similar Dial and Listen functions. - * - * The Dial function connects to a server: - * - * ``` - * conn, err := net.Dial("tcp", "golang.org:80") - * if err != nil { - * // handle error - * } - * fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") - * status, err := bufio.NewReader(conn).ReadString('\n') - * // ... - * ``` - * - * The Listen function creates servers: - * - * ``` - * ln, err := net.Listen("tcp", ":8080") - * if err != nil { - * // handle error - * } - * for { - * conn, err := ln.Accept() - * if err != nil { - * // handle error - * } - * go handleConnection(conn) - * } - * ``` - * - * # Name Resolution - * - * The method for resolving domain names, whether indirectly with functions like Dial - * or directly with functions like [LookupHost] and [LookupAddr], varies by operating system. - * - * On Unix systems, the resolver has two options for resolving names. - * It can use a pure Go resolver that sends DNS requests directly to the servers - * listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C - * library routines such as getaddrinfo and getnameinfo. - * - * By default the pure Go resolver is used, because a blocked DNS request consumes - * only a goroutine, while a blocked C call consumes an operating system thread. - * When cgo is available, the cgo-based resolver is used instead under a variety of - * conditions: on systems that do not let programs make direct DNS requests (OS X), - * when the LOCALDOMAIN environment variable is present (even if empty), - * when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, - * when the ASR_CONFIG environment variable is non-empty (OpenBSD only), - * when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the - * Go resolver does not implement, and when the name being looked up ends in .local - * or is an mDNS name. - * - * The resolver decision can be overridden by setting the netdns value of the - * GODEBUG environment variable (see package runtime) to go or cgo, as in: - * - * ``` - * export GODEBUG=netdns=go # force pure Go resolver - * export GODEBUG=netdns=cgo # force native resolver (cgo, win32) - * ``` - * - * The decision can also be forced while building the Go source tree - * by setting the netgo or netcgo build tag. - * - * A numeric netdns setting, as in GODEBUG=netdns=1, causes the resolver - * to print debugging information about its decisions. - * To force a particular resolver while also printing debugging information, - * join the two settings by a plus sign, as in GODEBUG=netdns=go+1. - * - * On macOS, if Go code that uses the net package is built with - * -buildmode=c-archive, linking the resulting archive into a C program - * requires passing -lresolv when linking the C code. - * - * On Plan 9, the resolver always accesses /net/cs and /net/dns. - * - * On Windows, in Go 1.18.x and earlier, the resolver always used C - * library functions, such as GetAddrInfo and DnsQuery. - */ -namespace net { - /** - * Addr represents a network end point address. - * - * The two methods [Addr.Network] and [Addr.String] conventionally return strings - * that can be passed as the arguments to [Dial], but the exact form - * and meaning of the strings is up to the implementation. - */ - interface Addr { - [key:string]: any; - network(): string // name of the network (for example, "tcp", "udp") - string(): string // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80") - } -} - -/** - * Package types implements some commonly used db serializable types - * like datetime, json, etc. - */ -namespace types { - /** - * JsonRaw defines a json value type that is safe for db read/write. - */ - interface JsonRaw extends Array{} - interface JsonRaw { - /** - * String returns the current JsonRaw instance as a json encoded string. - */ - string(): string - } - interface JsonRaw { - /** - * MarshalJSON implements the [json.Marshaler] interface. - */ - marshalJSON(): string|Array - } - interface JsonRaw { - /** - * UnmarshalJSON implements the [json.Unmarshaler] interface. - */ - unmarshalJSON(b: string|Array): void - } - interface JsonRaw { - /** - * Value implements the [driver.Valuer] interface. - */ - value(): any - } - interface JsonRaw { - /** - * Scan implements [sql.Scanner] interface to scan the provided value - * into the current JsonRaw instance. - */ - scan(value: any): void - } -} - -/** - * Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer - * object, creating another object (Reader or Writer) that also implements - * the interface but provides buffering and some help for textual I/O. - */ -namespace bufio { - /** - * ReadWriter stores pointers to a [Reader] and a [Writer]. - * It implements [io.ReadWriter]. - */ - type _subjuTbM = Reader&Writer - interface ReadWriter extends _subjuTbM { - } -} - -/** - * Package multipart implements MIME multipart parsing, as defined in RFC - * 2046. - * - * The implementation is sufficient for HTTP (RFC 2388) and the multipart - * bodies generated by popular browsers. - * - * # Limits - * - * To protect against malicious inputs, this package sets limits on the size - * of the MIME data it processes. - * - * Reader.NextPart and Reader.NextRawPart limit the number of headers in a - * part to 10000 and Reader.ReadForm limits the total number of headers in all - * FileHeaders to 10000. - * These limits may be adjusted with the GODEBUG=multipartmaxheaders= - * setting. - * - * Reader.ReadForm further limits the number of parts in a form to 1000. - * This limit may be adjusted with the GODEBUG=multipartmaxparts= - * setting. - */ -/** - * Copyright 2023 The Go Authors. All rights reserved. - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ -namespace multipart { - /** - * A Part represents a single part in a multipart body. - */ - interface Part { - /** - * The headers of the body, if any, with the keys canonicalized - * in the same fashion that the Go http.Request headers are. - * For example, "foo-bar" changes case to "Foo-Bar" - */ - header: textproto.MIMEHeader - } - interface Part { - /** - * FormName returns the name parameter if p has a Content-Disposition - * of type "form-data". Otherwise it returns the empty string. - */ - formName(): string - } - interface Part { - /** - * FileName returns the filename parameter of the Part's Content-Disposition - * header. If not empty, the filename is passed through filepath.Base (which is - * platform dependent) before being returned. - */ - fileName(): string - } - interface Part { - /** - * Read reads the body of a part, after its headers and before the - * next part (if any) begins. - */ - read(d: string|Array): number - } - interface Part { - close(): void - } -} - -/** - * Package http provides HTTP client and server implementations. - * - * [Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests: - * - * ``` - * resp, err := http.Get("http://example.com/") - * ... - * resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) - * ... - * resp, err := http.PostForm("http://example.com/form", - * url.Values{"key": {"Value"}, "id": {"123"}}) - * ``` - * - * The caller must close the response body when finished with it: - * - * ``` - * resp, err := http.Get("http://example.com/") - * if err != nil { - * // handle error - * } - * defer resp.Body.Close() - * body, err := io.ReadAll(resp.Body) - * // ... - * ``` - * - * # Clients and Transports - * - * For control over HTTP client headers, redirect policy, and other - * settings, create a [Client]: - * - * ``` - * client := &http.Client{ - * CheckRedirect: redirectPolicyFunc, - * } - * - * resp, err := client.Get("http://example.com") - * // ... - * - * req, err := http.NewRequest("GET", "http://example.com", nil) - * // ... - * req.Header.Add("If-None-Match", `W/"wyzzy"`) - * resp, err := client.Do(req) - * // ... - * ``` - * - * For control over proxies, TLS configuration, keep-alives, - * compression, and other settings, create a [Transport]: - * - * ``` - * tr := &http.Transport{ - * MaxIdleConns: 10, - * IdleConnTimeout: 30 * time.Second, - * DisableCompression: true, - * } - * client := &http.Client{Transport: tr} - * resp, err := client.Get("https://example.com") - * ``` - * - * Clients and Transports are safe for concurrent use by multiple - * goroutines and for efficiency should only be created once and re-used. - * - * # Servers - * - * ListenAndServe starts an HTTP server with a given address and handler. - * The handler is usually nil, which means to use [DefaultServeMux]. - * [Handle] and [HandleFunc] add handlers to [DefaultServeMux]: - * - * ``` - * http.Handle("/foo", fooHandler) - * - * http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { - * fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - * }) - * - * log.Fatal(http.ListenAndServe(":8080", nil)) - * ``` - * - * More control over the server's behavior is available by creating a - * custom Server: - * - * ``` - * s := &http.Server{ - * Addr: ":8080", - * Handler: myHandler, - * ReadTimeout: 10 * time.Second, - * WriteTimeout: 10 * time.Second, - * MaxHeaderBytes: 1 << 20, - * } - * log.Fatal(s.ListenAndServe()) - * ``` - * - * # HTTP/2 - * - * Starting with Go 1.6, the http package has transparent support for the - * HTTP/2 protocol when using HTTPS. Programs that must disable HTTP/2 - * can do so by setting [Transport.TLSNextProto] (for clients) or - * [Server.TLSNextProto] (for servers) to a non-nil, empty - * map. Alternatively, the following GODEBUG settings are - * currently supported: - * - * ``` - * GODEBUG=http2client=0 # disable HTTP/2 client support - * GODEBUG=http2server=0 # disable HTTP/2 server support - * GODEBUG=http2debug=1 # enable verbose HTTP/2 debug logs - * GODEBUG=http2debug=2 # ... even more verbose, with frame dumps - * ``` - * - * Please report any issues before disabling HTTP/2 support: https://golang.org/s/http2bug - * - * The http package's [Transport] and [Server] both automatically enable - * HTTP/2 support for simple configurations. To enable HTTP/2 for more - * complex configurations, to use lower-level HTTP/2 features, or to use - * a newer version of Go's http2 package, import "golang.org/x/net/http2" - * directly and use its ConfigureTransport and/or ConfigureServer - * functions. Manually configuring HTTP/2 via the golang.org/x/net/http2 - * package takes precedence over the net/http package's built-in HTTP/2 - * support. - */ -namespace http { - /** - * SameSite allows a server to define a cookie attribute making it impossible for - * the browser to send this cookie along with cross-site requests. The main - * goal is to mitigate the risk of cross-origin information leakage, and provide - * some protection against cross-site request forgery attacks. - * - * See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. - */ - interface SameSite extends Number{} - // @ts-ignore - import mathrand = rand - // @ts-ignore - import urlpkg = url -} - -namespace store { -} - -namespace mailer { - /** - * Message defines a generic email message struct. - */ - interface Message { - from: mail.Address - to: Array - bcc: Array - cc: Array - subject: string - html: string - text: string - headers: _TygojaDict - attachments: _TygojaDict - } -} - -/** - * Package echo implements high performance, minimalist Go web framework. - * - * Example: - * - * ``` - * package main - * - * import ( - * "github.com/labstack/echo/v5" - * "github.com/labstack/echo/v5/middleware" - * "log" - * "net/http" - * ) - * - * // Handler - * func hello(c echo.Context) error { - * return c.String(http.StatusOK, "Hello, World!") - * } - * - * func main() { - * // Echo instance - * e := echo.New() - * - * // Middleware - * e.Use(middleware.Logger()) - * e.Use(middleware.Recover()) - * - * // Routes - * e.GET("/", hello) - * - * // Start server - * if err := e.Start(":8080"); err != http.ErrServerClosed { - * log.Fatal(err) - * } - * } - * ``` - * - * Learn more at https://echo.labstack.com - */ -namespace echo { - // @ts-ignore - import stdContext = context - /** - * Route contains information to adding/registering new route with the router. - * Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path+Handler fields. - */ - interface Route { - method: string - path: string - handler: HandlerFunc - middlewares: Array - name: string - } - interface Route { - /** - * ToRouteInfo converts Route to RouteInfo - */ - toRouteInfo(params: Array): RouteInfo - } - interface Route { - /** - * ToRoute returns Route which Router uses to register the method handler for path. - */ - toRoute(): Route - } - interface Route { - /** - * ForGroup recreates Route with added group prefix and group middlewares it is grouped to. - */ - forGroup(pathPrefix: string, middlewares: Array): Routable - } - /** - * RoutableContext is additional interface that structures implementing Context must implement. Methods inside this - * interface are meant for request routing purposes and should not be used in middlewares. - */ - interface RoutableContext { - [key:string]: any; - /** - * Request returns `*http.Request`. - */ - request(): (http.Request) - /** - * RawPathParams returns raw path pathParams value. Allocation of PathParams is handled by Context. - */ - rawPathParams(): (PathParams) - /** - * SetRawPathParams replaces any existing param values with new values for this context lifetime (request). - * Do not set any other value than what you got from RawPathParams as allocation of PathParams is handled by Context. - */ - setRawPathParams(params: PathParams): void - /** - * SetPath sets the registered path for the handler. - */ - setPath(p: string): void - /** - * SetRouteInfo sets the route info of this request to the context. - */ - setRouteInfo(ri: RouteInfo): void - /** - * Set saves data in the context. Allows router to store arbitrary (that only router has access to) data in context - * for later use in middlewares/handler. - */ - set(key: string, val: { - }): void - } - /** - * PathParam is tuple pf path parameter name and its value in request path - */ - interface PathParam { - name: string - value: string - } -} - -namespace search { - /** - * Result defines the returned search result structure. - */ - interface Result { - page: number - perPage: number - totalItems: number - totalPages: number - items: any - } -} - -namespace settings { - // @ts-ignore - import validation = ozzo_validation - interface EmailTemplate { - body: string - subject: string - actionUrl: string - hidden: boolean - } - interface EmailTemplate { - /** - * Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface. - */ - validate(): void - } - interface EmailTemplate { - /** - * Resolve replaces the placeholder parameters in the current email - * template and returns its components as ready-to-use strings. - */ - resolve(appName: string, appUrl: string, token: string): string - } -} - -namespace subscriptions { - /** - * Message defines a client's channel data. - */ - interface Message { - name: string - data: string|Array - } - /** - * Client is an interface for a generic subscription client. - */ - interface Client { - [key:string]: any; - /** - * Id Returns the unique id of the client. - */ - id(): string - /** - * Channel returns the client's communication channel. - */ - channel(): undefined - /** - * Subscriptions returns a shallow copy of the client subscriptions matching the prefixes. - * If no prefix is specified, returns all subscriptions. - */ - subscriptions(...prefixes: string[]): _TygojaDict - /** - * Subscribe subscribes the client to the provided subscriptions list. - * - * Each subscription can also have "options" (json serialized SubscriptionOptions) as query parameter. - * - * Example: - * - * ``` - * Subscribe( - * "subscriptionA", - * `subscriptionB?options={"query":{"a":1},"headers":{"x_token":"abc"}}`, - * ) - * ``` - */ - subscribe(...subs: string[]): void - /** - * Unsubscribe unsubscribes the client from the provided subscriptions list. - */ - unsubscribe(...subs: string[]): void - /** - * HasSubscription checks if the client is subscribed to `sub`. - */ - hasSubscription(sub: string): boolean - /** - * Set stores any value to the client's context. - */ - set(key: string, value: any): void - /** - * Unset removes a single value from the client's context. - */ - unset(key: string): void - /** - * Get retrieves the key value from the client's context. - */ - get(key: string): any - /** - * Discard marks the client as "discarded", meaning that it - * shouldn't be used anymore for sending new messages. - * - * It is safe to call Discard() multiple times. - */ - discard(): void - /** - * IsDiscarded indicates whether the client has been "discarded" - * and should no longer be used. - */ - isDiscarded(): boolean - /** - * Send sends the specified message to the client's channel (if not discarded). - */ - send(m: Message): void - } -} - -/** - * Package slog provides structured logging, - * in which log records include a message, - * a severity level, and various other attributes - * expressed as key-value pairs. - * - * It defines a type, [Logger], - * which provides several methods (such as [Logger.Info] and [Logger.Error]) - * for reporting events of interest. - * - * Each Logger is associated with a [Handler]. - * A Logger output method creates a [Record] from the method arguments - * and passes it to the Handler, which decides how to handle it. - * There is a default Logger accessible through top-level functions - * (such as [Info] and [Error]) that call the corresponding Logger methods. - * - * A log record consists of a time, a level, a message, and a set of key-value - * pairs, where the keys are strings and the values may be of any type. - * As an example, - * - * ``` - * slog.Info("hello", "count", 3) - * ``` - * - * creates a record containing the time of the call, - * a level of Info, the message "hello", and a single - * pair with key "count" and value 3. - * - * The [Info] top-level function calls the [Logger.Info] method on the default Logger. - * In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. - * Besides these convenience methods for common levels, - * there is also a [Logger.Log] method which takes the level as an argument. - * Each of these methods has a corresponding top-level function that uses the - * default logger. - * - * The default handler formats the log record's message, time, level, and attributes - * as a string and passes it to the [log] package. - * - * ``` - * 2022/11/08 15:28:26 INFO hello count=3 - * ``` - * - * For more control over the output format, create a logger with a different handler. - * This statement uses [New] to create a new logger with a [TextHandler] - * that writes structured records in text form to standard error: - * - * ``` - * logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) - * ``` - * - * [TextHandler] output is a sequence of key=value pairs, easily and unambiguously - * parsed by machine. This statement: - * - * ``` - * logger.Info("hello", "count", 3) - * ``` - * - * produces this output: - * - * ``` - * time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 - * ``` - * - * The package also provides [JSONHandler], whose output is line-delimited JSON: - * - * ``` - * logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - * logger.Info("hello", "count", 3) - * ``` - * - * produces this output: - * - * ``` - * {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} - * ``` - * - * Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. - * There are options for setting the minimum level (see Levels, below), - * displaying the source file and line of the log call, and - * modifying attributes before they are logged. - * - * Setting a logger as the default with - * - * ``` - * slog.SetDefault(logger) - * ``` - * - * will cause the top-level functions like [Info] to use it. - * [SetDefault] also updates the default logger used by the [log] package, - * so that existing applications that use [log.Printf] and related functions - * will send log records to the logger's handler without needing to be rewritten. - * - * Some attributes are common to many log calls. - * For example, you may wish to include the URL or trace identifier of a server request - * with all log events arising from the request. - * Rather than repeat the attribute with every log call, you can use [Logger.With] - * to construct a new Logger containing the attributes: - * - * ``` - * logger2 := logger.With("url", r.URL) - * ``` - * - * The arguments to With are the same key-value pairs used in [Logger.Info]. - * The result is a new Logger with the same handler as the original, but additional - * attributes that will appear in the output of every call. - * - * # Levels - * - * A [Level] is an integer representing the importance or severity of a log event. - * The higher the level, the more severe the event. - * This package defines constants for the most common levels, - * but any int can be used as a level. - * - * In an application, you may wish to log messages only at a certain level or greater. - * One common configuration is to log messages at Info or higher levels, - * suppressing debug logging until it is needed. - * The built-in handlers can be configured with the minimum level to output by - * setting [HandlerOptions.Level]. - * The program's `main` function typically does this. - * The default value is LevelInfo. - * - * Setting the [HandlerOptions.Level] field to a [Level] value - * fixes the handler's minimum level throughout its lifetime. - * Setting it to a [LevelVar] allows the level to be varied dynamically. - * A LevelVar holds a Level and is safe to read or write from multiple - * goroutines. - * To vary the level dynamically for an entire program, first initialize - * a global LevelVar: - * - * ``` - * var programLevel = new(slog.LevelVar) // Info by default - * ``` - * - * Then use the LevelVar to construct a handler, and make it the default: - * - * ``` - * h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) - * slog.SetDefault(slog.New(h)) - * ``` - * - * Now the program can change its logging level with a single statement: - * - * ``` - * programLevel.Set(slog.LevelDebug) - * ``` - * - * # Groups - * - * Attributes can be collected into groups. - * A group has a name that is used to qualify the names of its attributes. - * How this qualification is displayed depends on the handler. - * [TextHandler] separates the group and attribute names with a dot. - * [JSONHandler] treats each group as a separate JSON object, with the group name as the key. - * - * Use [Group] to create a Group attribute from a name and a list of key-value pairs: - * - * ``` - * slog.Group("request", - * "method", r.Method, - * "url", r.URL) - * ``` - * - * TextHandler would display this group as - * - * ``` - * request.method=GET request.url=http://example.com - * ``` - * - * JSONHandler would display it as - * - * ``` - * "request":{"method":"GET","url":"http://example.com"} - * ``` - * - * Use [Logger.WithGroup] to qualify all of a Logger's output - * with a group name. Calling WithGroup on a Logger results in a - * new Logger with the same Handler as the original, but with all - * its attributes qualified by the group name. - * - * This can help prevent duplicate attribute keys in large systems, - * where subsystems might use the same keys. - * Pass each subsystem a different Logger with its own group name so that - * potential duplicates are qualified: - * - * ``` - * logger := slog.Default().With("id", systemID) - * parserLogger := logger.WithGroup("parser") - * parseInput(input, parserLogger) - * ``` - * - * When parseInput logs with parserLogger, its keys will be qualified with "parser", - * so even if it uses the common key "id", the log line will have distinct keys. - * - * # Contexts - * - * Some handlers may wish to include information from the [context.Context] that is - * available at the call site. One example of such information - * is the identifier for the current span when tracing is enabled. - * - * The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first - * argument, as do their corresponding top-level functions. - * - * Although the convenience methods on Logger (Info and so on) and the - * corresponding top-level functions do not take a context, the alternatives ending - * in "Context" do. For example, - * - * ``` - * slog.InfoContext(ctx, "message") - * ``` - * - * It is recommended to pass a context to an output method if one is available. - * - * # Attrs and Values - * - * An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as - * alternating keys and values. The statement - * - * ``` - * slog.Info("hello", slog.Int("count", 3)) - * ``` - * - * behaves the same as - * - * ``` - * slog.Info("hello", "count", 3) - * ``` - * - * There are convenience constructors for [Attr] such as [Int], [String], and [Bool] - * for common types, as well as the function [Any] for constructing Attrs of any - * type. - * - * The value part of an Attr is a type called [Value]. - * Like an [any], a Value can hold any Go value, - * but it can represent typical values, including all numbers and strings, - * without an allocation. - * - * For the most efficient log output, use [Logger.LogAttrs]. - * It is similar to [Logger.Log] but accepts only Attrs, not alternating - * keys and values; this allows it, too, to avoid allocation. - * - * The call - * - * ``` - * logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3)) - * ``` - * - * is the most efficient way to achieve the same output as - * - * ``` - * slog.InfoContext(ctx, "hello", "count", 3) - * ``` - * - * # Customizing a type's logging behavior - * - * If a type implements the [LogValuer] interface, the [Value] returned from its LogValue - * method is used for logging. You can use this to control how values of the type - * appear in logs. For example, you can redact secret information like passwords, - * or gather a struct's fields in a Group. See the examples under [LogValuer] for - * details. - * - * A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] - * method handles these cases carefully, avoiding infinite loops and unbounded recursion. - * Handler authors and others may wish to use [Value.Resolve] instead of calling LogValue directly. - * - * # Wrapping output methods - * - * The logger functions use reflection over the call stack to find the file name - * and line number of the logging call within the application. This can produce - * incorrect source information for functions that wrap slog. For instance, if you - * define this function in file mylog.go: - * - * ``` - * func Infof(logger *slog.Logger, format string, args ...any) { - * logger.Info(fmt.Sprintf(format, args...)) - * } - * ``` - * - * and you call it like this in main.go: - * - * ``` - * Infof(slog.Default(), "hello, %s", "world") - * ``` - * - * then slog will report the source file as mylog.go, not main.go. - * - * A correct implementation of Infof will obtain the source location - * (pc) and pass it to NewRecord. - * The Infof function in the package-level example called "wrapping" - * demonstrates how to do this. - * - * # Working with Records - * - * Sometimes a Handler will need to modify a Record - * before passing it on to another Handler or backend. - * A Record contains a mixture of simple public fields (e.g. Time, Level, Message) - * and hidden fields that refer to state (such as attributes) indirectly. This - * means that modifying a simple copy of a Record (e.g. by calling - * [Record.Add] or [Record.AddAttrs] to add attributes) - * may have unexpected effects on the original. - * Before modifying a Record, use [Record.Clone] to - * create a copy that shares no state with the original, - * or create a new Record with [NewRecord] - * and build up its Attrs by traversing the old ones with [Record.Attrs]. - * - * # Performance considerations - * - * If profiling your application demonstrates that logging is taking significant time, - * the following suggestions may help. - * - * If many log lines have a common attribute, use [Logger.With] to create a Logger with - * that attribute. The built-in handlers will format that attribute only once, at the - * call to [Logger.With]. The [Handler] interface is designed to allow that optimization, - * and a well-written Handler should take advantage of it. - * - * The arguments to a log call are always evaluated, even if the log event is discarded. - * If possible, defer computation so that it happens only if the value is actually logged. - * For example, consider the call - * - * ``` - * slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily - * ``` - * - * The URL.String method will be called even if the logger discards Info-level events. - * Instead, pass the URL directly: - * - * ``` - * slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed - * ``` - * - * The built-in [TextHandler] will call its String method, but only - * if the log event is enabled. - * Avoiding the call to String also preserves the structure of the underlying value. - * For example [JSONHandler] emits the components of the parsed URL as a JSON object. - * If you want to avoid eagerly paying the cost of the String call - * without causing the handler to potentially inspect the structure of the value, - * wrap the value in a fmt.Stringer implementation that hides its Marshal methods. - * - * You can also use the [LogValuer] interface to avoid unnecessary work in disabled log - * calls. Say you need to log some expensive value: - * - * ``` - * slog.Debug("frobbing", "value", computeExpensiveValue(arg)) - * ``` - * - * Even if this line is disabled, computeExpensiveValue will be called. - * To avoid that, define a type implementing LogValuer: - * - * ``` - * type expensive struct { arg int } - * - * func (e expensive) LogValue() slog.Value { - * return slog.AnyValue(computeExpensiveValue(e.arg)) - * } - * ``` - * - * Then use a value of that type in log calls: - * - * ``` - * slog.Debug("frobbing", "value", expensive{arg}) - * ``` - * - * Now computeExpensiveValue will only be called when the line is enabled. - * - * The built-in handlers acquire a lock before calling [io.Writer.Write] - * to ensure that each record is written in one piece. User-defined - * handlers are responsible for their own locking. - * - * # Writing a handler - * - * For a guide to writing a custom handler, see https://golang.org/s/slog-handler-guide. - */ -namespace slog { - /** - * An Attr is a key-value pair. - */ - interface Attr { - key: string - value: Value - } - interface Attr { - /** - * Equal reports whether a and b have equal keys and values. - */ - equal(b: Attr): boolean - } - interface Attr { - string(): string - } - /** - * A Handler handles log records produced by a Logger. - * - * A typical handler may print log records to standard error, - * or write them to a file or database, or perhaps augment them - * with additional attributes and pass them on to another handler. - * - * Any of the Handler's methods may be called concurrently with itself - * or with other methods. It is the responsibility of the Handler to - * manage this concurrency. - * - * Users of the slog package should not invoke Handler methods directly. - * They should use the methods of [Logger] instead. - */ - interface Handler { - [key:string]: any; - /** - * Enabled reports whether the handler handles records at the given level. - * The handler ignores records whose level is lower. - * It is called early, before any arguments are processed, - * to save effort if the log event should be discarded. - * If called from a Logger method, the first argument is the context - * passed to that method, or context.Background() if nil was passed - * or the method does not take a context. - * The context is passed so Enabled can use its values - * to make a decision. - */ - enabled(_arg0: context.Context, _arg1: Level): boolean - /** - * Handle handles the Record. - * It will only be called when Enabled returns true. - * The Context argument is as for Enabled. - * It is present solely to provide Handlers access to the context's values. - * Canceling the context should not affect record processing. - * (Among other things, log messages may be necessary to debug a - * cancellation-related problem.) - * - * Handle methods that produce output should observe the following rules: - * ``` - * - If r.Time is the zero time, ignore the time. - * - If r.PC is zero, ignore it. - * - Attr's values should be resolved. - * - If an Attr's key and value are both the zero value, ignore the Attr. - * This can be tested with attr.Equal(Attr{}). - * - If a group's key is empty, inline the group's Attrs. - * - If a group has no Attrs (even if it has a non-empty key), - * ignore it. - * ``` - */ - handle(_arg0: context.Context, _arg1: Record): void - /** - * WithAttrs returns a new Handler whose attributes consist of - * both the receiver's attributes and the arguments. - * The Handler owns the slice: it may retain, modify or discard it. - */ - withAttrs(attrs: Array): Handler - /** - * WithGroup returns a new Handler with the given group appended to - * the receiver's existing groups. - * The keys of all subsequent attributes, whether added by With or in a - * Record, should be qualified by the sequence of group names. - * - * How this qualification happens is up to the Handler, so long as - * this Handler's attribute keys differ from those of another Handler - * with a different sequence of group names. - * - * A Handler should treat WithGroup as starting a Group of Attrs that ends - * at the end of the log event. That is, - * - * ``` - * logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2)) - * ``` - * - * should behave like - * - * ``` - * logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) - * ``` - * - * If the name is empty, WithGroup returns the receiver. - */ - withGroup(name: string): Handler - } - /** - * A Level is the importance or severity of a log event. - * The higher the level, the more important or severe the event. - */ - interface Level extends Number{} - interface Level { - /** - * String returns a name for the level. - * If the level has a name, then that name - * in uppercase is returned. - * If the level is between named values, then - * an integer is appended to the uppercased name. - * Examples: - * - * ``` - * LevelWarn.String() => "WARN" - * (LevelInfo+2).String() => "INFO+2" - * ``` - */ - string(): string - } - interface Level { - /** - * MarshalJSON implements [encoding/json.Marshaler] - * by quoting the output of [Level.String]. - */ - marshalJSON(): string|Array - } - interface Level { - /** - * UnmarshalJSON implements [encoding/json.Unmarshaler] - * It accepts any string produced by [Level.MarshalJSON], - * ignoring case. - * It also accepts numeric offsets that would result in a different string on - * output. For example, "Error-8" would marshal as "INFO". - */ - unmarshalJSON(data: string|Array): void - } - interface Level { - /** - * MarshalText implements [encoding.TextMarshaler] - * by calling [Level.String]. - */ - marshalText(): string|Array - } - interface Level { - /** - * UnmarshalText implements [encoding.TextUnmarshaler]. - * It accepts any string produced by [Level.MarshalText], - * ignoring case. - * It also accepts numeric offsets that would result in a different string on - * output. For example, "Error-8" would marshal as "INFO". - */ - unmarshalText(data: string|Array): void - } - interface Level { - /** - * Level returns the receiver. - * It implements [Leveler]. - */ - level(): Level - } - // @ts-ignore - import loginternal = internal -} - -namespace hook { - /** - * Handler defines a hook handler function. - */ - interface Handler {(e: T): void } - /** - * wrapped local Hook embedded struct to limit the public API surface. - */ - type _subBldse = Hook - interface mainHook extends _subBldse { - } -} - -/** - * Package core is the backbone of PocketBase. - * - * It defines the main PocketBase App interface and its base implementation. - */ -namespace core { - interface BaseModelEvent { - model: models.Model - } - interface BaseModelEvent { - tags(): Array - } - interface BaseCollectionEvent { - collection?: models.Collection - } - interface BaseCollectionEvent { - tags(): Array - } -} - /** * Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer * object, creating another object (Reader or Writer) that also implements @@ -19600,52 +14732,975 @@ namespace bufio { } /** - * Package mail implements parsing of mail messages. + * Package net provides a portable interface for network I/O, including + * TCP/IP, UDP, domain name resolution, and Unix domain sockets. + * + * Although the package provides access to low-level networking + * primitives, most clients will need only the basic interface provided + * by the [Dial], [Listen], and Accept functions and the associated + * [Conn] and [Listener] interfaces. The crypto/tls package uses + * the same interfaces and similar Dial and Listen functions. + * + * The Dial function connects to a server: * - * For the most part, this package follows the syntax as specified by RFC 5322 and - * extended by RFC 6532. - * Notable divergences: * ``` - * - Obsolete address formats are not parsed, including addresses with - * embedded route information. - * - The full range of spacing (the CFWS syntax element) is not supported, - * such as breaking addresses across lines. - * - No unicode normalization is performed. - * - The special characters ()[]:;@\, are allowed to appear unquoted in names. - * - A leading From line is permitted, as in mbox format (RFC 4155). + * conn, err := net.Dial("tcp", "golang.org:80") + * if err != nil { + * // handle error + * } + * fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") + * status, err := bufio.NewReader(conn).ReadString('\n') + * // ... * ``` + * + * The Listen function creates servers: + * + * ``` + * ln, err := net.Listen("tcp", ":8080") + * if err != nil { + * // handle error + * } + * for { + * conn, err := ln.Accept() + * if err != nil { + * // handle error + * } + * go handleConnection(conn) + * } + * ``` + * + * # Name Resolution + * + * The method for resolving domain names, whether indirectly with functions like Dial + * or directly with functions like [LookupHost] and [LookupAddr], varies by operating system. + * + * On Unix systems, the resolver has two options for resolving names. + * It can use a pure Go resolver that sends DNS requests directly to the servers + * listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C + * library routines such as getaddrinfo and getnameinfo. + * + * On Unix the pure Go resolver is preferred over the cgo resolver, because a blocked DNS + * request consumes only a goroutine, while a blocked C call consumes an operating system thread. + * When cgo is available, the cgo-based resolver is used instead under a variety of + * conditions: on systems that do not let programs make direct DNS requests (OS X), + * when the LOCALDOMAIN environment variable is present (even if empty), + * when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, + * when the ASR_CONFIG environment variable is non-empty (OpenBSD only), + * when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the + * Go resolver does not implement. + * + * On all systems (except Plan 9), when the cgo resolver is being used + * this package applies a concurrent cgo lookup limit to prevent the system + * from running out of system threads. Currently, it is limited to 500 concurrent lookups. + * + * The resolver decision can be overridden by setting the netdns value of the + * GODEBUG environment variable (see package runtime) to go or cgo, as in: + * + * ``` + * export GODEBUG=netdns=go # force pure Go resolver + * export GODEBUG=netdns=cgo # force native resolver (cgo, win32) + * ``` + * + * The decision can also be forced while building the Go source tree + * by setting the netgo or netcgo build tag. + * + * A numeric netdns setting, as in GODEBUG=netdns=1, causes the resolver + * to print debugging information about its decisions. + * To force a particular resolver while also printing debugging information, + * join the two settings by a plus sign, as in GODEBUG=netdns=go+1. + * + * The Go resolver will send an EDNS0 additional header with a DNS request, + * to signal a willingness to accept a larger DNS packet size. + * This can reportedly cause sporadic failures with the DNS server run + * by some modems and routers. Setting GODEBUG=netedns0=0 will disable + * sending the additional header. + * + * On macOS, if Go code that uses the net package is built with + * -buildmode=c-archive, linking the resulting archive into a C program + * requires passing -lresolv when linking the C code. + * + * On Plan 9, the resolver always accesses /net/cs and /net/dns. + * + * On Windows, in Go 1.18.x and earlier, the resolver always used C + * library functions, such as GetAddrInfo and DnsQuery. */ -namespace mail { +namespace net { /** - * Address represents a single mail address. - * An address such as "Barry Gibbs " is represented - * as Address{Name: "Barry Gibbs", Address: "bg@example.com"}. + * Addr represents a network end point address. + * + * The two methods [Addr.Network] and [Addr.String] conventionally return strings + * that can be passed as the arguments to [Dial], but the exact form + * and meaning of the strings is up to the implementation. */ - interface Address { - name: string // Proper name; may be empty. - address: string // user@domain - } - interface Address { - /** - * String formats the address as a valid RFC 5322 address. - * If the address's name contains non-ASCII characters - * the name will be rendered according to RFC 2047. - */ - string(): string + interface Addr { + [key:string]: any; + network(): string // name of the network (for example, "tcp", "udp") + string(): string // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80") } } /** - * Package types implements some commonly used db serializable types - * like datetime, json, etc. + * Package textproto implements generic support for text-based request/response + * protocols in the style of HTTP, NNTP, and SMTP. + * + * The package provides: + * + * [Error], which represents a numeric error response from + * a server. + * + * [Pipeline], to manage pipelined requests and responses + * in a client. + * + * [Reader], to read numeric response code lines, + * key: value headers, lines wrapped with leading spaces + * on continuation lines, and whole text blocks ending + * with a dot on a line by itself. + * + * [Writer], to write dot-encoded text blocks. + * + * [Conn], a convenient packaging of [Reader], [Writer], and [Pipeline] for use + * with a single network connection. */ -namespace types { +namespace textproto { + /** + * A MIMEHeader represents a MIME-style header mapping + * keys to sets of values. + */ + interface MIMEHeader extends _TygojaDict{} + interface MIMEHeader { + /** + * Add adds the key, value pair to the header. + * It appends to any existing values associated with key. + */ + add(key: string, value: string): void + } + interface MIMEHeader { + /** + * Set sets the header entries associated with key to + * the single element value. It replaces any existing + * values associated with key. + */ + set(key: string, value: string): void + } + interface MIMEHeader { + /** + * Get gets the first value associated with the given key. + * It is case insensitive; [CanonicalMIMEHeaderKey] is used + * to canonicalize the provided key. + * If there are no values associated with the key, Get returns "". + * To use non-canonical keys, access the map directly. + */ + get(key: string): string + } + interface MIMEHeader { + /** + * Values returns all values associated with the given key. + * It is case insensitive; [CanonicalMIMEHeaderKey] is + * used to canonicalize the provided key. To use non-canonical + * keys, access the map directly. + * The returned slice is not a copy. + */ + values(key: string): Array + } + interface MIMEHeader { + /** + * Del deletes the values associated with key. + */ + del(key: string): void + } } -namespace search { +/** + * Package multipart implements MIME multipart parsing, as defined in RFC + * 2046. + * + * The implementation is sufficient for HTTP (RFC 2388) and the multipart + * bodies generated by popular browsers. + * + * # Limits + * + * To protect against malicious inputs, this package sets limits on the size + * of the MIME data it processes. + * + * [Reader.NextPart] and [Reader.NextRawPart] limit the number of headers in a + * part to 10000 and [Reader.ReadForm] limits the total number of headers in all + * FileHeaders to 10000. + * These limits may be adjusted with the GODEBUG=multipartmaxheaders= + * setting. + * + * Reader.ReadForm further limits the number of parts in a form to 1000. + * This limit may be adjusted with the GODEBUG=multipartmaxparts= + * setting. + */ +namespace multipart { + interface Reader { + /** + * ReadForm parses an entire multipart message whose parts have + * a Content-Disposition of "form-data". + * It stores up to maxMemory bytes + 10MB (reserved for non-file parts) + * in memory. File parts which can't be stored in memory will be stored on + * disk in temporary files. + * It returns [ErrMessageTooLarge] if all non-file parts can't be stored in + * memory. + */ + readForm(maxMemory: number): (Form) + } + /** + * Form is a parsed multipart form. + * Its File parts are stored either in memory or on disk, + * and are accessible via the [*FileHeader]'s Open method. + * Its Value parts are stored as strings. + * Both are keyed by field name. + */ + interface Form { + value: _TygojaDict + file: _TygojaDict + } + interface Form { + /** + * RemoveAll removes any temporary files associated with a [Form]. + */ + removeAll(): void + } + /** + * File is an interface to access the file part of a multipart message. + * Its contents may be either stored in memory or on disk. + * If stored on disk, the File's underlying concrete type will be an *os.File. + */ + interface File { + [key:string]: any; + } + /** + * Reader is an iterator over parts in a MIME multipart body. + * Reader's underlying parser consumes its input as needed. Seeking + * isn't supported. + */ + interface Reader { + } + interface Reader { + /** + * NextPart returns the next part in the multipart or an error. + * When there are no more parts, the error [io.EOF] is returned. + * + * As a special case, if the "Content-Transfer-Encoding" header + * has a value of "quoted-printable", that header is instead + * hidden and the body is transparently decoded during Read calls. + */ + nextPart(): (Part) + } + interface Reader { + /** + * NextRawPart returns the next part in the multipart or an error. + * When there are no more parts, the error [io.EOF] is returned. + * + * Unlike [Reader.NextPart], it does not have special handling for + * "Content-Transfer-Encoding: quoted-printable". + */ + nextRawPart(): (Part) + } } -namespace subscriptions { +/** + * Package http provides HTTP client and server implementations. + * + * [Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests: + * + * ``` + * resp, err := http.Get("http://example.com/") + * ... + * resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) + * ... + * resp, err := http.PostForm("http://example.com/form", + * url.Values{"key": {"Value"}, "id": {"123"}}) + * ``` + * + * The caller must close the response body when finished with it: + * + * ``` + * resp, err := http.Get("http://example.com/") + * if err != nil { + * // handle error + * } + * defer resp.Body.Close() + * body, err := io.ReadAll(resp.Body) + * // ... + * ``` + * + * # Clients and Transports + * + * For control over HTTP client headers, redirect policy, and other + * settings, create a [Client]: + * + * ``` + * client := &http.Client{ + * CheckRedirect: redirectPolicyFunc, + * } + * + * resp, err := client.Get("http://example.com") + * // ... + * + * req, err := http.NewRequest("GET", "http://example.com", nil) + * // ... + * req.Header.Add("If-None-Match", `W/"wyzzy"`) + * resp, err := client.Do(req) + * // ... + * ``` + * + * For control over proxies, TLS configuration, keep-alives, + * compression, and other settings, create a [Transport]: + * + * ``` + * tr := &http.Transport{ + * MaxIdleConns: 10, + * IdleConnTimeout: 30 * time.Second, + * DisableCompression: true, + * } + * client := &http.Client{Transport: tr} + * resp, err := client.Get("https://example.com") + * ``` + * + * Clients and Transports are safe for concurrent use by multiple + * goroutines and for efficiency should only be created once and re-used. + * + * # Servers + * + * ListenAndServe starts an HTTP server with a given address and handler. + * The handler is usually nil, which means to use [DefaultServeMux]. + * [Handle] and [HandleFunc] add handlers to [DefaultServeMux]: + * + * ``` + * http.Handle("/foo", fooHandler) + * + * http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + * fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + * }) + * + * log.Fatal(http.ListenAndServe(":8080", nil)) + * ``` + * + * More control over the server's behavior is available by creating a + * custom Server: + * + * ``` + * s := &http.Server{ + * Addr: ":8080", + * Handler: myHandler, + * ReadTimeout: 10 * time.Second, + * WriteTimeout: 10 * time.Second, + * MaxHeaderBytes: 1 << 20, + * } + * log.Fatal(s.ListenAndServe()) + * ``` + * + * # HTTP/2 + * + * Starting with Go 1.6, the http package has transparent support for the + * HTTP/2 protocol when using HTTPS. Programs that must disable HTTP/2 + * can do so by setting [Transport.TLSNextProto] (for clients) or + * [Server.TLSNextProto] (for servers) to a non-nil, empty + * map. Alternatively, the following GODEBUG settings are + * currently supported: + * + * ``` + * GODEBUG=http2client=0 # disable HTTP/2 client support + * GODEBUG=http2server=0 # disable HTTP/2 server support + * GODEBUG=http2debug=1 # enable verbose HTTP/2 debug logs + * GODEBUG=http2debug=2 # ... even more verbose, with frame dumps + * ``` + * + * Please report any issues before disabling HTTP/2 support: https://golang.org/s/http2bug + * + * The http package's [Transport] and [Server] both automatically enable + * HTTP/2 support for simple configurations. To enable HTTP/2 for more + * complex configurations, to use lower-level HTTP/2 features, or to use + * a newer version of Go's http2 package, import "golang.org/x/net/http2" + * directly and use its ConfigureTransport and/or ConfigureServer + * functions. Manually configuring HTTP/2 via the golang.org/x/net/http2 + * package takes precedence over the net/http package's built-in HTTP/2 + * support. + */ +namespace http { + /** + * A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an + * HTTP response or the Cookie header of an HTTP request. + * + * See https://tools.ietf.org/html/rfc6265 for details. + */ + interface Cookie { + name: string + value: string + quoted: boolean // indicates whether the Value was originally quoted + path: string // optional + domain: string // optional + expires: time.Time // optional + rawExpires: string // for reading cookies only + /** + * MaxAge=0 means no 'Max-Age' attribute specified. + * MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' + * MaxAge>0 means Max-Age attribute present and given in seconds + */ + maxAge: number + secure: boolean + httpOnly: boolean + sameSite: SameSite + partitioned: boolean + raw: string + unparsed: Array // Raw text of unparsed attribute-value pairs + } + interface Cookie { + /** + * String returns the serialization of the cookie for use in a [Cookie] + * header (if only Name and Value are set) or a Set-Cookie response + * header (if other fields are set). + * If c is nil or c.Name is invalid, the empty string is returned. + */ + string(): string + } + interface Cookie { + /** + * Valid reports whether the cookie is valid. + */ + valid(): void + } + // @ts-ignore + import mathrand = rand + /** + * A Header represents the key-value pairs in an HTTP header. + * + * The keys should be in canonical form, as returned by + * [CanonicalHeaderKey]. + */ + interface Header extends _TygojaDict{} + interface Header { + /** + * Add adds the key, value pair to the header. + * It appends to any existing values associated with key. + * The key is case insensitive; it is canonicalized by + * [CanonicalHeaderKey]. + */ + add(key: string, value: string): void + } + interface Header { + /** + * Set sets the header entries associated with key to the + * single element value. It replaces any existing values + * associated with key. The key is case insensitive; it is + * canonicalized by [textproto.CanonicalMIMEHeaderKey]. + * To use non-canonical keys, assign to the map directly. + */ + set(key: string, value: string): void + } + interface Header { + /** + * Get gets the first value associated with the given key. If + * there are no values associated with the key, Get returns "". + * It is case insensitive; [textproto.CanonicalMIMEHeaderKey] is + * used to canonicalize the provided key. Get assumes that all + * keys are stored in canonical form. To use non-canonical keys, + * access the map directly. + */ + get(key: string): string + } + interface Header { + /** + * Values returns all values associated with the given key. + * It is case insensitive; [textproto.CanonicalMIMEHeaderKey] is + * used to canonicalize the provided key. To use non-canonical + * keys, access the map directly. + * The returned slice is not a copy. + */ + values(key: string): Array + } + interface Header { + /** + * Del deletes the values associated with key. + * The key is case insensitive; it is canonicalized by + * [CanonicalHeaderKey]. + */ + del(key: string): void + } + interface Header { + /** + * Write writes a header in wire format. + */ + write(w: io.Writer): void + } + interface Header { + /** + * Clone returns a copy of h or nil if h is nil. + */ + clone(): Header + } + interface Header { + /** + * WriteSubset writes a header in wire format. + * If exclude is not nil, keys where exclude[key] == true are not written. + * Keys are not canonicalized before checking the exclude map. + */ + writeSubset(w: io.Writer, exclude: _TygojaDict): void + } + // @ts-ignore + import urlpkg = url + /** + * Response represents the response from an HTTP request. + * + * The [Client] and [Transport] return Responses from servers once + * the response headers have been received. The response body + * is streamed on demand as the Body field is read. + */ + interface Response { + status: string // e.g. "200 OK" + statusCode: number // e.g. 200 + proto: string // e.g. "HTTP/1.0" + protoMajor: number // e.g. 1 + protoMinor: number // e.g. 0 + /** + * Header maps header keys to values. If the response had multiple + * headers with the same key, they may be concatenated, with comma + * delimiters. (RFC 7230, section 3.2.2 requires that multiple headers + * be semantically equivalent to a comma-delimited sequence.) When + * Header values are duplicated by other fields in this struct (e.g., + * ContentLength, TransferEncoding, Trailer), the field values are + * authoritative. + * + * Keys in the map are canonicalized (see CanonicalHeaderKey). + */ + header: Header + /** + * Body represents the response body. + * + * The response body is streamed on demand as the Body field + * is read. If the network connection fails or the server + * terminates the response, Body.Read calls return an error. + * + * The http Client and Transport guarantee that Body is always + * non-nil, even on responses without a body or responses with + * a zero-length body. It is the caller's responsibility to + * close Body. The default HTTP client's Transport may not + * reuse HTTP/1.x "keep-alive" TCP connections if the Body is + * not read to completion and closed. + * + * The Body is automatically dechunked if the server replied + * with a "chunked" Transfer-Encoding. + * + * As of Go 1.12, the Body will also implement io.Writer + * on a successful "101 Switching Protocols" response, + * as used by WebSockets and HTTP/2's "h2c" mode. + */ + body: io.ReadCloser + /** + * ContentLength records the length of the associated content. The + * value -1 indicates that the length is unknown. Unless Request.Method + * is "HEAD", values >= 0 indicate that the given number of bytes may + * be read from Body. + */ + contentLength: number + /** + * Contains transfer encodings from outer-most to inner-most. Value is + * nil, means that "identity" encoding is used. + */ + transferEncoding: Array + /** + * Close records whether the header directed that the connection be + * closed after reading Body. The value is advice for clients: neither + * ReadResponse nor Response.Write ever closes a connection. + */ + close: boolean + /** + * Uncompressed reports whether the response was sent compressed but + * was decompressed by the http package. When true, reading from + * Body yields the uncompressed content instead of the compressed + * content actually set from the server, ContentLength is set to -1, + * and the "Content-Length" and "Content-Encoding" fields are deleted + * from the responseHeader. To get the original response from + * the server, set Transport.DisableCompression to true. + */ + uncompressed: boolean + /** + * Trailer maps trailer keys to values in the same + * format as Header. + * + * The Trailer initially contains only nil values, one for + * each key specified in the server's "Trailer" header + * value. Those values are not added to Header. + * + * Trailer must not be accessed concurrently with Read calls + * on the Body. + * + * After Body.Read has returned io.EOF, Trailer will contain + * any trailer values sent by the server. + */ + trailer: Header + /** + * Request is the request that was sent to obtain this Response. + * Request's Body is nil (having already been consumed). + * This is only populated for Client requests. + */ + request?: Request + /** + * TLS contains information about the TLS connection on which the + * response was received. It is nil for unencrypted responses. + * The pointer is shared between responses and should not be + * modified. + */ + tls?: any + } + interface Response { + /** + * Cookies parses and returns the cookies set in the Set-Cookie headers. + */ + cookies(): Array<(Cookie | undefined)> + } + interface Response { + /** + * Location returns the URL of the response's "Location" header, + * if present. Relative redirects are resolved relative to + * [Response.Request]. [ErrNoLocation] is returned if no + * Location header is present. + */ + location(): (url.URL) + } + interface Response { + /** + * ProtoAtLeast reports whether the HTTP protocol used + * in the response is at least major.minor. + */ + protoAtLeast(major: number, minor: number): boolean + } + interface Response { + /** + * Write writes r to w in the HTTP/1.x server response format, + * including the status line, headers, body, and optional trailer. + * + * This method consults the following fields of the response r: + * + * ``` + * StatusCode + * ProtoMajor + * ProtoMinor + * Request.Method + * TransferEncoding + * Trailer + * Body + * ContentLength + * Header, values for non-canonical keys will have unpredictable behavior + * ``` + * + * The Response Body is closed after it is sent. + */ + write(w: io.Writer): void + } +} + +namespace store { + /** + * Store defines a concurrent safe in memory key-value data store. + */ + interface Store { + } + interface Store { + /** + * Reset clears the store and replaces the store data with a + * shallow copy of the provided newData. + */ + reset(newData: _TygojaDict): void + } + interface Store { + /** + * Length returns the current number of elements in the store. + */ + length(): number + } + interface Store { + /** + * RemoveAll removes all the existing store entries. + */ + removeAll(): void + } + interface Store { + /** + * Remove removes a single entry from the store. + * + * Remove does nothing if key doesn't exist in the store. + */ + remove(key: string): void + } + interface Store { + /** + * Has checks if element with the specified key exist or not. + */ + has(key: string): boolean + } + interface Store { + /** + * Get returns a single element value from the store. + * + * If key is not set, the zero T value is returned. + */ + get(key: string): T + } + interface Store { + /** + * GetOk is similar to Get but returns also a boolean indicating whether the key exists or not. + */ + getOk(key: string): [T, boolean] + } + interface Store { + /** + * GetAll returns a shallow copy of the current store data. + */ + getAll(): _TygojaDict + } + interface Store { + /** + * Values returns a slice with all of the current store values. + */ + values(): Array + } + interface Store { + /** + * Set sets (or overwrite if already exist) a new value for key. + */ + set(key: string, value: T): void + } + interface Store { + /** + * GetOrSet retrieves a single existing value for the provided key + * or stores a new one if it doesn't exist. + */ + getOrSet(key: string, setFunc: () => T): T + } + interface Store { + /** + * SetIfLessThanLimit sets (or overwrite if already exist) a new value for key. + * + * This method is similar to Set() but **it will skip adding new elements** + * to the store if the store length has reached the specified limit. + * false is returned if maxAllowedElements limit is reached. + */ + setIfLessThanLimit(key: string, value: T, maxAllowedElements: number): boolean + } + interface Store { + /** + * UnmarshalJSON implements [json.Unmarshaler] and imports the + * provided JSON data into the store. + * + * The store entries that match with the ones from the data will be overwritten with the new value. + */ + unmarshalJSON(data: string|Array): void + } + interface Store { + /** + * MarshalJSON implements [json.Marshaler] and export the current + * store data into valid JSON. + */ + marshalJSON(): string|Array + } +} + +/** + * Package cron implements a crontab-like service to execute and schedule + * repeative tasks/jobs. + * + * Example: + * + * ``` + * c := cron.New() + * c.MustAdd("dailyReport", "0 0 * * *", func() { ... }) + * c.Start() + * ``` + */ +namespace cron { + /** + * Cron is a crontab-like struct for tasks/jobs scheduling. + */ + interface Cron { + } + interface Cron { + /** + * SetInterval changes the current cron tick interval + * (it usually should be >= 1 minute). + */ + setInterval(d: time.Duration): void + } + interface Cron { + /** + * SetTimezone changes the current cron tick timezone. + */ + setTimezone(l: time.Location): void + } + interface Cron { + /** + * MustAdd is similar to Add() but panic on failure. + */ + mustAdd(jobId: string, cronExpr: string, run: () => void): void + } + interface Cron { + /** + * Add registers a single cron job. + * + * If there is already a job with the provided id, then the old job + * will be replaced with the new one. + * + * cronExpr is a regular cron expression, eg. "0 *\/3 * * *" (aka. at minute 0 past every 3rd hour). + * Check cron.NewSchedule() for the supported tokens. + */ + add(jobId: string, cronExpr: string, run: () => void): void + } + interface Cron { + /** + * Remove removes a single cron job by its id. + */ + remove(jobId: string): void + } + interface Cron { + /** + * RemoveAll removes all registered cron jobs. + */ + removeAll(): void + } + interface Cron { + /** + * Total returns the current total number of registered cron jobs. + */ + total(): number + } + interface Cron { + /** + * Stop stops the current cron ticker (if not already). + * + * You can resume the ticker by calling Start(). + */ + stop(): void + } + interface Cron { + /** + * Start starts the cron ticker. + * + * Calling Start() on already started cron will restart the ticker. + */ + start(): void + } + interface Cron { + /** + * HasStarted checks whether the current Cron ticker has been started. + */ + hasStarted(): boolean + } +} + +namespace hook { + /** + * Hook defines a generic concurrent safe structure for managing event hooks. + * + * When using custom a event it must embed the base [hook.Event]. + * + * Example: + * + * ``` + * type CustomEvent struct { + * hook.Event + * SomeField int + * } + * + * h := Hook[*CustomEvent]{} + * + * h.BindFunc(func(e *CustomEvent) error { + * println(e.SomeField) + * + * return e.Next() + * }) + * + * h.Trigger(&CustomEvent{ SomeField: 123 }) + * ``` + */ + interface Hook { + } + interface Hook { + /** + * Bind registers the provided handler to the current hooks queue. + * + * If handler.Id is empty it is updated with autogenerated value. + * + * If a handler from the current hook list has Id matching handler.Id + * then the old handler is replaced with the new one. + */ + bind(handler: Handler): string + } + interface Hook { + /** + * BindFunc is similar to Bind but registers a new handler from just the provided function. + * + * 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. + */ + bindFunc(fn: HandlerFunc): string + } + interface Hook { + /** + * Unbind removes a single hook handler by its id. + */ + unbind(id: string): void + } + interface Hook { + /** + * UnbindAll removes all registered handlers. + */ + unbindAll(): void + } + interface Hook { + /** + * Length returns to total number of registered hook handlers. + */ + length(): number + } + interface Hook { + /** + * Trigger executes all registered hook handlers one by one + * with the specified event as an argument. + * + * Optionally, this method allows also to register additional one off + * handlers that will be temporary appended to the handlers queue. + * + * NB! Each hook handler must call event.Next() in order the hook chain to proceed. + */ + trigger(event: T, ...oneOffHandlers: HandlerFunc[]): void + } + /** + * TaggedHook defines a proxy hook which register handlers that are triggered only + * if the TaggedHook.tags are empty or includes at least one of the event data tag(s). + */ + type _subPqnJU = mainHook + interface TaggedHook extends _subPqnJU { + } + interface TaggedHook { + /** + * CanTriggerOn checks if the current TaggedHook can be triggered with + * the provided event data tags. + * + * It returns always true if the hook doens't have any tags. + */ + canTriggerOn(tagsToCheck: Array): boolean + } + interface TaggedHook { + /** + * Bind registers the provided handler to the current hooks queue. + * + * It is similar to [Hook.Bind] with the difference that the handler + * function is invoked only if the event data tags satisfy h.CanTriggerOn. + */ + bind(handler: Handler): string + } + interface TaggedHook { + /** + * BindFunc registers a new handler with the specified function. + * + * It is similar to [Hook.Bind] with the difference that the handler + * function is invoked only if the event data tags satisfy h.CanTriggerOn. + */ + bindFunc(fn: HandlerFunc): string + } } /** @@ -20012,8 +16067,3808 @@ namespace subscriptions { * Now computeExpensiveValue will only be called when the line is enabled. * * The built-in handlers acquire a lock before calling [io.Writer.Write] - * to ensure that each record is written in one piece. User-defined - * handlers are responsible for their own locking. + * to ensure that exactly one [Record] is written at a time in its entirety. + * Although each log record has a timestamp, + * the built-in handlers do not use that time to sort the written records. + * User-defined handlers are responsible for their own locking and sorting. + * + * # Writing a handler + * + * For a guide to writing a custom handler, see https://golang.org/s/slog-handler-guide. + */ +namespace slog { + // @ts-ignore + import loginternal = internal + /** + * A Logger records structured information about each call to its + * Log, Debug, Info, Warn, and Error methods. + * For each call, it creates a [Record] and passes it to a [Handler]. + * + * To create a new Logger, call [New] or a Logger method + * that begins "With". + */ + interface Logger { + } + interface Logger { + /** + * Handler returns l's Handler. + */ + handler(): Handler + } + interface Logger { + /** + * With returns a Logger that includes the given attributes + * in each output operation. Arguments are converted to + * attributes as if by [Logger.Log]. + */ + with(...args: any[]): (Logger) + } + interface Logger { + /** + * WithGroup returns a Logger that starts a group, if name is non-empty. + * The keys of all attributes added to the Logger will be qualified by the given + * name. (How that qualification happens depends on the [Handler.WithGroup] + * method of the Logger's Handler.) + * + * If name is empty, WithGroup returns the receiver. + */ + withGroup(name: string): (Logger) + } + interface Logger { + /** + * Enabled reports whether l emits log records at the given context and level. + */ + enabled(ctx: context.Context, level: Level): boolean + } + interface Logger { + /** + * Log emits a log record with the current time and the given level and message. + * The Record's Attrs consist of the Logger's attributes followed by + * the Attrs specified by args. + * + * The attribute arguments are processed as follows: + * ``` + * - If an argument is an Attr, it is used as is. + * - If an argument is a string and this is not the last argument, + * the following argument is treated as the value and the two are combined + * into an Attr. + * - Otherwise, the argument is treated as a value with key "!BADKEY". + * ``` + */ + log(ctx: context.Context, level: Level, msg: string, ...args: any[]): void + } + interface Logger { + /** + * LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs. + */ + logAttrs(ctx: context.Context, level: Level, msg: string, ...attrs: Attr[]): void + } + interface Logger { + /** + * Debug logs at [LevelDebug]. + */ + debug(msg: string, ...args: any[]): void + } + interface Logger { + /** + * DebugContext logs at [LevelDebug] with the given context. + */ + debugContext(ctx: context.Context, msg: string, ...args: any[]): void + } + interface Logger { + /** + * Info logs at [LevelInfo]. + */ + info(msg: string, ...args: any[]): void + } + interface Logger { + /** + * InfoContext logs at [LevelInfo] with the given context. + */ + infoContext(ctx: context.Context, msg: string, ...args: any[]): void + } + interface Logger { + /** + * Warn logs at [LevelWarn]. + */ + warn(msg: string, ...args: any[]): void + } + interface Logger { + /** + * WarnContext logs at [LevelWarn] with the given context. + */ + warnContext(ctx: context.Context, msg: string, ...args: any[]): void + } + interface Logger { + /** + * Error logs at [LevelError]. + */ + error(msg: string, ...args: any[]): void + } + interface Logger { + /** + * ErrorContext logs at [LevelError] with the given context. + */ + errorContext(ctx: context.Context, msg: string, ...args: any[]): void + } +} + +namespace mailer { + /** + * Mailer defines a base mail client interface. + */ + interface Mailer { + [key:string]: any; + /** + * Send sends an email with the provided Message. + */ + send(message: Message): void + } +} + +namespace router { + // @ts-ignore + import validation = ozzo_validation + /** + * Event specifies based Route handler event that is usually intended + * to be embedded as part of a custom event struct. + * + * NB! It is expected that the Response and Request fields are always set. + */ + type _subPBHhi = hook.Event + interface Event extends _subPBHhi { + response: http.ResponseWriter + request?: http.Request + } + interface Event { + /** + * Written reports whether the current response has already been written. + * + * This method always returns false if e.ResponseWritter doesn't implement the WriteTracker interface + * (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). + */ + written(): boolean + } + interface Event { + /** + * Status reports the status code of the current response. + * + * This method always returns 0 if e.Response doesn't implement the StatusTracker interface + * (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). + */ + status(): number + } + interface Event { + /** + * Flush flushes buffered data to the current response. + * + * Returns [http.ErrNotSupported] if e.Response doesn't implement the [http.Flusher] interface + * (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). + */ + flush(): void + } + interface Event { + /** + * IsTLS reports whether the connection on which the request was received is TLS. + */ + isTLS(): boolean + } + interface Event { + /** + * SetCookie is an alias for [http.SetCookie]. + * + * SetCookie adds a Set-Cookie header to the current response's headers. + * The provided cookie must have a valid Name. + * Invalid cookies may be silently dropped. + */ + setCookie(cookie: http.Cookie): void + } + interface Event { + /** + * RemoteIP returns the IP address of the client that sent the request. + * + * IPv6 addresses are returned expanded. + * For example, "2001:db8::1" becomes "2001:0db8:0000:0000:0000:0000:0000:0001". + * + * Note that if you are behind reverse proxy(ies), this method returns + * the IP of the last connecting proxy. + */ + remoteIP(): string + } + interface Event { + /** + * UnsafeRealIP returns the "real" client IP from common proxy headers + * OR fallbacks to the RemoteIP if none is found. + * + * NB! The returned IP value could be anything and it shouldn't be trusted if not behind a trusted reverse proxy! + */ + unsafeRealIP(): string + } + interface Event { + /** + * Get retrieves single value from the current event data store. + */ + get(key: string): any + } + interface Event { + /** + * GetAll returns a copy of the current event data store. + */ + getAll(): _TygojaDict + } + interface Event { + /** + * Set saves single value into the current event data store. + */ + set(key: string, value: any): void + } + interface Event { + /** + * SetAll saves all items from m into the current event data store. + */ + setAll(m: _TygojaDict): void + } + interface Event { + /** + * String writes a plain string response. + */ + string(status: number, data: string): void + } + interface Event { + /** + * HTML writes an HTML response. + */ + html(status: number, data: string): void + } + interface Event { + /** + * JSON writes a JSON response. + * + * It also provides a generic response data fields picker if the "fields" query parameter is set. + */ + json(status: number, data: any): void + } + interface Event { + /** + * XML writes an XML response. + * It automatically prepends the generic [xml.Header] string to the response. + */ + xml(status: number, data: any): void + } + interface Event { + /** + * Stream streams the specified reader into the response. + */ + stream(status: number, contentType: string, reader: io.Reader): void + } + interface Event { + /** + * FileFS serves the specified filename from fsys. + * + * It is similar to [echo.FileFS] for consistency with earlier versions. + */ + fileFS(fsys: fs.FS, filename: string): void + } + interface Event { + /** + * NoContent writes a response with no body (ex. 204). + */ + noContent(status: number): void + } + interface Event { + /** + * Redirect writes a redirect response to the specified url. + * The status code must be in between 300 – 399 range. + */ + redirect(status: number, url: string): void + } + interface Event { + error(status: number, message: string, errData: any): (ApiError) + } + interface Event { + badRequestError(message: string, errData: any): (ApiError) + } + interface Event { + notFoundError(message: string, errData: any): (ApiError) + } + interface Event { + forbiddenError(message: string, errData: any): (ApiError) + } + interface Event { + unauthorizedError(message: string, errData: any): (ApiError) + } + interface Event { + tooManyRequestsError(message: string, errData: any): (ApiError) + } + interface Event { + internalServerError(message: string, errData: any): (ApiError) + } + interface Event { + /** + * Supports the following content-types: + * + * ``` + * - application/json + * - multipart/form-data + * - application/x-www-form-urlencoded + * - text/xml, application/xml + * ``` + */ + bindBody(dst: any): void + } + /** + * RouterGroup represents a collection of routes and other sub groups + * that share common pattern prefix and middlewares. + */ + interface RouterGroup { + prefix: string + middlewares: Array<(hook.Handler | undefined)> + } + interface RouterGroup { + /** + * Group creates and register a new child Group into the current one + * with the specified prefix. + * + * The prefix follows the standard Go net/http ServeMux pattern format ("[HOST]/[PATH]") + * and will be concatenated recursively into the final route path, meaning that + * only the root level group could have HOST as part of the prefix. + * + * Returns the newly created group to allow chaining and registering + * sub-routes and group specific middlewares. + */ + group(prefix: string): (RouterGroup) + } + interface RouterGroup { + /** + * BindFunc registers one or multiple middleware functions to the current group. + * + * The registered middleware functions are "anonymous" and with default priority, + * 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. + */ + bindFunc(...middlewareFuncs: hook.HandlerFunc[]): (RouterGroup) + } + interface RouterGroup { + /** + * Bind registers one or multiple middleware handlers to the current group. + */ + bind(...middlewares: (hook.Handler | undefined)[]): (RouterGroup) + } + interface RouterGroup { + /** + * Unbind removes one or more middlewares with the specified id(s) + * from the current group and its children (if any). + * + * Anonymous middlewares are not removable, aka. this method does nothing + * if the middleware id is an empty string. + */ + unbind(...middlewareIds: string[]): (RouterGroup) + } + interface RouterGroup { + /** + * Route registers a single route into the current group. + * + * Note that the final route path will be the concatenation of all parent groups prefixes + the route path. + * The path follows the standard Go net/http ServeMux format ("[HOST]/[PATH]"), + * 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. + */ + route(method: string, path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * Any is a shorthand for [Group.AddRoute] with "" as route method (aka. matches any method). + */ + any(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * GET is a shorthand for [Group.AddRoute] with GET as route method. + */ + get(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * POST is a shorthand for [Group.AddRoute] with POST as route method. + */ + post(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * DELETE is a shorthand for [Group.AddRoute] with DELETE as route method. + */ + delete(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * PATCH is a shorthand for [Group.AddRoute] with PATCH as route method. + */ + patch(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * PUT is a shorthand for [Group.AddRoute] with PUT as route method. + */ + put(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * HEAD is a shorthand for [Group.AddRoute] with HEAD as route method. + */ + head(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * OPTIONS is a shorthand for [Group.AddRoute] with OPTIONS as route method. + */ + options(path: string, action: hook.HandlerFunc): (Route) + } + interface RouterGroup { + /** + * HasRoute checks whether the specified route pattern (method + path) + * is registered in the current group or its children. + * + * This could be useful to conditionally register and checks for routes + * in order prevent panic on duplicated routes. + * + * Note that routes with anonymous and named wildcard placeholder are treated as equal, + * aka. "GET /abc/" is considered the same as "GET /abc/{something...}". + */ + hasRoute(method: string, path: string): boolean + } +} + +namespace subscriptions { + /** + * Broker defines a struct for managing subscriptions clients. + */ + interface Broker { + } + interface Broker { + /** + * Clients returns a shallow copy of all registered clients indexed + * with their connection id. + */ + clients(): _TygojaDict + } + interface Broker { + /** + * ChunkedClients splits the current clients into a chunked slice. + */ + chunkedClients(chunkSize: number): Array> + } + interface Broker { + /** + * ClientById finds a registered client by its id. + * + * Returns non-nil error when client with clientId is not registered. + */ + clientById(clientId: string): Client + } + interface Broker { + /** + * Register adds a new client to the broker instance. + */ + register(client: Client): void + } + interface Broker { + /** + * Unregister removes a single client by its id. + * + * If client with clientId doesn't exist, this method does nothing. + */ + unregister(clientId: string): void + } +} + +/** + * Package core is the backbone of PocketBase. + * + * It defines the main PocketBase App interface and its base implementation. + */ +namespace core { + // @ts-ignore + import validation = ozzo_validation + /** + * AuthOrigin defines a Record proxy for working with the authOrigins collection. + */ + type _subkIVie = Record + interface AuthOrigin extends _subkIVie { + } + interface AuthOrigin { + /** + * PreValidate implements the [PreValidator] interface and checks + * whether the proxy is properly loaded. + */ + preValidate(ctx: context.Context, app: App): void + } + interface AuthOrigin { + /** + * ProxyRecord returns the proxied Record model. + */ + proxyRecord(): (Record) + } + interface AuthOrigin { + /** + * SetProxyRecord loads the specified record model into the current proxy. + */ + setProxyRecord(record: Record): void + } + interface AuthOrigin { + /** + * CollectionRef returns the "collectionRef" field value. + */ + collectionRef(): string + } + interface AuthOrigin { + /** + * SetCollectionRef updates the "collectionRef" record field value. + */ + setCollectionRef(collectionId: string): void + } + interface AuthOrigin { + /** + * RecordRef returns the "recordRef" record field value. + */ + recordRef(): string + } + interface AuthOrigin { + /** + * SetRecordRef updates the "recordRef" record field value. + */ + setRecordRef(recordId: string): void + } + interface AuthOrigin { + /** + * Fingerprint returns the "fingerprint" record field value. + */ + fingerprint(): string + } + interface AuthOrigin { + /** + * SetFingerprint updates the "fingerprint" record field value. + */ + setFingerprint(fingerprint: string): void + } + interface AuthOrigin { + /** + * Created returns the "created" record field value. + */ + created(): types.DateTime + } + interface AuthOrigin { + /** + * Updated returns the "updated" record field value. + */ + updated(): types.DateTime + } + /** + * Collection defines the table, fields and various options related to a set of records. + */ + type _subDkqXF = baseCollection&collectionAuthOptions&collectionViewOptions + interface Collection extends _subDkqXF { + } + interface Collection { + /** + * TableName returns the Collection model SQL table name. + */ + tableName(): string + } + interface Collection { + /** + * BaseFilesPath returns the storage dir path used by the collection. + */ + baseFilesPath(): string + } + interface Collection { + /** + * IsBase checks if the current collection has "base" type. + */ + isBase(): boolean + } + interface Collection { + /** + * IsAuth checks if the current collection has "auth" type. + */ + isAuth(): boolean + } + interface Collection { + /** + * IsView checks if the current collection has "view" type. + */ + isView(): boolean + } + interface Collection { + /** + * IntegrityChecks toggles the current collection integrity checks (ex. checking references on delete). + */ + integrityChecks(enable: boolean): void + } + interface Collection { + /** + * PostScan implements the [dbx.PostScanner] interface to auto unmarshal + * the raw serialized options into the concrete type specific fields. + */ + postScan(): void + } + interface Collection { + /** + * UnmarshalJSON implements the [json.Unmarshaler] interface. + * + * For new/"blank" Collection models it replaces the model with a factory + * instance and then unmarshal the provided data one on top of it. + */ + unmarshalJSON(b: string|Array): void + } + interface Collection { + /** + * MarshalJSON implements the [json.Marshaler] interface. + * + * Note that non-type related fields are ignored from the serialization + * (ex. for "view" colections the "auth" fields are skipped). + */ + marshalJSON(): string|Array + } + interface Collection { + /** + * String returns a string representation of the current collection. + */ + string(): string + } + interface Collection { + /** + * DBExport prepares and exports the current collection data for db persistence. + */ + dbExport(app: App): _TygojaDict + } + interface Collection { + /** + * GetIndex returns s single Collection index expression by its name. + */ + getIndex(name: string): string + } + interface Collection { + /** + * AddIndex adds a new index into the current collection. + * + * If the collection has an existing index matching the new name it will be replaced with the new one. + */ + addIndex(name: string, unique: boolean, columnsExpr: string, optWhereExpr: string): void + } + interface Collection { + /** + * RemoveIndex removes a single index with the specified name from the current collection. + */ + removeIndex(name: string): void + } + /** + * Model defines an interface with common methods that all db models should have. + * + * Note: for simplicity composite pk are not supported. + */ + interface Model { + [key:string]: any; + tableName(): string + pk(): any + lastSavedPK(): any + isNew(): boolean + markAsNew(): void + markAsNotNew(): void + } + /** + * BaseModel defines a base struct that is intended to be embedded into other custom models. + */ + interface BaseModel { + /** + * Id is the primary key of the model. + * It is usually autogenerated by the parent model implementation. + */ + id: string + } + interface BaseModel { + /** + * LastSavedPK returns the last saved primary key of the model. + * + * Its value is updated to the latest PK value after MarkAsNotNew() or PostScan() calls. + */ + lastSavedPK(): any + } + interface BaseModel { + pk(): any + } + interface BaseModel { + /** + * IsNew indicates what type of db query (insert or update) + * should be used with the model instance. + */ + isNew(): boolean + } + interface BaseModel { + /** + * MarkAsNew clears the pk field and marks the current model as "new" + * (aka. forces m.IsNew() to be true). + */ + markAsNew(): void + } + interface BaseModel { + /** + * MarkAsNew set the pk field to the Id value and marks the current model + * as NOT "new" (aka. forces m.IsNew() to be false). + */ + markAsNotNew(): void + } + interface BaseModel { + /** + * PostScan implements the [dbx.PostScanner] interface. + * + * It is usually executed right after the model is populated with the db row values. + */ + postScan(): void + } + interface TableInfoRow { + /** + * the `db:"pk"` tag has special semantic so we cannot rename + * the original field without specifying a custom mapper + */ + pk: number + index: number + name: string + type: string + notNull: boolean + defaultValue: sql.NullString + } + /** + * RequestInfo defines a HTTP request data struct, usually used + * as part of the `@request.*` filter resolver. + * + * The Query and Headers fields contains only the first value for each found entry. + */ + interface RequestInfo { + query: _TygojaDict + headers: _TygojaDict + body: _TygojaDict + auth?: Record + method: string + context: string + } + interface RequestInfo { + /** + * HasSuperuserAuth checks whether the current RequestInfo instance + * has superuser authentication loaded. + */ + hasSuperuserAuth(): boolean + } + interface RequestInfo { + /** + * Clone creates a new shallow copy of the current RequestInfo and its Auth record (if any). + */ + clone(): (RequestInfo) + } + type _subBNlNh = RequestEvent + interface BatchRequestEvent extends _subBNlNh { + batch: Array<(InternalRequest | undefined)> + } + type _subnnRAX = hook.Event + interface BootstrapEvent extends _subnnRAX { + app: App + } + type _subSVUUq = hook.Event + interface TerminateEvent extends _subSVUUq { + app: App + isRestart: boolean + } + type _subzSRiv = hook.Event + interface BackupEvent extends _subzSRiv { + app: App + context: context.Context + name: string // the name of the backup to create/restore. + exclude: Array // list of dir entries to exclude from the backup create/restore. + } + type _subGNCOc = hook.Event + interface ServeEvent extends _subGNCOc { + app: App + router?: router.Router + server?: http.Server + certManager?: any + } + type _suboOzgW = hook.Event&RequestEvent + interface SettingsListRequestEvent extends _suboOzgW { + settings?: Settings + } + type _subuSRkZ = hook.Event&RequestEvent + interface SettingsUpdateRequestEvent extends _subuSRkZ { + oldSettings?: Settings + newSettings?: Settings + } + type _subPmEmB = hook.Event + interface SettingsReloadEvent extends _subPmEmB { + app: App + } + type _subbEHXk = hook.Event + interface MailerEvent extends _subbEHXk { + app: App + mailer: mailer.Mailer + message?: mailer.Message + } + type _subpCTxH = MailerEvent&baseRecordEventData + interface MailerRecordEvent extends _subpCTxH { + meta: _TygojaDict + } + type _subDpIRL = hook.Event&baseModelEventData + interface ModelEvent extends _subDpIRL { + app: App + context: context.Context + /** + * Could be any of the ModelEventType* constants, like: + * - create + * - update + * - delete + * - validate + */ + type: string + } + type _subaEMaJ = ModelEvent + interface ModelErrorEvent extends _subaEMaJ { + error: Error + } + type _subKLiEq = hook.Event&baseRecordEventData + interface RecordEvent extends _subKLiEq { + app: App + context: context.Context + /** + * Could be any of the ModelEventType* constants, like: + * - create + * - update + * - delete + * - validate + */ + type: string + } + type _subiGyZm = RecordEvent + interface RecordErrorEvent extends _subiGyZm { + error: Error + } + type _subEFDdz = hook.Event&baseCollectionEventData + interface CollectionEvent extends _subEFDdz { + app: App + context: context.Context + /** + * Could be any of the ModelEventType* constants, like: + * - create + * - update + * - delete + * - validate + */ + type: string + } + type _subSmHLD = CollectionEvent + interface CollectionErrorEvent extends _subSmHLD { + error: Error + } + type _subbQXBO = hook.Event&RequestEvent + interface FileTokenRequestEvent extends _subbQXBO { + token: string + } + type _subYMuec = hook.Event&RequestEvent&baseCollectionEventData + interface FileDownloadRequestEvent extends _subYMuec { + record?: Record + fileField?: FileField + servedPath: string + servedName: string + } + type _subnnRQj = hook.Event&RequestEvent + interface CollectionsListRequestEvent extends _subnnRQj { + collections: Array<(Collection | undefined)> + result?: search.Result + } + type _subnDfhN = hook.Event&RequestEvent + interface CollectionsImportRequestEvent extends _subnDfhN { + collectionsData: Array<_TygojaDict> + deleteMissing: boolean + } + type _submlAhx = hook.Event&RequestEvent&baseCollectionEventData + interface CollectionRequestEvent extends _submlAhx { + } + type _subxHHlk = hook.Event&RequestEvent + interface RealtimeConnectRequestEvent extends _subxHHlk { + client: subscriptions.Client + /** + * note: modifying it after the connect has no effect + */ + idleTimeout: time.Duration + } + type _subtuMXT = hook.Event&RequestEvent + interface RealtimeMessageEvent extends _subtuMXT { + client: subscriptions.Client + message?: subscriptions.Message + } + type _subPSWBK = hook.Event&RequestEvent + interface RealtimeSubscribeRequestEvent extends _subPSWBK { + client: subscriptions.Client + subscriptions: Array + } + type _subJKFgr = hook.Event&RequestEvent&baseCollectionEventData + interface RecordsListRequestEvent extends _subJKFgr { + /** + * @todo consider removing and maybe add as generic to the search.Result? + */ + records: Array<(Record | undefined)> + result?: search.Result + } + type _subXfSAT = hook.Event&RequestEvent&baseCollectionEventData + interface RecordRequestEvent extends _subXfSAT { + record?: Record + } + type _subgZfBm = hook.Event&baseRecordEventData + interface RecordEnrichEvent extends _subgZfBm { + app: App + requestInfo?: RequestInfo + } + type _subTYVTo = hook.Event&RequestEvent&baseCollectionEventData + interface RecordCreateOTPRequestEvent extends _subTYVTo { + record?: Record + password: string + } + type _subJyJES = hook.Event&RequestEvent&baseCollectionEventData + interface RecordAuthWithOTPRequestEvent extends _subJyJES { + record?: Record + otp?: OTP + } + type _submpLNF = hook.Event&RequestEvent&baseCollectionEventData + interface RecordAuthRequestEvent extends _submpLNF { + record?: Record + token: string + meta: any + authMethod: string + } + type _subMXzKa = hook.Event&RequestEvent&baseCollectionEventData + interface RecordAuthWithPasswordRequestEvent extends _subMXzKa { + record?: Record + identity: string + identityField: string + password: string + } + type _substJLq = hook.Event&RequestEvent&baseCollectionEventData + interface RecordAuthWithOAuth2RequestEvent extends _substJLq { + providerName: string + providerClient: auth.Provider + record?: Record + oAuth2User?: auth.AuthUser + createData: _TygojaDict + isNewRecord: boolean + } + type _submURIz = hook.Event&RequestEvent&baseCollectionEventData + interface RecordAuthRefreshRequestEvent extends _submURIz { + record?: Record + } + type _subcKIan = hook.Event&RequestEvent&baseCollectionEventData + interface RecordRequestPasswordResetRequestEvent extends _subcKIan { + record?: Record + } + type _subRMqVJ = hook.Event&RequestEvent&baseCollectionEventData + interface RecordConfirmPasswordResetRequestEvent extends _subRMqVJ { + record?: Record + } + type _suboblGi = hook.Event&RequestEvent&baseCollectionEventData + interface RecordRequestVerificationRequestEvent extends _suboblGi { + record?: Record + } + type _subvTLkV = hook.Event&RequestEvent&baseCollectionEventData + interface RecordConfirmVerificationRequestEvent extends _subvTLkV { + record?: Record + } + type _subYFlNg = hook.Event&RequestEvent&baseCollectionEventData + interface RecordRequestEmailChangeRequestEvent extends _subYFlNg { + record?: Record + newEmail: string + } + type _subntuOr = hook.Event&RequestEvent&baseCollectionEventData + interface RecordConfirmEmailChangeRequestEvent extends _subntuOr { + record?: Record + newEmail: string + } + /** + * ExternalAuth defines a Record proxy for working with the externalAuths collection. + */ + type _subZhRGO = Record + interface ExternalAuth extends _subZhRGO { + } + interface ExternalAuth { + /** + * PreValidate implements the [PreValidator] interface and checks + * whether the proxy is properly loaded. + */ + preValidate(ctx: context.Context, app: App): void + } + interface ExternalAuth { + /** + * ProxyRecord returns the proxied Record model. + */ + proxyRecord(): (Record) + } + interface ExternalAuth { + /** + * SetProxyRecord loads the specified record model into the current proxy. + */ + setProxyRecord(record: Record): void + } + interface ExternalAuth { + /** + * CollectionRef returns the "collectionRef" field value. + */ + collectionRef(): string + } + interface ExternalAuth { + /** + * SetCollectionRef updates the "collectionRef" record field value. + */ + setCollectionRef(collectionId: string): void + } + interface ExternalAuth { + /** + * RecordRef returns the "recordRef" record field value. + */ + recordRef(): string + } + interface ExternalAuth { + /** + * SetRecordRef updates the "recordRef" record field value. + */ + setRecordRef(recordId: string): void + } + interface ExternalAuth { + /** + * Provider returns the "provider" record field value. + */ + provider(): string + } + interface ExternalAuth { + /** + * SetProvider updates the "provider" record field value. + */ + setProvider(provider: string): void + } + interface ExternalAuth { + /** + * Provider returns the "providerId" record field value. + */ + providerId(): string + } + interface ExternalAuth { + /** + * SetProvider updates the "providerId" record field value. + */ + setProviderId(providerId: string): void + } + interface ExternalAuth { + /** + * Created returns the "created" record field value. + */ + created(): types.DateTime + } + interface ExternalAuth { + /** + * Updated returns the "updated" record field value. + */ + updated(): types.DateTime + } + /** + * Field defines a common interface that all Collection fields should implement. + */ + interface Field { + [key:string]: any; + /** + * GetId returns the field id. + */ + getId(): string + /** + * SetId changes the field id. + */ + setId(id: string): void + /** + * GetName returns the field name. + */ + getName(): string + /** + * SetName changes the field name. + */ + setName(name: string): void + /** + * GetSystem returns the field system flag state. + */ + getSystem(): boolean + /** + * SetSystem changes the field system flag state. + */ + setSystem(system: boolean): void + /** + * GetHidden returns the field hidden flag state. + */ + getHidden(): boolean + /** + * SetHidden changes the field hidden flag state. + */ + setHidden(hidden: boolean): void + /** + * Type returns the unique type of the field. + */ + type(): string + /** + * ColumnType returns the DB column definition of the field. + */ + columnType(app: App): string + /** + * PrepareValue returns a properly formatted field value based on the provided raw one. + * + * This method is also called on record construction to initialize its default field value. + */ + prepareValue(record: Record, raw: any): any + /** + * ValidateSettings validates the current field value associated with the provided record. + */ + validateValue(ctx: context.Context, app: App, record: Record): void + /** + * ValidateSettings validates the current field settings. + */ + validateSettings(ctx: context.Context, app: App, collection: Collection): void + } + /** + * FileField defines "file" type field for managing record file(s). + * + * Only the file name is stored as part of the record value. + * New files (aka. files to upload) are expected to be of *filesytem.File. + * + * If MaxSelect is not set or <= 1, then the field value is expected to be a single record id. + * + * If MaxSelect is > 1, then the field value is expected to be a slice of record ids. + * + * --- + * + * The following additional setter keys are available: + * + * ``` + * - "fieldName+" - append one or more files to the existing record one. For example: + * + * // []string{"old1.txt", "old2.txt", "new1_ajkvass.txt", "new2_klhfnwd.txt"} + * record.Set("documents+", []*filesystem.File{new1, new2}) + * + * - "+fieldName" - prepend one or more files to the existing record one. For example: + * + * // []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: + * + * // []string{"old2.txt",} + * record.Set("documents-", "old1.txt") + * ``` + */ + interface FileField { + id: string + name: string + system: boolean + hidden: boolean + presentable: boolean + /** + * MaxSize specifies the maximum size of a single uploaded file (in bytes). + * + * If zero, a default limit of 5MB is applied. + */ + maxSize: number + /** + * MaxSelect specifies the max allowed files. + * + * For multiple files the value must be > 1, otherwise fallbacks to single (default). + */ + maxSelect: number + /** + * MimeTypes specifies an optional list of the allowed file mime types. + * + * Leave it empty to disable the validator. + */ + mimeTypes: Array + /** + * Thumbs specifies an optional list of the supported thumbs for image based files. + * + * Each entry must be in one of the following formats: + * + * ``` + * - WxH (eg. 100x300) - crop to WxH viewbox (from center) + * - WxHt (eg. 100x300t) - crop to WxH viewbox (from top) + * - WxHb (eg. 100x300b) - crop to WxH viewbox (from bottom) + * - WxHf (eg. 100x300f) - fit inside a WxH viewbox (without cropping) + * - 0xH (eg. 0x300) - resize to H height preserving the aspect ratio + * - Wx0 (eg. 100x0) - resize to W width preserving the aspect ratio + * ``` + */ + thumbs: Array + /** + * Protected will require the users to provide a special file token to access the file. + * + * Note that by default all files are publicly accessible. + * + * For the majority of the cases this is fine because by default + * all file names have random part appended to their name which + * need to be known by the user before accessing the file. + */ + protected: boolean + /** + * Required will require the field value to have at least one file. + */ + required: boolean + } + interface FileField { + /** + * Type implements [Field.Type] interface method. + */ + type(): string + } + interface FileField { + /** + * GetId implements [Field.GetId] interface method. + */ + getId(): string + } + interface FileField { + /** + * SetId implements [Field.SetId] interface method. + */ + setId(id: string): void + } + interface FileField { + /** + * GetName implements [Field.GetName] interface method. + */ + getName(): string + } + interface FileField { + /** + * SetName implements [Field.SetName] interface method. + */ + setName(name: string): void + } + interface FileField { + /** + * GetSystem implements [Field.GetSystem] interface method. + */ + getSystem(): boolean + } + interface FileField { + /** + * SetSystem implements [Field.SetSystem] interface method. + */ + setSystem(system: boolean): void + } + interface FileField { + /** + * GetHidden implements [Field.GetHidden] interface method. + */ + getHidden(): boolean + } + interface FileField { + /** + * SetHidden implements [Field.SetHidden] interface method. + */ + setHidden(hidden: boolean): void + } + interface FileField { + /** + * IsMultiple implements MultiValuer interface and checks whether the + * current field options support multiple values. + */ + isMultiple(): boolean + } + interface FileField { + /** + * ColumnType implements [Field.ColumnType] interface method. + */ + columnType(app: App): string + } + interface FileField { + /** + * PrepareValue implements [Field.PrepareValue] interface method. + */ + prepareValue(record: Record, raw: any): any + } + interface FileField { + /** + * DriverValue implements the [DriverValuer] interface. + */ + driverValue(record: Record): any + } + interface FileField { + /** + * ValidateSettings implements [Field.ValidateSettings] interface method. + */ + validateSettings(ctx: context.Context, app: App, collection: Collection): void + } + interface FileField { + /** + * ValidateValue implements [Field.ValidateValue] interface method. + */ + validateValue(ctx: context.Context, app: App, record: Record): void + } + interface FileField { + /** + * CalculateMaxBodySize implements the [MaxBodySizeCalculator] interface. + */ + calculateMaxBodySize(): number + } + interface FileField { + /** + * Intercept implements the [RecordInterceptor] interface. + * + * note: files delete after records deletion is handled globally by the app FileManager hook + */ + intercept(ctx: context.Context, app: App, record: Record, actionName: string, actionFunc: () => void): void + } + interface FileField { + /** + * FindGetter implements the [GetterFinder] interface. + */ + findGetter(key: string): GetterFunc + } + interface FileField { + /** + * FindSetter implements the [SetterFinder] interface. + */ + findSetter(key: string): SetterFunc + } + /** + * FieldsList defines a Collection slice of fields. + */ + interface FieldsList extends Array{} + interface FieldsList { + /** + * Clone creates a deep clone of the current list. + */ + clone(): FieldsList + } + interface FieldsList { + /** + * FieldNames returns a slice with the name of all list fields. + */ + fieldNames(): Array + } + interface FieldsList { + /** + * AsMap returns a map with all registered list field. + * The returned map is indexed with each field name. + */ + asMap(): _TygojaDict + } + interface FieldsList { + /** + * GetById returns a single field by its id. + */ + getById(fieldId: string): Field + } + interface FieldsList { + /** + * GetByName returns a single field by its name. + */ + getByName(fieldName: string): Field + } + interface FieldsList { + /** + * RemoveById removes a single field by its id. + * + * This method does nothing if field with the specified id doesn't exist. + */ + removeById(fieldId: string): void + } + interface FieldsList { + /** + * RemoveByName removes a single field by its name. + * + * This method does nothing if field with the specified name doesn't exist. + */ + removeByName(fieldName: string): void + } + interface FieldsList { + /** + * 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. + * + * If the list already has a field with the same id, + * then the existing field is replaced with the new one. + * + * Otherwise the new field is appended after the other list fields. + */ + add(...fields: Field[]): void + } + interface FieldsList { + /** + * String returns the string representation of the current list. + */ + string(): string + } + interface FieldsList { + /** + * UnmarshalJSON implements [json.Unmarshaler] and + * loads the provided json data into the current FieldsList. + */ + unmarshalJSON(data: string|Array): void + } + interface FieldsList { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface FieldsList { + /** + * Value implements the [driver.Valuer] interface. + */ + value(): any + } + interface FieldsList { + /** + * Scan implements [sql.Scanner] interface to scan the provided value + * into the current FieldsList instance. + */ + scan(value: any): void + } + type _subrQOrz = BaseModel + interface Log extends _subrQOrz { + created: types.DateTime + data: types.JSONMap + message: string + level: number + } + interface Log { + tableName(): string + } + /** + * LogsStatsItem defines the total number of logs for a specific time period. + */ + interface LogsStatsItem { + date: types.DateTime + total: number + } + /** + * MFA defines a Record proxy for working with the mfas collection. + */ + type _subOOyQG = Record + interface MFA extends _subOOyQG { + } + interface MFA { + /** + * PreValidate implements the [PreValidator] interface and checks + * whether the proxy is properly loaded. + */ + preValidate(ctx: context.Context, app: App): void + } + interface MFA { + /** + * ProxyRecord returns the proxied Record model. + */ + proxyRecord(): (Record) + } + interface MFA { + /** + * SetProxyRecord loads the specified record model into the current proxy. + */ + setProxyRecord(record: Record): void + } + interface MFA { + /** + * CollectionRef returns the "collectionRef" field value. + */ + collectionRef(): string + } + interface MFA { + /** + * SetCollectionRef updates the "collectionRef" record field value. + */ + setCollectionRef(collectionId: string): void + } + interface MFA { + /** + * RecordRef returns the "recordRef" record field value. + */ + recordRef(): string + } + interface MFA { + /** + * SetRecordRef updates the "recordRef" record field value. + */ + setRecordRef(recordId: string): void + } + interface MFA { + /** + * Method returns the "method" record field value. + */ + method(): string + } + interface MFA { + /** + * SetMethod updates the "method" record field value. + */ + setMethod(method: string): void + } + interface MFA { + /** + * Created returns the "created" record field value. + */ + created(): types.DateTime + } + interface MFA { + /** + * Updated returns the "updated" record field value. + */ + updated(): types.DateTime + } + interface MFA { + /** + * HasExpired checks if the mfa is expired, aka. whether it has been + * more than maxElapsed time since its creation. + */ + hasExpired(maxElapsed: time.Duration): boolean + } + /** + * OTP defines a Record proxy for working with the otps collection. + */ + type _submjxxq = Record + interface OTP extends _submjxxq { + } + interface OTP { + /** + * PreValidate implements the [PreValidator] interface and checks + * whether the proxy is properly loaded. + */ + preValidate(ctx: context.Context, app: App): void + } + interface OTP { + /** + * ProxyRecord returns the proxied Record model. + */ + proxyRecord(): (Record) + } + interface OTP { + /** + * SetProxyRecord loads the specified record model into the current proxy. + */ + setProxyRecord(record: Record): void + } + interface OTP { + /** + * CollectionRef returns the "collectionRef" field value. + */ + collectionRef(): string + } + interface OTP { + /** + * SetCollectionRef updates the "collectionRef" record field value. + */ + setCollectionRef(collectionId: string): void + } + interface OTP { + /** + * RecordRef returns the "recordRef" record field value. + */ + recordRef(): string + } + interface OTP { + /** + * SetRecordRef updates the "recordRef" record field value. + */ + setRecordRef(recordId: string): void + } + interface OTP { + /** + * Created returns the "created" record field value. + */ + created(): types.DateTime + } + interface OTP { + /** + * Updated returns the "updated" record field value. + */ + updated(): types.DateTime + } + interface OTP { + /** + * HasExpired checks if the otp is expired, aka. whether it has been + * more than maxElapsed time since its creation. + */ + hasExpired(maxElapsed: time.Duration): boolean + } + /** + * ExpandFetchFunc defines the function that is used to fetch the expanded relation records. + */ + interface ExpandFetchFunc {(relCollection: Collection, relIds: Array): Array<(Record | undefined)> } + /** + * Settings defines the PocketBase app settings. + */ + type _subIVrQW = settings + interface Settings extends _subIVrQW { + } + interface Settings { + /** + * TableName implements [Model.TableName] interface method. + */ + tableName(): string + } + interface Settings { + /** + * PK implements [Model.LastSavedPK] interface method. + */ + lastSavedPK(): any + } + interface Settings { + /** + * PK implements [Model.PK] interface method. + */ + pk(): any + } + interface Settings { + /** + * IsNew implements [Model.IsNew] interface method. + */ + isNew(): boolean + } + interface Settings { + /** + * MarkAsNew implements [Model.MarkAsNew] interface method. + */ + markAsNew(): void + } + interface Settings { + /** + * MarkAsNew implements [Model.MarkAsNotNew] interface method. + */ + markAsNotNew(): void + } + interface Settings { + /** + * PostScan implements [Model.PostScan] interface method. + */ + postScan(): void + } + interface Settings { + /** + * String returns a serialized string representation of the current settings. + */ + string(): string + } + interface Settings { + /** + * DBExport prepares and exports the current settings for db persistence. + */ + dbExport(app: App): _TygojaDict + } + interface Settings { + /** + * PostValidate implements the [PostValidator] interface and defines + * the Settings model validations. + */ + postValidate(ctx: context.Context, app: App): void + } + interface Settings { + /** + * Merge merges the "other" settings into the current one. + */ + merge(other: Settings): void + } + interface Settings { + /** + * Clone creates a new deep copy of the current settings. + */ + clone(): (Settings) + } + interface Settings { + /** + * MarshalJSON implements the [json.Marshaler] interface. + * + * Note that sensitive fields (S3 secret, SMTP password, etc.) are excluded. + */ + marshalJSON(): string|Array + } +} + +/** + * Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. + * In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code. + */ +namespace cobra { + interface PositionalArgs {(cmd: Command, args: Array): void } + // @ts-ignore + import flag = pflag + /** + * FParseErrWhitelist configures Flag parse errors to be ignored + */ + interface FParseErrWhitelist extends _TygojaAny{} + /** + * Group Structure to manage groups for commands + */ + interface Group { + id: string + title: string + } + /** + * ShellCompDirective is a bit map representing the different behaviors the shell + * can be instructed to have once completions have been provided. + */ + interface ShellCompDirective extends Number{} + /** + * CompletionOptions are the options to control shell completion + */ + interface CompletionOptions { + /** + * DisableDefaultCmd prevents Cobra from creating a default 'completion' command + */ + disableDefaultCmd: boolean + /** + * DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag + * for shells that support completion descriptions + */ + disableNoDescFlag: boolean + /** + * DisableDescriptions turns off all completion descriptions for shells + * that support them + */ + disableDescriptions: boolean + /** + * HiddenDefaultCmd makes the default 'completion' command hidden + */ + hiddenDefaultCmd: boolean + } +} + +/** + * Package url parses URLs and implements query escaping. + */ +namespace url { + /** + * The Userinfo type is an immutable encapsulation of username and + * password details for a [URL]. An existing Userinfo value is guaranteed + * to have a username set (potentially empty, as allowed by RFC 2396), + * and optionally a password. + */ + interface Userinfo { + } + interface Userinfo { + /** + * Username returns the username. + */ + username(): string + } + interface Userinfo { + /** + * Password returns the password in case it is set, and whether it is set. + */ + password(): [string, boolean] + } + interface Userinfo { + /** + * String returns the encoded userinfo information in the standard form + * of "username[:password]". + */ + string(): string + } +} + +/** + * Package sql provides a generic interface around SQL (or SQL-like) + * databases. + * + * The sql package must be used in conjunction with a database driver. + * See https://golang.org/s/sqldrivers for a list of drivers. + * + * Drivers that do not support context cancellation will not return until + * after the query is completed. + * + * For usage examples, see the wiki page at + * https://golang.org/s/sqlwiki. + */ +namespace sql { + /** + * NullString represents a string that may be null. + * NullString implements the [Scanner] interface so + * it can be used as a scan destination: + * + * ``` + * var s NullString + * err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s) + * ... + * if s.Valid { + * // use s.String + * } else { + * // NULL value + * } + * ``` + */ + interface NullString { + string: string + valid: boolean // Valid is true if String is not NULL + } + interface NullString { + /** + * Scan implements the [Scanner] interface. + */ + scan(value: any): void + } + interface NullString { + /** + * Value implements the [driver.Valuer] interface. + */ + value(): any + } +} + +/** + * Package multipart implements MIME multipart parsing, as defined in RFC + * 2046. + * + * The implementation is sufficient for HTTP (RFC 2388) and the multipart + * bodies generated by popular browsers. + * + * # Limits + * + * To protect against malicious inputs, this package sets limits on the size + * of the MIME data it processes. + * + * [Reader.NextPart] and [Reader.NextRawPart] limit the number of headers in a + * part to 10000 and [Reader.ReadForm] limits the total number of headers in all + * FileHeaders to 10000. + * These limits may be adjusted with the GODEBUG=multipartmaxheaders= + * setting. + * + * Reader.ReadForm further limits the number of parts in a form to 1000. + * This limit may be adjusted with the GODEBUG=multipartmaxparts= + * setting. + */ +namespace multipart { + /** + * A Part represents a single part in a multipart body. + */ + interface Part { + /** + * The headers of the body, if any, with the keys canonicalized + * in the same fashion that the Go http.Request headers are. + * For example, "foo-bar" changes case to "Foo-Bar" + */ + header: textproto.MIMEHeader + } + interface Part { + /** + * FormName returns the name parameter if p has a Content-Disposition + * of type "form-data". Otherwise it returns the empty string. + */ + formName(): string + } + interface Part { + /** + * FileName returns the filename parameter of the [Part]'s Content-Disposition + * header. If not empty, the filename is passed through filepath.Base (which is + * platform dependent) before being returned. + */ + fileName(): string + } + interface Part { + /** + * Read reads the body of a part, after its headers and before the + * next part (if any) begins. + */ + read(d: string|Array): number + } + interface Part { + close(): void + } +} + +/** + * Package http provides HTTP client and server implementations. + * + * [Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests: + * + * ``` + * resp, err := http.Get("http://example.com/") + * ... + * resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) + * ... + * resp, err := http.PostForm("http://example.com/form", + * url.Values{"key": {"Value"}, "id": {"123"}}) + * ``` + * + * The caller must close the response body when finished with it: + * + * ``` + * resp, err := http.Get("http://example.com/") + * if err != nil { + * // handle error + * } + * defer resp.Body.Close() + * body, err := io.ReadAll(resp.Body) + * // ... + * ``` + * + * # Clients and Transports + * + * For control over HTTP client headers, redirect policy, and other + * settings, create a [Client]: + * + * ``` + * client := &http.Client{ + * CheckRedirect: redirectPolicyFunc, + * } + * + * resp, err := client.Get("http://example.com") + * // ... + * + * req, err := http.NewRequest("GET", "http://example.com", nil) + * // ... + * req.Header.Add("If-None-Match", `W/"wyzzy"`) + * resp, err := client.Do(req) + * // ... + * ``` + * + * For control over proxies, TLS configuration, keep-alives, + * compression, and other settings, create a [Transport]: + * + * ``` + * tr := &http.Transport{ + * MaxIdleConns: 10, + * IdleConnTimeout: 30 * time.Second, + * DisableCompression: true, + * } + * client := &http.Client{Transport: tr} + * resp, err := client.Get("https://example.com") + * ``` + * + * Clients and Transports are safe for concurrent use by multiple + * goroutines and for efficiency should only be created once and re-used. + * + * # Servers + * + * ListenAndServe starts an HTTP server with a given address and handler. + * The handler is usually nil, which means to use [DefaultServeMux]. + * [Handle] and [HandleFunc] add handlers to [DefaultServeMux]: + * + * ``` + * http.Handle("/foo", fooHandler) + * + * http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + * fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + * }) + * + * log.Fatal(http.ListenAndServe(":8080", nil)) + * ``` + * + * More control over the server's behavior is available by creating a + * custom Server: + * + * ``` + * s := &http.Server{ + * Addr: ":8080", + * Handler: myHandler, + * ReadTimeout: 10 * time.Second, + * WriteTimeout: 10 * time.Second, + * MaxHeaderBytes: 1 << 20, + * } + * log.Fatal(s.ListenAndServe()) + * ``` + * + * # HTTP/2 + * + * Starting with Go 1.6, the http package has transparent support for the + * HTTP/2 protocol when using HTTPS. Programs that must disable HTTP/2 + * can do so by setting [Transport.TLSNextProto] (for clients) or + * [Server.TLSNextProto] (for servers) to a non-nil, empty + * map. Alternatively, the following GODEBUG settings are + * currently supported: + * + * ``` + * GODEBUG=http2client=0 # disable HTTP/2 client support + * GODEBUG=http2server=0 # disable HTTP/2 server support + * GODEBUG=http2debug=1 # enable verbose HTTP/2 debug logs + * GODEBUG=http2debug=2 # ... even more verbose, with frame dumps + * ``` + * + * Please report any issues before disabling HTTP/2 support: https://golang.org/s/http2bug + * + * The http package's [Transport] and [Server] both automatically enable + * HTTP/2 support for simple configurations. To enable HTTP/2 for more + * complex configurations, to use lower-level HTTP/2 features, or to use + * a newer version of Go's http2 package, import "golang.org/x/net/http2" + * directly and use its ConfigureTransport and/or ConfigureServer + * functions. Manually configuring HTTP/2 via the golang.org/x/net/http2 + * package takes precedence over the net/http package's built-in HTTP/2 + * support. + */ +namespace http { + /** + * SameSite allows a server to define a cookie attribute making it impossible for + * the browser to send this cookie along with cross-site requests. The main + * goal is to mitigate the risk of cross-origin information leakage, and provide + * some protection against cross-site request forgery attacks. + * + * See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. + */ + interface SameSite extends Number{} + // @ts-ignore + import mathrand = rand + // @ts-ignore + import urlpkg = url + /** + * A Server defines parameters for running an HTTP server. + * The zero value for Server is a valid configuration. + */ + interface Server { + /** + * Addr optionally specifies the TCP address for the server to listen on, + * in the form "host:port". If empty, ":http" (port 80) is used. + * The service names are defined in RFC 6335 and assigned by IANA. + * See net.Dial for details of the address format. + */ + addr: string + handler: Handler // handler to invoke, http.DefaultServeMux if nil + /** + * DisableGeneralOptionsHandler, if true, passes "OPTIONS *" requests to the Handler, + * otherwise responds with 200 OK and Content-Length: 0. + */ + disableGeneralOptionsHandler: boolean + /** + * TLSConfig optionally provides a TLS configuration for use + * by ServeTLS and ListenAndServeTLS. Note that this value is + * cloned by ServeTLS and ListenAndServeTLS, so it's not + * possible to modify the configuration with methods like + * tls.Config.SetSessionTicketKeys. To use + * SetSessionTicketKeys, use Server.Serve with a TLS Listener + * instead. + */ + tlsConfig?: any + /** + * ReadTimeout is the maximum duration for reading the entire + * request, including the body. A zero or negative value means + * there will be no timeout. + * + * Because ReadTimeout does not let Handlers make per-request + * decisions on each request body's acceptable deadline or + * upload rate, most users will prefer to use + * ReadHeaderTimeout. It is valid to use them both. + */ + readTimeout: time.Duration + /** + * ReadHeaderTimeout is the amount of time allowed to read + * request headers. The connection's read deadline is reset + * after reading the headers and the Handler can decide what + * is considered too slow for the body. If zero, the value of + * ReadTimeout is used. If negative, or if zero and ReadTimeout + * is zero or negative, there is no timeout. + */ + readHeaderTimeout: time.Duration + /** + * WriteTimeout is the maximum duration before timing out + * writes of the response. It is reset whenever a new + * request's header is read. Like ReadTimeout, it does not + * let Handlers make decisions on a per-request basis. + * A zero or negative value means there will be no timeout. + */ + writeTimeout: time.Duration + /** + * IdleTimeout is the maximum amount of time to wait for the + * next request when keep-alives are enabled. If zero, the value + * of ReadTimeout is used. If negative, or if zero and ReadTimeout + * is zero or negative, there is no timeout. + */ + idleTimeout: time.Duration + /** + * MaxHeaderBytes controls the maximum number of bytes the + * server will read parsing the request header's keys and + * values, including the request line. It does not limit the + * size of the request body. + * If zero, DefaultMaxHeaderBytes is used. + */ + maxHeaderBytes: number + /** + * TLSNextProto optionally specifies a function to take over + * ownership of the provided TLS connection when an ALPN + * protocol upgrade has occurred. The map key is the protocol + * name negotiated. The Handler argument should be used to + * handle HTTP requests and will initialize the Request's TLS + * and RemoteAddr if not already set. The connection is + * automatically closed when the function returns. + * If TLSNextProto is not nil, HTTP/2 support is not enabled + * automatically. + */ + tlsNextProto: _TygojaDict + /** + * ConnState specifies an optional callback function that is + * called when a client connection changes state. See the + * ConnState type and associated constants for details. + */ + connState: (_arg0: net.Conn, _arg1: ConnState) => void + /** + * ErrorLog specifies an optional logger for errors accepting + * connections, unexpected behavior from handlers, and + * underlying FileSystem errors. + * If nil, logging is done via the log package's standard logger. + */ + errorLog?: any + /** + * BaseContext optionally specifies a function that returns + * the base context for incoming requests on this server. + * The provided Listener is the specific Listener that's + * about to start accepting requests. + * If BaseContext is nil, the default is context.Background(). + * If non-nil, it must return a non-nil context. + */ + baseContext: (_arg0: net.Listener) => context.Context + /** + * ConnContext optionally specifies a function that modifies + * the context used for a new connection c. The provided ctx + * is derived from the base context and has a ServerContextKey + * value. + */ + connContext: (ctx: context.Context, c: net.Conn) => context.Context + } + interface Server { + /** + * Close immediately closes all active net.Listeners and any + * connections in state [StateNew], [StateActive], or [StateIdle]. For a + * graceful shutdown, use [Server.Shutdown]. + * + * Close does not attempt to close (and does not even know about) + * any hijacked connections, such as WebSockets. + * + * Close returns any error returned from closing the [Server]'s + * underlying Listener(s). + */ + close(): void + } + interface Server { + /** + * Shutdown gracefully shuts down the server without interrupting any + * active connections. Shutdown works by first closing all open + * listeners, then closing all idle connections, and then waiting + * indefinitely for connections to return to idle and then shut down. + * If the provided context expires before the shutdown is complete, + * Shutdown returns the context's error, otherwise it returns any + * error returned from closing the [Server]'s underlying Listener(s). + * + * When Shutdown is called, [Serve], [ListenAndServe], and + * [ListenAndServeTLS] immediately return [ErrServerClosed]. Make sure the + * program doesn't exit and waits instead for Shutdown to return. + * + * Shutdown does not attempt to close nor wait for hijacked + * connections such as WebSockets. The caller of Shutdown should + * separately notify such long-lived connections of shutdown and wait + * for them to close, if desired. See [Server.RegisterOnShutdown] for a way to + * register shutdown notification functions. + * + * Once Shutdown has been called on a server, it may not be reused; + * future calls to methods such as Serve will return ErrServerClosed. + */ + shutdown(ctx: context.Context): void + } + interface Server { + /** + * RegisterOnShutdown registers a function to call on [Server.Shutdown]. + * This can be used to gracefully shutdown connections that have + * undergone ALPN protocol upgrade or that have been hijacked. + * This function should start protocol-specific graceful shutdown, + * but should not wait for shutdown to complete. + */ + registerOnShutdown(f: () => void): void + } + interface Server { + /** + * ListenAndServe listens on the TCP network address srv.Addr and then + * calls [Serve] to handle requests on incoming connections. + * Accepted connections are configured to enable TCP keep-alives. + * + * If srv.Addr is blank, ":http" is used. + * + * ListenAndServe always returns a non-nil error. After [Server.Shutdown] or [Server.Close], + * the returned error is [ErrServerClosed]. + */ + listenAndServe(): void + } + interface Server { + /** + * Serve accepts incoming connections on the Listener l, creating a + * new service goroutine for each. The service goroutines read requests and + * then call srv.Handler to reply to them. + * + * HTTP/2 support is only enabled if the Listener returns [*tls.Conn] + * connections and they were configured with "h2" in the TLS + * Config.NextProtos. + * + * Serve always returns a non-nil error and closes l. + * After [Server.Shutdown] or [Server.Close], the returned error is [ErrServerClosed]. + */ + serve(l: net.Listener): void + } + interface Server { + /** + * ServeTLS accepts incoming connections on the Listener l, creating a + * new service goroutine for each. The service goroutines perform TLS + * setup and then read requests, calling srv.Handler to reply to them. + * + * Files containing a certificate and matching private key for the + * server must be provided if neither the [Server]'s + * TLSConfig.Certificates, TLSConfig.GetCertificate nor + * config.GetConfigForClient are populated. + * If the certificate is signed by a certificate authority, the + * certFile should be the concatenation of the server's certificate, + * any intermediates, and the CA's certificate. + * + * ServeTLS always returns a non-nil error. After [Server.Shutdown] or [Server.Close], the + * returned error is [ErrServerClosed]. + */ + serveTLS(l: net.Listener, certFile: string, keyFile: string): void + } + interface Server { + /** + * SetKeepAlivesEnabled controls whether HTTP keep-alives are enabled. + * By default, keep-alives are always enabled. Only very + * resource-constrained environments or servers in the process of + * shutting down should disable them. + */ + setKeepAlivesEnabled(v: boolean): void + } + interface Server { + /** + * ListenAndServeTLS listens on the TCP network address srv.Addr and + * then calls [ServeTLS] to handle requests on incoming TLS connections. + * Accepted connections are configured to enable TCP keep-alives. + * + * Filenames containing a certificate and matching private key for the + * server must be provided if neither the [Server]'s TLSConfig.Certificates + * nor TLSConfig.GetCertificate are populated. If the certificate is + * signed by a certificate authority, the certFile should be the + * concatenation of the server's certificate, any intermediates, and + * the CA's certificate. + * + * If srv.Addr is blank, ":https" is used. + * + * ListenAndServeTLS always returns a non-nil error. After [Server.Shutdown] or + * [Server.Close], the returned error is [ErrServerClosed]. + */ + listenAndServeTLS(certFile: string, keyFile: string): void + } +} + +namespace store { +} + +/** + * Package types implements some commonly used db serializable types + * like datetime, json, etc. + */ +namespace types { + /** + * JSONMap defines a map that is safe for json and db read/write. + */ + interface JSONMap extends _TygojaDict{} + interface JSONMap { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface JSONMap { + /** + * String returns the string representation of the current json map. + */ + string(): string + } + interface JSONMap { + /** + * Get retrieves a single value from the current JSONMap[T]. + * + * This helper was added primarily to assist the goja integration since custom map types + * don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). + */ + get(key: string): T + } + interface JSONMap { + /** + * Set sets a single value in the current JSONMap[T]. + * + * This helper was added primarily to assist the goja integration since custom map types + * don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). + */ + set(key: string, value: T): void + } + interface JSONMap { + /** + * Value implements the [driver.Valuer] interface. + */ + value(): any + } + interface JSONMap { + /** + * Scan implements [sql.Scanner] interface to scan the provided value + * into the current JSONMap[T] instance. + */ + scan(value: any): void + } +} + +namespace auth { + /** + * Provider defines a common interface for an OAuth2 client. + */ + interface Provider { + [key:string]: any; + /** + * Context returns the context associated with the provider (if any). + */ + context(): context.Context + /** + * SetContext assigns the specified context to the current provider. + */ + setContext(ctx: context.Context): void + /** + * PKCE indicates whether the provider can use the PKCE flow. + */ + pkce(): boolean + /** + * SetPKCE toggles the state whether the provider can use the PKCE flow or not. + */ + setPKCE(enable: boolean): void + /** + * DisplayName usually returns provider name as it is officially written + * and it could be used directly in the UI. + */ + displayName(): string + /** + * SetDisplayName sets the provider's display name. + */ + setDisplayName(displayName: string): void + /** + * Scopes returns the provider access permissions that will be requested. + */ + scopes(): Array + /** + * SetScopes sets the provider access permissions that will be requested later. + */ + setScopes(scopes: Array): void + /** + * ClientId returns the provider client's app ID. + */ + clientId(): string + /** + * SetClientId sets the provider client's ID. + */ + setClientId(clientId: string): void + /** + * ClientSecret returns the provider client's app secret. + */ + clientSecret(): string + /** + * SetClientSecret sets the provider client's app secret. + */ + setClientSecret(secret: string): void + /** + * RedirectURL returns the end address to redirect the user + * going through the OAuth flow. + */ + redirectURL(): string + /** + * SetRedirectURL sets the provider's RedirectURL. + */ + setRedirectURL(url: string): void + /** + * AuthURL returns the provider's authorization service url. + */ + authURL(): string + /** + * SetAuthURL sets the provider's AuthURL. + */ + setAuthURL(url: string): void + /** + * TokenURL returns the provider's token exchange service url. + */ + tokenURL(): string + /** + * SetTokenURL sets the provider's TokenURL. + */ + setTokenURL(url: string): void + /** + * UserInfoURL returns the provider's user info api url. + */ + userInfoURL(): string + /** + * SetUserInfoURL sets the provider's UserInfoURL. + */ + setUserInfoURL(url: string): void + /** + * Client returns an http client using the provided token. + */ + client(token: oauth2.Token): (any) + /** + * BuildAuthURL returns a URL to the provider's consent page + * that asks for permissions for the required scopes explicitly. + */ + buildAuthURL(state: string, ...opts: oauth2.AuthCodeOption[]): string + /** + * FetchToken converts an authorization code to token. + */ + fetchToken(code: string, ...opts: oauth2.AuthCodeOption[]): (oauth2.Token) + /** + * FetchRawUserInfo requests and marshalizes into `result` the + * the OAuth user api response. + */ + fetchRawUserInfo(token: oauth2.Token): string|Array + /** + * FetchAuthUser is similar to FetchRawUserInfo, but normalizes and + * marshalizes the user api response into a standardized AuthUser struct. + */ + fetchAuthUser(token: oauth2.Token): (AuthUser) + } + /** + * AuthUser defines a standardized OAuth2 user data structure. + */ + interface AuthUser { + expiry: types.DateTime + rawUser: _TygojaDict + id: string + name: string + username: string + email: string + avatarURL: string + accessToken: string + refreshToken: string + /** + * @todo + * deprecated: use AvatarURL instead + * AvatarUrl will be removed after dropping v0.22 support + */ + avatarUrl: string + } + interface AuthUser { + /** + * MarshalJSON implements the [json.Marshaler] interface. + * + * @todo remove after dropping v0.22 support + */ + marshalJSON(): string|Array + } +} + +namespace hook { + /** + * Event implements [Resolver] and it is intended to be used as a base + * Hook event that you can embed in your custom typed event structs. + * + * Example: + * + * ``` + * type CustomEvent struct { + * hook.Event + * + * SomeField int + * } + * ``` + */ + interface Event { + } + interface Event { + /** + * Next calls the next hook handler. + */ + next(): void + } + /** + * wrapped local Hook embedded struct to limit the public API surface. + */ + type _subopNqc = Hook + interface mainHook extends _subopNqc { + } +} + +/** + * Package slog provides structured logging, + * in which log records include a message, + * a severity level, and various other attributes + * expressed as key-value pairs. + * + * It defines a type, [Logger], + * which provides several methods (such as [Logger.Info] and [Logger.Error]) + * for reporting events of interest. + * + * Each Logger is associated with a [Handler]. + * A Logger output method creates a [Record] from the method arguments + * and passes it to the Handler, which decides how to handle it. + * There is a default Logger accessible through top-level functions + * (such as [Info] and [Error]) that call the corresponding Logger methods. + * + * A log record consists of a time, a level, a message, and a set of key-value + * pairs, where the keys are strings and the values may be of any type. + * As an example, + * + * ``` + * slog.Info("hello", "count", 3) + * ``` + * + * creates a record containing the time of the call, + * a level of Info, the message "hello", and a single + * pair with key "count" and value 3. + * + * The [Info] top-level function calls the [Logger.Info] method on the default Logger. + * In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. + * Besides these convenience methods for common levels, + * there is also a [Logger.Log] method which takes the level as an argument. + * Each of these methods has a corresponding top-level function that uses the + * default logger. + * + * The default handler formats the log record's message, time, level, and attributes + * as a string and passes it to the [log] package. + * + * ``` + * 2022/11/08 15:28:26 INFO hello count=3 + * ``` + * + * For more control over the output format, create a logger with a different handler. + * This statement uses [New] to create a new logger with a [TextHandler] + * that writes structured records in text form to standard error: + * + * ``` + * logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + * ``` + * + * [TextHandler] output is a sequence of key=value pairs, easily and unambiguously + * parsed by machine. This statement: + * + * ``` + * logger.Info("hello", "count", 3) + * ``` + * + * produces this output: + * + * ``` + * time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 + * ``` + * + * The package also provides [JSONHandler], whose output is line-delimited JSON: + * + * ``` + * logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + * logger.Info("hello", "count", 3) + * ``` + * + * produces this output: + * + * ``` + * {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} + * ``` + * + * Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. + * There are options for setting the minimum level (see Levels, below), + * displaying the source file and line of the log call, and + * modifying attributes before they are logged. + * + * Setting a logger as the default with + * + * ``` + * slog.SetDefault(logger) + * ``` + * + * will cause the top-level functions like [Info] to use it. + * [SetDefault] also updates the default logger used by the [log] package, + * so that existing applications that use [log.Printf] and related functions + * will send log records to the logger's handler without needing to be rewritten. + * + * Some attributes are common to many log calls. + * For example, you may wish to include the URL or trace identifier of a server request + * with all log events arising from the request. + * Rather than repeat the attribute with every log call, you can use [Logger.With] + * to construct a new Logger containing the attributes: + * + * ``` + * logger2 := logger.With("url", r.URL) + * ``` + * + * The arguments to With are the same key-value pairs used in [Logger.Info]. + * The result is a new Logger with the same handler as the original, but additional + * attributes that will appear in the output of every call. + * + * # Levels + * + * A [Level] is an integer representing the importance or severity of a log event. + * The higher the level, the more severe the event. + * This package defines constants for the most common levels, + * but any int can be used as a level. + * + * In an application, you may wish to log messages only at a certain level or greater. + * One common configuration is to log messages at Info or higher levels, + * suppressing debug logging until it is needed. + * The built-in handlers can be configured with the minimum level to output by + * setting [HandlerOptions.Level]. + * The program's `main` function typically does this. + * The default value is LevelInfo. + * + * Setting the [HandlerOptions.Level] field to a [Level] value + * fixes the handler's minimum level throughout its lifetime. + * Setting it to a [LevelVar] allows the level to be varied dynamically. + * A LevelVar holds a Level and is safe to read or write from multiple + * goroutines. + * To vary the level dynamically for an entire program, first initialize + * a global LevelVar: + * + * ``` + * var programLevel = new(slog.LevelVar) // Info by default + * ``` + * + * Then use the LevelVar to construct a handler, and make it the default: + * + * ``` + * h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) + * slog.SetDefault(slog.New(h)) + * ``` + * + * Now the program can change its logging level with a single statement: + * + * ``` + * programLevel.Set(slog.LevelDebug) + * ``` + * + * # Groups + * + * Attributes can be collected into groups. + * A group has a name that is used to qualify the names of its attributes. + * How this qualification is displayed depends on the handler. + * [TextHandler] separates the group and attribute names with a dot. + * [JSONHandler] treats each group as a separate JSON object, with the group name as the key. + * + * Use [Group] to create a Group attribute from a name and a list of key-value pairs: + * + * ``` + * slog.Group("request", + * "method", r.Method, + * "url", r.URL) + * ``` + * + * TextHandler would display this group as + * + * ``` + * request.method=GET request.url=http://example.com + * ``` + * + * JSONHandler would display it as + * + * ``` + * "request":{"method":"GET","url":"http://example.com"} + * ``` + * + * Use [Logger.WithGroup] to qualify all of a Logger's output + * with a group name. Calling WithGroup on a Logger results in a + * new Logger with the same Handler as the original, but with all + * its attributes qualified by the group name. + * + * This can help prevent duplicate attribute keys in large systems, + * where subsystems might use the same keys. + * Pass each subsystem a different Logger with its own group name so that + * potential duplicates are qualified: + * + * ``` + * logger := slog.Default().With("id", systemID) + * parserLogger := logger.WithGroup("parser") + * parseInput(input, parserLogger) + * ``` + * + * When parseInput logs with parserLogger, its keys will be qualified with "parser", + * so even if it uses the common key "id", the log line will have distinct keys. + * + * # Contexts + * + * Some handlers may wish to include information from the [context.Context] that is + * available at the call site. One example of such information + * is the identifier for the current span when tracing is enabled. + * + * The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first + * argument, as do their corresponding top-level functions. + * + * Although the convenience methods on Logger (Info and so on) and the + * corresponding top-level functions do not take a context, the alternatives ending + * in "Context" do. For example, + * + * ``` + * slog.InfoContext(ctx, "message") + * ``` + * + * It is recommended to pass a context to an output method if one is available. + * + * # Attrs and Values + * + * An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as + * alternating keys and values. The statement + * + * ``` + * slog.Info("hello", slog.Int("count", 3)) + * ``` + * + * behaves the same as + * + * ``` + * slog.Info("hello", "count", 3) + * ``` + * + * There are convenience constructors for [Attr] such as [Int], [String], and [Bool] + * for common types, as well as the function [Any] for constructing Attrs of any + * type. + * + * The value part of an Attr is a type called [Value]. + * Like an [any], a Value can hold any Go value, + * but it can represent typical values, including all numbers and strings, + * without an allocation. + * + * For the most efficient log output, use [Logger.LogAttrs]. + * It is similar to [Logger.Log] but accepts only Attrs, not alternating + * keys and values; this allows it, too, to avoid allocation. + * + * The call + * + * ``` + * logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3)) + * ``` + * + * is the most efficient way to achieve the same output as + * + * ``` + * slog.InfoContext(ctx, "hello", "count", 3) + * ``` + * + * # Customizing a type's logging behavior + * + * If a type implements the [LogValuer] interface, the [Value] returned from its LogValue + * method is used for logging. You can use this to control how values of the type + * appear in logs. For example, you can redact secret information like passwords, + * or gather a struct's fields in a Group. See the examples under [LogValuer] for + * details. + * + * A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] + * method handles these cases carefully, avoiding infinite loops and unbounded recursion. + * Handler authors and others may wish to use [Value.Resolve] instead of calling LogValue directly. + * + * # Wrapping output methods + * + * The logger functions use reflection over the call stack to find the file name + * and line number of the logging call within the application. This can produce + * incorrect source information for functions that wrap slog. For instance, if you + * define this function in file mylog.go: + * + * ``` + * func Infof(logger *slog.Logger, format string, args ...any) { + * logger.Info(fmt.Sprintf(format, args...)) + * } + * ``` + * + * and you call it like this in main.go: + * + * ``` + * Infof(slog.Default(), "hello, %s", "world") + * ``` + * + * then slog will report the source file as mylog.go, not main.go. + * + * A correct implementation of Infof will obtain the source location + * (pc) and pass it to NewRecord. + * The Infof function in the package-level example called "wrapping" + * demonstrates how to do this. + * + * # Working with Records + * + * Sometimes a Handler will need to modify a Record + * before passing it on to another Handler or backend. + * A Record contains a mixture of simple public fields (e.g. Time, Level, Message) + * and hidden fields that refer to state (such as attributes) indirectly. This + * means that modifying a simple copy of a Record (e.g. by calling + * [Record.Add] or [Record.AddAttrs] to add attributes) + * may have unexpected effects on the original. + * Before modifying a Record, use [Record.Clone] to + * create a copy that shares no state with the original, + * or create a new Record with [NewRecord] + * and build up its Attrs by traversing the old ones with [Record.Attrs]. + * + * # Performance considerations + * + * If profiling your application demonstrates that logging is taking significant time, + * the following suggestions may help. + * + * If many log lines have a common attribute, use [Logger.With] to create a Logger with + * that attribute. The built-in handlers will format that attribute only once, at the + * call to [Logger.With]. The [Handler] interface is designed to allow that optimization, + * and a well-written Handler should take advantage of it. + * + * The arguments to a log call are always evaluated, even if the log event is discarded. + * If possible, defer computation so that it happens only if the value is actually logged. + * For example, consider the call + * + * ``` + * slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily + * ``` + * + * The URL.String method will be called even if the logger discards Info-level events. + * Instead, pass the URL directly: + * + * ``` + * slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed + * ``` + * + * The built-in [TextHandler] will call its String method, but only + * if the log event is enabled. + * Avoiding the call to String also preserves the structure of the underlying value. + * For example [JSONHandler] emits the components of the parsed URL as a JSON object. + * If you want to avoid eagerly paying the cost of the String call + * without causing the handler to potentially inspect the structure of the value, + * wrap the value in a fmt.Stringer implementation that hides its Marshal methods. + * + * You can also use the [LogValuer] interface to avoid unnecessary work in disabled log + * calls. Say you need to log some expensive value: + * + * ``` + * slog.Debug("frobbing", "value", computeExpensiveValue(arg)) + * ``` + * + * Even if this line is disabled, computeExpensiveValue will be called. + * To avoid that, define a type implementing LogValuer: + * + * ``` + * type expensive struct { arg int } + * + * func (e expensive) LogValue() slog.Value { + * return slog.AnyValue(computeExpensiveValue(e.arg)) + * } + * ``` + * + * Then use a value of that type in log calls: + * + * ``` + * slog.Debug("frobbing", "value", expensive{arg}) + * ``` + * + * Now computeExpensiveValue will only be called when the line is enabled. + * + * The built-in handlers acquire a lock before calling [io.Writer.Write] + * to ensure that exactly one [Record] is written at a time in its entirety. + * Although each log record has a timestamp, + * the built-in handlers do not use that time to sort the written records. + * User-defined handlers are responsible for their own locking and sorting. + * + * # Writing a handler + * + * For a guide to writing a custom handler, see https://golang.org/s/slog-handler-guide. + */ +namespace slog { + /** + * An Attr is a key-value pair. + */ + interface Attr { + key: string + value: Value + } + interface Attr { + /** + * Equal reports whether a and b have equal keys and values. + */ + equal(b: Attr): boolean + } + interface Attr { + string(): string + } + /** + * A Handler handles log records produced by a Logger. + * + * A typical handler may print log records to standard error, + * or write them to a file or database, or perhaps augment them + * with additional attributes and pass them on to another handler. + * + * Any of the Handler's methods may be called concurrently with itself + * or with other methods. It is the responsibility of the Handler to + * manage this concurrency. + * + * Users of the slog package should not invoke Handler methods directly. + * They should use the methods of [Logger] instead. + */ + interface Handler { + [key:string]: any; + /** + * Enabled reports whether the handler handles records at the given level. + * The handler ignores records whose level is lower. + * It is called early, before any arguments are processed, + * to save effort if the log event should be discarded. + * If called from a Logger method, the first argument is the context + * passed to that method, or context.Background() if nil was passed + * or the method does not take a context. + * The context is passed so Enabled can use its values + * to make a decision. + */ + enabled(_arg0: context.Context, _arg1: Level): boolean + /** + * Handle handles the Record. + * It will only be called when Enabled returns true. + * The Context argument is as for Enabled. + * It is present solely to provide Handlers access to the context's values. + * Canceling the context should not affect record processing. + * (Among other things, log messages may be necessary to debug a + * cancellation-related problem.) + * + * Handle methods that produce output should observe the following rules: + * ``` + * - If r.Time is the zero time, ignore the time. + * - If r.PC is zero, ignore it. + * - Attr's values should be resolved. + * - If an Attr's key and value are both the zero value, ignore the Attr. + * This can be tested with attr.Equal(Attr{}). + * - If a group's key is empty, inline the group's Attrs. + * - If a group has no Attrs (even if it has a non-empty key), + * ignore it. + * ``` + */ + handle(_arg0: context.Context, _arg1: Record): void + /** + * WithAttrs returns a new Handler whose attributes consist of + * both the receiver's attributes and the arguments. + * The Handler owns the slice: it may retain, modify or discard it. + */ + withAttrs(attrs: Array): Handler + /** + * WithGroup returns a new Handler with the given group appended to + * the receiver's existing groups. + * The keys of all subsequent attributes, whether added by With or in a + * Record, should be qualified by the sequence of group names. + * + * How this qualification happens is up to the Handler, so long as + * this Handler's attribute keys differ from those of another Handler + * with a different sequence of group names. + * + * A Handler should treat WithGroup as starting a Group of Attrs that ends + * at the end of the log event. That is, + * + * ``` + * logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2)) + * ``` + * + * should behave like + * + * ``` + * logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) + * ``` + * + * If the name is empty, WithGroup returns the receiver. + */ + withGroup(name: string): Handler + } + /** + * A Level is the importance or severity of a log event. + * The higher the level, the more important or severe the event. + */ + interface Level extends Number{} + interface Level { + /** + * String returns a name for the level. + * If the level has a name, then that name + * in uppercase is returned. + * If the level is between named values, then + * an integer is appended to the uppercased name. + * Examples: + * + * ``` + * LevelWarn.String() => "WARN" + * (LevelInfo+2).String() => "INFO+2" + * ``` + */ + string(): string + } + interface Level { + /** + * MarshalJSON implements [encoding/json.Marshaler] + * by quoting the output of [Level.String]. + */ + marshalJSON(): string|Array + } + interface Level { + /** + * UnmarshalJSON implements [encoding/json.Unmarshaler] + * It accepts any string produced by [Level.MarshalJSON], + * ignoring case. + * It also accepts numeric offsets that would result in a different string on + * output. For example, "Error-8" would marshal as "INFO". + */ + unmarshalJSON(data: string|Array): void + } + interface Level { + /** + * MarshalText implements [encoding.TextMarshaler] + * by calling [Level.String]. + */ + marshalText(): string|Array + } + interface Level { + /** + * UnmarshalText implements [encoding.TextUnmarshaler]. + * It accepts any string produced by [Level.MarshalText], + * ignoring case. + * It also accepts numeric offsets that would result in a different string on + * output. For example, "Error-8" would marshal as "INFO". + */ + unmarshalText(data: string|Array): void + } + interface Level { + /** + * Level returns the receiver. + * It implements [Leveler]. + */ + level(): Level + } + // @ts-ignore + import loginternal = internal +} + +namespace mailer { + /** + * Message defines a generic email message struct. + */ + interface Message { + from: { address: string; name?: string; } + to: Array<{ address: string; name?: string; }> + bcc: Array<{ address: string; name?: string; }> + cc: Array<{ address: string; name?: string; }> + subject: string + html: string + text: string + headers: _TygojaDict + attachments: _TygojaDict + } +} + +namespace search { + /** + * Result defines the returned search result structure. + */ + interface Result { + items: any + page: number + perPage: number + totalItems: number + totalPages: number + } +} + +namespace router { + // @ts-ignore + import validation = ozzo_validation + interface Route { + action: hook.HandlerFunc + method: string + path: string + middlewares: Array<(hook.Handler | undefined)> + } + interface Route { + /** + * BindFunc registers one or multiple middleware functions to the current route. + * + * The registered middleware functions are "anonymous" and with default priority, + * 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. + */ + bindFunc(...middlewareFuncs: hook.HandlerFunc[]): (Route) + } + interface Route { + /** + * Bind registers one or multiple middleware handlers to the current route. + */ + bind(...middlewares: (hook.Handler | undefined)[]): (Route) + } + interface Route { + /** + * Unbind removes one or more middlewares with the specified id(s) from the current route. + * + * It also adds the removed middleware ids to an exclude list so that they could be skipped from + * the execution chain in case the middleware is registered in a parent group. + * + * Anonymous middlewares are considered non-removable, aka. this method + * does nothing if the middleware id is an empty string. + */ + unbind(...middlewareIds: string[]): (Route) + } +} + +namespace subscriptions { + /** + * Message defines a client's channel data. + */ + interface Message { + name: string + data: string|Array + } + /** + * Client is an interface for a generic subscription client. + */ + interface Client { + [key:string]: any; + /** + * Id Returns the unique id of the client. + */ + id(): string + /** + * Channel returns the client's communication channel. + */ + channel(): undefined + /** + * Subscriptions returns a shallow copy of the client subscriptions matching the prefixes. + * If no prefix is specified, returns all subscriptions. + */ + subscriptions(...prefixes: string[]): _TygojaDict + /** + * Subscribe subscribes the client to the provided subscriptions list. + * + * Each subscription can also have "options" (json serialized SubscriptionOptions) as query parameter. + * + * Example: + * + * ``` + * Subscribe( + * "subscriptionA", + * `subscriptionB?options={"query":{"a":1},"headers":{"x_token":"abc"}}`, + * ) + * ``` + */ + subscribe(...subs: string[]): void + /** + * Unsubscribe unsubscribes the client from the provided subscriptions list. + */ + unsubscribe(...subs: string[]): void + /** + * HasSubscription checks if the client is subscribed to `sub`. + */ + hasSubscription(sub: string): boolean + /** + * Set stores any value to the client's context. + */ + set(key: string, value: any): void + /** + * Unset removes a single value from the client's context. + */ + unset(key: string): void + /** + * Get retrieves the key value from the client's context. + */ + get(key: string): any + /** + * Discard marks the client as "discarded", meaning that it + * shouldn't be used anymore for sending new messages. + * + * It is safe to call Discard() multiple times. + */ + discard(): void + /** + * IsDiscarded indicates whether the client has been "discarded" + * and should no longer be used. + */ + isDiscarded(): boolean + /** + * Send sends the specified message to the client's channel (if not discarded). + */ + send(m: Message): void + } +} + +/** + * Package core is the backbone of PocketBase. + * + * It defines the main PocketBase App interface and its base implementation. + */ +namespace core { + // @ts-ignore + import validation = ozzo_validation + /** + * @todo experiment eventually replacing the rules *string with a struct? + */ + type _subZRoho = BaseModel + interface baseCollection extends _subZRoho { + listRule?: string + viewRule?: string + createRule?: string + updateRule?: string + deleteRule?: string + /** + * RawOptions represents the raw serialized collection option loaded from the DB. + * NB! This field shouldn't be modified manually. It is automatically updated + * with the collection type specific option before save. + */ + rawOptions: types.JSONRaw + name: string + type: string + fields: FieldsList + indexes: types.JSONArray + system: boolean + created: types.DateTime + updated: types.DateTime + } + /** + * collectionAuthOptions defines the options for the "auth" type collection. + */ + interface collectionAuthOptions { + /** + * AuthRule could be used to specify additional record constraints + * applied after record authentication and right before returning the + * auth token response to the client. + * + * For example, to allow only verified users you could set it to + * "verified = true". + * + * Set it to empty string to allow any Auth collection record to authenticate. + * + * Set it to nil to disallow authentication altogether for the collection + * (that includes password, OAuth2, etc.). + */ + authRule?: string + /** + * ManageRule gives admin-like permissions to allow fully managing + * the auth record(s), eg. changing the password without requiring + * to enter the old one, directly updating the verified state and email, etc. + * + * This rule is executed in addition to the Create and Update API rules. + */ + manageRule?: string + /** + * AuthAlert defines options related to the auth alerts on new device login. + */ + authAlert: AuthAlertConfig + /** + * OAuth2 specifies whether OAuth2 auth is enabled for the collection + * and which OAuth2 providers are allowed. + */ + oAuth2: OAuth2Config + passwordAuth: PasswordAuthConfig + mfa: MFAConfig + otp: OTPConfig + /** + * Various token configurations + * --- + */ + authToken: TokenConfig + passwordResetToken: TokenConfig + emailChangeToken: TokenConfig + verificationToken: TokenConfig + fileToken: TokenConfig + /** + * default email templates + * --- + */ + verificationTemplate: EmailTemplate + resetPasswordTemplate: EmailTemplate + confirmEmailChangeTemplate: EmailTemplate + } + /** + * collectionViewOptions defines the options for the "view" type collection. + */ + interface collectionViewOptions { + viewQuery: string + } + interface baseModelEventData { + model: Model + } + interface baseModelEventData { + tags(): Array + } + interface baseRecordEventData { + record?: Record + } + interface baseRecordEventData { + tags(): Array + } + interface baseCollectionEventData { + collection?: Collection + } + interface baseCollectionEventData { + tags(): Array + } + interface SetterFunc {(record: Record, raw: any): void } + interface GetterFunc {(record: Record): any } + interface settings { + smtp: SMTPConfig + backups: BackupsConfig + s3: S3Config + meta: MetaConfig + logs: LogsConfig + batch: BatchConfig + rateLimits: RateLimitsConfig + trustedProxy: TrustedProxyConfig + } +} + +/** + * Package net provides a portable interface for network I/O, including + * TCP/IP, UDP, domain name resolution, and Unix domain sockets. + * + * Although the package provides access to low-level networking + * primitives, most clients will need only the basic interface provided + * by the [Dial], [Listen], and Accept functions and the associated + * [Conn] and [Listener] interfaces. The crypto/tls package uses + * the same interfaces and similar Dial and Listen functions. + * + * The Dial function connects to a server: + * + * ``` + * conn, err := net.Dial("tcp", "golang.org:80") + * if err != nil { + * // handle error + * } + * fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") + * status, err := bufio.NewReader(conn).ReadString('\n') + * // ... + * ``` + * + * The Listen function creates servers: + * + * ``` + * ln, err := net.Listen("tcp", ":8080") + * if err != nil { + * // handle error + * } + * for { + * conn, err := ln.Accept() + * if err != nil { + * // handle error + * } + * go handleConnection(conn) + * } + * ``` + * + * # Name Resolution + * + * The method for resolving domain names, whether indirectly with functions like Dial + * or directly with functions like [LookupHost] and [LookupAddr], varies by operating system. + * + * On Unix systems, the resolver has two options for resolving names. + * It can use a pure Go resolver that sends DNS requests directly to the servers + * listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C + * library routines such as getaddrinfo and getnameinfo. + * + * On Unix the pure Go resolver is preferred over the cgo resolver, because a blocked DNS + * request consumes only a goroutine, while a blocked C call consumes an operating system thread. + * When cgo is available, the cgo-based resolver is used instead under a variety of + * conditions: on systems that do not let programs make direct DNS requests (OS X), + * when the LOCALDOMAIN environment variable is present (even if empty), + * when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, + * when the ASR_CONFIG environment variable is non-empty (OpenBSD only), + * when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the + * Go resolver does not implement. + * + * On all systems (except Plan 9), when the cgo resolver is being used + * this package applies a concurrent cgo lookup limit to prevent the system + * from running out of system threads. Currently, it is limited to 500 concurrent lookups. + * + * The resolver decision can be overridden by setting the netdns value of the + * GODEBUG environment variable (see package runtime) to go or cgo, as in: + * + * ``` + * export GODEBUG=netdns=go # force pure Go resolver + * export GODEBUG=netdns=cgo # force native resolver (cgo, win32) + * ``` + * + * The decision can also be forced while building the Go source tree + * by setting the netgo or netcgo build tag. + * + * A numeric netdns setting, as in GODEBUG=netdns=1, causes the resolver + * to print debugging information about its decisions. + * To force a particular resolver while also printing debugging information, + * join the two settings by a plus sign, as in GODEBUG=netdns=go+1. + * + * The Go resolver will send an EDNS0 additional header with a DNS request, + * to signal a willingness to accept a larger DNS packet size. + * This can reportedly cause sporadic failures with the DNS server run + * by some modems and routers. Setting GODEBUG=netedns0=0 will disable + * sending the additional header. + * + * On macOS, if Go code that uses the net package is built with + * -buildmode=c-archive, linking the resulting archive into a C program + * requires passing -lresolv when linking the C code. + * + * On Plan 9, the resolver always accesses /net/cs and /net/dns. + * + * On Windows, in Go 1.18.x and earlier, the resolver always used C + * library functions, such as GetAddrInfo and DnsQuery. + */ +namespace net { + /** + * A Listener is a generic network listener for stream-oriented protocols. + * + * Multiple goroutines may invoke methods on a Listener simultaneously. + */ + interface Listener { + [key:string]: any; + /** + * Accept waits for and returns the next connection to the listener. + */ + accept(): Conn + /** + * Close closes the listener. + * Any blocked Accept operations will be unblocked and return errors. + */ + close(): void + /** + * Addr returns the listener's network address. + */ + addr(): Addr + } +} + +namespace hook { +} + +/** + * Package slog provides structured logging, + * in which log records include a message, + * a severity level, and various other attributes + * expressed as key-value pairs. + * + * It defines a type, [Logger], + * which provides several methods (such as [Logger.Info] and [Logger.Error]) + * for reporting events of interest. + * + * Each Logger is associated with a [Handler]. + * A Logger output method creates a [Record] from the method arguments + * and passes it to the Handler, which decides how to handle it. + * There is a default Logger accessible through top-level functions + * (such as [Info] and [Error]) that call the corresponding Logger methods. + * + * A log record consists of a time, a level, a message, and a set of key-value + * pairs, where the keys are strings and the values may be of any type. + * As an example, + * + * ``` + * slog.Info("hello", "count", 3) + * ``` + * + * creates a record containing the time of the call, + * a level of Info, the message "hello", and a single + * pair with key "count" and value 3. + * + * The [Info] top-level function calls the [Logger.Info] method on the default Logger. + * In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. + * Besides these convenience methods for common levels, + * there is also a [Logger.Log] method which takes the level as an argument. + * Each of these methods has a corresponding top-level function that uses the + * default logger. + * + * The default handler formats the log record's message, time, level, and attributes + * as a string and passes it to the [log] package. + * + * ``` + * 2022/11/08 15:28:26 INFO hello count=3 + * ``` + * + * For more control over the output format, create a logger with a different handler. + * This statement uses [New] to create a new logger with a [TextHandler] + * that writes structured records in text form to standard error: + * + * ``` + * logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + * ``` + * + * [TextHandler] output is a sequence of key=value pairs, easily and unambiguously + * parsed by machine. This statement: + * + * ``` + * logger.Info("hello", "count", 3) + * ``` + * + * produces this output: + * + * ``` + * time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 + * ``` + * + * The package also provides [JSONHandler], whose output is line-delimited JSON: + * + * ``` + * logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + * logger.Info("hello", "count", 3) + * ``` + * + * produces this output: + * + * ``` + * {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} + * ``` + * + * Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. + * There are options for setting the minimum level (see Levels, below), + * displaying the source file and line of the log call, and + * modifying attributes before they are logged. + * + * Setting a logger as the default with + * + * ``` + * slog.SetDefault(logger) + * ``` + * + * will cause the top-level functions like [Info] to use it. + * [SetDefault] also updates the default logger used by the [log] package, + * so that existing applications that use [log.Printf] and related functions + * will send log records to the logger's handler without needing to be rewritten. + * + * Some attributes are common to many log calls. + * For example, you may wish to include the URL or trace identifier of a server request + * with all log events arising from the request. + * Rather than repeat the attribute with every log call, you can use [Logger.With] + * to construct a new Logger containing the attributes: + * + * ``` + * logger2 := logger.With("url", r.URL) + * ``` + * + * The arguments to With are the same key-value pairs used in [Logger.Info]. + * The result is a new Logger with the same handler as the original, but additional + * attributes that will appear in the output of every call. + * + * # Levels + * + * A [Level] is an integer representing the importance or severity of a log event. + * The higher the level, the more severe the event. + * This package defines constants for the most common levels, + * but any int can be used as a level. + * + * In an application, you may wish to log messages only at a certain level or greater. + * One common configuration is to log messages at Info or higher levels, + * suppressing debug logging until it is needed. + * The built-in handlers can be configured with the minimum level to output by + * setting [HandlerOptions.Level]. + * The program's `main` function typically does this. + * The default value is LevelInfo. + * + * Setting the [HandlerOptions.Level] field to a [Level] value + * fixes the handler's minimum level throughout its lifetime. + * Setting it to a [LevelVar] allows the level to be varied dynamically. + * A LevelVar holds a Level and is safe to read or write from multiple + * goroutines. + * To vary the level dynamically for an entire program, first initialize + * a global LevelVar: + * + * ``` + * var programLevel = new(slog.LevelVar) // Info by default + * ``` + * + * Then use the LevelVar to construct a handler, and make it the default: + * + * ``` + * h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) + * slog.SetDefault(slog.New(h)) + * ``` + * + * Now the program can change its logging level with a single statement: + * + * ``` + * programLevel.Set(slog.LevelDebug) + * ``` + * + * # Groups + * + * Attributes can be collected into groups. + * A group has a name that is used to qualify the names of its attributes. + * How this qualification is displayed depends on the handler. + * [TextHandler] separates the group and attribute names with a dot. + * [JSONHandler] treats each group as a separate JSON object, with the group name as the key. + * + * Use [Group] to create a Group attribute from a name and a list of key-value pairs: + * + * ``` + * slog.Group("request", + * "method", r.Method, + * "url", r.URL) + * ``` + * + * TextHandler would display this group as + * + * ``` + * request.method=GET request.url=http://example.com + * ``` + * + * JSONHandler would display it as + * + * ``` + * "request":{"method":"GET","url":"http://example.com"} + * ``` + * + * Use [Logger.WithGroup] to qualify all of a Logger's output + * with a group name. Calling WithGroup on a Logger results in a + * new Logger with the same Handler as the original, but with all + * its attributes qualified by the group name. + * + * This can help prevent duplicate attribute keys in large systems, + * where subsystems might use the same keys. + * Pass each subsystem a different Logger with its own group name so that + * potential duplicates are qualified: + * + * ``` + * logger := slog.Default().With("id", systemID) + * parserLogger := logger.WithGroup("parser") + * parseInput(input, parserLogger) + * ``` + * + * When parseInput logs with parserLogger, its keys will be qualified with "parser", + * so even if it uses the common key "id", the log line will have distinct keys. + * + * # Contexts + * + * Some handlers may wish to include information from the [context.Context] that is + * available at the call site. One example of such information + * is the identifier for the current span when tracing is enabled. + * + * The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first + * argument, as do their corresponding top-level functions. + * + * Although the convenience methods on Logger (Info and so on) and the + * corresponding top-level functions do not take a context, the alternatives ending + * in "Context" do. For example, + * + * ``` + * slog.InfoContext(ctx, "message") + * ``` + * + * It is recommended to pass a context to an output method if one is available. + * + * # Attrs and Values + * + * An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as + * alternating keys and values. The statement + * + * ``` + * slog.Info("hello", slog.Int("count", 3)) + * ``` + * + * behaves the same as + * + * ``` + * slog.Info("hello", "count", 3) + * ``` + * + * There are convenience constructors for [Attr] such as [Int], [String], and [Bool] + * for common types, as well as the function [Any] for constructing Attrs of any + * type. + * + * The value part of an Attr is a type called [Value]. + * Like an [any], a Value can hold any Go value, + * but it can represent typical values, including all numbers and strings, + * without an allocation. + * + * For the most efficient log output, use [Logger.LogAttrs]. + * It is similar to [Logger.Log] but accepts only Attrs, not alternating + * keys and values; this allows it, too, to avoid allocation. + * + * The call + * + * ``` + * logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3)) + * ``` + * + * is the most efficient way to achieve the same output as + * + * ``` + * slog.InfoContext(ctx, "hello", "count", 3) + * ``` + * + * # Customizing a type's logging behavior + * + * If a type implements the [LogValuer] interface, the [Value] returned from its LogValue + * method is used for logging. You can use this to control how values of the type + * appear in logs. For example, you can redact secret information like passwords, + * or gather a struct's fields in a Group. See the examples under [LogValuer] for + * details. + * + * A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] + * method handles these cases carefully, avoiding infinite loops and unbounded recursion. + * Handler authors and others may wish to use [Value.Resolve] instead of calling LogValue directly. + * + * # Wrapping output methods + * + * The logger functions use reflection over the call stack to find the file name + * and line number of the logging call within the application. This can produce + * incorrect source information for functions that wrap slog. For instance, if you + * define this function in file mylog.go: + * + * ``` + * func Infof(logger *slog.Logger, format string, args ...any) { + * logger.Info(fmt.Sprintf(format, args...)) + * } + * ``` + * + * and you call it like this in main.go: + * + * ``` + * Infof(slog.Default(), "hello, %s", "world") + * ``` + * + * then slog will report the source file as mylog.go, not main.go. + * + * A correct implementation of Infof will obtain the source location + * (pc) and pass it to NewRecord. + * The Infof function in the package-level example called "wrapping" + * demonstrates how to do this. + * + * # Working with Records + * + * Sometimes a Handler will need to modify a Record + * before passing it on to another Handler or backend. + * A Record contains a mixture of simple public fields (e.g. Time, Level, Message) + * and hidden fields that refer to state (such as attributes) indirectly. This + * means that modifying a simple copy of a Record (e.g. by calling + * [Record.Add] or [Record.AddAttrs] to add attributes) + * may have unexpected effects on the original. + * Before modifying a Record, use [Record.Clone] to + * create a copy that shares no state with the original, + * or create a new Record with [NewRecord] + * and build up its Attrs by traversing the old ones with [Record.Attrs]. + * + * # Performance considerations + * + * If profiling your application demonstrates that logging is taking significant time, + * the following suggestions may help. + * + * If many log lines have a common attribute, use [Logger.With] to create a Logger with + * that attribute. The built-in handlers will format that attribute only once, at the + * call to [Logger.With]. The [Handler] interface is designed to allow that optimization, + * and a well-written Handler should take advantage of it. + * + * The arguments to a log call are always evaluated, even if the log event is discarded. + * If possible, defer computation so that it happens only if the value is actually logged. + * For example, consider the call + * + * ``` + * slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily + * ``` + * + * The URL.String method will be called even if the logger discards Info-level events. + * Instead, pass the URL directly: + * + * ``` + * slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed + * ``` + * + * The built-in [TextHandler] will call its String method, but only + * if the log event is enabled. + * Avoiding the call to String also preserves the structure of the underlying value. + * For example [JSONHandler] emits the components of the parsed URL as a JSON object. + * If you want to avoid eagerly paying the cost of the String call + * without causing the handler to potentially inspect the structure of the value, + * wrap the value in a fmt.Stringer implementation that hides its Marshal methods. + * + * You can also use the [LogValuer] interface to avoid unnecessary work in disabled log + * calls. Say you need to log some expensive value: + * + * ``` + * slog.Debug("frobbing", "value", computeExpensiveValue(arg)) + * ``` + * + * Even if this line is disabled, computeExpensiveValue will be called. + * To avoid that, define a type implementing LogValuer: + * + * ``` + * type expensive struct { arg int } + * + * func (e expensive) LogValue() slog.Value { + * return slog.AnyValue(computeExpensiveValue(e.arg)) + * } + * ``` + * + * Then use a value of that type in log calls: + * + * ``` + * slog.Debug("frobbing", "value", expensive{arg}) + * ``` + * + * Now computeExpensiveValue will only be called when the line is enabled. + * + * The built-in handlers acquire a lock before calling [io.Writer.Write] + * to ensure that exactly one [Record] is written at a time in its entirety. + * Although each log record has a timestamp, + * the built-in handlers do not use that time to sort the written records. + * User-defined handlers are responsible for their own locking and sorting. * * # Writing a handler * @@ -20191,6 +20046,654 @@ namespace slog { } } +/** + * Package sql provides a generic interface around SQL (or SQL-like) + * databases. + * + * The sql package must be used in conjunction with a database driver. + * See https://golang.org/s/sqldrivers for a list of drivers. + * + * Drivers that do not support context cancellation will not return until + * after the query is completed. + * + * For usage examples, see the wiki page at + * https://golang.org/s/sqlwiki. + */ +namespace sql { +} + +/** + * Package types implements some commonly used db serializable types + * like datetime, json, etc. + */ +namespace types { + /** + * JSONArray defines a slice that is safe for json and db read/write. + */ + interface JSONArray extends Array{} + interface JSONArray { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface JSONArray { + /** + * String returns the string representation of the current json array. + */ + string(): string + } + interface JSONArray { + /** + * Value implements the [driver.Valuer] interface. + */ + value(): any + } + interface JSONArray { + /** + * Scan implements [sql.Scanner] interface to scan the provided value + * into the current JSONArray[T] instance. + */ + scan(value: any): void + } + /** + * JSONRaw defines a json value type that is safe for db read/write. + */ + interface JSONRaw extends Array{} + interface JSONRaw { + /** + * String returns the current JSONRaw instance as a json encoded string. + */ + string(): string + } + interface JSONRaw { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface JSONRaw { + /** + * UnmarshalJSON implements the [json.Unmarshaler] interface. + */ + unmarshalJSON(b: string|Array): void + } + interface JSONRaw { + /** + * Value implements the [driver.Valuer] interface. + */ + value(): any + } + interface JSONRaw { + /** + * Scan implements [sql.Scanner] interface to scan the provided value + * into the current JSONRaw instance. + */ + scan(value: any): void + } +} + +namespace search { +} + +/** + * Package http provides HTTP client and server implementations. + * + * [Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests: + * + * ``` + * resp, err := http.Get("http://example.com/") + * ... + * resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) + * ... + * resp, err := http.PostForm("http://example.com/form", + * url.Values{"key": {"Value"}, "id": {"123"}}) + * ``` + * + * The caller must close the response body when finished with it: + * + * ``` + * resp, err := http.Get("http://example.com/") + * if err != nil { + * // handle error + * } + * defer resp.Body.Close() + * body, err := io.ReadAll(resp.Body) + * // ... + * ``` + * + * # Clients and Transports + * + * For control over HTTP client headers, redirect policy, and other + * settings, create a [Client]: + * + * ``` + * client := &http.Client{ + * CheckRedirect: redirectPolicyFunc, + * } + * + * resp, err := client.Get("http://example.com") + * // ... + * + * req, err := http.NewRequest("GET", "http://example.com", nil) + * // ... + * req.Header.Add("If-None-Match", `W/"wyzzy"`) + * resp, err := client.Do(req) + * // ... + * ``` + * + * For control over proxies, TLS configuration, keep-alives, + * compression, and other settings, create a [Transport]: + * + * ``` + * tr := &http.Transport{ + * MaxIdleConns: 10, + * IdleConnTimeout: 30 * time.Second, + * DisableCompression: true, + * } + * client := &http.Client{Transport: tr} + * resp, err := client.Get("https://example.com") + * ``` + * + * Clients and Transports are safe for concurrent use by multiple + * goroutines and for efficiency should only be created once and re-used. + * + * # Servers + * + * ListenAndServe starts an HTTP server with a given address and handler. + * The handler is usually nil, which means to use [DefaultServeMux]. + * [Handle] and [HandleFunc] add handlers to [DefaultServeMux]: + * + * ``` + * http.Handle("/foo", fooHandler) + * + * http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + * fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + * }) + * + * log.Fatal(http.ListenAndServe(":8080", nil)) + * ``` + * + * More control over the server's behavior is available by creating a + * custom Server: + * + * ``` + * s := &http.Server{ + * Addr: ":8080", + * Handler: myHandler, + * ReadTimeout: 10 * time.Second, + * WriteTimeout: 10 * time.Second, + * MaxHeaderBytes: 1 << 20, + * } + * log.Fatal(s.ListenAndServe()) + * ``` + * + * # HTTP/2 + * + * Starting with Go 1.6, the http package has transparent support for the + * HTTP/2 protocol when using HTTPS. Programs that must disable HTTP/2 + * can do so by setting [Transport.TLSNextProto] (for clients) or + * [Server.TLSNextProto] (for servers) to a non-nil, empty + * map. Alternatively, the following GODEBUG settings are + * currently supported: + * + * ``` + * GODEBUG=http2client=0 # disable HTTP/2 client support + * GODEBUG=http2server=0 # disable HTTP/2 server support + * GODEBUG=http2debug=1 # enable verbose HTTP/2 debug logs + * GODEBUG=http2debug=2 # ... even more verbose, with frame dumps + * ``` + * + * Please report any issues before disabling HTTP/2 support: https://golang.org/s/http2bug + * + * The http package's [Transport] and [Server] both automatically enable + * HTTP/2 support for simple configurations. To enable HTTP/2 for more + * complex configurations, to use lower-level HTTP/2 features, or to use + * a newer version of Go's http2 package, import "golang.org/x/net/http2" + * directly and use its ConfigureTransport and/or ConfigureServer + * functions. Manually configuring HTTP/2 via the golang.org/x/net/http2 + * package takes precedence over the net/http package's built-in HTTP/2 + * support. + */ +namespace http { + // @ts-ignore + import mathrand = rand + // @ts-ignore + import urlpkg = url + /** + * A ConnState represents the state of a client connection to a server. + * It's used by the optional [Server.ConnState] hook. + */ + interface ConnState extends Number{} + interface ConnState { + string(): string + } +} + +namespace router { + // @ts-ignore + import validation = ozzo_validation +} + +namespace subscriptions { +} + +/** + * Package oauth2 provides support for making + * OAuth2 authorized and authenticated HTTP requests, + * as specified in RFC 6749. + * It can additionally grant authorization with Bearer JWT. + */ +/** + * Copyright 2023 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +namespace oauth2 { + /** + * An AuthCodeOption is passed to Config.AuthCodeURL. + */ + interface AuthCodeOption { + [key:string]: any; + } + /** + * Token represents the credentials used to authorize + * the requests to access protected resources on the OAuth 2.0 + * provider's backend. + * + * Most users of this package should not access fields of Token + * directly. They're exported mostly for use by related packages + * implementing derivative OAuth2 flows. + */ + interface Token { + /** + * AccessToken is the token that authorizes and authenticates + * the requests. + */ + accessToken: string + /** + * TokenType is the type of token. + * The Type method returns either this or "Bearer", the default. + */ + tokenType: string + /** + * RefreshToken is a token that's used by the application + * (as opposed to the user) to refresh the access token + * if it expires. + */ + refreshToken: string + /** + * Expiry is the optional expiration time of the access token. + * + * If zero, TokenSource implementations will reuse the same + * token forever and RefreshToken or equivalent + * mechanisms for that TokenSource will not be used. + */ + expiry: time.Time + /** + * ExpiresIn is the OAuth2 wire format "expires_in" field, + * which specifies how many seconds later the token expires, + * relative to an unknown time base approximately around "now". + * It is the application's responsibility to populate + * `Expiry` from `ExpiresIn` when required. + */ + expiresIn: number + } + interface Token { + /** + * Type returns t.TokenType if non-empty, else "Bearer". + */ + type(): string + } + interface Token { + /** + * SetAuthHeader sets the Authorization header to r using the access + * token in t. + * + * This method is unnecessary when using Transport or an HTTP Client + * returned by this package. + */ + setAuthHeader(r: http.Request): void + } + interface Token { + /** + * WithExtra returns a new Token that's a clone of t, but using the + * provided raw extra map. This is only intended for use by packages + * implementing derivative OAuth2 flows. + */ + withExtra(extra: { + }): (Token) + } + interface Token { + /** + * Extra returns an extra field. + * Extra fields are key-value pairs returned by the server as a + * part of the token retrieval response. + */ + extra(key: string): { + } + } + interface Token { + /** + * Valid reports whether t is non-nil, has an AccessToken, and is not expired. + */ + valid(): boolean + } +} + +/** + * Package core is the backbone of PocketBase. + * + * It defines the main PocketBase App interface and its base implementation. + */ +namespace core { + // @ts-ignore + import validation = ozzo_validation + interface EmailTemplate { + subject: string + body: string + } + interface EmailTemplate { + /** + * Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface EmailTemplate { + /** + * Resolve replaces the placeholder parameters in the current email + * template and returns its components as ready-to-use strings. + */ + resolve(placeholders: _TygojaDict): string + } + interface AuthAlertConfig { + enabled: boolean + emailTemplate: EmailTemplate + } + interface AuthAlertConfig { + /** + * Validate makes AuthAlertConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface TokenConfig { + secret: string + /** + * Duration specifies how long an issued token to be valid (in seconds) + */ + duration: number + } + interface TokenConfig { + /** + * Validate makes TokenConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface TokenConfig { + /** + * DurationTime returns the current Duration as [time.Duration]. + */ + durationTime(): time.Duration + } + interface OTPConfig { + enabled: boolean + /** + * Duration specifies how long the OTP to be valid (in seconds) + */ + duration: number + /** + * Length specifies the auto generated password length. + */ + length: number + /** + * EmailTemplate is the default OTP email template that will be send to the auth record. + * + * In addition to the system placeholders you can also make use of + * [core.EmailPlaceholderOTPId] and [core.EmailPlaceholderOTP]. + */ + emailTemplate: EmailTemplate + } + interface OTPConfig { + /** + * Validate makes OTPConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface OTPConfig { + /** + * DurationTime returns the current Duration as [time.Duration]. + */ + durationTime(): time.Duration + } + interface MFAConfig { + enabled: boolean + /** + * Duration specifies how long an issued MFA to be valid (in seconds) + */ + duration: number + /** + * Rule is an optional field to restrict MFA only for the records that satisfy the rule. + * + * Leave it empty to enable MFA for everyone. + */ + rule: string + } + interface MFAConfig { + /** + * Validate makes MFAConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface MFAConfig { + /** + * DurationTime returns the current Duration as [time.Duration]. + */ + durationTime(): time.Duration + } + interface PasswordAuthConfig { + enabled: boolean + /** + * IdentityFields is a list of field names that could be used as + * identity during password authentication. + * + * Usually only fields that has single column UNIQUE index are accepted as values. + */ + identityFields: Array + } + interface PasswordAuthConfig { + /** + * Validate makes PasswordAuthConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface OAuth2Config { + providers: Array + mappedFields: OAuth2KnownFields + enabled: boolean + } + interface OAuth2Config { + /** + * GetProviderConfig returns the first OAuth2ProviderConfig that matches the specified name. + * + * Returns false and zero config if no such provider is available in c.Providers. + */ + getProviderConfig(name: string): [OAuth2ProviderConfig, boolean] + } + interface OAuth2Config { + /** + * Validate makes OAuth2Config validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface SMTPConfig { + enabled: boolean + port: number + host: string + username: string + password: string + /** + * SMTP AUTH - PLAIN (default) or LOGIN + */ + authMethod: string + /** + * Whether to enforce TLS encryption for the mail server connection. + * + * When set to false StartTLS command is send, leaving the server + * to decide whether to upgrade the connection or not. + */ + tls: boolean + /** + * LocalName is optional domain name or IP address used for the + * EHLO/HELO exchange (if not explicitly set, defaults to "localhost"). + * + * This is required only by some SMTP servers, such as Gmail SMTP-relay. + */ + localName: string + } + interface SMTPConfig { + /** + * Validate makes SMTPConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface S3Config { + enabled: boolean + bucket: string + region: string + endpoint: string + accessKey: string + secret: string + forcePathStyle: boolean + } + interface S3Config { + /** + * Validate makes S3Config validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface BatchConfig { + enabled: boolean + /** + * MaxRequests is the maximum allowed batch request to execute. + */ + maxRequests: number + /** + * Timeout is the the max duration in seconds to wait before cancelling the batch transaction. + */ + timeout: number + /** + * MaxBodySize is the maximum allowed batch request body size in bytes. + * + * If not set, fallbacks to max ~128MB. + */ + maxBodySize: number + } + interface BatchConfig { + /** + * Validate makes BatchConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface BackupsConfig { + /** + * Cron is a cron expression to schedule auto backups, eg. "* * * * *". + * + * Leave it empty to disable the auto backups functionality. + */ + cron: string + /** + * CronMaxKeep is the the max number of cron generated backups to + * keep before removing older entries. + * + * This field works only when the cron config has valid cron expression. + */ + cronMaxKeep: number + /** + * S3 is an optional S3 storage config specifying where to store the app backups. + */ + s3: S3Config + } + interface BackupsConfig { + /** + * Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface MetaConfig { + appName: string + appURL: string + senderName: string + senderAddress: string + hideControls: boolean + } + interface MetaConfig { + /** + * Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface LogsConfig { + maxDays: number + minLevel: number + logIP: boolean + logAuthId: boolean + } + interface LogsConfig { + /** + * Validate makes LogsConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface TrustedProxyConfig { + /** + * Headers is a list of explicit trusted header(s) to check. + */ + headers: Array + /** + * UseLeftmostIP specifies to use the left-mostish IP from the trusted headers. + * + * Note that this could be insecure when used with X-Forward-For header + * because some proxies like AWS ELB allow users to prepend their own header value + * before appending the trusted ones. + */ + useLeftmostIP: boolean + } + interface TrustedProxyConfig { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface TrustedProxyConfig { + /** + * Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface RateLimitsConfig { + rules: Array + enabled: boolean + } + interface RateLimitsConfig { + /** + * FindRateLimitRule returns the first matching rule based on the provided labels. + */ + findRateLimitRule(searchLabels: Array): [RateLimitRule, boolean] + } + interface RateLimitsConfig { + /** + * MarshalJSON implements the [json.Marshaler] interface. + */ + marshalJSON(): string|Array + } + interface RateLimitsConfig { + /** + * Validate makes RateLimitsConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } +} + /** * Package slog provides structured logging, * in which log records include a message, @@ -20555,8 +21058,10 @@ namespace slog { * Now computeExpensiveValue will only be called when the line is enabled. * * The built-in handlers acquire a lock before calling [io.Writer.Write] - * to ensure that each record is written in one piece. User-defined - * handlers are responsible for their own locking. + * to ensure that exactly one [Record] is written at a time in its entirety. + * Although each log record has a timestamp, + * the built-in handlers do not use that time to sort the written records. + * User-defined handlers are responsible for their own locking and sorting. * * # Writing a handler * @@ -20583,3 +21088,87 @@ namespace slog { logValue(): Value } } + +/** + * Package core is the backbone of PocketBase. + * + * It defines the main PocketBase App interface and its base implementation. + */ +namespace core { + // @ts-ignore + import validation = ozzo_validation + interface OAuth2KnownFields { + id: string + name: string + username: string + avatarURL: string + } + interface OAuth2ProviderConfig { + /** + * PKCE overwrites the default provider PKCE config option. + * + * This usually shouldn't be needed but some OAuth2 vendors, like the LinkedIn OIDC, + * may require manual adjustment due to returning error if extra parameters are added to the request + * (https://github.com/pocketbase/pocketbase/discussions/3799#discussioncomment-7640312) + */ + pkce?: boolean + name: string + clientId: string + clientSecret: string + authURL: string + tokenURL: string + userInfoURL: string + displayName: string + } + interface OAuth2ProviderConfig { + /** + * Validate makes OAuth2ProviderConfig validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface OAuth2ProviderConfig { + /** + * InitProvider returns a new auth.Provider instance loaded with the current OAuth2ProviderConfig options. + */ + initProvider(): auth.Provider + } + interface RateLimitRule { + /** + * Label is the identifier of the current rule. + * + * It could be a tag, complete path or path prerefix (when ends with `/`). + * + * Example supported labels: + * ``` + * - test_a (plain text "tag") + * - users:create + * - *:create + * - / + * - /api + * - POST /api/collections/ + * ``` + */ + label: string + /** + * MaxRequests is the max allowed number of requests per Duration. + */ + maxRequests: number + /** + * Duration specifies the interval (in seconds) per which to reset + * the counted/accumulated rate limiter tokens. + */ + duration: number + } + interface RateLimitRule { + /** + * Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface. + */ + validate(): void + } + interface RateLimitRule { + /** + * DurationTime returns the tag's Duration as [time.Duration]. + */ + durationTime(): time.Duration + } +} diff --git a/plugins/jsvm/internal/types/types.go b/plugins/jsvm/internal/types/types.go index e4f77286..9d754804 100644 --- a/plugins/jsvm/internal/types/types.go +++ b/plugins/jsvm/internal/types/types.go @@ -71,9 +71,9 @@ declare function cronRemove(jobId: string): void; * Example: * * ` + "```" + `js - * routerAdd("GET", "/hello", (c) => { - * return c.json(200, {"message": "Hello!"}) - * }, $apis.requireAdminOrRecordAuth()) + * routerAdd("GET", "/hello", (e) => { + * return e.json(200, {"message": "Hello!"}) + * }, $apis.requireAuth()) * ` + "```" + ` * * _Note that this method is available only in pb_hooks context._ @@ -83,8 +83,8 @@ declare function cronRemove(jobId: string): void; declare function routerAdd( method: string, path: string, - handler: echo.HandlerFunc, - ...middlewares: Array, + handler: (e: core.RequestEvent) => void, + ...middlewares: Array void)|Middleware>, ): void; /** @@ -94,11 +94,9 @@ declare function routerAdd( * Example: * * ` + "```" + `js - * routerUse((next) => { - * return (c) => { - * console.log(c.path()) - * return next(c) - * } + * routerUse((e) => { + * console.log(e.request.url.path) + * return e.next() * }) * ` + "```" + ` * @@ -106,34 +104,7 @@ declare function routerAdd( * * @group PocketBase */ -declare function routerUse(...middlewares: Array): void; - -/** - * RouterPre registers one or more global middlewares that are executed - * BEFORE the router processes the request. It is usually used for making - * changes to the request properties, for example, adding or removing - * a trailing slash or adding segments to a path so it matches a route. - * - * NB! Since the router will not have processed the request yet, - * middlewares registered at this level won't have access to any path - * related APIs from echo.Context. - * - * Example: - * - * ` + "```" + `js - * routerPre((next) => { - * return (c) => { - * console.log(c.request().url) - * return next(c) - * } - * }) - * ` + "```" + ` - * - * _Note that this method is available only in pb_hooks context._ - * - * @group PocketBase - */ -declare function routerPre(...middlewares: Array): void; +declare function routerUse(...middlewares: Array void)|Middleware): void; // ------------------------------------------------------------------- // baseBinds @@ -151,7 +122,7 @@ declare var __hooks: string // // See https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as type excludeHooks = { - [Property in keyof Type as Exclude]: Type[Property] + [Property in keyof Type as Exclude]: Type[Property] }; // core.App without the on* hook methods @@ -194,22 +165,34 @@ declare var $app: PocketBase declare var $template: template.Registry /** - * readerToString reads the content of the specified io.Reader until - * EOF or maxBytes are reached. + * This method is superseded by toString. * - * If maxBytes is not specified it will read up to 32MB. + * @deprecated + * @group PocketBase + */ +declare function readerToString(reader: any, maxBytes?: number): string; + +/** + * toString stringifies the specified value. * - * Note that after this call the reader can't be used anymore. + * Support optional second maxBytes argument to limit the max read bytes + * when the value is a io.Reader (default to 32MB). + * + * Types that don't have explicit string representation are json serialized. * * Example: * * ` + "```" + `js - * const rawBody = readerToString(c.request().body) + * // io.Reader + * const ex1 = toString(e.request.body) + * + * // slice of bytes ("hello") + * const ex2 = toString([104 101 108 108 111]) * ` + "```" + ` * * @group PocketBase */ -declare function readerToString(reader: any, maxBytes?: number): string; +declare function toString(val: any, maxBytes?: number): string; /** * sleep pauses the current goroutine for at least the specified user duration (in ms). @@ -244,15 +227,18 @@ declare function arrayOf(model: T): Array; /** * DynamicModel creates a new dynamic model with fields from the provided data shape. * + * Note that in order to use 0 as double/float initialization number you have to use negative zero (` + "`-0`" + `). + * * Example: * * ` + "```" + `js * const model = new DynamicModel({ - * name: "" - * age: 0, - * active: false, - * roles: [], - * meta: {} + * name: "" + * age: 0, // int64 + * totalSpent: -0, // float64 + * active: false, + * roles: [], + * meta: {} * }) * ` + "```" + ` * @@ -279,12 +265,12 @@ declare class DynamicModel { * @group PocketBase */ declare const Record: { - new(collection?: models.Collection, data?: { [key:string]: any }): models.Record + new(collection?: core.Collection, data?: { [key:string]: any }): core.Record // note: declare as "newable" const due to conflict with the Record TS utility type } -interface Collection extends models.Collection{} // merge +interface Collection extends core.Collection{} // merge /** * Collection model class. * @@ -295,12 +281,13 @@ interface Collection extends models.Collection{} // merge * listRule: "@request.auth.id != '' || status = 'public'", * viewRule: "@request.auth.id != '' || status = 'public'", * deleteRule: "@request.auth.id != ''", - * schema: [ + * fields: [ * { * name: "title", * type: "text", * required: true, - * options: { min: 6, max: 100 }, + * min: 6, + * max: 100, * }, * { * name: "description", @@ -312,44 +299,242 @@ interface Collection extends models.Collection{} // merge * * @group PocketBase */ -declare class Collection implements models.Collection { - constructor(data?: Partial) +declare class Collection implements core.Collection { + constructor(data?: Partial) } -interface Admin extends models.Admin{} // merge +interface BaseCollection extends core.Collection{} // merge /** - * Admin model class. + * Alias for a "base" collection class. * * ` + "```" + `js - * const admin = new Admin() - * admin.email = "test@example.com" - * admin.setPassword(1234567890) + * 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 Admin implements models.Admin { - constructor(data?: Partial) +declare class BaseCollection implements core.Collection { + constructor(data?: Partial) } -interface Schema extends schema.Schema{} // merge +interface AuthCollection extends core.Collection{} // merge /** - * Schema model class, usually used to define the Collection.schema field. + * 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 Schema implements schema.Schema { - constructor(data?: Partial) +declare class AuthCollection implements core.Collection { + constructor(data?: Partial) } -interface SchemaField extends schema.SchemaField{} // merge +interface ViewCollection extends core.Collection{} // merge /** - * SchemaField model class, usually used as part of the Schema model. + * 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 SchemaField implements schema.SchemaField { - constructor(data?: Partial) +declare class ViewCollection implements core.Collection { + constructor(data?: Partial) +} + +interface FieldsList extends core.FieldsList{} // merge +/** + * FieldsList model class, usually used to define the Collection.fields. + * + * @group PocketBase + */ +declare class FieldsList implements core.FieldsList { + constructor(data?: Partial) +} + +interface Field extends core.Field{} // merge +/** + * Field model class, usually used as part of the FieldsList model. + * + * @group PocketBase + */ +declare class Field implements core.Field { + constructor(data?: Partial) +} + +interface NumberField extends core.NumberField{} // merge +/** + * NumberField class defines a single "number" collection field. + * + * @group PocketBase + */ +declare class NumberField implements core.NumberField { + constructor(data?: Partial) +} + +interface BoolField extends core.BoolField{} // merge +/** + * BoolField class defines a single "bool" collection field. + * + * @group PocketBase + */ +declare class BoolField implements core.BoolField { + constructor(data?: Partial) +} + +interface TextField extends core.TextField{} // merge +/** + * TextField class defines a single "text" collection field. + * + * @group PocketBase + */ +declare class TextField implements core.TextField { + constructor(data?: Partial) +} + +interface URLField extends core.URLField{} // merge +/** + * URLField class defines a single "url" collection field. + * + * @group PocketBase + */ +declare class URLField implements core.URLField { + constructor(data?: Partial) +} + +interface EmailField extends core.EmailField{} // merge +/** + * EmailField class defines a single "email" collection field. + * + * @group PocketBase + */ +declare class EmailField implements core.EmailField { + constructor(data?: Partial) +} + +interface EditorField extends core.EditorField{} // merge +/** + * EditorField class defines a single "editor" collection field. + * + * @group PocketBase + */ +declare class EditorField implements core.EditorField { + constructor(data?: Partial) +} + +interface PasswordField extends core.PasswordField{} // merge +/** + * PasswordField class defines a single "password" collection field. + * + * @group PocketBase + */ +declare class PasswordField implements core.PasswordField { + constructor(data?: Partial) +} + +interface DateField extends core.DateField{} // merge +/** + * DateField class defines a single "date" collection field. + * + * @group PocketBase + */ +declare class DateField implements core.DateField { + constructor(data?: Partial) +} + +interface AutodateField extends core.AutodateField{} // merge +/** + * AutodateField class defines a single "autodate" collection field. + * + * @group PocketBase + */ +declare class AutodateField implements core.AutodateField { + constructor(data?: Partial) +} + +interface JSONField extends core.JSONField{} // merge +/** + * JSONField class defines a single "json" collection field. + * + * @group PocketBase + */ +declare class JSONField implements core.JSONField { + constructor(data?: Partial) +} + +interface RelationField extends core.RelationField{} // merge +/** + * RelationField class defines a single "relation" collection field. + * + * @group PocketBase + */ +declare class RelationField implements core.RelationField { + constructor(data?: Partial) +} + +interface SelectField extends core.SelectField{} // merge +/** + * SelectField class defines a single "select" collection field. + * + * @group PocketBase + */ +declare class SelectField implements core.SelectField { + constructor(data?: Partial) +} + +interface FileField extends core.FileField{} // merge +/** + * FileField class defines a single "file" collection field. + * + * @group PocketBase + */ +declare class FileField implements core.FileField { + constructor(data?: Partial) } interface MailerMessage extends mailer.Message{} // merge @@ -397,31 +582,55 @@ declare class Command implements cobra.Command { constructor(cmd?: Partial) } -interface RequestInfo extends models.RequestInfo{} // merge +interface RequestInfo extends core.RequestInfo{} // merge /** - * RequestInfo defines a single models.RequestInfo instance, usually used + * RequestInfo defines a single core.RequestInfo instance, usually used * as part of various filter checks. * * Example: * * ` + "```" + `js - * const authRecord = $app.dao().findAuthRecordByEmail("users", "test@example.com") + * const authRecord = $app.findAuthRecordByEmail("users", "test@example.com") * * const info = new RequestInfo({ - * authRecord: authRecord, - * data: {"name": 123}, - * headers: {"x-token": "..."}, + * auth: authRecord, + * body: {"name": 123}, + * headers: {"x-token": "..."}, * }) * - * const record = $app.dao().findFirstRecordByData("articles", "slug", "hello") + * const record = $app.findFirstRecordByData("articles", "slug", "hello") * - * const canAccess = $app.dao().canAccessRecord(record, info, "@request.auth.id != '' && @request.data.name = 123") + * const canAccess = $app.canAccessRecord(record, info, "@request.auth.id != '' && @request.body.name = 123") * ` + "```" + ` * * @group PocketBase */ -declare class RequestInfo implements models.RequestInfo { - constructor(date?: Partial) +declare class RequestInfo implements core.RequestInfo { + constructor(info?: Partial) +} + +/** + * Middleware defines a single request middleware handler. + * + * This class is usually used when you want to explicitly specify a priority to your custom route middleware. + * + * Example: + * + * ` + "```" + `js + * routerUse(new Middleware((e) => { + * console.log(e.request.url.path) + * return e.next() + * }, -10)) + * ` + "```" + ` + * + * @group PocketBase + */ +declare class Middleware { + constructor( + func: string|((e: core.RequestEvent) => void), + priority?: number, + id?: string, + ) } interface DateTime extends types.DateTime{} // merge @@ -457,15 +666,6 @@ declare class ValidationError implements ozzo_validation.Error { constructor(code?: string, message?: string) } -interface Dao extends daos.Dao{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class Dao implements daos.Dao { - constructor(concurrentDB?: dbx.Builder, nonconcurrentDB?: dbx.Builder) -} - interface Cookie extends http.Cookie{} // merge /** * A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an @@ -551,44 +751,21 @@ declare namespace $dbx { export let notBetween: dbx.notBetween } -// ------------------------------------------------------------------- -// tokensBinds -// ------------------------------------------------------------------- - -/** - * ` + "`" + `$tokens` + "`" + ` defines high level helpers to generate - * various admins and auth records tokens (auth, forgotten password, etc.). - * - * For more control over the generated token, you can check ` + "`" + `$security` + "`" + `. - * - * @group PocketBase - */ -declare namespace $tokens { - let adminAuthToken: tokens.newAdminAuthToken - let adminResetPasswordToken: tokens.newAdminResetPasswordToken - let adminFileToken: tokens.newAdminFileToken - let recordAuthToken: tokens.newRecordAuthToken - let recordVerifyToken: tokens.newRecordVerifyToken - let recordResetPasswordToken: tokens.newRecordResetPasswordToken - let recordChangeEmailToken: tokens.newRecordChangeEmailToken - let recordFileToken: tokens.newRecordFileToken -} - // ------------------------------------------------------------------- // mailsBinds // ------------------------------------------------------------------- /** * ` + "`" + `$mails` + "`" + ` defines helpers to send common - * admins and auth records emails like verification, password reset, etc. + * auth records emails like verification, password reset, etc. * * @group PocketBase */ declare namespace $mails { - let sendAdminPasswordReset: mails.sendAdminPasswordReset let sendRecordPasswordReset: mails.sendRecordPasswordReset let sendRecordVerification: mails.sendRecordVerification let sendRecordChangeEmail: mails.sendRecordChangeEmail + let sendRecordOTP: mails.sendRecordOTP } // ------------------------------------------------------------------- @@ -604,6 +781,7 @@ declare namespace $mails { declare namespace $security { let randomString: security.randomString let randomStringWithAlphabet: security.randomStringWithAlphabet + let randomStringByRegex: security.randomStringByRegex let pseudorandomString: security.pseudorandomString let pseudorandomStringWithAlphabet: security.pseudorandomStringWithAlphabet let encrypt: security.encrypt @@ -614,7 +792,11 @@ declare namespace $security { let md5: security.md5 let sha256: security.sha256 let sha512: security.sha512 - let createJWT: security.newJWT + + /** + * {@inheritDoc security.newJWT} + */ + export function createJWT(payload: { [key:string]: any }, signingKey: string, secDuration: number): string /** * {@inheritDoc security.parseUnverifiedJWT} @@ -739,42 +921,6 @@ declare namespace $os { // formsBinds // ------------------------------------------------------------------- -interface AdminLoginForm extends forms.AdminLogin{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminLoginForm implements forms.AdminLogin { - constructor(app: CoreApp) -} - -interface AdminPasswordResetConfirmForm extends forms.AdminPasswordResetConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminPasswordResetConfirmForm implements forms.AdminPasswordResetConfirm { - constructor(app: CoreApp) -} - -interface AdminPasswordResetRequestForm extends forms.AdminPasswordResetRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminPasswordResetRequestForm implements forms.AdminPasswordResetRequest { - constructor(app: CoreApp) -} - -interface AdminUpsertForm extends forms.AdminUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class AdminUpsertForm implements forms.AdminUpsert { - constructor(app: CoreApp, admin: models.Admin) -} - interface AppleClientSecretCreateForm extends forms.AppleClientSecretCreate{} // merge /** * @inheritDoc @@ -784,119 +930,13 @@ declare class AppleClientSecretCreateForm implements forms.AppleClientSecretCrea constructor(app: CoreApp) } -interface CollectionUpsertForm extends forms.CollectionUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class CollectionUpsertForm implements forms.CollectionUpsert { - constructor(app: CoreApp, collection: models.Collection) -} - -interface CollectionsImportForm extends forms.CollectionsImport{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class CollectionsImportForm implements forms.CollectionsImport { - constructor(app: CoreApp) -} - -interface RealtimeSubscribeForm extends forms.RealtimeSubscribe{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RealtimeSubscribeForm implements forms.RealtimeSubscribe {} - -interface RecordEmailChangeConfirmForm extends forms.RecordEmailChangeConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordEmailChangeConfirmForm implements forms.RecordEmailChangeConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordEmailChangeRequestForm extends forms.RecordEmailChangeRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordEmailChangeRequestForm implements forms.RecordEmailChangeRequest { - constructor(app: CoreApp, record: models.Record) -} - -interface RecordOAuth2LoginForm extends forms.RecordOAuth2Login{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordOAuth2LoginForm implements forms.RecordOAuth2Login { - constructor(app: CoreApp, collection: models.Collection, optAuthRecord?: models.Record) -} - -interface RecordPasswordLoginForm extends forms.RecordPasswordLogin{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordLoginForm implements forms.RecordPasswordLogin { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordPasswordResetConfirmForm extends forms.RecordPasswordResetConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordResetConfirmForm implements forms.RecordPasswordResetConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordPasswordResetRequestForm extends forms.RecordPasswordResetRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordPasswordResetRequestForm implements forms.RecordPasswordResetRequest { - constructor(app: CoreApp, collection: models.Collection) -} - interface RecordUpsertForm extends forms.RecordUpsert{} // merge /** * @inheritDoc * @group PocketBase */ declare class RecordUpsertForm implements forms.RecordUpsert { - constructor(app: CoreApp, record: models.Record) -} - -interface RecordVerificationConfirmForm extends forms.RecordVerificationConfirm{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordVerificationConfirmForm implements forms.RecordVerificationConfirm { - constructor(app: CoreApp, collection: models.Collection) -} - -interface RecordVerificationRequestForm extends forms.RecordVerificationRequest{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class RecordVerificationRequestForm implements forms.RecordVerificationRequest { - constructor(app: CoreApp, collection: models.Collection) -} - -interface SettingsUpsertForm extends forms.SettingsUpsert{} // merge -/** - * @inheritDoc - * @group PocketBase - */ -declare class SettingsUpsertForm implements forms.SettingsUpsert { - constructor(app: CoreApp) + constructor(app: CoreApp, record: core.Record) } interface TestEmailSendForm extends forms.TestEmailSend{} // merge @@ -971,6 +1011,26 @@ declare class UnauthorizedError implements apis.ApiError { constructor(message?: string, data?: any) } +interface TooManyRequestsError extends apis.ApiError{} // merge +/** + * TooManyRequestsError returns 429 ApiError. + * + * @group PocketBase + */ +declare class TooManyRequestsError implements apis.ApiError { + constructor(message?: string, data?: any) +} + +interface InternalServerError extends apis.ApiError{} // merge +/** + * InternalServerError returns 429 ApiError. + * + * @group PocketBase + */ +declare class InternalServerError implements apis.ApiError { + constructor(message?: string, data?: any) +} + /** * ` + "`" + `$apis` + "`" + ` defines commonly used PocketBase api helpers and middlewares. * @@ -983,21 +1043,19 @@ declare namespace $apis { * If a file resource is missing and indexFallback is set, the request * will be forwarded to the base index.html (useful for SPA). */ - export function staticDirectoryHandler(dir: string, indexFallback: boolean): echo.HandlerFunc + export function static(dir: string, indexFallback: boolean): (e: core.RequestEvent) => void - let requireGuestOnly: apis.requireGuestOnly - let requireRecordAuth: apis.requireRecordAuth - let requireAdminAuth: apis.requireAdminAuth - let requireAdminAuthOnlyIfAny: apis.requireAdminAuthOnlyIfAny - let requireAdminOrRecordAuth: apis.requireAdminOrRecordAuth - let requireAdminOrOwnerAuth: apis.requireAdminOrOwnerAuth - let activityLogger: apis.activityLogger - let requestInfo: apis.requestInfo - let recordAuthResponse: apis.recordAuthResponse - let gzip: middleware.gzip - let bodyLimit: middleware.bodyLimit - let enrichRecord: apis.enrichRecord - let enrichRecords: apis.enrichRecords + let requireGuestOnly: apis.requireGuestOnly + let requireAuth: apis.requireAuth + let requireSuperuserAuth: apis.requireSuperuserAuth + let requireSuperuserAuthOnlyIfAny: apis.requireSuperuserAuthOnlyIfAny + let requireSuperuserOrOwnerAuth: apis.requireSuperuserOrOwnerAuth + let skipSuccessActivityLog: apis.skipSuccessActivityLog + let gzip: apis.gzip + let bodyLimit: apis.bodyLimit + let recordAuthResponse: apis.recordAuthResponse + let enrichRecord: apis.enrichRecord + let enrichRecords: apis.enrichRecords } // ------------------------------------------------------------------- @@ -1023,9 +1081,10 @@ declare namespace $http { * * ` + "```" + `js * const res = $http.send({ - * url: "https://example.com", - * body: JSON.stringify({"title": "test"}) - * method: "post", + * method: "POST", + * url: "https://example.com", + * body: JSON.stringify({"title": "test"}), + * headers: { 'Content-Type': 'application/json' } * }) * * console.log(res.statusCode) // the response HTTP status code @@ -1042,7 +1101,7 @@ declare namespace $http { headers?: { [key:string]: string }, timeout?: number, // default to 120 - // deprecated, please use body instead + // @deprecated please use body instead data?: { [key:string]: any }, }): { statusCode: number, @@ -1065,8 +1124,8 @@ declare namespace $http { * @group PocketBase */ declare function migrate( - up: (db: dbx.Builder) => void, - down?: (db: dbx.Builder) => void + up: (txApp: CoreApp) => void, + down?: (txApp: CoreApp) => void ): void; ` @@ -1077,13 +1136,11 @@ func main() { gen := tygoja.New(tygoja.Config{ Packages: map[string][]string{ - "github.com/labstack/echo/v5/middleware": {"Gzip", "BodyLimit"}, "github.com/go-ozzo/ozzo-validation/v4": {"Error"}, "github.com/pocketbase/dbx": {"*"}, "github.com/pocketbase/pocketbase/tools/security": {"*"}, "github.com/pocketbase/pocketbase/tools/filesystem": {"*"}, "github.com/pocketbase/pocketbase/tools/template": {"*"}, - "github.com/pocketbase/pocketbase/tokens": {"*"}, "github.com/pocketbase/pocketbase/mails": {"*"}, "github.com/pocketbase/pocketbase/apis": {"*"}, "github.com/pocketbase/pocketbase/forms": {"*"}, @@ -1099,23 +1156,25 @@ func main() { return mapper.MethodName(nil, reflect.Method{Name: s}) }, TypeMappings: map[string]string{ - "crypto.*": "any", - "acme.*": "any", - "autocert.*": "any", - "driver.*": "any", - "reflect.*": "any", - "fmt.*": "any", - "rand.*": "any", - "tls.*": "any", - "asn1.*": "any", - "pkix.*": "any", - "x509.*": "any", - "pflag.*": "any", - "flag.*": "any", - "log.*": "any", - "http.Client": "any", + "crypto.*": "any", + "acme.*": "any", + "autocert.*": "any", + "driver.*": "any", + "reflect.*": "any", + "fmt.*": "any", + "rand.*": "any", + "tls.*": "any", + "asn1.*": "any", + "pkix.*": "any", + "x509.*": "any", + "pflag.*": "any", + "flag.*": "any", + "log.*": "any", + "aws.*": "any", + "http.Client": "any", + "mail.Address": "{ address: string; name?: string; }", // prevents the LSP to complain in case no name is provided }, - Indent: " ", // use only a single space to reduce slight the size + Indent: " ", // use only a single space to reduce slightly the size WithPackageFunctions: true, Heading: declarations, }) @@ -1151,7 +1210,7 @@ func main() { func hooksDeclarations() string { var result strings.Builder - excluded := []string{"OnBeforeServe"} + excluded := []string{"OnServe"} appType := reflect.TypeOf(struct{ core.App }{}) totalMethods := appType.NumMethod() @@ -1165,7 +1224,7 @@ func hooksDeclarations() string { withTags := strings.HasPrefix(hookType.String(), "*hook.TaggedHook") - addMethod, ok := hookType.MethodByName("Add") + addMethod, ok := hookType.MethodByName("BindFunc") if !ok { continue } diff --git a/plugins/jsvm/jsvm.go b/plugins/jsvm/jsvm.go index d47d6419..01c0d9cb 100644 --- a/plugins/jsvm/jsvm.go +++ b/plugins/jsvm/jsvm.go @@ -27,17 +27,12 @@ import ( "github.com/dop251/goja_nodejs/require" "github.com/fatih/color" "github.com/fsnotify/fsnotify" - "github.com/labstack/echo/v5" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - m "github.com/pocketbase/pocketbase/migrations" "github.com/pocketbase/pocketbase/plugins/jsvm/internal/types/generated" "github.com/pocketbase/pocketbase/tools/template" ) -const ( - typesFileName = "types.d.ts" -) +const typesFileName = "types.d.ts" // Config defines the config options of the jsvm plugin. type Config struct { @@ -131,9 +126,15 @@ func Register(app core.App, config Config) error { p.config.TypesDir = app.DataDir() } - p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error { + p.app.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error { + err := e.Next() + if err != nil { + return err + } + // ensure that the user has the latest types declaration - if err := p.refreshTypesFile(); err != nil { + err = p.refreshTypesFile() + if err != nil { color.Yellow("Unable to refresh app types file: %v", err) } @@ -173,14 +174,13 @@ func (p *plugin) registerMigrations() error { process.Enable(vm) baseBinds(vm) dbxBinds(vm) - tokensBinds(vm) securityBinds(vm) osBinds(vm) filepathBinds(vm) httpClientBinds(vm) - vm.Set("migrate", func(up, down func(db dbx.Builder) error) { - m.AppMigrations.Register(up, down, file) + vm.Set("migrate", func(up, down func(txApp core.App) error) { + core.AppMigrations.Register(up, down, file) }) if p.config.OnInit != nil { @@ -238,9 +238,10 @@ func (p *plugin) registerHooks() error { return err } - p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.HTTPErrorHandler = p.normalizeServeExceptions(e.Router.HTTPErrorHandler) - return nil + p.app.OnServe().BindFunc(func(e *core.ServeEvent) error { + e.Router.BindFunc(p.normalizeServeExceptions) + + return e.Next() }) // safe to be shared across multiple vms @@ -255,7 +256,6 @@ func (p *plugin) registerHooks() error { baseBinds(vm) dbxBinds(vm) filesystemBinds(vm) - tokensBinds(vm) securityBinds(vm) osBinds(vm) filepathBinds(vm) @@ -311,32 +311,31 @@ func (p *plugin) registerHooks() error { return nil } -// normalizeExceptions wraps the provided error handler and returns a new one -// with extracted goja exception error value for consistency when throwing or returning errors. -func (p *plugin) normalizeServeExceptions(oldErrorHandler echo.HTTPErrorHandler) echo.HTTPErrorHandler { - return func(c echo.Context, err error) { - defer func() { - oldErrorHandler(c, err) - }() +// normalizeExceptions registers a global error handler that +// wraps the extracted goja exception error value for consistency +// when throwing or returning errors. +func (p *plugin) normalizeServeExceptions(e *core.RequestEvent) error { + err := e.Next() - if err == nil || c.Response().Committed { - return // no error or already committed - } + if err == nil || e.Written() { + return err // no error or already committed + } - jsException, ok := err.(*goja.Exception) - if !ok { - return // no exception - } + jsException, ok := err.(*goja.Exception) + if !ok { + return err // no exception + } - switch v := jsException.Value().Export().(type) { - case error: - err = v - case map[string]any: // goja.GoError - if vErr, ok := v["value"].(error); ok { - err = vErr - } + switch v := jsException.Value().Export().(type) { + case error: + err = v + case map[string]any: // goja.GoError + if vErr, ok := v["value"].(error); ok { + err = vErr } } + + return err } // watchHooks initializes a hooks file watcher that will restart the @@ -365,12 +364,12 @@ func (p *plugin) watchHooks() error { } } - p.app.OnTerminate().Add(func(e *core.TerminateEvent) error { + p.app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { watcher.Close() stopDebounceTimer() - return nil + return e.Next() }) // start listening for events. diff --git a/plugins/migratecmd/automigrate.go b/plugins/migratecmd/automigrate.go index f74fa2b4..b94dabc0 100644 --- a/plugins/migratecmd/automigrate.go +++ b/plugins/migratecmd/automigrate.go @@ -10,130 +10,86 @@ import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/migrate" ) -const collectionsStoreKey = "migratecmd_collections" - -// onCollectionChange handles the automigration snapshot generation on -// collection change event (create/update/delete). -func (p *plugin) afterCollectionChange() func(*core.ModelEvent) error { - return func(e *core.ModelEvent) error { - if e.Model.TableName() != "_collections" { - return nil // not a collection +// automigrateOnCollectionChange handles the automigration snapshot +// generation on collection change request event (create/update/delete). +func (p *plugin) automigrateOnCollectionChange(e *core.CollectionRequestEvent) error { + var err error + var old *core.Collection + if !e.Collection.IsNew() { + old, err = e.App.FindCollectionByNameOrId(e.Collection.Id) + if err != nil { + return err } + } - // @todo replace with the OldModel when added to the ModelEvent - oldCollections, err := p.getCachedCollections() + err = e.Next() + if err != nil { + return err + } + + new, err := p.app.FindCollectionByNameOrId(e.Collection.Id) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + // for now exclude OAuth2 configs from the migration + if old != nil && old.IsAuth() { + old.OAuth2.Providers = nil + } + if new != nil && new.IsAuth() { + new.OAuth2.Providers = nil + } + + var template string + var templateErr error + if p.config.TemplateLang == TemplateLangJS { + template, templateErr = p.jsDiffTemplate(new, old) + } else { + template, templateErr = p.goDiffTemplate(new, old) + } + if templateErr != nil { + if errors.Is(templateErr, ErrEmptyTemplate) { + return nil // no changes + } + return fmt.Errorf("failed to resolve template: %w", templateErr) + } + + var action string + switch { + case new == nil: + action = "deleted_" + old.Name + case old == nil: + action = "created_" + new.Name + default: + action = "updated_" + old.Name + } + + name := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), action, p.config.TemplateLang) + filePath := filepath.Join(p.config.Dir, name) + + return p.app.RunInTransaction(func(txApp core.App) error { + // insert the migration entry + _, err := txApp.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ + "file": name, + // use microseconds for more granular applied time in case + // multiple collection changes happens at the ~exact time + "applied": time.Now().UnixMicro(), + }).Execute() if err != nil { return err } - old := oldCollections[e.Model.GetId()] - - new, err := p.app.Dao().FindCollectionByNameOrId(e.Model.GetId()) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err + // ensure that the local migrations dir exist + if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create migration dir: %w", err) } - var template string - var templateErr error - if p.config.TemplateLang == TemplateLangJS { - template, templateErr = p.jsDiffTemplate(new, old) - } else { - template, templateErr = p.goDiffTemplate(new, old) - } - if templateErr != nil { - if errors.Is(templateErr, emptyTemplateErr) { - return nil // no changes - } - return fmt.Errorf("failed to resolve template: %w", templateErr) + if err := os.WriteFile(filePath, []byte(template), 0644); err != nil { + return fmt.Errorf("failed to save automigrate file: %w", err) } - var action string - switch { - case new == nil: - action = "deleted_" + old.Name - case old == nil: - action = "created_" + new.Name - default: - action = "updated_" + old.Name - } - - name := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), action, p.config.TemplateLang) - filePath := filepath.Join(p.config.Dir, name) - - return p.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { - // insert the migration entry - _, err := txDao.DB().Insert(migrate.DefaultMigrationsTable, dbx.Params{ - "file": name, - // use microseconds for more granular applied time in case - // multiple collection changes happens at the ~exact time - "applied": time.Now().UnixMicro(), - }).Execute() - if err != nil { - return err - } - - // ensure that the local migrations dir exist - if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create migration dir: %w", err) - } - - if err := os.WriteFile(filePath, []byte(template), 0644); err != nil { - return fmt.Errorf("failed to save automigrate file: %w", err) - } - - p.updateSingleCachedCollection(new, old) - - return nil - }) - } -} - -func (p *plugin) updateSingleCachedCollection(new, old *models.Collection) { - cached, _ := p.app.Store().Get(collectionsStoreKey).(map[string]*models.Collection) - - switch { - case new == nil: - delete(cached, old.Id) - default: - cached[new.Id] = new - } - - p.app.Store().Set(collectionsStoreKey, cached) -} - -func (p *plugin) refreshCachedCollections() error { - if p.app.Dao() == nil { - return errors.New("app is not initialized yet") - } - - var collections []*models.Collection - if err := p.app.Dao().CollectionQuery().All(&collections); err != nil { - return err - } - - cached := map[string]*models.Collection{} - for _, c := range collections { - cached[c.Id] = c - } - - p.app.Store().Set(collectionsStoreKey, cached) - - return nil -} - -func (p *plugin) getCachedCollections() (map[string]*models.Collection, error) { - if !p.app.Store().Has(collectionsStoreKey) { - if err := p.refreshCachedCollections(); err != nil { - return nil, err - } - } - - result, _ := p.app.Store().Get(collectionsStoreKey).(map[string]*models.Collection) - - return result, nil + return nil + }) } diff --git a/plugins/migratecmd/migratecmd.go b/plugins/migratecmd/migratecmd.go index 185a0a88..fd23f3f3 100644 --- a/plugins/migratecmd/migratecmd.go +++ b/plugins/migratecmd/migratecmd.go @@ -16,6 +16,7 @@ package migratecmd import ( + "errors" "fmt" "os" "path" @@ -24,10 +25,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/inflector" - "github.com/pocketbase/pocketbase/tools/migrate" "github.com/spf13/cobra" ) @@ -82,22 +80,9 @@ func Register(app core.App, rootCmd *cobra.Command, config Config) error { // watch for collection changes if p.config.Automigrate { - // refresh the cache right after app bootstap - p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error { - p.refreshCachedCollections() - return nil - }) - - // refresh the cache to ensure that it constains the latest changes - // when migrations are applied on server start - p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - p.refreshCachedCollections() - return nil - }) - - p.app.OnModelAfterCreate().Add(p.afterCollectionChange()) - p.app.OnModelAfterUpdate().Add(p.afterCollectionChange()) - p.app.OnModelAfterDelete().Add(p.afterCollectionChange()) + p.app.OnCollectionCreateRequest().BindFunc(p.automigrateOnCollectionChange) + p.app.OnCollectionUpdateRequest().BindFunc(p.automigrateOnCollectionChange) + p.app.OnCollectionDeleteRequest().BindFunc(p.automigrateOnCollectionChange) } return nil @@ -139,10 +124,12 @@ func (p *plugin) createCommand() *cobra.Command { return err } default: - runner, err := migrate.NewRunner(p.app.DB(), migrations.AppMigrations) - if err != nil { - return err - } + // note: system migrations are always applied as part of the bootstrap process + var list = core.MigrationsList{} + list.Copy(core.SystemMigrations) + list.Copy(core.AppMigrations) + + runner := core.NewMigrationsRunner(p.app, list) if err := runner.Run(args...); err != nil { return err @@ -158,7 +145,7 @@ func (p *plugin) createCommand() *cobra.Command { func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) (string, error) { if len(args) < 1 { - return "", fmt.Errorf("Missing migration file name") + return "", errors.New("Missing migration file name") } name := args[0] @@ -214,9 +201,9 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (str createArgs := []string{"collections_snapshot"} createArgs = append(createArgs, args...) - collections := []*models.Collection{} - if err := p.app.Dao().CollectionQuery().OrderBy("created ASC").All(&collections); err != nil { - return "", fmt.Errorf("Failed to fetch migrations list: %v", err) + collections := []*core.Collection{} + if err := p.app.CollectionQuery().OrderBy("created ASC").All(&collections); err != nil { + return "", fmt.Errorf("Failed to fetch migrations list: %v\n", err) } var template string @@ -227,7 +214,7 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (str template, templateErr = p.goSnapshotTemplate(collections) } if templateErr != nil { - return "", fmt.Errorf("Failed to resolve template: %v", templateErr) + return "", fmt.Errorf("Failed to resolve template: %v\n", templateErr) } return p.migrateCreateHandler(template, createArgs, interactive) diff --git a/plugins/migratecmd/migratecmd_test.go b/plugins/migratecmd/migratecmd_test.go index 121de9c4..043ca865 100644 --- a/plugins/migratecmd/migratecmd_test.go +++ b/plugins/migratecmd/migratecmd_test.go @@ -1,21 +1,20 @@ package migratecmd_test import ( - "fmt" "os" "path/filepath" "strings" "testing" - "github.com/pocketbase/pocketbase/daos" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/types" ) func TestAutomigrateCollectionCreate(t *testing.T) { + t.Parallel() + scenarios := []struct { lang string expectedTemplate string @@ -24,42 +23,166 @@ func TestAutomigrateCollectionCreate(t *testing.T) { migratecmd.TemplateLangJS, ` /// -migrate((db) => { +migrate((app) => { const collection = new Collection({ - "id": "new_id", - "created": "2022-01-01 00:00:00.000Z", - "updated": "2022-01-01 00:00:00.000Z", - "name": "new_name", - "type": "auth", - "system": true, - "schema": [], - "indexes": [ - "create index test on new_name (id)" - ], - "listRule": "@request.auth.id != '' && created > 0 || 'backtick` + "`" + `test' = 0", - "viewRule": "id = \"1\"", + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 604800 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, "createRule": null, - "updateRule": null, "deleteRule": null, - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - } + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "_pbc_2865679067", + "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` + "`" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0", + "manageRule": "1 != 2", + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "new_name", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "", + "id": "", + "name": "", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": true, + "type": "auth", + "updateRule": null, + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": "id = \"1\"" }); - return Dao(db).saveCollection(collection); -}, (db) => { - const dao = new Dao(db); - const collection = dao.findCollectionByNameOrId("new_id"); + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("_pbc_2865679067"); - return dao.deleteCollection(collection); + return app.delete(collection); }) `, }, @@ -71,66 +194,187 @@ package _test_migrations import ( "encoding/json" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" ) func init() { - m.Register(func(db dbx.Builder) error { + m.Register(func(app core.App) error { jsonData := ` + "`" + `{ - "id": "new_id", - "created": "2022-01-01 00:00:00.000Z", - "updated": "2022-01-01 00:00:00.000Z", - "name": "new_name", - "type": "auth", - "system": true, - "schema": [], - "indexes": [ - "create index test on new_name (id)" - ], - "listRule": "@request.auth.id != '' && created > 0 || ` + "'backtick` + \"`\" + `test' = 0" + `", - "viewRule": "id = \"1\"", + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 604800 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, "createRule": null, - "updateRule": null, "deleteRule": null, - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - } + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "_pbc_2865679067", + "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` + "` + \"`\" + `" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0", + "manageRule": "1 != 2", + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "new_name", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "", + "id": "", + "name": "", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": true, + "type": "auth", + "updateRule": null, + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": "id = \"1\"" }` + "`" + ` - collection := &models.Collection{} + collection := &core.Collection{} if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { return err } - return daos.New(db).SaveCollection(collection) - }, func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId("new_id") + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("_pbc_2865679067") if err != nil { return err } - return dao.DeleteCollection(collection) + return app.Delete(collection) }) } `, }, } - for i, s := range scenarios { - t.Run(fmt.Sprintf("s%d", i), func(t *testing.T) { + for _, s := range scenarios { + t.Run(s.lang, func(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() @@ -142,27 +386,33 @@ func init() { Dir: migrationsDir, }) - // @todo remove after collections cache is replaced app.Bootstrap() - collection := &models.Collection{} - collection.Id = "new_id" - collection.Name = "new_name" - collection.Type = models.CollectionTypeAuth + collection := core.NewAuthCollection("new_name") collection.System = true - collection.Created, _ = types.ParseDateTime("2022-01-01 00:00:00.000Z") - collection.Updated = collection.Created - collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0") + collection.ListRule = types.Pointer("@request.auth.id != '' && 1 > 0 || 'backtick`test' = 0") collection.ViewRule = types.Pointer(`id = "1"`) - collection.Indexes = types.JsonArray[string]{"create index test on new_name (id)"} - collection.SetOptions(models.CollectionAuthOptions{ - ManageRule: types.Pointer("created > 0"), - MinPasswordLength: 20, - }) - collection.MarkAsNew() + collection.Indexes = types.JSONArray[string]{"create index test on new_name (id)"} + collection.ManageRule = types.Pointer("1 != 2") + // should be ignored + collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}} + testSecret := strings.Repeat("a", 30) + collection.AuthToken.Secret = testSecret + collection.FileToken.Secret = testSecret + collection.EmailChangeToken.Secret = testSecret + collection.PasswordResetToken.Secret = testSecret + collection.VerificationToken.Secret = testSecret - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatalf("Failed to save collection, got %v", err) + // save the newly created dummy collection (with mock request event) + event := new(core.CollectionRequestEvent) + event.RequestEvent = &core.RequestEvent{} + event.App = app + event.Collection = collection + err := app.OnCollectionCreateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + return e.App.Save(e.Collection) + }) + if err != nil { + t.Fatalf("Failed to save the created dummy collection, got: %v", err) } files, err := os.ReadDir(migrationsDir) @@ -193,6 +443,8 @@ func init() { } func TestAutomigrateCollectionDelete(t *testing.T) { + t.Parallel() + scenarios := []struct { lang string expectedTemplate string @@ -201,42 +453,166 @@ func TestAutomigrateCollectionDelete(t *testing.T) { migratecmd.TemplateLangJS, ` /// -migrate((db) => { - const dao = new Dao(db); - const collection = dao.findCollectionByNameOrId("test123"); +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pbc_4032078523"); - return dao.deleteCollection(collection); -}, (db) => { + return app.delete(collection); +}, (app) => { const collection = new Collection({ - "id": "test123", - "created": "2022-01-01 00:00:00.000Z", - "updated": "2022-01-01 00:00:00.000Z", - "name": "test456", - "type": "auth", - "system": false, - "schema": [], - "indexes": [ - "create index test on test456 (id)" - ], - "listRule": "@request.auth.id != '' && created > 0 || 'backtick` + "`" + `test' = 0", - "viewRule": "id = \"1\"", + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 604800 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, "createRule": null, - "updateRule": null, "deleteRule": null, - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - } + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "_pbc_4032078523", + "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` + "`" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0", + "manageRule": "1 != 2", + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "test123", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "", + "id": "", + "name": "", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": false, + "type": "auth", + "updateRule": null, + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": "id = \"1\"" }); - return Dao(db).saveCollection(collection); + return app.save(collection); }) `, }, @@ -248,104 +624,220 @@ package _test_migrations import ( "encoding/json" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" ) func init() { - m.Register(func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId("test123") + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("_pbc_4032078523") if err != nil { return err } - return dao.DeleteCollection(collection) - }, func(db dbx.Builder) error { + return app.Delete(collection) + }, func(app core.App) error { jsonData := ` + "`" + `{ - "id": "test123", - "created": "2022-01-01 00:00:00.000Z", - "updated": "2022-01-01 00:00:00.000Z", - "name": "test456", - "type": "auth", - "system": false, - "schema": [], - "indexes": [ - "create index test on test456 (id)" - ], - "listRule": "@request.auth.id != '' && created > 0 || ` + "'backtick` + \"`\" + `test' = 0" + `", - "viewRule": "id = \"1\"", + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 604800 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, "createRule": null, - "updateRule": null, "deleteRule": null, - "options": { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - } + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "_pbc_4032078523", + "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` + "` + \"`\" + `" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0", + "manageRule": "1 != 2", + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "test123", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "", + "id": "", + "name": "", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": false, + "type": "auth", + "updateRule": null, + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": "id = \"1\"" }` + "`" + ` - collection := &models.Collection{} + collection := &core.Collection{} if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { return err } - return daos.New(db).SaveCollection(collection) + return app.Save(collection) }) } `, }, } - for i, s := range scenarios { - t.Run(fmt.Sprintf("s%d", i), func(t *testing.T) { + for _, s := range scenarios { + t.Run(s.lang, func(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() migrationsDir := filepath.Join(app.DataDir(), "_test_migrations") + // create dummy collection + collection := core.NewAuthCollection("test123") + collection.ListRule = types.Pointer("@request.auth.id != '' && 1 > 0 || 'backtick`test' = 0") + collection.ViewRule = types.Pointer(`id = "1"`) + collection.Indexes = types.JSONArray[string]{"create index test on test123 (id)"} + collection.ManageRule = types.Pointer("1 != 2") + if err := app.Save(collection); err != nil { + t.Fatalf("Failed to save dummy collection, got: %v", err) + } + migratecmd.MustRegister(app, nil, migratecmd.Config{ TemplateLang: s.lang, Automigrate: true, Dir: migrationsDir, }) - // create dummy collection - collection := &models.Collection{} - collection.Id = "test123" - collection.Name = "test456" - collection.Type = models.CollectionTypeAuth - collection.Created, _ = types.ParseDateTime("2022-01-01 00:00:00.000Z") - collection.Updated = collection.Created - collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0") - collection.ViewRule = types.Pointer(`id = "1"`) - collection.Indexes = types.JsonArray[string]{"create index test on test456 (id)"} - collection.SetOptions(models.CollectionAuthOptions{ - ManageRule: types.Pointer("created > 0"), - MinPasswordLength: 20, - }) - collection.MarkAsNew() - - // use different dao to avoid triggering automigrate while saving the dummy collection - if err := daos.New(app.DB()).SaveCollection(collection); err != nil { - t.Fatalf("Failed to save dummy collection, got %v", err) - } - - // @todo remove after collections cache is replaced app.Bootstrap() - // delete the newly created dummy collection - if err := app.Dao().DeleteCollection(collection); err != nil { - t.Fatalf("Failed to delete dummy collection, got %v", err) + // delete the newly created dummy collection (with mock request event) + event := new(core.CollectionRequestEvent) + event.RequestEvent = &core.RequestEvent{} + event.App = app + event.Collection = collection + err := app.OnCollectionDeleteRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + return e.App.Delete(e.Collection) + }) + if err != nil { + t.Fatalf("Failed to delete dummy collection, got: %v", err) } files, err := os.ReadDir(migrationsDir) @@ -357,7 +849,7 @@ func init() { t.Fatalf("Expected 1 file to be generated, got %d", total) } - expectedName := "_deleted_test456." + s.lang + expectedName := "_deleted_test123." + s.lang if !strings.Contains(files[0].Name(), expectedName) { t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name()) } @@ -376,6 +868,8 @@ func init() { } func TestAutomigrateCollectionUpdate(t *testing.T) { + t.Parallel() + scenarios := []struct { lang string expectedTemplate string @@ -384,115 +878,117 @@ func TestAutomigrateCollectionUpdate(t *testing.T) { migratecmd.TemplateLangJS, ` /// -migrate((db) => { - const dao = new Dao(db) - const collection = dao.findCollectionByNameOrId("test123") +migrate((app) => { + const collection = app.findCollectionByNameOrId("_pbc_4032078523") - collection.name = "test456_update" - collection.type = "base" - collection.listRule = "@request.auth.id != ''" - collection.createRule = "id = \"nil_update\"" - collection.updateRule = "id = \"2_update\"" - collection.deleteRule = null - collection.options = {} - collection.indexes = [ - "create index test1 on test456_update (f1_name)" - ] + // update collection data + unmarshal({ + "createRule": "id = \"nil_update\"", + "deleteRule": null, + "fileToken": { + "duration": 10 + }, + "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` + "`" + ` != ''" + ], + "listRule": "@request.auth.id != ''", + "name": "test123_update", + "oauth2": { + "enabled": true + }, + "updateRule": "id = \"2_update\"" + }, collection) - // remove - collection.schema.removeField("f3_id") + // remove field + collection.fields.removeById("f3_id") - // add - collection.schema.addField(new SchemaField({ - "system": false, + // add field + collection.fields.add(new Field({ + "autogeneratePattern": "", + "hidden": false, "id": "f4_id", + "max": 0, + "min": 0, "name": "f4_name", - "type": "text", - "required": false, + "pattern": "` + "`" + `test backtick` + "`" + `123", "presentable": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": "` + "`" + `test backtick` + "`" + `123" - } + "primaryKey": false, + "required": false, + "system": false, + "type": "text" })) - // update - collection.schema.addField(new SchemaField({ - "system": false, + // update field + collection.fields.add(new Field({ + "hidden": false, "id": "f2_id", + "max": null, + "min": 10, "name": "f2_name_new", - "type": "number", - "required": false, + "onlyInt": false, "presentable": false, - "unique": true, - "options": { - "min": 10, - "max": null, - "noDecimal": false - } + "required": false, + "system": false, + "type": "number" })) - return dao.saveCollection(collection) -}, (db) => { - const dao = new Dao(db) - const collection = dao.findCollectionByNameOrId("test123") + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("_pbc_4032078523") - collection.name = "test456" - collection.type = "auth" - collection.listRule = "@request.auth.id != '' && created > 0" - collection.createRule = null - collection.updateRule = "id = \"2\"" - collection.deleteRule = "id = \"3\"" - collection.options = { - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - } - collection.indexes = [ - "create index test1 on test456 (f1_name)" - ] + // update collection data + unmarshal({ + "createRule": null, + "deleteRule": "id = \"3\"", + "fileToken": { + "duration": 180 + }, + "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` + "`" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 != 2", + "name": "test123", + "oauth2": { + "enabled": false + }, + "updateRule": "id = \"2\"" + }, collection) - // add - collection.schema.addField(new SchemaField({ - "system": false, + // add field + collection.fields.add(new Field({ + "hidden": false, "id": "f3_id", "name": "f3_name", - "type": "bool", - "required": false, "presentable": false, - "unique": false, - "options": {} - })) - - // remove - collection.schema.removeField("f4_id") - - // update - collection.schema.addField(new SchemaField({ + "required": false, "system": false, - "id": "f2_id", - "name": "f2_name", - "type": "number", - "required": false, - "presentable": false, - "unique": true, - "options": { - "min": 10, - "max": null, - "noDecimal": false - } + "type": "bool" })) - return dao.saveCollection(collection) + // remove field + collection.fields.removeById("f4_id") + + // update field + collection.fields.add(new Field({ + "hidden": false, + "id": "f2_id", + "max": null, + "min": 10, + "name": "f2_name", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + return app.save(collection) }) + `, }, { @@ -503,264 +999,225 @@ package _test_migrations import ( "encoding/json" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/types" ) func init() { - m.Register(func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId("test123") + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("_pbc_4032078523") if err != nil { return err } - collection.Name = "test456_update" - - collection.Type = "base" - - collection.ListRule = types.Pointer("@request.auth.id != ''") - - collection.CreateRule = types.Pointer("id = \"nil_update\"") - - collection.UpdateRule = types.Pointer("id = \"2_update\"") - - collection.DeleteRule = nil - - options := map[string]any{} - if err := json.Unmarshal([]byte(` + "`" + `{}` + "`" + `), &options); err != nil { - return err - } - collection.SetOptions(options) - - if err := json.Unmarshal([]byte(` + "`" + `[ - "create index test1 on test456_update (f1_name)" - ]` + "`" + `), &collection.Indexes); err != nil { - return err - } - - // remove - collection.Schema.RemoveField("f3_id") - - // add - new_f4_name := &schema.SchemaField{} + // update collection data if err := json.Unmarshal([]byte(` + "`" + `{ - "system": false, + "createRule": "id = \"nil_update\"", + "deleteRule": null, + "fileToken": { + "duration": 10 + }, + "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` + "` + \"`\" + `" + ` != ''" + ], + "listRule": "@request.auth.id != ''", + "name": "test123_update", + "oauth2": { + "enabled": true + }, + "updateRule": "id = \"2_update\"" + }` + "`" + `), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("f3_id") + + // add field + if err := json.Unmarshal([]byte(` + "`" + `[{ + "autogeneratePattern": "", + "hidden": false, "id": "f4_id", + "max": 0, + "min": 0, "name": "f4_name", - "type": "text", - "required": false, + "pattern": "` + "` + \"`\" + `" + `test backtick` + "` + \"`\" + `" + `123", "presentable": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": ` + "\"` + \"`\" + `test backtick` + \"`\" + `123\"" + ` - } - }` + "`" + `), new_f4_name); err != nil { - return err - } - collection.Schema.AddField(new_f4_name) - - // update - edit_f2_name_new := &schema.SchemaField{} - if err := json.Unmarshal([]byte(` + "`" + `{ + "primaryKey": false, + "required": false, "system": false, - "id": "f2_id", - "name": "f2_name_new", - "type": "number", - "required": false, - "presentable": false, - "unique": true, - "options": { - "min": 10, - "max": null, - "noDecimal": false - } - }` + "`" + `), edit_f2_name_new); err != nil { + "type": "text" + }]` + "`" + `), &collection.Fields); err != nil { return err } - collection.Schema.AddField(edit_f2_name_new) - return dao.SaveCollection(collection) - }, func(db dbx.Builder) error { - dao := daos.New(db); + // update field + if err := json.Unmarshal([]byte(` + "`" + `[{ + "hidden": false, + "id": "f2_id", + "max": null, + "min": 10, + "name": "f2_name_new", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }]` + "`" + `), &collection.Fields); err != nil { + return err + } - collection, err := dao.FindCollectionByNameOrId("test123") + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("_pbc_4032078523") if err != nil { return err } - collection.Name = "test456" - - collection.Type = "auth" - - collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0") - - collection.CreateRule = nil - - collection.UpdateRule = types.Pointer("id = \"2\"") - - collection.DeleteRule = types.Pointer("id = \"3\"") - - options := map[string]any{} + // update collection data if err := json.Unmarshal([]byte(` + "`" + `{ - "allowEmailAuth": false, - "allowOAuth2Auth": false, - "allowUsernameAuth": false, - "exceptEmailDomains": null, - "manageRule": "created > 0", - "minPasswordLength": 20, - "onlyEmailDomains": null, - "onlyVerified": false, - "requireEmail": false - }` + "`" + `), &options); err != nil { - return err - } - collection.SetOptions(options) - - if err := json.Unmarshal([]byte(` + "`" + `[ - "create index test1 on test456 (f1_name)" - ]` + "`" + `), &collection.Indexes); err != nil { + "createRule": null, + "deleteRule": "id = \"3\"", + "fileToken": { + "duration": 180 + }, + "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` + "` + \"`\" + `" + ` != ''" + ], + "listRule": "@request.auth.id != '' && 1 != 2", + "name": "test123", + "oauth2": { + "enabled": false + }, + "updateRule": "id = \"2\"" + }` + "`" + `), &collection); err != nil { return err } - // add - del_f3_name := &schema.SchemaField{} - if err := json.Unmarshal([]byte(` + "`" + `{ - "system": false, + // add field + if err := json.Unmarshal([]byte(` + "`" + `[{ + "hidden": false, "id": "f3_id", "name": "f3_name", - "type": "bool", - "required": false, "presentable": false, - "unique": false, - "options": {} - }` + "`" + `), del_f3_name); err != nil { - return err - } - collection.Schema.AddField(del_f3_name) - - // remove - collection.Schema.RemoveField("f4_id") - - // update - edit_f2_name_new := &schema.SchemaField{} - if err := json.Unmarshal([]byte(` + "`" + `{ + "required": false, "system": false, - "id": "f2_id", - "name": "f2_name", - "type": "number", - "required": false, - "presentable": false, - "unique": true, - "options": { - "min": 10, - "max": null, - "noDecimal": false - } - }` + "`" + `), edit_f2_name_new); err != nil { + "type": "bool" + }]` + "`" + `), &collection.Fields); err != nil { return err } - collection.Schema.AddField(edit_f2_name_new) - return dao.SaveCollection(collection) + // remove field + collection.Fields.RemoveById("f4_id") + + // update field + if err := json.Unmarshal([]byte(` + "`" + `[{ + "hidden": false, + "id": "f2_id", + "max": null, + "min": 10, + "name": "f2_name", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }]` + "`" + `), &collection.Fields); err != nil { + return err + } + + return app.Save(collection) }) } `, }, } - for i, s := range scenarios { - t.Run(fmt.Sprintf("s%d", i), func(t *testing.T) { + for _, s := range scenarios { + t.Run(s.lang, func(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() migrationsDir := filepath.Join(app.DataDir(), "_test_migrations") + // create dummy collection + collection := core.NewAuthCollection("test123") + collection.ListRule = types.Pointer("@request.auth.id != '' && 1 != 2") + collection.ViewRule = types.Pointer(`id = "1"`) + collection.UpdateRule = types.Pointer(`id = "2"`) + collection.CreateRule = nil + collection.DeleteRule = types.Pointer(`id = "3"`) + collection.Indexes = types.JSONArray[string]{"create index test1 on test123 (f1_name)"} + collection.ManageRule = types.Pointer("1 != 2") + collection.Fields.Add(&core.TextField{ + Id: "f1_id", + Name: "f1_name", + Required: true, + }) + collection.Fields.Add(&core.NumberField{ + Id: "f2_id", + Name: "f2_name", + Min: types.Pointer(10.0), + }) + collection.Fields.Add(&core.BoolField{ + Id: "f3_id", + Name: "f3_name", + }) + + if err := app.Save(collection); err != nil { + t.Fatalf("Failed to save dummy collection, got %v", err) + } + + // init plugin migratecmd.MustRegister(app, nil, migratecmd.Config{ TemplateLang: s.lang, Automigrate: true, Dir: migrationsDir, }) - - // create dummy collection - collection := &models.Collection{} - collection.Id = "test123" - collection.Name = "test456" - collection.Type = models.CollectionTypeAuth - collection.Created, _ = types.ParseDateTime("2022-01-01 00:00:00.000Z") - collection.Updated = collection.Created - collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0") - collection.ViewRule = types.Pointer(`id = "1"`) - collection.UpdateRule = types.Pointer(`id = "2"`) - collection.CreateRule = nil - collection.DeleteRule = types.Pointer(`id = "3"`) - collection.Indexes = types.JsonArray[string]{"create index test1 on test456 (f1_name)"} - collection.SetOptions(models.CollectionAuthOptions{ - ManageRule: types.Pointer("created > 0"), - MinPasswordLength: 20, - }) - collection.MarkAsNew() - collection.Schema.AddField(&schema.SchemaField{ - Id: "f1_id", - Name: "f1_name", - Type: schema.FieldTypeText, - Required: true, - }) - collection.Schema.AddField(&schema.SchemaField{ - Id: "f2_id", - Name: "f2_name", - Type: schema.FieldTypeNumber, - Unique: true, - Options: &schema.NumberOptions{ - Min: types.Pointer(10.0), - }, - }) - collection.Schema.AddField(&schema.SchemaField{ - Id: "f3_id", - Name: "f3_name", - Type: schema.FieldTypeBool, - }) - - // use different dao to avoid triggering automigrate while saving the dummy collection - if err := daos.New(app.DB()).SaveCollection(collection); err != nil { - t.Fatalf("Failed to save dummy collection, got %v", err) - } - - // @todo remove after collections cache is replaced app.Bootstrap() - collection.Name = "test456_update" - collection.Type = models.CollectionTypeBase - collection.DeleteRule = types.Pointer(`updated > 0 && @request.auth.id != ''`) + // update the dummy collection + collection.Name = "test123_update" collection.ListRule = types.Pointer("@request.auth.id != ''") collection.ViewRule = types.Pointer(`id = "1"`) // no change collection.UpdateRule = types.Pointer(`id = "2_update"`) collection.CreateRule = types.Pointer(`id = "nil_update"`) collection.DeleteRule = nil - collection.Indexes = types.JsonArray[string]{ - "create index test1 on test456_update (f1_name)", + collection.Indexes = types.JSONArray[string]{ + "create index test1 on test123_update (f1_name)", } - collection.NormalizeOptions() - collection.Schema.RemoveField("f3_id") - collection.Schema.AddField(&schema.SchemaField{ - Id: "f4_id", - Name: "f4_name", - Type: schema.FieldTypeText, - Options: &schema.TextOptions{ - Pattern: "`test backtick`123", - }, + collection.Fields.RemoveById("f3_id") + collection.Fields.Add(&core.TextField{ + Id: "f4_id", + Name: "f4_name", + Pattern: "`test backtick`123", }) - f := collection.Schema.GetFieldById("f2_id") - f.Name = "f2_name_new" + f := collection.Fields.GetById("f2_id") + f.SetName("f2_name_new") + collection.OAuth2.Enabled = true + collection.FileToken.Duration = 10 + // should be ignored + collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}} + testSecret := strings.Repeat("b", 30) + collection.AuthToken.Secret = testSecret + collection.FileToken.Secret = testSecret + collection.EmailChangeToken.Secret = testSecret + collection.PasswordResetToken.Secret = testSecret + collection.VerificationToken.Secret = testSecret - // save the changes and trigger automigrate - if err := app.Dao().SaveCollection(collection); err != nil { + // save the changes and trigger automigrate (with mock request event) + event := new(core.CollectionRequestEvent) + event.RequestEvent = &core.RequestEvent{} + event.App = app + event.Collection = collection + err := app.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + return e.App.Save(e.Collection) + }) + if err != nil { t.Fatalf("Failed to save dummy collection changes, got %v", err) } @@ -773,7 +1230,7 @@ func init() { t.Fatalf("Expected 1 file to be generated, got %d", total) } - expectedName := "_updated_test456." + s.lang + expectedName := "_updated_test123." + s.lang if !strings.Contains(files[0].Name(), expectedName) { t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name()) } @@ -792,6 +1249,8 @@ func init() { } func TestAutomigrateCollectionNoChanges(t *testing.T) { + t.Parallel() + scenarios := []struct { lang string }{ @@ -803,39 +1262,53 @@ func TestAutomigrateCollectionNoChanges(t *testing.T) { }, } - for i, s := range scenarios { - app, _ := tests.NewTestApp() - defer app.Cleanup() + for _, s := range scenarios { + t.Run(s.lang, func(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() - migrationsDir := filepath.Join(app.DataDir(), "_test_migrations") + migrationsDir := filepath.Join(app.DataDir(), "_test_migrations") - migratecmd.MustRegister(app, nil, migratecmd.Config{ - TemplateLang: s.lang, - Automigrate: true, - Dir: migrationsDir, + // create dummy collection + collection := core.NewAuthCollection("test123") + + if err := app.Save(collection); err != nil { + t.Fatalf("Failed to save dummy collection, got %v", err) + } + + // init plugin + migratecmd.MustRegister(app, nil, migratecmd.Config{ + TemplateLang: s.lang, + Automigrate: true, + Dir: migrationsDir, + }) + app.Bootstrap() + + // should be ignored + collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}} + testSecret := strings.Repeat("b", 30) + collection.AuthToken.Secret = testSecret + collection.FileToken.Secret = testSecret + collection.EmailChangeToken.Secret = testSecret + collection.PasswordResetToken.Secret = testSecret + collection.VerificationToken.Secret = testSecret + + // resave without other changes and trigger automigrate (with mock request event) + event := new(core.CollectionRequestEvent) + event.RequestEvent = &core.RequestEvent{} + event.App = app + event.Collection = collection + err := app.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error { + return e.App.Save(e.Collection) + }) + if err != nil { + t.Fatalf("Failed to save dummy collection update, got %v", err) + } + + files, _ := os.ReadDir(migrationsDir) + if total := len(files); total != 0 { + t.Fatalf("Expected 0 files to be generated, got %d", total) + } }) - - // create dummy collection - collection := &models.Collection{} - collection.Name = "test123" - collection.Type = models.CollectionTypeAuth - - // use different dao to avoid triggering automigrate while saving the dummy collection - if err := daos.New(app.DB()).SaveCollection(collection); err != nil { - t.Fatalf("[%d] Failed to save dummy collection, got %v", i, err) - } - - // @todo remove after collections cache is replaced - app.Bootstrap() - - // resave without changes and trigger automigrate - if err := app.Dao().SaveCollection(collection); err != nil { - t.Fatalf("[%d] Failed to save dummy collection update, got %v", i, err) - } - - files, _ := os.ReadDir(migrationsDir) - if total := len(files); total != 0 { - t.Fatalf("[%d] Expected 0 files to be generated, got %d", i, total) - } } } diff --git a/plugins/migratecmd/templates.go b/plugins/migratecmd/templates.go index a1f6641d..3ce69a6e 100644 --- a/plugins/migratecmd/templates.go +++ b/plugins/migratecmd/templates.go @@ -6,10 +6,11 @@ import ( "errors" "fmt" "path/filepath" + "slices" "strconv" "strings" - "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/core" ) const ( @@ -22,16 +23,16 @@ const ( jsTypesDirective = `/// ` + "\n" ) -var emptyTemplateErr = errors.New("empty template") +var ErrEmptyTemplate = errors.New("empty template") // ------------------------------------------------------------------- // JavaScript templates // ------------------------------------------------------------------- func (p *plugin) jsBlankTemplate() (string, error) { - const template = jsTypesDirective + `migrate((db) => { + const template = jsTypesDirective + `migrate((app) => { // add up queries... -}, (db) => { +}, (app) => { // add down queries... }) ` @@ -39,19 +40,30 @@ func (p *plugin) jsBlankTemplate() (string, error) { return template, nil } -func (p *plugin) jsSnapshotTemplate(collections []*models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collections, " ", " ") +func (p *plugin) jsSnapshotTemplate(collections []*core.Collection) (string, error) { + // unset timestamp fields + var collectionsData = make([]map[string]any, len(collections)) + for i, c := range collections { + data, err := toMap(c) + if err != nil { + return "", fmt.Errorf("failed to serialize %q into a map: %w", c.Name, err) + } + delete(data, "created") + delete(data, "updated") + deleteNestedMapKey(data, "oauth2", "providers") + collectionsData[i] = data + } + + jsonData, err := marhshalWithoutEscape(collectionsData, " ", " ") if err != nil { return "", fmt.Errorf("failed to serialize collections list: %w", err) } - const template = jsTypesDirective + `migrate((db) => { + const template = jsTypesDirective + `migrate((app) => { const snapshot = %s; - const collections = snapshot.map((item) => new Collection(item)); - - return Dao(db).importCollections(collections, true, null); -}, (db) => { + return app.importCollections(snapshot, true); +}, (app) => { return null; }) ` @@ -59,49 +71,65 @@ func (p *plugin) jsSnapshotTemplate(collections []*models.Collection) (string, e return fmt.Sprintf(template, string(jsonData)), nil } -func (p *plugin) jsCreateTemplate(collection *models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collection, " ", " ") +func (p *plugin) jsCreateTemplate(collection *core.Collection) (string, error) { + // unset timestamp fields + collectionData, err := toMap(collection) if err != nil { - return "", fmt.Errorf("failed to serialize collections list: %w", err) + return "", err + } + delete(collectionData, "created") + delete(collectionData, "updated") + deleteNestedMapKey(collectionData, "oauth2", "providers") + + jsonData, err := marhshalWithoutEscape(collectionData, " ", " ") + if err != nil { + return "", fmt.Errorf("failed to serialize collection: %w", err) } - const template = jsTypesDirective + `migrate((db) => { + const template = jsTypesDirective + `migrate((app) => { const collection = new Collection(%s); - return Dao(db).saveCollection(collection); -}, (db) => { - const dao = new Dao(db); - const collection = dao.findCollectionByNameOrId(%q); + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId(%q); - return dao.deleteCollection(collection); + return app.delete(collection); }) ` return fmt.Sprintf(template, string(jsonData), collection.Id), nil } -func (p *plugin) jsDeleteTemplate(collection *models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collection, " ", " ") +func (p *plugin) jsDeleteTemplate(collection *core.Collection) (string, error) { + // unset timestamp fields + collectionData, err := toMap(collection) + if err != nil { + return "", err + } + delete(collectionData, "created") + delete(collectionData, "updated") + deleteNestedMapKey(collectionData, "oauth2", "providers") + + jsonData, err := marhshalWithoutEscape(collectionData, " ", " ") if err != nil { return "", fmt.Errorf("failed to serialize collections list: %w", err) } - const template = jsTypesDirective + `migrate((db) => { - const dao = new Dao(db); - const collection = dao.findCollectionByNameOrId(%q); + const template = jsTypesDirective + `migrate((app) => { + const collection = app.findCollectionByNameOrId(%q); - return dao.deleteCollection(collection); -}, (db) => { + return app.delete(collection); +}, (app) => { const collection = new Collection(%s); - return Dao(db).saveCollection(collection); + return app.save(collection); }) ` return fmt.Sprintf(template, collection.Id, string(jsonData)), nil } -func (p *plugin) jsDiffTemplate(new *models.Collection, old *models.Collection) (string, error) { +func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (string, error) { if new == nil && old == nil { return "", errors.New("the diff template require at least one of the collection to be non-nil") } @@ -118,209 +146,140 @@ func (p *plugin) jsDiffTemplate(new *models.Collection, old *models.Collection) downParts := []string{} varName := "collection" - if old.Name != new.Name { - upParts = append(upParts, fmt.Sprintf("%s.name = %q", varName, new.Name)) - downParts = append(downParts, fmt.Sprintf("%s.name = %q", varName, old.Name)) - } - - if old.Type != new.Type { - upParts = append(upParts, fmt.Sprintf("%s.type = %q", varName, new.Type)) - downParts = append(downParts, fmt.Sprintf("%s.type = %q", varName, old.Type)) - } - - if old.System != new.System { - upParts = append(upParts, fmt.Sprintf("%s.system = %t", varName, new.System)) - downParts = append(downParts, fmt.Sprintf("%s.system = %t", varName, old.System)) - } - - // --- - // note: strconv.Quote is used because %q converts the rule operators in unicode char codes - // --- - - formatRule := func(prop string, rule *string) string { - if rule == nil { - return fmt.Sprintf("%s.%s = null", varName, prop) - } - - return fmt.Sprintf("%s.%s = %s", varName, prop, strconv.Quote(*rule)) - } - - if old.ListRule != new.ListRule { - oldRule := formatRule("listRule", old.ListRule) - newRule := formatRule("listRule", new.ListRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.ViewRule != new.ViewRule { - oldRule := formatRule("viewRule", old.ViewRule) - newRule := formatRule("viewRule", new.ViewRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.CreateRule != new.CreateRule { - oldRule := formatRule("createRule", old.CreateRule) - newRule := formatRule("createRule", new.CreateRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.UpdateRule != new.UpdateRule { - oldRule := formatRule("updateRule", old.UpdateRule) - newRule := formatRule("updateRule", new.UpdateRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.DeleteRule != new.DeleteRule { - oldRule := formatRule("deleteRule", old.DeleteRule) - newRule := formatRule("deleteRule", new.DeleteRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - // Options - rawNewOptions, err := marhshalWithoutEscape(new.Options, " ", " ") + newMap, err := toMap(new) if err != nil { return "", err } - rawOldOptions, err := marhshalWithoutEscape(old.Options, " ", " ") + + oldMap, err := toMap(old) if err != nil { return "", err } - if !bytes.Equal(rawNewOptions, rawOldOptions) { - upParts = append(upParts, fmt.Sprintf("%s.options = %s", varName, rawNewOptions)) - downParts = append(downParts, fmt.Sprintf("%s.options = %s", varName, rawOldOptions)) - } - // Indexes - rawNewIndexes, err := marhshalWithoutEscape(new.Indexes, " ", " ") - if err != nil { - return "", err - } - rawOldIndexes, err := marhshalWithoutEscape(old.Indexes, " ", " ") - if err != nil { - return "", err - } - if !bytes.Equal(rawNewIndexes, rawOldIndexes) { - upParts = append(upParts, fmt.Sprintf("%s.indexes = %s", varName, rawNewIndexes)) - downParts = append(downParts, fmt.Sprintf("%s.indexes = %s", varName, rawOldIndexes)) - } - - // ensure new line between regular and collection fields - if len(upParts) > 0 { - upParts[len(upParts)-1] += "\n" - } - if len(downParts) > 0 { - downParts[len(downParts)-1] += "\n" - } - - // Schema + // non-fields // ----------------------------------------------------------------- - // deleted fields - for _, oldField := range old.Schema.Fields() { - if new.Schema.GetFieldById(oldField.Id) != nil { - continue // exist - } + upDiff := diffMaps(oldMap, newMap, "fields", "created", "updated") + if len(upDiff) > 0 { + downDiff := diffMaps(newMap, oldMap, "fields", "created", "updated") - rawOldField, err := marhshalWithoutEscape(oldField, " ", " ") + rawUpDiff, err := marhshalWithoutEscape(upDiff, " ", " ") if err != nil { return "", err } - upParts = append(upParts, "// remove") - upParts = append(upParts, fmt.Sprintf("%s.schema.removeField(%q)\n", varName, oldField.Id)) + rawDownDiff, err := marhshalWithoutEscape(downDiff, " ", " ") + if err != nil { + return "", err + } - downParts = append(downParts, "// add") - downParts = append(downParts, fmt.Sprintf("%s.schema.addField(new SchemaField(%s))\n", varName, rawOldField)) + upParts = append(upParts, "// update collection data") + upParts = append(upParts, fmt.Sprintf("unmarshal(%s, %s)", string(rawUpDiff), varName)+"\n") + // --- + downParts = append(downParts, "// update collection data") + downParts = append(downParts, fmt.Sprintf("unmarshal(%s, %s)", string(rawDownDiff), varName)+"\n") + } + + // fields + // ----------------------------------------------------------------- + + oldFieldsSlice, ok := oldMap["fields"].([]any) + if !ok { + return "", errors.New(`oldMap["fields"] is not []any`) + } + + newFieldsSlice, ok := newMap["fields"].([]any) + if !ok { + return "", errors.New(`newMap["fields"] is not []any`) + } + + // deleted fields + for i, oldField := range old.Fields { + if new.Fields.GetById(oldField.GetId()) != nil { + continue // exist + } + + rawOldField, err := marhshalWithoutEscape(oldFieldsSlice[i], " ", " ") + if err != nil { + return "", err + } + + upParts = append(upParts, "// remove field") + upParts = append(upParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, oldField.GetId())) + + downParts = append(downParts, "// add field") + downParts = append(downParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawOldField)) } // created fields - for _, newField := range new.Schema.Fields() { - if old.Schema.GetFieldById(newField.Id) != nil { + for i, newField := range new.Fields { + if old.Fields.GetById(newField.GetId()) != nil { continue // exist } - rawNewField, err := marhshalWithoutEscape(newField, " ", " ") + rawNewField, err := marhshalWithoutEscape(newFieldsSlice[i], " ", " ") if err != nil { return "", err } - upParts = append(upParts, "// add") - upParts = append(upParts, fmt.Sprintf("%s.schema.addField(new SchemaField(%s))\n", varName, rawNewField)) + upParts = append(upParts, "// add field") + upParts = append(upParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawNewField)) - downParts = append(downParts, "// remove") - downParts = append(downParts, fmt.Sprintf("%s.schema.removeField(%q)\n", varName, newField.Id)) + downParts = append(downParts, "// remove field") + downParts = append(downParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, newField.GetId())) } // modified fields - for _, newField := range new.Schema.Fields() { - oldField := old.Schema.GetFieldById(newField.Id) - if oldField == nil { - continue - } + for i, newField := range new.Fields { + var rawNewField, rawOldField []byte - rawNewField, err := marhshalWithoutEscape(newField, " ", " ") + rawNewField, err = marhshalWithoutEscape(newFieldsSlice[i], " ", " ") if err != nil { return "", err } - rawOldField, err := marhshalWithoutEscape(oldField, " ", " ") - if err != nil { - return "", err + for j, oldField := range old.Fields { + if oldField.GetId() == newField.GetId() { + rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], " ", " ") + if err != nil { + return "", err + } + break + } } - if bytes.Equal(rawNewField, rawOldField) { - continue // no change + if rawOldField == nil || bytes.Equal(rawNewField, rawOldField) { + continue // new field or no change } - upParts = append(upParts, "// update") - upParts = append(upParts, fmt.Sprintf("%s.schema.addField(new SchemaField(%s))\n", varName, rawNewField)) + upParts = append(upParts, "// update field") + upParts = append(upParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawNewField)) - downParts = append(downParts, "// update") - downParts = append(downParts, fmt.Sprintf("%s.schema.addField(new SchemaField(%s))\n", varName, rawOldField)) + downParts = append(downParts, "// update field") + downParts = append(downParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawOldField)) } // ----------------------------------------------------------------- if len(upParts) == 0 && len(downParts) == 0 { - return "", emptyTemplateErr + return "", ErrEmptyTemplate } up := strings.Join(upParts, "\n ") down := strings.Join(downParts, "\n ") - const template = jsTypesDirective + `migrate((db) => { - const dao = new Dao(db) - const collection = dao.findCollectionByNameOrId(%q) + const template = jsTypesDirective + `migrate((app) => { + const collection = app.findCollectionByNameOrId(%q) %s - return dao.saveCollection(collection) -}, (db) => { - const dao = new Dao(db) - const collection = dao.findCollectionByNameOrId(%q) + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId(%q) %s - return dao.saveCollection(collection) + return app.save(collection) }) ` @@ -339,16 +298,16 @@ func (p *plugin) goBlankTemplate() (string, error) { const template = `package %s import ( - "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" ) func init() { - m.Register(func(db dbx.Builder) error { + m.Register(func(app core.App) error { // add up queries... return nil - }, func(db dbx.Builder) error { + }, func(app core.App) error { // add down queries... return nil @@ -359,8 +318,21 @@ func init() { return fmt.Sprintf(template, filepath.Base(p.config.Dir)), nil } -func (p *plugin) goSnapshotTemplate(collections []*models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collections, "\t\t", "\t") +func (p *plugin) goSnapshotTemplate(collections []*core.Collection) (string, error) { + // unset timestamp fields + var collectionsData = make([]map[string]any, len(collections)) + for i, c := range collections { + data, err := toMap(c) + if err != nil { + return "", fmt.Errorf("failed to serialize %q into a map: %w", c.Name, err) + } + delete(data, "created") + delete(data, "updated") + deleteNestedMapKey(data, "oauth2", "providers") + collectionsData[i] = data + } + + jsonData, err := marhshalWithoutEscape(collectionsData, "\t\t", "\t") if err != nil { return "", fmt.Errorf("failed to serialize collections list: %w", err) } @@ -368,25 +340,16 @@ func (p *plugin) goSnapshotTemplate(collections []*models.Collection) (string, e const template = `package %s import ( - "encoding/json" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" ) func init() { - m.Register(func(db dbx.Builder) error { + m.Register(func(app core.App) error { jsonData := ` + "`%s`" + ` - collections := []*models.Collection{} - if err := json.Unmarshal([]byte(jsonData), &collections); err != nil { - return err - } - - return daos.New(db).ImportCollections(collections, true, nil) - }, func(db dbx.Builder) error { + return app.ImportCollectionsByMarshaledJSON([]byte(jsonData), true) + }, func(app core.App) error { return nil }) } @@ -398,8 +361,17 @@ func init() { ), nil } -func (p *plugin) goCreateTemplate(collection *models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collection, "\t\t", "\t") +func (p *plugin) goCreateTemplate(collection *core.Collection) (string, error) { + // unset timestamp fields + collectionData, err := toMap(collection) + if err != nil { + return "", err + } + delete(collectionData, "created") + delete(collectionData, "updated") + deleteNestedMapKey(collectionData, "oauth2", "providers") + + jsonData, err := marhshalWithoutEscape(collectionData, "\t\t", "\t") if err != nil { return "", fmt.Errorf("failed to serialize collections list: %w", err) } @@ -409,31 +381,27 @@ func (p *plugin) goCreateTemplate(collection *models.Collection) (string, error) import ( "encoding/json" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" ) func init() { - m.Register(func(db dbx.Builder) error { + m.Register(func(app core.App) error { jsonData := ` + "`%s`" + ` - collection := &models.Collection{} + collection := &core.Collection{} if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { return err } - return daos.New(db).SaveCollection(collection) - }, func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId(%q) + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId(%q) if err != nil { return err } - return dao.DeleteCollection(collection) + return app.Delete(collection) }) } ` @@ -446,8 +414,17 @@ func init() { ), nil } -func (p *plugin) goDeleteTemplate(collection *models.Collection) (string, error) { - jsonData, err := marhshalWithoutEscape(collection, "\t\t", "\t") +func (p *plugin) goDeleteTemplate(collection *core.Collection) (string, error) { + // unset timestamp fields + collectionData, err := toMap(collection) + if err != nil { + return "", err + } + delete(collectionData, "created") + delete(collectionData, "updated") + deleteNestedMapKey(collectionData, "oauth2", "providers") + + jsonData, err := marhshalWithoutEscape(collectionData, "\t\t", "\t") if err != nil { return "", fmt.Errorf("failed to serialize collections list: %w", err) } @@ -457,31 +434,27 @@ func (p *plugin) goDeleteTemplate(collection *models.Collection) (string, error) import ( "encoding/json" - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/models" ) func init() { - m.Register(func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId(%q) + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId(%q) if err != nil { return err } - return dao.DeleteCollection(collection) - }, func(db dbx.Builder) error { + return app.Delete(collection) + }, func(app core.App) error { jsonData := ` + "`%s`" + ` - collection := &models.Collection{} + collection := &core.Collection{} if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { return err } - return daos.New(db).SaveCollection(collection) + return app.Save(collection) }) } ` @@ -494,7 +467,7 @@ func init() { ), nil } -func (p *plugin) goDiffTemplate(new *models.Collection, old *models.Collection) (string, error) { +func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (string, error) { if new == nil && old == nil { return "", errors.New("the diff template require at least one of the collection to be non-nil") } @@ -510,200 +483,124 @@ func (p *plugin) goDiffTemplate(new *models.Collection, old *models.Collection) upParts := []string{} downParts := []string{} varName := "collection" - if old.Name != new.Name { - upParts = append(upParts, fmt.Sprintf("%s.Name = %q\n", varName, new.Name)) - downParts = append(downParts, fmt.Sprintf("%s.Name = %q\n", varName, old.Name)) - } - if old.Type != new.Type { - upParts = append(upParts, fmt.Sprintf("%s.Type = %q\n", varName, new.Type)) - downParts = append(downParts, fmt.Sprintf("%s.Type = %q\n", varName, old.Type)) - } - - if old.System != new.System { - upParts = append(upParts, fmt.Sprintf("%s.System = %t\n", varName, new.System)) - downParts = append(downParts, fmt.Sprintf("%s.System = %t\n", varName, old.System)) - } - - // --- - // note: strconv.Quote is used because %q converts the rule operators in unicode char codes - // --- - - formatRule := func(prop string, rule *string) string { - if rule == nil { - return fmt.Sprintf("%s.%s = nil\n", varName, prop) - } - - return fmt.Sprintf("%s.%s = types.Pointer(%s)\n", varName, prop, strconv.Quote(*rule)) - } - - if old.ListRule != new.ListRule { - oldRule := formatRule("ListRule", old.ListRule) - newRule := formatRule("ListRule", new.ListRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.ViewRule != new.ViewRule { - oldRule := formatRule("ViewRule", old.ViewRule) - newRule := formatRule("ViewRule", new.ViewRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.CreateRule != new.CreateRule { - oldRule := formatRule("CreateRule", old.CreateRule) - newRule := formatRule("CreateRule", new.CreateRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.UpdateRule != new.UpdateRule { - oldRule := formatRule("UpdateRule", old.UpdateRule) - newRule := formatRule("UpdateRule", new.UpdateRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - if old.DeleteRule != new.DeleteRule { - oldRule := formatRule("DeleteRule", old.DeleteRule) - newRule := formatRule("DeleteRule", new.DeleteRule) - - if oldRule != newRule { - upParts = append(upParts, newRule) - downParts = append(downParts, oldRule) - } - } - - // Options - rawNewOptions, err := marhshalWithoutEscape(new.Options, "\t\t", "\t") + newMap, err := toMap(new) if err != nil { return "", err } - rawOldOptions, err := marhshalWithoutEscape(old.Options, "\t\t", "\t") + + oldMap, err := toMap(old) if err != nil { return "", err } - if !bytes.Equal(rawNewOptions, rawOldOptions) { - upParts = append(upParts, "options := map[string]any{}") - upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &options)", escapeBacktick(string(rawNewOptions))))) - upParts = append(upParts, fmt.Sprintf("%s.SetOptions(options)\n", varName)) - // --- - downParts = append(downParts, "options := map[string]any{}") - downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &options)", escapeBacktick(string(rawOldOptions))))) - downParts = append(downParts, fmt.Sprintf("%s.SetOptions(options)\n", varName)) - } - // Indexes - rawNewIndexes, err := marhshalWithoutEscape(new.Indexes, "\t\t", "\t") - if err != nil { - return "", err - } - rawOldIndexes, err := marhshalWithoutEscape(old.Indexes, "\t\t", "\t") - if err != nil { - return "", err - } - if !bytes.Equal(rawNewIndexes, rawOldIndexes) { - upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s.Indexes)", escapeBacktick(string(rawNewIndexes)), varName))+"\n") - // --- - downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s.Indexes)", escapeBacktick(string(rawOldIndexes)), varName))+"\n") - } + // non-fields + // ----------------------------------------------------------------- - // Schema - // --------------------------------------------------------------- - // deleted fields - for _, oldField := range old.Schema.Fields() { - if new.Schema.GetFieldById(oldField.Id) != nil { - continue // exist - } + upDiff := diffMaps(oldMap, newMap, "fields", "created", "updated") + if len(upDiff) > 0 { + downDiff := diffMaps(newMap, oldMap, "fields", "created", "updated") - rawOldField, err := marhshalWithoutEscape(oldField, "\t\t", "\t") + rawUpDiff, err := marhshalWithoutEscape(upDiff, "\t\t", "\t") if err != nil { return "", err } - fieldVar := fmt.Sprintf("del_%s", oldField.Name) + rawDownDiff, err := marhshalWithoutEscape(downDiff, "\t\t", "\t") + if err != nil { + return "", err + } - upParts = append(upParts, "// remove") - upParts = append(upParts, fmt.Sprintf("%s.Schema.RemoveField(%q)\n", varName, oldField.Id)) + upParts = append(upParts, "// update collection data") + upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s)", escapeBacktick(string(rawUpDiff)), varName))) + // --- + downParts = append(downParts, "// update collection data") + downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s)", escapeBacktick(string(rawDownDiff)), varName))) + } - downParts = append(downParts, "// add") - downParts = append(downParts, fmt.Sprintf("%s := &schema.SchemaField{}", fieldVar)) - downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), %s)", escapeBacktick(string(rawOldField)), fieldVar))) - downParts = append(downParts, fmt.Sprintf("%s.Schema.AddField(%s)\n", varName, fieldVar)) + // fields + // ----------------------------------------------------------------- + + oldFieldsSlice, ok := oldMap["fields"].([]any) + if !ok { + return "", errors.New(`oldMap["fields"] is not []any`) + } + + newFieldsSlice, ok := newMap["fields"].([]any) + if !ok { + return "", errors.New(`newMap["fields"] is not []any`) + } + + // deleted fields + for i, oldField := range old.Fields { + if new.Fields.GetById(oldField.GetId()) != nil { + continue // exist + } + + rawOldField, err := marhshalWithoutEscape(oldFieldsSlice[i], "\t\t", "\t") + if err != nil { + return "", err + } + + upParts = append(upParts, "// remove field") + upParts = append(upParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, oldField.GetId())) + + downParts = append(downParts, "// add field") + downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`[%s]`), &%s.Fields)", escapeBacktick(string(rawOldField)), varName))) } // created fields - for _, newField := range new.Schema.Fields() { - if old.Schema.GetFieldById(newField.Id) != nil { + for i, newField := range new.Fields { + if old.Fields.GetById(newField.GetId()) != nil { continue // exist } - rawNewField, err := marhshalWithoutEscape(newField, "\t\t", "\t") + rawNewField, err := marhshalWithoutEscape(newFieldsSlice[i], "\t\t", "\t") if err != nil { return "", err } - fieldVar := fmt.Sprintf("new_%s", newField.Name) + upParts = append(upParts, "// add field") + upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`[%s]`), &%s.Fields)", escapeBacktick(string(rawNewField)), varName))) - upParts = append(upParts, "// add") - upParts = append(upParts, fmt.Sprintf("%s := &schema.SchemaField{}", fieldVar)) - upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), %s)", escapeBacktick(string(rawNewField)), fieldVar))) - upParts = append(upParts, fmt.Sprintf("%s.Schema.AddField(%s)\n", varName, fieldVar)) - - downParts = append(downParts, "// remove") - downParts = append(downParts, fmt.Sprintf("%s.Schema.RemoveField(%q)\n", varName, newField.Id)) + downParts = append(downParts, "// remove field") + downParts = append(downParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, newField.GetId())) } // modified fields - for _, newField := range new.Schema.Fields() { - oldField := old.Schema.GetFieldById(newField.Id) - if oldField == nil { - continue - } + for i, newField := range new.Fields { + var rawNewField, rawOldField []byte - rawNewField, err := marhshalWithoutEscape(newField, "\t\t", "\t") + rawNewField, err = marhshalWithoutEscape(newFieldsSlice[i], "\t\t", "\t") if err != nil { return "", err } - rawOldField, err := marhshalWithoutEscape(oldField, "\t\t", "\t") - if err != nil { - return "", err + for j, oldField := range old.Fields { + if oldField.GetId() == newField.GetId() { + rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], "\t\t", "\t") + if err != nil { + return "", err + } + break + } } - if bytes.Equal(rawNewField, rawOldField) { - continue // no change + if rawOldField == nil || bytes.Equal(rawNewField, rawOldField) { + continue // new field or no change } - fieldVar := fmt.Sprintf("edit_%s", newField.Name) + upParts = append(upParts, "// update field") + upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`[%s]`), &%s.Fields)", escapeBacktick(string(rawNewField)), varName))) - upParts = append(upParts, "// update") - upParts = append(upParts, fmt.Sprintf("%s := &schema.SchemaField{}", fieldVar)) - upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), %s)", escapeBacktick(string(rawNewField)), fieldVar))) - upParts = append(upParts, fmt.Sprintf("%s.Schema.AddField(%s)\n", varName, fieldVar)) - - downParts = append(downParts, "// update") - downParts = append(downParts, fmt.Sprintf("%s := &schema.SchemaField{}", fieldVar)) - downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), %s)", escapeBacktick(string(rawOldField)), fieldVar))) - downParts = append(downParts, fmt.Sprintf("%s.Schema.AddField(%s)\n", varName, fieldVar)) + downParts = append(downParts, "// update field") + downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`[%s]`), &%s.Fields)", escapeBacktick(string(rawOldField)), varName))) } + // --------------------------------------------------------------- if len(upParts) == 0 && len(downParts) == 0 { - return "", emptyTemplateErr + return "", ErrEmptyTemplate } up := strings.Join(upParts, "\n\t\t") @@ -719,17 +616,8 @@ func (p *plugin) goDiffTemplate(new *models.Collection, old *models.Collection) imports += "\n\t\"encoding/json\"\n" } - imports += "\n\t\"github.com/pocketbase/dbx\"" - imports += "\n\t\"github.com/pocketbase/pocketbase/daos\"" + imports += "\n\t\"github.com/pocketbase/pocketbase/core\"" imports += "\n\tm \"github.com/pocketbase/pocketbase/migrations\"" - - if strings.Contains(combined, "schema.SchemaField{") { - imports += "\n\t\"github.com/pocketbase/pocketbase/models/schema\"" - } - - if strings.Contains(combined, "types.Pointer(") { - imports += "\n\t\"github.com/pocketbase/pocketbase/tools/types\"" - } // --- const template = `package %s @@ -738,28 +626,24 @@ import (%s ) func init() { - m.Register(func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId(%q) + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId(%q) if err != nil { return err } %s - return dao.SaveCollection(collection) - }, func(db dbx.Builder) error { - dao := daos.New(db); - - collection, err := dao.FindCollectionByNameOrId(%q) + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId(%q) if err != nil { return err } %s - return dao.SaveCollection(collection) + return app.Save(collection) }) } ` @@ -793,5 +677,85 @@ func escapeBacktick(v string) string { } func goErrIf(v string) string { - return "if err := " + v + "; err != nil {\n\t\t\treturn err\n\t\t}" + return "if err := " + v + "; err != nil {\n\t\t\treturn err\n\t\t}\n" +} + +func toMap(v any) (map[string]any, error) { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + + result := map[string]any{} + + err = json.Unmarshal(raw, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func diffMaps(old, new map[string]any, excludeKeys ...string) map[string]any { + diff := map[string]any{} + + for k, vNew := range new { + if slices.Contains(excludeKeys, k) { + continue + } + + vOld, ok := old[k] + if !ok { + // new field + diff[k] = vNew + continue + } + + // compare the serialized version of the values in case of slice or other custom type + rawOld, _ := json.Marshal(vOld) + rawNew, _ := json.Marshal(vNew) + + if !bytes.Equal(rawOld, rawNew) { + // if both are maps add recursively only the changed fields + vOldMap, ok1 := vOld.(map[string]any) + vNewMap, ok2 := vNew.(map[string]any) + if ok1 && ok2 { + subDiff := diffMaps(vOldMap, vNewMap) + if len(subDiff) > 0 { + diff[k] = subDiff + } + } else { + diff[k] = vNew + } + } + } + + // unset missing fields + for k := range old { + if _, ok := diff[k]; ok || slices.Contains(excludeKeys, k) { + continue // already added + } + + if _, ok := new[k]; !ok { + diff[k] = nil + } + } + + return diff +} + +func deleteNestedMapKey(data map[string]any, parts ...string) { + if len(parts) == 0 { + return + } + + if len(parts) == 1 { + delete(data, parts[0]) + return + } + + v, ok := data[parts[0]].(map[string]any) + if ok { + deleteNestedMapKey(v, parts[1:]...) + } } diff --git a/pocketbase.go b/pocketbase.go index 9b68c254..73211003 100644 --- a/pocketbase.go +++ b/pocketbase.go @@ -7,12 +7,15 @@ import ( "path/filepath" "strings" "syscall" + "time" "github.com/fatih/color" "github.com/pocketbase/pocketbase/cmd" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/list" "github.com/spf13/cobra" + + _ "github.com/pocketbase/pocketbase/migrations" ) var _ core.App = (*PocketBase)(nil) @@ -20,21 +23,17 @@ var _ core.App = (*PocketBase)(nil) // Version of PocketBase var Version = "(untracked)" -// appWrapper serves as a private core.App instance wrapper. -type appWrapper struct { - core.App -} - // PocketBase defines a PocketBase app launcher. // // It implements [core.App] via embedding and all of the app interface methods // could be accessed directly through the instance (eg. PocketBase.DataDir()). type PocketBase struct { - *appWrapper + core.App devFlag bool dataDirFlag string encryptionEnvFlag string + queryTimeout int hideStartBanner bool // RootCmd is the main console command @@ -43,19 +42,21 @@ type PocketBase struct { // Config is the PocketBase initialization config struct. type Config struct { + // hide the default console server info on app startup + HideStartBanner bool + // optional default values for the console flags DefaultDev bool DefaultDataDir string // if not set, it will fallback to "./pb_data" DefaultEncryptionEnv string - // hide the default console server info on app startup - HideStartBanner bool - // optional DB configurations - DataMaxOpenConns int // default to core.DefaultDataMaxOpenConns - DataMaxIdleConns int // default to core.DefaultDataMaxIdleConns - LogsMaxOpenConns int // default to core.DefaultLogsMaxOpenConns - LogsMaxIdleConns int // default to core.DefaultLogsMaxIdleConns + DataMaxOpenConns int // default to core.DefaultDataMaxOpenConns + DataMaxIdleConns int // default to core.DefaultDataMaxIdleConns + AuxMaxOpenConns int // default to core.DefaultAuxMaxOpenConns + AuxMaxIdleConns int // default to core.DefaultAuxMaxIdleConns + QueryTimeout int // default to core.DefaultQueryTimeout (in seconds) + DBConnect core.DBConnectFunc // default to core.dbConnect } // New creates a new PocketBase instance with the default configuration. @@ -88,10 +89,12 @@ func NewWithConfig(config Config) *PocketBase { config.DefaultDataDir = filepath.Join(baseDir, "pb_data") } + executableName := filepath.Base(os.Args[0]) + pb := &PocketBase{ RootCmd: &cobra.Command{ - Use: filepath.Base(os.Args[0]), - Short: "PocketBase CLI", + Use: executableName, + Short: executableName + " CLI", Version: Version, FParseErrWhitelist: cobra.FParseErrWhitelist{ UnknownFlags: true, @@ -115,15 +118,17 @@ func NewWithConfig(config Config) *PocketBase { pb.eagerParseFlags(&config) // initialize the app instance - pb.appWrapper = &appWrapper{core.NewBaseApp(core.BaseAppConfig{ + pb.App = core.NewBaseApp(core.BaseAppConfig{ IsDev: pb.devFlag, DataDir: pb.dataDirFlag, EncryptionEnv: pb.encryptionEnvFlag, DataMaxOpenConns: config.DataMaxOpenConns, DataMaxIdleConns: config.DataMaxIdleConns, - LogsMaxOpenConns: config.LogsMaxOpenConns, - LogsMaxIdleConns: config.LogsMaxIdleConns, - })} + AuxMaxOpenConns: config.AuxMaxOpenConns, + AuxMaxIdleConns: config.AuxMaxIdleConns, + QueryTimeout: time.Duration(config.QueryTimeout) * time.Second, + DBConnect: config.DBConnect, + }) // hide the default help command (allow only `--help` flag) pb.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) @@ -135,7 +140,7 @@ func NewWithConfig(config Config) *PocketBase { // commands (serve, migrate, version) and executes pb.RootCmd. func (pb *PocketBase) Start() error { // register system commands - pb.RootCmd.AddCommand(cmd.NewAdminCommand(pb)) + pb.RootCmd.AddCommand(cmd.NewSuperuserCommand(pb)) pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner)) return pb.Execute() @@ -175,9 +180,9 @@ func (pb *PocketBase) Execute() error { <-done // trigger cleanups - return pb.OnTerminate().Trigger(&core.TerminateEvent{ - App: pb, - }, func(e *core.TerminateEvent) error { + event := new(core.TerminateEvent) + event.App = pb + return pb.OnTerminate().Trigger(event, func(e *core.TerminateEvent) error { return e.App.ResetBootstrapState() }) } @@ -206,6 +211,13 @@ func (pb *PocketBase) eagerParseFlags(config *Config) error { "enable dev mode, aka. printing logs and sql statements to the console", ) + pb.RootCmd.PersistentFlags().IntVar( + &pb.queryTimeout, + "queryTimeout", + int(core.DefaultQueryTimeout.Seconds()), + "the default SELECT queries timeout in seconds", + ) + return pb.RootCmd.ParseFlags(os.Args[1:]) } @@ -253,6 +265,9 @@ func (pb *PocketBase) skipBootstrap() bool { } // inspectRuntime tries to find the base executable directory and how it was run. +// +// note: we are using os.Args[0] and not os.Executable() since it could +// break existing aliased binaries (eg. the community maintained homebrew package) func inspectRuntime() (baseDir string, withGoRun bool) { if strings.HasPrefix(os.Args[0], os.TempDir()) { // probably ran with go run diff --git a/pocketbase_test.go b/pocketbase_test.go index 8ccc65c5..101883e1 100644 --- a/pocketbase_test.go +++ b/pocketbase_test.go @@ -36,8 +36,8 @@ func TestNew(t *testing.T) { t.Fatal("Expected RootCmd to be initialized, got nil") } - if app.appWrapper == nil { - t.Fatal("Expected appWrapper to be initialized, got nil") + if app.App == nil { + t.Fatal("Expected App to be initialized, got nil") } if app.DataDir() != "test_dir" { @@ -64,8 +64,8 @@ func TestNewWithConfig(t *testing.T) { t.Fatal("Expected RootCmd to be initialized, got nil") } - if app.appWrapper == nil { - t.Fatal("Expected appWrapper to be initialized, got nil") + if app.App == nil { + t.Fatal("Expected App to be initialized, got nil") } if app.hideStartBanner != true { @@ -113,8 +113,8 @@ func TestNewWithConfigAndFlags(t *testing.T) { t.Fatal("Expected RootCmd to be initialized, got nil") } - if app.appWrapper == nil { - t.Fatal("Expected appWrapper to be initialized, got nil") + if app.App == nil { + t.Fatal("Expected App to be initialized, got nil") } if app.hideStartBanner != true { diff --git a/resolvers/resolvers.go b/resolvers/resolvers.go deleted file mode 100644 index 8c045a89..00000000 --- a/resolvers/resolvers.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package resolvers contains custom search.FieldResolver implementations. -package resolvers diff --git a/tests/api.go b/tests/api.go index 4e82b849..893a0850 100644 --- a/tests/api.go +++ b/tests/api.go @@ -6,24 +6,38 @@ import ( "encoding/json" "fmt" "io" + "maps" "net/http" "net/http/httptest" "strings" "testing" "time" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/hook" ) // ApiScenario defines a single api request test case/scenario. type ApiScenario struct { - Name string - Method string - Url string - Body io.Reader - RequestHeaders map[string]string + // Name is the test name. + Name string + + // Method is the HTTP method of the test request to use. + Method string + + // URL is the url/path of the endpoint you want to test. + URL string + + // Body specifies the body to send with the request. + // + // For example: + // + // strings.NewReader(`{"title":"abc"}`) + Body io.Reader + + // Headers specifies the headers to send with the request (e.g. "Authorization": "abc") + Headers map[string]string // Delay adds a delay before checking the expectations usually // to ensure that all fired non-awaited go routines have finished @@ -35,30 +49,116 @@ type ApiScenario struct { Timeout time.Duration // expectations - // --- - ExpectedStatus int - ExpectedContent []string + // --------------------------------------------------------------- + + // ExpectedStatus specifies the expected response HTTP status code. + ExpectedStatus int + + // List of keywords that MUST exist in the response body. + // + // Either ExpectedContent or NotExpectedContent must be set if the response body is non-empty. + // Leave both fields empty if you want to ensure that the response didn't have any body (e.g. 204). + ExpectedContent []string + + // List of keywords that MUST NOT exist in the response body. + // + // Either ExpectedContent or NotExpectedContent must be set if the response body is non-empty. + // Leave both fields empty if you want to ensure that the response didn't have any body (e.g. 204). NotExpectedContent []string - ExpectedEvents map[string]int + + // List of hook events to check whether they were fired or not. + // + // You can use the wildcard "*" event key if you want to ensure + // that no other hook events except those listed have been fired. + // + // For example: + // + // map[string]int{ "*": 0 } // no hook events were fired + // map[string]int{ "*": 0, "EventA": 2 } // no hook events, except EventA were fired + // map[string]int{ EventA": 2, "EventB": 0 } // ensures that EventA was fired exactly 2 times and EventB exactly 0 times. + ExpectedEvents map[string]int // test hooks - // --- - TestAppFactory func(t *testing.T) *TestApp - BeforeTestFunc func(t *testing.T, app *TestApp, e *echo.Echo) - AfterTestFunc func(t *testing.T, app *TestApp, res *http.Response) + // --------------------------------------------------------------- + + TestAppFactory func(t testing.TB) *TestApp + BeforeTestFunc func(t testing.TB, app *TestApp, e *core.ServeEvent) + AfterTestFunc func(t testing.TB, app *TestApp, res *http.Response) } // Test executes the test scenario. +// +// Example: +// +// func TestListExample(t *testing.T) { +// scenario := tests.ApiScenario{ +// Name: "list example collection", +// Method: http.MethodGet, +// URL: "/api/collections/example/records", +// ExpectedStatus: 200, +// ExpectedContent: []string{ +// `"totalItems":3`, +// `"id":"0yxhwia2amd8gec"`, +// `"id":"achvryl401bhse3"`, +// `"id":"llvuca81nly1qls"`, +// }, +// ExpectedEvents: map[string]int{ +// "OnRecordsListRequest": 1, +// "OnRecordEnrich": 3, +// }, +// } +// +// scenario.Test(t) +// } func (scenario *ApiScenario) Test(t *testing.T) { - var name = scenario.Name - if name == "" { - name = fmt.Sprintf("%s:%s", scenario.Method, scenario.Url) - } - - t.Run(name, scenario.test) + t.Run(scenario.normalizedName(), func(t *testing.T) { + scenario.test(t) + }) } -func (scenario *ApiScenario) test(t *testing.T) { +// Benchmark benchmarks the test scenario. +// +// Example: +// +// func BenchmarkListExample(b *testing.B) { +// scenario := tests.ApiScenario{ +// Name: "list example collection", +// Method: http.MethodGet, +// URL: "/api/collections/example/records", +// ExpectedStatus: 200, +// ExpectedContent: []string{ +// `"totalItems":3`, +// `"id":"0yxhwia2amd8gec"`, +// `"id":"achvryl401bhse3"`, +// `"id":"llvuca81nly1qls"`, +// }, +// ExpectedEvents: map[string]int{ +// "OnRecordsListRequest": 1, +// "OnRecordEnrich": 3, +// }, +// } +// +// scenario.Benchmark(b) +// } +func (scenario *ApiScenario) Benchmark(b *testing.B) { + b.Run(scenario.normalizedName(), func(b *testing.B) { + for i := 0; i < b.N; i++ { + scenario.test(b) + } + }) +} + +func (scenario *ApiScenario) normalizedName() string { + var name = scenario.Name + + if name == "" { + name = fmt.Sprintf("%s:%s", scenario.Method, scenario.URL) + } + + return name +} + +func (scenario *ApiScenario) test(t testing.TB) { var testApp *TestApp if scenario.TestAppFactory != nil { testApp = scenario.TestAppFactory(t) @@ -74,120 +174,131 @@ func (scenario *ApiScenario) test(t *testing.T) { } defer testApp.Cleanup() - e, err := apis.InitApi(testApp) + baseRouter, err := apis.NewRouter(testApp) if err != nil { t.Fatal(err) } // manually trigger the serve event to ensure that custom app routes and middlewares are registered - testApp.OnBeforeServe().Trigger(&core.ServeEvent{ - App: testApp, - Router: e, - }) + serveEvent := new(core.ServeEvent) + serveEvent.App = testApp + serveEvent.Router = baseRouter - if scenario.BeforeTestFunc != nil { - scenario.BeforeTestFunc(t, testApp, e) - } - - recorder := httptest.NewRecorder() - req := httptest.NewRequest(scenario.Method, scenario.Url, scenario.Body) - - // add middleware to timeout long-running requests (eg. keep-alive routes) - e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - slowTimer := time.AfterFunc(3*time.Second, func() { - t.Logf("[WARN] Long running test %q", scenario.Name) - }) - defer slowTimer.Stop() - - if scenario.Timeout > 0 { - ctx, cancelFunc := context.WithTimeout(c.Request().Context(), scenario.Timeout) - defer cancelFunc() - c.SetRequest(c.Request().Clone(ctx)) - } - - return next(c) + serveErr := testApp.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error { + if scenario.BeforeTestFunc != nil { + scenario.BeforeTestFunc(t, testApp, e) } - }) - // set default header - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + // reset the event counters in case a hook was triggered from a before func (eg. db save) + testApp.ResetEventCalls() - // set scenario headers - for k, v := range scenario.RequestHeaders { - req.Header.Set(k, v) - } + // add middleware to timeout long-running requests (eg. keep-alive routes) + e.Router.Bind(&hook.Handler[*core.RequestEvent]{ + Func: func(re *core.RequestEvent) error { + slowTimer := time.AfterFunc(3*time.Second, func() { + t.Logf("[WARN] Long running test %q", scenario.Name) + }) + defer slowTimer.Stop() - // execute request - e.ServeHTTP(recorder, req) + if scenario.Timeout > 0 { + ctx, cancelFunc := context.WithTimeout(re.Request.Context(), scenario.Timeout) + defer cancelFunc() + re.Request = re.Request.Clone(ctx) + } - res := recorder.Result() + return re.Next() + }, + Priority: -9999, + }) - if res.StatusCode != scenario.ExpectedStatus { - t.Errorf("Expected status code %d, got %d", scenario.ExpectedStatus, res.StatusCode) - } + recorder := httptest.NewRecorder() - if scenario.Delay > 0 { - time.Sleep(scenario.Delay) - } + req := httptest.NewRequest(scenario.Method, scenario.URL, scenario.Body) - if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 { - if len(recorder.Body.Bytes()) != 0 { - t.Errorf("Expected empty body, got \n%v", recorder.Body.String()) + // set default header + req.Header.Set("content-type", "application/json") + + // set scenario headers + for k, v := range scenario.Headers { + req.Header.Set(k, v) } - } else { - // normalize json response format - buffer := new(bytes.Buffer) - err := json.Compact(buffer, recorder.Body.Bytes()) - var normalizedBody string + + // execute request + mux, err := e.Router.BuildMux() if err != nil { - // not a json... - normalizedBody = recorder.Body.String() + t.Fatalf("Failed to build router mux: %v", err) + } + mux.ServeHTTP(recorder, req) + + res := recorder.Result() + + if res.StatusCode != scenario.ExpectedStatus { + t.Errorf("Expected status code %d, got %d", scenario.ExpectedStatus, res.StatusCode) + } + + if scenario.Delay > 0 { + time.Sleep(scenario.Delay) + } + + if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 { + if len(recorder.Body.Bytes()) != 0 { + t.Errorf("Expected empty body, got \n%v", recorder.Body.String()) + } } else { - normalizedBody = buffer.String() - } + // normalize json response format + buffer := new(bytes.Buffer) + err := json.Compact(buffer, recorder.Body.Bytes()) + var normalizedBody string + if err != nil { + // not a json... + normalizedBody = recorder.Body.String() + } else { + normalizedBody = buffer.String() + } - for _, item := range scenario.ExpectedContent { - if !strings.Contains(normalizedBody, item) { - t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody) - break + for _, item := range scenario.ExpectedContent { + if !strings.Contains(normalizedBody, item) { + t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody) + break + } + } + + for _, item := range scenario.NotExpectedContent { + if strings.Contains(normalizedBody, item) { + t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody) + break + } } } - for _, item := range scenario.NotExpectedContent { - if strings.Contains(normalizedBody, item) { - t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody) - break + remainingEvents := maps.Clone(testApp.EventCalls) + + var noOtherEventsShouldRemain bool + for event, expectedNum := range scenario.ExpectedEvents { + if event == "*" && expectedNum <= 0 { + noOtherEventsShouldRemain = true + continue } - } - } - // to minimize the breaking changes we always expect the error - // events to be called on API error - if res.StatusCode >= 400 { - if scenario.ExpectedEvents == nil { - scenario.ExpectedEvents = map[string]int{} - } - if _, ok := scenario.ExpectedEvents["OnBeforeApiError"]; !ok { - scenario.ExpectedEvents["OnBeforeApiError"] = 1 - } - if _, ok := scenario.ExpectedEvents["OnAfterApiError"]; !ok { - scenario.ExpectedEvents["OnAfterApiError"] = 1 - } - } + actualNum := remainingEvents[event] + if actualNum != expectedNum { + t.Errorf("Expected event %s to be called %d, got %d", event, expectedNum, actualNum) + } - if len(testApp.EventCalls) > len(scenario.ExpectedEvents) { - t.Errorf("Expected events %v, got %v", scenario.ExpectedEvents, testApp.EventCalls) - } - - for event, expectedCalls := range scenario.ExpectedEvents { - actualCalls := testApp.EventCalls[event] - if actualCalls != expectedCalls { - t.Errorf("Expected event %s to be called %d, got %d", event, expectedCalls, actualCalls) + delete(remainingEvents, event) } - } - if scenario.AfterTestFunc != nil { - scenario.AfterTestFunc(t, testApp, res) + if noOtherEventsShouldRemain && len(remainingEvents) > 0 { + t.Errorf("Missing expected remaining events:\n%#v\nAll triggered app events are:\n%#v", remainingEvents, testApp.EventCalls) + } + + if scenario.AfterTestFunc != nil { + scenario.AfterTestFunc(t, testApp, res) + } + + return nil + }) + if serveErr != nil { + t.Fatalf("Failed to trigger app serve hook: %v", serveErr) } } diff --git a/tests/app.go b/tests/app.go index e638f252..90940062 100644 --- a/tests/app.go +++ b/tests/app.go @@ -9,12 +9,10 @@ import ( "runtime" "sync" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/migrations" - "github.com/pocketbase/pocketbase/migrations/logs" - "github.com/pocketbase/pocketbase/tools/mailer" - "github.com/pocketbase/pocketbase/tools/migrate" + "github.com/pocketbase/pocketbase/tools/hook" + + _ "github.com/pocketbase/pocketbase/migrations" ) // TestApp is a wrapper app instance used for testing. @@ -25,11 +23,6 @@ type TestApp struct { // EventCalls defines a map to inspect which app events // (and how many times) were triggered. - // - // The following events are not counted because they execute always: - // - OnBeforeBootstrap - // - OnAfterBootstrap - // - OnBeforeServe EventCalls map[string]int TestMailer *TestMailer @@ -40,12 +33,15 @@ type TestApp struct { // // After this call, the app instance shouldn't be used anymore. func (t *TestApp) Cleanup() { - t.OnTerminate().Trigger(&core.TerminateEvent{App: t}, func(e *core.TerminateEvent) error { + event := new(core.TerminateEvent) + event.App = t + + t.OnTerminate().Trigger(event, func(e *core.TerminateEvent) error { t.TestMailer.Reset() t.ResetEventCalls() t.ResetBootstrapState() - return nil + return e.Next() }) if t.DataDir() != "" { @@ -53,18 +49,6 @@ func (t *TestApp) Cleanup() { } } -// NewMailClient initializes (if not already) a test app mail client. -func (t *TestApp) NewMailClient() mailer.Mailer { - t.mux.Lock() - defer t.mux.Unlock() - - if t.TestMailer == nil { - t.TestMailer = &TestMailer{} - } - - return t.TestMailer -} - // ResetEventCalls resets the EventCalls counter. func (t *TestApp) ResetEventCalls() { t.mux.Lock() @@ -73,7 +57,7 @@ func (t *TestApp) ResetEventCalls() { t.EventCalls = make(map[string]int) } -func (t *TestApp) registerEventCall(name string) error { +func (t *TestApp) registerEventCall(name string) { t.mux.Lock() defer t.mux.Unlock() @@ -82,8 +66,6 @@ func (t *TestApp) registerEventCall(name string) error { } t.EventCalls[name]++ - - return nil } // NewTestApp creates and initializes a test application instance. @@ -116,15 +98,15 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) { } // ensure that the Dao and DB configurations are properly loaded - if _, err := app.Dao().DB().NewQuery("Select 1").Execute(); err != nil { + if _, err := app.DB().NewQuery("Select 1").Execute(); err != nil { return nil, err } - if _, err := app.LogsDao().DB().NewQuery("Select 1").Execute(); err != nil { + if _, err := app.AuxDB().NewQuery("Select 1").Execute(); err != nil { return nil, err } // apply any missing migrations - if err := runMigrations(app); err != nil { + if err := app.RunAllMigrations(); err != nil { return nil, err } @@ -138,344 +120,672 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) { TestMailer: &TestMailer{}, } - t.OnBeforeApiError().Add(func(e *core.ApiErrorEvent) error { - return t.registerEventCall("OnBeforeApiError") + t.OnBootstrap().Bind(&hook.Handler[*core.BootstrapEvent]{ + Func: func(e *core.BootstrapEvent) error { + t.registerEventCall("OnBootstrap") + return e.Next() + }, + Priority: -99999, }) - - t.OnAfterApiError().Add(func(e *core.ApiErrorEvent) error { - return t.registerEventCall("OnAfterApiError") + + t.OnServe().Bind(&hook.Handler[*core.ServeEvent]{ + Func: func(e *core.ServeEvent) error { + t.registerEventCall("OnServe") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelBeforeCreate") + + t.OnTerminate().Bind(&hook.Handler[*core.TerminateEvent]{ + Func: func(e *core.TerminateEvent) error { + t.registerEventCall("OnTerminate") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelAfterCreate().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelAfterCreate") + + t.OnBackupCreate().Bind(&hook.Handler[*core.BackupEvent]{ + Func: func(e *core.BackupEvent) error { + t.registerEventCall("OnBackupCreate") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelBeforeUpdate().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelBeforeUpdate") + + t.OnBackupRestore().Bind(&hook.Handler[*core.BackupEvent]{ + Func: func(e *core.BackupEvent) error { + t.registerEventCall("OnBackupRestore") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelAfterUpdate") + + t.OnModelCreate().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelCreate") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelBeforeDelete") + + t.OnModelCreateExecute().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelCreateExecute") + return e.Next() + }, + Priority: -99999, }) - - t.OnModelAfterDelete().Add(func(e *core.ModelEvent) error { - return t.registerEventCall("OnModelAfterDelete") + + t.OnModelAfterCreateSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelAfterCreateSuccess") + return e.Next() + }, + Priority: -99999, }) - - t.OnRecordsListRequest().Add(func(e *core.RecordsListEvent) error { - return t.registerEventCall("OnRecordsListRequest") - }) - - t.OnRecordViewRequest().Add(func(e *core.RecordViewEvent) error { - return t.registerEventCall("OnRecordViewRequest") + + t.OnModelAfterCreateError().Bind(&hook.Handler[*core.ModelErrorEvent]{ + Func: func(e *core.ModelErrorEvent) error { + t.registerEventCall("OnModelAfterCreateError") + return e.Next() + }, + Priority: -99999, }) - - t.OnRecordBeforeCreateRequest().Add(func(e *core.RecordCreateEvent) error { - return t.registerEventCall("OnRecordBeforeCreateRequest") - }) - - t.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { - return t.registerEventCall("OnRecordAfterCreateRequest") + + t.OnModelUpdate().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelUpdate") + return e.Next() + }, + Priority: -99999, }) - - t.OnRecordBeforeUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { - return t.registerEventCall("OnRecordBeforeUpdateRequest") + + t.OnModelUpdateExecute().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelUpdateExecute") + return e.Next() + }, + Priority: -99999, }) - - t.OnRecordAfterUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { - return t.registerEventCall("OnRecordAfterUpdateRequest") + + t.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelAfterUpdateSuccess") + return e.Next() + }, + Priority: -99999, }) - - t.OnRecordBeforeDeleteRequest().Add(func(e *core.RecordDeleteEvent) error { - return t.registerEventCall("OnRecordBeforeDeleteRequest") + + t.OnModelAfterUpdateError().Bind(&hook.Handler[*core.ModelErrorEvent]{ + Func: func(e *core.ModelErrorEvent) error { + t.registerEventCall("OnModelAfterUpdateError") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterDeleteRequest().Add(func(e *core.RecordDeleteEvent) error { - return t.registerEventCall("OnRecordAfterDeleteRequest") + t.OnModelValidate().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelValidate") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAuthRequest().Add(func(e *core.RecordAuthEvent) error { - return t.registerEventCall("OnRecordAuthRequest") + t.OnModelDelete().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelDelete") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeAuthWithPasswordRequest().Add(func(e *core.RecordAuthWithPasswordEvent) error { - return t.registerEventCall("OnRecordBeforeAuthWithPasswordRequest") + t.OnModelDeleteExecute().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelDeleteExecute") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterAuthWithPasswordRequest().Add(func(e *core.RecordAuthWithPasswordEvent) error { - return t.registerEventCall("OnRecordAfterAuthWithPasswordRequest") + t.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*core.ModelEvent]{ + Func: func(e *core.ModelEvent) error { + t.registerEventCall("OnModelAfterDeleteSuccess") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeAuthWithOAuth2Request().Add(func(e *core.RecordAuthWithOAuth2Event) error { - return t.registerEventCall("OnRecordBeforeAuthWithOAuth2Request") + t.OnModelAfterDeleteError().Bind(&hook.Handler[*core.ModelErrorEvent]{ + Func: func(e *core.ModelErrorEvent) error { + t.registerEventCall("OnModelAfterDeleteError") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterAuthWithOAuth2Request().Add(func(e *core.RecordAuthWithOAuth2Event) error { - return t.registerEventCall("OnRecordAfterAuthWithOAuth2Request") + t.OnRecordEnrich().Bind(&hook.Handler[*core.RecordEnrichEvent]{ + Func: func(e *core.RecordEnrichEvent) error { + t.registerEventCall("OnRecordEnrich") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeAuthRefreshRequest().Add(func(e *core.RecordAuthRefreshEvent) error { - return t.registerEventCall("OnRecordBeforeAuthRefreshRequest") + t.OnRecordValidate().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordValidate") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterAuthRefreshRequest().Add(func(e *core.RecordAuthRefreshEvent) error { - return t.registerEventCall("OnRecordAfterAuthRefreshRequest") + t.OnRecordCreate().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordCreate") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeRequestPasswordResetRequest().Add(func(e *core.RecordRequestPasswordResetEvent) error { - return t.registerEventCall("OnRecordBeforeRequestPasswordResetRequest") + t.OnRecordCreateExecute().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordCreateExecute") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterRequestPasswordResetRequest().Add(func(e *core.RecordRequestPasswordResetEvent) error { - return t.registerEventCall("OnRecordAfterRequestPasswordResetRequest") + t.OnRecordAfterCreateSuccess().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordAfterCreateSuccess") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeConfirmPasswordResetRequest().Add(func(e *core.RecordConfirmPasswordResetEvent) error { - return t.registerEventCall("OnRecordBeforeConfirmPasswordResetRequest") + t.OnRecordAfterCreateError().Bind(&hook.Handler[*core.RecordErrorEvent]{ + Func: func(e *core.RecordErrorEvent) error { + t.registerEventCall("OnRecordAfterCreateError") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterConfirmPasswordResetRequest().Add(func(e *core.RecordConfirmPasswordResetEvent) error { - return t.registerEventCall("OnRecordAfterConfirmPasswordResetRequest") + t.OnRecordUpdate().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordUpdate") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeRequestVerificationRequest().Add(func(e *core.RecordRequestVerificationEvent) error { - return t.registerEventCall("OnRecordBeforeRequestVerificationRequest") + t.OnRecordUpdateExecute().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordUpdateExecute") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterRequestVerificationRequest().Add(func(e *core.RecordRequestVerificationEvent) error { - return t.registerEventCall("OnRecordAfterRequestVerificationRequest") + t.OnRecordAfterUpdateSuccess().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordAfterUpdateSuccess") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeConfirmVerificationRequest().Add(func(e *core.RecordConfirmVerificationEvent) error { - return t.registerEventCall("OnRecordBeforeConfirmVerificationRequest") + t.OnRecordAfterUpdateError().Bind(&hook.Handler[*core.RecordErrorEvent]{ + Func: func(e *core.RecordErrorEvent) error { + t.registerEventCall("OnRecordAfterUpdateError") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterConfirmVerificationRequest().Add(func(e *core.RecordConfirmVerificationEvent) error { - return t.registerEventCall("OnRecordAfterConfirmVerificationRequest") + t.OnRecordDelete().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordDelete") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeRequestEmailChangeRequest().Add(func(e *core.RecordRequestEmailChangeEvent) error { - return t.registerEventCall("OnRecordBeforeRequestEmailChangeRequest") + t.OnRecordDeleteExecute().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordDeleteExecute") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterRequestEmailChangeRequest().Add(func(e *core.RecordRequestEmailChangeEvent) error { - return t.registerEventCall("OnRecordAfterRequestEmailChangeRequest") + t.OnRecordAfterDeleteSuccess().Bind(&hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + t.registerEventCall("OnRecordAfterDeleteSuccess") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordBeforeConfirmEmailChangeRequest().Add(func(e *core.RecordConfirmEmailChangeEvent) error { - return t.registerEventCall("OnRecordBeforeConfirmEmailChangeRequest") + t.OnRecordAfterDeleteError().Bind(&hook.Handler[*core.RecordErrorEvent]{ + Func: func(e *core.RecordErrorEvent) error { + t.registerEventCall("OnRecordAfterDeleteError") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordAfterConfirmEmailChangeRequest().Add(func(e *core.RecordConfirmEmailChangeEvent) error { - return t.registerEventCall("OnRecordAfterConfirmEmailChangeRequest") + t.OnCollectionValidate().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionValidate") + return e.Next() + }, + Priority: -99999, }) - t.OnRecordListExternalAuthsRequest().Add(func(e *core.RecordListExternalAuthsEvent) error { - return t.registerEventCall("OnRecordListExternalAuthsRequest") - }) + t.OnCollectionCreate().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionCreate") + return e.Next() + }, + Priority: -99999, + }) - t.OnRecordBeforeUnlinkExternalAuthRequest().Add(func(e *core.RecordUnlinkExternalAuthEvent) error { - return t.registerEventCall("OnRecordBeforeUnlinkExternalAuthRequest") - }) + t.OnCollectionCreateExecute().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionCreateExecute") + return e.Next() + }, + Priority: -99999, + }) - t.OnRecordAfterUnlinkExternalAuthRequest().Add(func(e *core.RecordUnlinkExternalAuthEvent) error { - return t.registerEventCall("OnRecordAfterUnlinkExternalAuthRequest") - }) + t.OnCollectionAfterCreateSuccess().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionAfterCreateSuccess") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerBeforeAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error { - return t.registerEventCall("OnMailerBeforeAdminResetPasswordSend") - }) + t.OnCollectionAfterCreateError().Bind(&hook.Handler[*core.CollectionErrorEvent]{ + Func: func(e *core.CollectionErrorEvent) error { + t.registerEventCall("OnCollectionAfterCreateError") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerAfterAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error { - return t.registerEventCall("OnMailerAfterAdminResetPasswordSend") - }) + t.OnCollectionUpdate().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionUpdate") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerBeforeRecordResetPasswordSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerBeforeRecordResetPasswordSend") - }) + t.OnCollectionUpdateExecute().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionUpdateExecute") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerAfterRecordResetPasswordSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerAfterRecordResetPasswordSend") - }) + t.OnCollectionAfterUpdateSuccess().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionAfterUpdateSuccess") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerBeforeRecordVerificationSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerBeforeRecordVerificationSend") - }) + t.OnCollectionAfterUpdateError().Bind(&hook.Handler[*core.CollectionErrorEvent]{ + Func: func(e *core.CollectionErrorEvent) error { + t.registerEventCall("OnCollectionAfterUpdateError") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerAfterRecordVerificationSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerAfterRecordVerificationSend") - }) + t.OnCollectionDelete().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionDelete") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerBeforeRecordChangeEmailSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerBeforeRecordChangeEmailSend") - }) + t.OnCollectionDeleteExecute().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionDeleteExecute") + return e.Next() + }, + Priority: -99999, + }) - t.OnMailerAfterRecordChangeEmailSend().Add(func(e *core.MailerRecordEvent) error { - return t.registerEventCall("OnMailerAfterRecordChangeEmailSend") - }) + t.OnCollectionAfterDeleteSuccess().Bind(&hook.Handler[*core.CollectionEvent]{ + Func: func(e *core.CollectionEvent) error { + t.registerEventCall("OnCollectionAfterDeleteSuccess") + return e.Next() + }, + Priority: -99999, + }) - t.OnRealtimeConnectRequest().Add(func(e *core.RealtimeConnectEvent) error { - return t.registerEventCall("OnRealtimeConnectRequest") - }) + t.OnCollectionAfterDeleteError().Bind(&hook.Handler[*core.CollectionErrorEvent]{ + Func: func(e *core.CollectionErrorEvent) error { + t.registerEventCall("OnCollectionAfterDeleteError") + return e.Next() + }, + Priority: -99999, + }) - t.OnRealtimeDisconnectRequest().Add(func(e *core.RealtimeDisconnectEvent) error { - return t.registerEventCall("OnRealtimeDisconnectRequest") + t.OnMailerSend().Bind(&hook.Handler[*core.MailerEvent]{ + Func: func(e *core.MailerEvent) error { + if t.TestMailer == nil { + t.TestMailer = &TestMailer{} + } + e.Mailer = t.TestMailer + t.registerEventCall("OnMailerSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnRealtimeBeforeMessageSend().Add(func(e *core.RealtimeMessageEvent) error { - return t.registerEventCall("OnRealtimeBeforeMessageSend") + + t.OnMailerRecordAuthAlertSend().Bind(&hook.Handler[*core.MailerRecordEvent]{ + Func: func(e *core.MailerRecordEvent) error { + t.registerEventCall("OnMailerRecordAuthAlertSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnRealtimeAfterMessageSend().Add(func(e *core.RealtimeMessageEvent) error { - return t.registerEventCall("OnRealtimeAfterMessageSend") + + t.OnMailerRecordPasswordResetSend().Bind(&hook.Handler[*core.MailerRecordEvent]{ + Func: func(e *core.MailerRecordEvent) error { + t.registerEventCall("OnMailerRecordPasswordResetSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnRealtimeBeforeSubscribeRequest().Add(func(e *core.RealtimeSubscribeEvent) error { - return t.registerEventCall("OnRealtimeBeforeSubscribeRequest") + + t.OnMailerRecordVerificationSend().Bind(&hook.Handler[*core.MailerRecordEvent]{ + Func: func(e *core.MailerRecordEvent) error { + t.registerEventCall("OnMailerRecordVerificationSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnRealtimeAfterSubscribeRequest().Add(func(e *core.RealtimeSubscribeEvent) error { - return t.registerEventCall("OnRealtimeAfterSubscribeRequest") + + t.OnMailerRecordEmailChangeSend().Bind(&hook.Handler[*core.MailerRecordEvent]{ + Func: func(e *core.MailerRecordEvent) error { + t.registerEventCall("OnMailerRecordEmailChangeSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnSettingsListRequest().Add(func(e *core.SettingsListEvent) error { - return t.registerEventCall("OnSettingsListRequest") + + t.OnMailerRecordOTPSend().Bind(&hook.Handler[*core.MailerRecordEvent]{ + Func: func(e *core.MailerRecordEvent) error { + t.registerEventCall("OnMailerRecordOTPSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnSettingsBeforeUpdateRequest().Add(func(e *core.SettingsUpdateEvent) error { - return t.registerEventCall("OnSettingsBeforeUpdateRequest") + + t.OnRealtimeConnectRequest().Bind(&hook.Handler[*core.RealtimeConnectRequestEvent]{ + Func: func(e *core.RealtimeConnectRequestEvent) error { + t.registerEventCall("OnRealtimeConnectRequest") + return e.Next() + }, + Priority: -99999, }) - - t.OnSettingsAfterUpdateRequest().Add(func(e *core.SettingsUpdateEvent) error { - return t.registerEventCall("OnSettingsAfterUpdateRequest") + + t.OnRealtimeMessageSend().Bind(&hook.Handler[*core.RealtimeMessageEvent]{ + Func: func(e *core.RealtimeMessageEvent) error { + t.registerEventCall("OnRealtimeMessageSend") + return e.Next() + }, + Priority: -99999, }) - - t.OnCollectionsListRequest().Add(func(e *core.CollectionsListEvent) error { - return t.registerEventCall("OnCollectionsListRequest") + + t.OnRealtimeSubscribeRequest().Bind(&hook.Handler[*core.RealtimeSubscribeRequestEvent]{ + Func: func(e *core.RealtimeSubscribeRequestEvent) error { + t.registerEventCall("OnRealtimeSubscribeRequest") + return e.Next() + }, + Priority: -99999, }) - - t.OnCollectionViewRequest().Add(func(e *core.CollectionViewEvent) error { - return t.registerEventCall("OnCollectionViewRequest") + + t.OnSettingsListRequest().Bind(&hook.Handler[*core.SettingsListRequestEvent]{ + Func: func(e *core.SettingsListRequestEvent) error { + t.registerEventCall("OnSettingsListRequest") + return e.Next() + }, + Priority: -99999, }) - - t.OnCollectionBeforeCreateRequest().Add(func(e *core.CollectionCreateEvent) error { - return t.registerEventCall("OnCollectionBeforeCreateRequest") + + t.OnSettingsUpdateRequest().Bind(&hook.Handler[*core.SettingsUpdateRequestEvent]{ + Func: func(e *core.SettingsUpdateRequestEvent) error { + t.registerEventCall("OnSettingsUpdateRequest") + return e.Next() + }, + Priority: -99999, }) - - t.OnCollectionAfterCreateRequest().Add(func(e *core.CollectionCreateEvent) error { - return t.registerEventCall("OnCollectionAfterCreateRequest") + + t.OnSettingsReload().Bind(&hook.Handler[*core.SettingsReloadEvent]{ + Func: func(e *core.SettingsReloadEvent) error { + t.registerEventCall("OnSettingsReload") + return e.Next() + }, + Priority: -99999, }) - - t.OnCollectionBeforeUpdateRequest().Add(func(e *core.CollectionUpdateEvent) error { - return t.registerEventCall("OnCollectionBeforeUpdateRequest") + + t.OnFileDownloadRequest().Bind(&hook.Handler[*core.FileDownloadRequestEvent]{ + Func: func(e *core.FileDownloadRequestEvent) error { + t.registerEventCall("OnFileDownloadRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnCollectionAfterUpdateRequest().Add(func(e *core.CollectionUpdateEvent) error { - return t.registerEventCall("OnCollectionAfterUpdateRequest") + t.OnFileTokenRequest().Bind(&hook.Handler[*core.FileTokenRequestEvent]{ + Func: func(e *core.FileTokenRequestEvent) error { + t.registerEventCall("OnFileTokenRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnCollectionBeforeDeleteRequest().Add(func(e *core.CollectionDeleteEvent) error { - return t.registerEventCall("OnCollectionBeforeDeleteRequest") + t.OnRecordAuthRequest().Bind(&hook.Handler[*core.RecordAuthRequestEvent]{ + Func: func(e *core.RecordAuthRequestEvent) error { + t.registerEventCall("OnRecordAuthRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnCollectionAfterDeleteRequest().Add(func(e *core.CollectionDeleteEvent) error { - return t.registerEventCall("OnCollectionAfterDeleteRequest") + t.OnRecordAuthWithPasswordRequest().Bind(&hook.Handler[*core.RecordAuthWithPasswordRequestEvent]{ + Func: func(e *core.RecordAuthWithPasswordRequestEvent) error { + t.registerEventCall("OnRecordAuthWithPasswordRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnCollectionsBeforeImportRequest().Add(func(e *core.CollectionsImportEvent) error { - return t.registerEventCall("OnCollectionsBeforeImportRequest") + t.OnRecordAuthWithOAuth2Request().Bind(&hook.Handler[*core.RecordAuthWithOAuth2RequestEvent]{ + Func: func(e *core.RecordAuthWithOAuth2RequestEvent) error { + t.registerEventCall("OnRecordAuthWithOAuth2Request") + return e.Next() + }, + Priority: -99999, }) - t.OnCollectionsAfterImportRequest().Add(func(e *core.CollectionsImportEvent) error { - return t.registerEventCall("OnCollectionsAfterImportRequest") + t.OnRecordAuthRefreshRequest().Bind(&hook.Handler[*core.RecordAuthRefreshRequestEvent]{ + Func: func(e *core.RecordAuthRefreshRequestEvent) error { + t.registerEventCall("OnRecordAuthRefreshRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminsListRequest().Add(func(e *core.AdminsListEvent) error { - return t.registerEventCall("OnAdminsListRequest") + t.OnRecordRequestPasswordResetRequest().Bind(&hook.Handler[*core.RecordRequestPasswordResetRequestEvent]{ + Func: func(e *core.RecordRequestPasswordResetRequestEvent) error { + t.registerEventCall("OnRecordRequestPasswordResetRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminViewRequest().Add(func(e *core.AdminViewEvent) error { - return t.registerEventCall("OnAdminViewRequest") + t.OnRecordConfirmPasswordResetRequest().Bind(&hook.Handler[*core.RecordConfirmPasswordResetRequestEvent]{ + Func: func(e *core.RecordConfirmPasswordResetRequestEvent) error { + t.registerEventCall("OnRecordConfirmPasswordResetRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminBeforeCreateRequest().Add(func(e *core.AdminCreateEvent) error { - return t.registerEventCall("OnAdminBeforeCreateRequest") + t.OnRecordRequestVerificationRequest().Bind(&hook.Handler[*core.RecordRequestVerificationRequestEvent]{ + Func: func(e *core.RecordRequestVerificationRequestEvent) error { + t.registerEventCall("OnRecordRequestVerificationRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminAfterCreateRequest().Add(func(e *core.AdminCreateEvent) error { - return t.registerEventCall("OnAdminAfterCreateRequest") + t.OnRecordConfirmVerificationRequest().Bind(&hook.Handler[*core.RecordConfirmVerificationRequestEvent]{ + Func: func(e *core.RecordConfirmVerificationRequestEvent) error { + t.registerEventCall("OnRecordConfirmVerificationRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminBeforeUpdateRequest().Add(func(e *core.AdminUpdateEvent) error { - return t.registerEventCall("OnAdminBeforeUpdateRequest") + t.OnRecordRequestEmailChangeRequest().Bind(&hook.Handler[*core.RecordRequestEmailChangeRequestEvent]{ + Func: func(e *core.RecordRequestEmailChangeRequestEvent) error { + t.registerEventCall("OnRecordRequestEmailChangeRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminAfterUpdateRequest().Add(func(e *core.AdminUpdateEvent) error { - return t.registerEventCall("OnAdminAfterUpdateRequest") + t.OnRecordConfirmEmailChangeRequest().Bind(&hook.Handler[*core.RecordConfirmEmailChangeRequestEvent]{ + Func: func(e *core.RecordConfirmEmailChangeRequestEvent) error { + t.registerEventCall("OnRecordConfirmEmailChangeRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminBeforeDeleteRequest().Add(func(e *core.AdminDeleteEvent) error { - return t.registerEventCall("OnAdminBeforeDeleteRequest") + t.OnRecordRequestOTPRequest().Bind(&hook.Handler[*core.RecordCreateOTPRequestEvent]{ + Func: func(e *core.RecordCreateOTPRequestEvent) error { + t.registerEventCall("OnRecordRequestOTPRequest") + return e.Next() + }, + Priority: -99999, }) - t.OnAdminAfterDeleteRequest().Add(func(e *core.AdminDeleteEvent) error { - return t.registerEventCall("OnAdminAfterDeleteRequest") - }) + t.OnRecordAuthWithOTPRequest().Bind(&hook.Handler[*core.RecordAuthWithOTPRequestEvent]{ + Func: func(e *core.RecordAuthWithOTPRequestEvent) error { + t.registerEventCall("OnRecordAuthWithOTPRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error { - return t.registerEventCall("OnAdminAuthRequest") - }) + t.OnRecordsListRequest().Bind(&hook.Handler[*core.RecordsListRequestEvent]{ + Func: func(e *core.RecordsListRequestEvent) error { + t.registerEventCall("OnRecordsListRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminBeforeAuthWithPasswordRequest().Add(func(e *core.AdminAuthWithPasswordEvent) error { - return t.registerEventCall("OnAdminBeforeAuthWithPasswordRequest") - }) + t.OnRecordViewRequest().Bind(&hook.Handler[*core.RecordRequestEvent]{ + Func: func(e *core.RecordRequestEvent) error { + t.registerEventCall("OnRecordViewRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminAfterAuthWithPasswordRequest().Add(func(e *core.AdminAuthWithPasswordEvent) error { - return t.registerEventCall("OnAdminAfterAuthWithPasswordRequest") - }) + t.OnRecordCreateRequest().Bind(&hook.Handler[*core.RecordRequestEvent]{ + Func: func(e *core.RecordRequestEvent) error { + t.registerEventCall("OnRecordCreateRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminBeforeAuthRefreshRequest().Add(func(e *core.AdminAuthRefreshEvent) error { - return t.registerEventCall("OnAdminBeforeAuthRefreshRequest") - }) + t.OnRecordUpdateRequest().Bind(&hook.Handler[*core.RecordRequestEvent]{ + Func: func(e *core.RecordRequestEvent) error { + t.registerEventCall("OnRecordUpdateRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminAfterAuthRefreshRequest().Add(func(e *core.AdminAuthRefreshEvent) error { - return t.registerEventCall("OnAdminAfterAuthRefreshRequest") - }) + t.OnRecordDeleteRequest().Bind(&hook.Handler[*core.RecordRequestEvent]{ + Func: func(e *core.RecordRequestEvent) error { + t.registerEventCall("OnRecordDeleteRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminBeforeRequestPasswordResetRequest().Add(func(e *core.AdminRequestPasswordResetEvent) error { - return t.registerEventCall("OnAdminBeforeRequestPasswordResetRequest") - }) + t.OnCollectionsListRequest().Bind(&hook.Handler[*core.CollectionsListRequestEvent]{ + Func: func(e *core.CollectionsListRequestEvent) error { + t.registerEventCall("OnCollectionsListRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminAfterRequestPasswordResetRequest().Add(func(e *core.AdminRequestPasswordResetEvent) error { - return t.registerEventCall("OnAdminAfterRequestPasswordResetRequest") - }) + t.OnCollectionViewRequest().Bind(&hook.Handler[*core.CollectionRequestEvent]{ + Func: func(e *core.CollectionRequestEvent) error { + t.registerEventCall("OnCollectionViewRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminBeforeConfirmPasswordResetRequest().Add(func(e *core.AdminConfirmPasswordResetEvent) error { - return t.registerEventCall("OnAdminBeforeConfirmPasswordResetRequest") - }) + t.OnCollectionCreateRequest().Bind(&hook.Handler[*core.CollectionRequestEvent]{ + Func: func(e *core.CollectionRequestEvent) error { + t.registerEventCall("OnCollectionCreateRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnAdminAfterConfirmPasswordResetRequest().Add(func(e *core.AdminConfirmPasswordResetEvent) error { - return t.registerEventCall("OnAdminAfterConfirmPasswordResetRequest") - }) + t.OnCollectionUpdateRequest().Bind(&hook.Handler[*core.CollectionRequestEvent]{ + Func: func(e *core.CollectionRequestEvent) error { + t.registerEventCall("OnCollectionUpdateRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnFileDownloadRequest().Add(func(e *core.FileDownloadEvent) error { - return t.registerEventCall("OnFileDownloadRequest") - }) + t.OnCollectionDeleteRequest().Bind(&hook.Handler[*core.CollectionRequestEvent]{ + Func: func(e *core.CollectionRequestEvent) error { + t.registerEventCall("OnCollectionDeleteRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnFileBeforeTokenRequest().Add(func(e *core.FileTokenEvent) error { - return t.registerEventCall("OnFileBeforeTokenRequest") - }) + t.OnCollectionsImportRequest().Bind(&hook.Handler[*core.CollectionsImportRequestEvent]{ + Func: func(e *core.CollectionsImportRequestEvent) error { + t.registerEventCall("OnCollectionsImportRequest") + return e.Next() + }, + Priority: -99999, + }) - t.OnFileAfterTokenRequest().Add(func(e *core.FileTokenEvent) error { - return t.registerEventCall("OnFileAfterTokenRequest") + t.OnBatchRequest().Bind(&hook.Handler[*core.BatchRequestEvent]{ + Func: func(e *core.BatchRequestEvent) error { + t.registerEventCall("OnBatchRequest") + return e.Next() + }, + Priority: -99999, }) return t, nil @@ -558,33 +868,3 @@ func copyFile(src string, dest string) error { return nil } - -// @todo replace with app.RunMigrations on merge with the refactoring. -func runMigrations(app core.App) error { - connections := []struct { - db *dbx.DB - migrationsList migrate.MigrationsList - }{ - { - db: app.DB(), - migrationsList: migrations.AppMigrations, - }, - { - db: app.LogsDB(), - migrationsList: logs.LogsMigrations, - }, - } - - for _, c := range connections { - runner, err := migrate.NewRunner(c.db, c.migrationsList) - if err != nil { - return err - } - - if _, err := runner.Up(); err != nil { - return err - } - } - - return nil -} diff --git a/tests/data/aux.db b/tests/data/aux.db new file mode 100644 index 00000000..c816ee74 Binary files /dev/null and b/tests/data/aux.db differ diff --git a/tests/data/data.db b/tests/data/data.db index e7dc8d64..1108c456 100644 Binary files a/tests/data/data.db and b/tests/data/data.db differ diff --git a/tests/data/logs.db b/tests/data/logs.db deleted file mode 100644 index 0e530721..00000000 Binary files a/tests/data/logs.db and /dev/null differ diff --git a/tests/dynamic_stubs.go b/tests/dynamic_stubs.go new file mode 100644 index 00000000..6fda676b --- /dev/null +++ b/tests/dynamic_stubs.go @@ -0,0 +1,147 @@ +package tests + +import ( + "strconv" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/types" +) + +func StubOTPRecords(app core.App) error { + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + return err + } + superuser2.SetRaw("stubId", "superuser2") + + superuser3, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test3@example.com") + if err != nil { + return err + } + superuser3.SetRaw("stubId", "superuser3") + + user1, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + return err + } + user1.SetRaw("stubId", "user1") + + now := types.NowDateTime() + old := types.NowDateTime().Add(-1 * time.Hour) + + stubs := map[*core.Record][]types.DateTime{ + superuser2: {now, now.Add(-1 * time.Millisecond), old, now.Add(-2 * time.Millisecond), old.Add(-1 * time.Millisecond)}, + superuser3: {now.Add(-3 * time.Millisecond), now.Add(-2 * time.Minute)}, + user1: {old}, + } + for record, idDates := range stubs { + for i, date := range idDates { + otp := core.NewOTP(app) + otp.Id = record.GetString("stubId") + "_" + strconv.Itoa(i) + otp.SetRecordRef(record.Id) + otp.SetCollectionRef(record.Collection().Id) + otp.SetPassword("test123") + otp.SetRaw("created", date) + if err := app.SaveNoValidate(otp); err != nil { + return err + } + } + } + + return nil +} + +func StubMFARecords(app core.App) error { + superuser2, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test2@example.com") + if err != nil { + return err + } + superuser2.SetRaw("stubId", "superuser2") + + superuser3, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test3@example.com") + if err != nil { + return err + } + superuser3.SetRaw("stubId", "superuser3") + + user1, err := app.FindAuthRecordByEmail("users", "test@example.com") + if err != nil { + return err + } + user1.SetRaw("stubId", "user1") + + now := types.NowDateTime() + old := types.NowDateTime().Add(-1 * time.Hour) + + type mfaData struct { + method string + date types.DateTime + } + + stubs := map[*core.Record][]mfaData{ + superuser2: { + {core.MFAMethodOTP, now}, + {core.MFAMethodOTP, old}, + {core.MFAMethodPassword, now.Add(-2 * time.Minute)}, + {core.MFAMethodPassword, now.Add(-1 * time.Millisecond)}, + {core.MFAMethodOAuth2, old.Add(-1 * time.Millisecond)}, + }, + superuser3: { + {core.MFAMethodOAuth2, now.Add(-3 * time.Millisecond)}, + {core.MFAMethodPassword, now.Add(-3 * time.Minute)}, + }, + user1: { + {core.MFAMethodOAuth2, old}, + }, + } + for record, idDates := range stubs { + for i, data := range idDates { + otp := core.NewMFA(app) + otp.Id = record.GetString("stubId") + "_" + strconv.Itoa(i) + otp.SetRecordRef(record.Id) + otp.SetCollectionRef(record.Collection().Id) + otp.SetMethod(data.method) + otp.SetRaw("created", data.date) + if err := app.SaveNoValidate(otp); err != nil { + return err + } + } + } + + return nil +} + +func StubLogsData(app *TestApp) error { + _, err := app.AuxDB().NewQuery(` + delete from {{_logs}}; + + insert into {{_logs}} ( + [[id]], + [[level]], + [[message]], + [[data]], + [[created]], + [[updated]] + ) + values + ( + "873f2133-9f38-44fb-bf82-c8f53b310d91", + 0, + "test_message1", + '{"status":200}', + "2022-05-01 10:00:00.123Z", + "2022-05-01 10:00:00.123Z" + ), + ( + "f2133873-44fb-9f38-bf82-c918f53b310d", + 8, + "test_message2", + '{"status":400}', + "2022-05-02 10:00:00.123Z", + "2022-05-02 10:00:00.123Z" + ); + `).Execute() + + return err +} diff --git a/tests/logs.go b/tests/logs.go deleted file mode 100644 index 3014e01f..00000000 --- a/tests/logs.go +++ /dev/null @@ -1,35 +0,0 @@ -package tests - -func MockLogsData(app *TestApp) error { - _, err := app.LogsDB().NewQuery(` - delete from {{_logs}}; - - insert into {{_logs}} ( - [[id]], - [[level]], - [[message]], - [[data]], - [[created]], - [[updated]] - ) - values - ( - "873f2133-9f38-44fb-bf82-c8f53b310d91", - 0, - "test_message1", - '{"status":200}', - "2022-05-01 10:00:00.123Z", - "2022-05-01 10:00:00.123Z" - ), - ( - "f2133873-44fb-9f38-bf82-c918f53b310d", - 8, - "test_message2", - '{"status":400}', - "2022-05-02 10:00:00.123Z", - "2022-05-02 10:00:00.123Z" - ); - `).Execute() - - return err -} diff --git a/tests/mailer.go b/tests/mailer.go index 9672ad8b..15d6190c 100644 --- a/tests/mailer.go +++ b/tests/mailer.go @@ -1,6 +1,7 @@ package tests import ( + "slices" "sync" "github.com/pocketbase/pocketbase/tools/mailer" @@ -8,15 +9,19 @@ import ( var _ mailer.Mailer = (*TestMailer)(nil) -// TestMailer is a mock `mailer.Mailer` implementation. +// TestMailer is a mock [mailer.Mailer] implementation. type TestMailer struct { - mux sync.Mutex + mux sync.Mutex + messages []*mailer.Message +} - TotalSend int - LastMessage mailer.Message +// Send implements [mailer.Mailer] interface. +func (tm *TestMailer) Send(m *mailer.Message) error { + tm.mux.Lock() + defer tm.mux.Unlock() - // @todo consider deprecating the above 2 fields? - SentMessages []mailer.Message + tm.messages = append(tm.messages, m) + return nil } // Reset clears any previously test collected data. @@ -24,19 +29,53 @@ func (tm *TestMailer) Reset() { tm.mux.Lock() defer tm.mux.Unlock() - tm.TotalSend = 0 - tm.LastMessage = mailer.Message{} - tm.SentMessages = nil + tm.messages = nil } -// Send implements `mailer.Mailer` interface. -func (tm *TestMailer) Send(m *mailer.Message) error { +// TotalSend returns the total number of sent messages. +func (tm *TestMailer) TotalSend() int { tm.mux.Lock() defer tm.mux.Unlock() - tm.TotalSend++ - tm.LastMessage = *m - tm.SentMessages = append(tm.SentMessages, tm.LastMessage) - - return nil + return len(tm.messages) +} + +// Messages returns a shallow copy of all of the collected test messages. +func (tm *TestMailer) Messages() []*mailer.Message { + tm.mux.Lock() + defer tm.mux.Unlock() + + return slices.Clone(tm.messages) +} + +// FirstMessage returns a shallow copy of the first sent message. +// +// Returns an empty mailer.Message struct if there are no sent messages. +func (tm *TestMailer) FirstMessage() mailer.Message { + tm.mux.Lock() + defer tm.mux.Unlock() + + var m mailer.Message + + if len(tm.messages) > 0 { + return *tm.messages[0] + } + + return m +} + +// LastMessage returns a shallow copy of the last sent message. +// +// Returns an empty mailer.Message struct if there are no sent messages. +func (tm *TestMailer) LastMessage() mailer.Message { + tm.mux.Lock() + defer tm.mux.Unlock() + + var m mailer.Message + + if len(tm.messages) > 0 { + return *tm.messages[len(tm.messages)-1] + } + + return m } diff --git a/tests/validation_errors.go b/tests/validation_errors.go new file mode 100644 index 00000000..c0a37f91 --- /dev/null +++ b/tests/validation_errors.go @@ -0,0 +1,32 @@ +package tests + +import ( + "errors" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +// TestValidationErrors checks whether the provided rawErrors are +// instance of [validation.Errors] and contains the expectedErrors keys. +func TestValidationErrors(t *testing.T, rawErrors error, expectedErrors []string) { + var errs validation.Errors + + if rawErrors != nil && !errors.As(rawErrors, &errs) { + t.Fatalf("Failed to parse errors, expected to find validation.Errors, got %T\n%v", rawErrors, rawErrors) + } + + if len(errs) != len(expectedErrors) { + keys := make([]string, 0, len(errs)) + for k := range errs { + keys = append(keys, k) + } + t.Fatalf("Expected error keys \n%v\ngot\n%v\n%v", expectedErrors, keys, errs) + } + + for _, k := range expectedErrors { + if _, ok := errs[k]; !ok { + t.Fatalf("Missing expected error key %q in %v", k, errs) + } + } +} diff --git a/tokens/admin.go b/tokens/admin.go deleted file mode 100644 index c5e7e01d..00000000 --- a/tokens/admin.go +++ /dev/null @@ -1,35 +0,0 @@ -package tokens - -import ( - "github.com/golang-jwt/jwt/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/security" -) - -// NewAdminAuthToken generates and returns a new admin authentication token. -func NewAdminAuthToken(app core.App, admin *models.Admin) (string, error) { - return security.NewJWT( - jwt.MapClaims{"id": admin.Id, "type": TypeAdmin}, - (admin.TokenKey + app.Settings().AdminAuthToken.Secret), - app.Settings().AdminAuthToken.Duration, - ) -} - -// NewAdminResetPasswordToken generates and returns a new admin password reset request token. -func NewAdminResetPasswordToken(app core.App, admin *models.Admin) (string, error) { - return security.NewJWT( - jwt.MapClaims{"id": admin.Id, "type": TypeAdmin, "email": admin.Email}, - (admin.TokenKey + app.Settings().AdminPasswordResetToken.Secret), - app.Settings().AdminPasswordResetToken.Duration, - ) -} - -// NewAdminFileToken generates and returns a new admin private file access token. -func NewAdminFileToken(app core.App, admin *models.Admin) (string, error) { - return security.NewJWT( - jwt.MapClaims{"id": admin.Id, "type": TypeAdmin}, - (admin.TokenKey + app.Settings().AdminFileToken.Secret), - app.Settings().AdminFileToken.Duration, - ) -} diff --git a/tokens/admin_test.go b/tokens/admin_test.go deleted file mode 100644 index 8826e682..00000000 --- a/tokens/admin_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package tokens_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tokens" -) - -func TestNewAdminAuthToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewAdminAuthToken(app, admin) - if err != nil { - t.Fatal(err) - } - - tokenAdmin, _ := app.Dao().FindAdminByToken( - token, - app.Settings().AdminAuthToken.Secret, - ) - if tokenAdmin == nil || tokenAdmin.Id != admin.Id { - t.Fatalf("Expected admin %v, got %v", admin, tokenAdmin) - } -} - -func TestNewAdminResetPasswordToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewAdminResetPasswordToken(app, admin) - if err != nil { - t.Fatal(err) - } - - tokenAdmin, _ := app.Dao().FindAdminByToken( - token, - app.Settings().AdminPasswordResetToken.Secret, - ) - if tokenAdmin == nil || tokenAdmin.Id != admin.Id { - t.Fatalf("Expected admin %v, got %v", admin, tokenAdmin) - } -} - -func TestNewAdminFileToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - admin, err := app.Dao().FindAdminByEmail("test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewAdminFileToken(app, admin) - if err != nil { - t.Fatal(err) - } - - tokenAdmin, _ := app.Dao().FindAdminByToken( - token, - app.Settings().AdminFileToken.Secret, - ) - if tokenAdmin == nil || tokenAdmin.Id != admin.Id { - t.Fatalf("Expected admin %v, got %v", admin, tokenAdmin) - } -} diff --git a/tokens/record.go b/tokens/record.go deleted file mode 100644 index 6bb7dbe9..00000000 --- a/tokens/record.go +++ /dev/null @@ -1,95 +0,0 @@ -package tokens - -import ( - "errors" - - "github.com/golang-jwt/jwt/v4" - "github.com/pocketbase/pocketbase/core" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/security" -) - -// NewRecordAuthToken generates and returns a new auth record authentication token. -func NewRecordAuthToken(app core.App, record *models.Record) (string, error) { - if !record.Collection().IsAuth() { - return "", errors.New("the record is not from an auth collection") - } - - return security.NewJWT( - jwt.MapClaims{ - "id": record.Id, - "type": TypeAuthRecord, - "collectionId": record.Collection().Id, - }, - (record.TokenKey() + app.Settings().RecordAuthToken.Secret), - app.Settings().RecordAuthToken.Duration, - ) -} - -// NewRecordVerifyToken generates and returns a new record verification token. -func NewRecordVerifyToken(app core.App, record *models.Record) (string, error) { - if !record.Collection().IsAuth() { - return "", errors.New("the record is not from an auth collection") - } - - return security.NewJWT( - jwt.MapClaims{ - "id": record.Id, - "type": TypeAuthRecord, - "collectionId": record.Collection().Id, - "email": record.Email(), - }, - (record.TokenKey() + app.Settings().RecordVerificationToken.Secret), - app.Settings().RecordVerificationToken.Duration, - ) -} - -// NewRecordResetPasswordToken generates and returns a new auth record password reset request token. -func NewRecordResetPasswordToken(app core.App, record *models.Record) (string, error) { - if !record.Collection().IsAuth() { - return "", errors.New("the record is not from an auth collection") - } - - return security.NewJWT( - jwt.MapClaims{ - "id": record.Id, - "type": TypeAuthRecord, - "collectionId": record.Collection().Id, - "email": record.Email(), - }, - (record.TokenKey() + app.Settings().RecordPasswordResetToken.Secret), - app.Settings().RecordPasswordResetToken.Duration, - ) -} - -// NewRecordChangeEmailToken generates and returns a new auth record change email request token. -func NewRecordChangeEmailToken(app core.App, record *models.Record, newEmail string) (string, error) { - return security.NewJWT( - jwt.MapClaims{ - "id": record.Id, - "type": TypeAuthRecord, - "collectionId": record.Collection().Id, - "email": record.Email(), - "newEmail": newEmail, - }, - (record.TokenKey() + app.Settings().RecordEmailChangeToken.Secret), - app.Settings().RecordEmailChangeToken.Duration, - ) -} - -// NewRecordFileToken generates and returns a new record private file access token. -func NewRecordFileToken(app core.App, record *models.Record) (string, error) { - if !record.Collection().IsAuth() { - return "", errors.New("the record is not from an auth collection") - } - - return security.NewJWT( - jwt.MapClaims{ - "id": record.Id, - "type": TypeAuthRecord, - "collectionId": record.Collection().Id, - }, - (record.TokenKey() + app.Settings().RecordFileToken.Secret), - app.Settings().RecordFileToken.Duration, - ) -} diff --git a/tokens/record_test.go b/tokens/record_test.go deleted file mode 100644 index 82e94fb6..00000000 --- a/tokens/record_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package tokens_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/tests" - "github.com/pocketbase/pocketbase/tokens" -) - -func TestNewRecordAuthToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewRecordAuthToken(app, user) - if err != nil { - t.Fatal(err) - } - - tokenRecord, _ := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordAuthToken.Secret, - ) - if tokenRecord == nil || tokenRecord.Id != user.Id { - t.Fatalf("Expected auth record %v, got %v", user, tokenRecord) - } -} - -func TestNewRecordVerifyToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewRecordVerifyToken(app, user) - if err != nil { - t.Fatal(err) - } - - tokenRecord, _ := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordVerificationToken.Secret, - ) - if tokenRecord == nil || tokenRecord.Id != user.Id { - t.Fatalf("Expected auth record %v, got %v", user, tokenRecord) - } -} - -func TestNewRecordResetPasswordToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewRecordResetPasswordToken(app, user) - if err != nil { - t.Fatal(err) - } - - tokenRecord, _ := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordPasswordResetToken.Secret, - ) - if tokenRecord == nil || tokenRecord.Id != user.Id { - t.Fatalf("Expected auth record %v, got %v", user, tokenRecord) - } -} - -func TestNewRecordChangeEmailToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewRecordChangeEmailToken(app, user, "test_new@example.com") - if err != nil { - t.Fatal(err) - } - - tokenRecord, _ := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordEmailChangeToken.Secret, - ) - if tokenRecord == nil || tokenRecord.Id != user.Id { - t.Fatalf("Expected auth record %v, got %v", user, tokenRecord) - } -} - -func TestNewRecordFileToken(t *testing.T) { - t.Parallel() - - app, _ := tests.NewTestApp() - defer app.Cleanup() - - user, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com") - if err != nil { - t.Fatal(err) - } - - token, err := tokens.NewRecordFileToken(app, user) - if err != nil { - t.Fatal(err) - } - - tokenRecord, _ := app.Dao().FindAuthRecordByToken( - token, - app.Settings().RecordFileToken.Secret, - ) - if tokenRecord == nil || tokenRecord.Id != user.Id { - t.Fatalf("Expected auth record %v, got %v", user, tokenRecord) - } -} diff --git a/tokens/tokens.go b/tokens/tokens.go deleted file mode 100644 index 7a0a928a..00000000 --- a/tokens/tokens.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package tokens implements various user and admin tokens generation methods. -package tokens - -const ( - TypeAdmin = "admin" - TypeAuthRecord = "authRecord" -) diff --git a/tools/archive/create.go b/tools/archive/create.go index 4907fbbe..fcd20ca5 100644 --- a/tools/archive/create.go +++ b/tools/archive/create.go @@ -3,6 +3,7 @@ package archive import ( "archive/zip" "compress/flate" + "errors" "io" "io/fs" "os" @@ -23,24 +24,21 @@ func Create(src string, dest string, skipPaths ...string) error { if err != nil { return err } - defer zf.Close() zw := zip.NewWriter(zf) - defer zw.Close() // register a custom Deflate compressor zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { return flate.NewWriter(out, flate.BestSpeed) }) - if err := zipAddFS(zw, os.DirFS(src), skipPaths...); err != nil { + err = zipAddFS(zw, os.DirFS(src), skipPaths...) + if err != nil { // try to cleanup at least the created zip file - os.Remove(dest) - - return err + return errors.Join(err, zw.Close(), zf.Close(), os.Remove(dest)) } - return nil + return errors.Join(zw.Close(), zf.Close()) } // note remove after similar method is added in the std lib (https://github.com/golang/go/issues/54898) diff --git a/tools/auth/apple.go b/tools/auth/apple.go index dbfef41b..4e2524a9 100644 --- a/tools/auth/apple.go +++ b/tools/auth/apple.go @@ -18,6 +18,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameApple] = wrapFactory(NewAppleProvider) +} + var _ Provider = (*Apple)(nil) // NameApple is the unique name of the Apple provider. @@ -27,23 +31,23 @@ const NameApple string = "apple" // // [OIDC differences]: https://bitbucket.org/openid/connect/src/master/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md type Apple struct { - *baseProvider + BaseProvider - jwksUrl string + jwksURL string } // NewAppleProvider creates a new Apple provider instance with some defaults. func NewAppleProvider() *Apple { return &Apple{ - baseProvider: &baseProvider{ + BaseProvider: BaseProvider{ ctx: context.Background(), displayName: "Apple", pkce: true, scopes: []string{"name", "email"}, - authUrl: "https://appleid.apple.com/auth/authorize", - tokenUrl: "https://appleid.apple.com/auth/token", + authURL: "https://appleid.apple.com/auth/authorize", + tokenURL: "https://appleid.apple.com/auth/token", }, - jwksUrl: "https://appleid.apple.com/auth/keys", + jwksURL: "https://appleid.apple.com/auth/keys", } } @@ -51,7 +55,7 @@ func NewAppleProvider() *Apple { // // API reference: https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse. func (p *Apple) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -98,11 +102,11 @@ func (p *Apple) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { return user, nil } -// FetchRawUserData implements Provider.FetchRawUserData interface. +// FetchRawUserInfo implements Provider.FetchRawUserInfo interface. // // Apple doesn't have a UserInfo endpoint and claims about users // are instead included in the "id_token" (https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample) -func (p *Apple) FetchRawUserData(token *oauth2.Token) ([]byte, error) { +func (p *Apple) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) { idToken, _ := token.Extra("id_token").(string) claims, err := p.parseAndVerifyIdToken(idToken) @@ -209,7 +213,7 @@ type jwk struct { } func (p *Apple) fetchJWK(kid string) (*jwk, error) { - req, err := http.NewRequestWithContext(p.ctx, "GET", p.jwksUrl, nil) + req, err := http.NewRequestWithContext(p.ctx, "GET", p.jwksURL, nil) if err != nil { return nil, err } diff --git a/tools/auth/auth.go b/tools/auth/auth.go index 9b23a1c4..8adb0460 100644 --- a/tools/auth/auth.go +++ b/tools/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "context" + "encoding/json" "errors" "net/http" @@ -9,17 +10,22 @@ import ( "golang.org/x/oauth2" ) -// AuthUser defines a standardized oauth2 user data structure. -type AuthUser struct { - Id string `json:"id"` - Name string `json:"name"` - Username string `json:"username"` - Email string `json:"email"` - AvatarUrl string `json:"avatarUrl"` - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` - Expiry types.DateTime `json:"expiry"` - RawUser map[string]any `json:"rawUser"` +// ProviderFactoryFunc defines a function for initializing a new OAuth2 provider. +type ProviderFactoryFunc func() Provider + +// Providers defines a map with all of the available OAuth2 providers. +// +// To register a new provider append a new entry in the map. +var Providers = map[string]ProviderFactoryFunc{} + +// NewProviderByName returns a new preconfigured provider instance by its name identifier. +func NewProviderByName(name string) (Provider, error) { + factory, ok := Providers[name] + if !ok { + return nil, errors.New("missing provider " + name) + } + + return factory(), nil } // Provider defines a common interface for an OAuth2 client. @@ -61,104 +67,84 @@ type Provider interface { // SetClientSecret sets the provider client's app secret. SetClientSecret(secret string) - // RedirectUrl returns the end address to redirect the user + // RedirectURL returns the end address to redirect the user // going through the OAuth flow. - RedirectUrl() string + RedirectURL() string - // SetRedirectUrl sets the provider's RedirectUrl. - SetRedirectUrl(url string) + // SetRedirectURL sets the provider's RedirectURL. + SetRedirectURL(url string) - // AuthUrl returns the provider's authorization service url. - AuthUrl() string + // AuthURL returns the provider's authorization service url. + AuthURL() string - // SetAuthUrl sets the provider's AuthUrl. - SetAuthUrl(url string) + // SetAuthURL sets the provider's AuthURL. + SetAuthURL(url string) - // TokenUrl returns the provider's token exchange service url. - TokenUrl() string + // TokenURL returns the provider's token exchange service url. + TokenURL() string - // SetTokenUrl sets the provider's TokenUrl. - SetTokenUrl(url string) + // SetTokenURL sets the provider's TokenURL. + SetTokenURL(url string) - // UserApiUrl returns the provider's user info api url. - UserApiUrl() string + // UserInfoURL returns the provider's user info api url. + UserInfoURL() string - // SetUserApiUrl sets the provider's UserApiUrl. - SetUserApiUrl(url string) + // SetUserInfoURL sets the provider's UserInfoURL. + SetUserInfoURL(url string) // Client returns an http client using the provided token. Client(token *oauth2.Token) *http.Client - // BuildAuthUrl returns a URL to the provider's consent page + // BuildAuthURL returns a URL to the provider's consent page // that asks for permissions for the required scopes explicitly. - BuildAuthUrl(state string, opts ...oauth2.AuthCodeOption) string + BuildAuthURL(state string, opts ...oauth2.AuthCodeOption) string // FetchToken converts an authorization code to token. FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) - // FetchRawUserData requests and marshalizes into `result` the + // FetchRawUserInfo requests and marshalizes into `result` the // the OAuth user api response. - FetchRawUserData(token *oauth2.Token) ([]byte, error) + FetchRawUserInfo(token *oauth2.Token) ([]byte, error) - // FetchAuthUser is similar to FetchRawUserData, but normalizes and + // FetchAuthUser is similar to FetchRawUserInfo, but normalizes and // marshalizes the user api response into a standardized AuthUser struct. FetchAuthUser(token *oauth2.Token) (user *AuthUser, err error) } -// NewProviderByName returns a new preconfigured provider instance by its name identifier. -func NewProviderByName(name string) (Provider, error) { - switch name { - case NameGoogle: - return NewGoogleProvider(), nil - case NameFacebook: - return NewFacebookProvider(), nil - case NameGithub: - return NewGithubProvider(), nil - case NameGitlab: - return NewGitlabProvider(), nil - case NameDiscord: - return NewDiscordProvider(), nil - case NameTwitter: - return NewTwitterProvider(), nil - case NameMicrosoft: - return NewMicrosoftProvider(), nil - case NameSpotify: - return NewSpotifyProvider(), nil - case NameKakao: - return NewKakaoProvider(), nil - case NameTwitch: - return NewTwitchProvider(), nil - case NameStrava: - return NewStravaProvider(), nil - case NameGitee: - return NewGiteeProvider(), nil - case NameLivechat: - return NewLivechatProvider(), nil - case NameGitea: - return NewGiteaProvider(), nil - case NameOIDC: - return NewOIDCProvider(), nil - case NameOIDC + "2": - return NewOIDCProvider(), nil - case NameOIDC + "3": - return NewOIDCProvider(), nil - case NameApple: - return NewAppleProvider(), nil - case NameInstagram: - return NewInstagramProvider(), nil - case NameVK: - return NewVKProvider(), nil - case NameYandex: - return NewYandexProvider(), nil - case NamePatreon: - return NewPatreonProvider(), nil - case NameMailcow: - return NewMailcowProvider(), nil - case NameBitbucket: - return NewBitbucketProvider(), nil - case NamePlanningcenter: - return NewPlanningcenterProvider(), nil - default: - return nil, errors.New("Missing provider " + name) +// wrapFactory is a helper that wraps a Provider specific factory +// function and returns its result as Provider interface. +func wrapFactory[T Provider](factory func() T) ProviderFactoryFunc { + return func() Provider { + return factory() } } + +// AuthUser defines a standardized OAuth2 user data structure. +type AuthUser struct { + Expiry types.DateTime `json:"expiry"` + RawUser map[string]any `json:"rawUser"` + Id string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Email string `json:"email"` + AvatarURL string `json:"avatarURL"` + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + + // @todo + // deprecated: use AvatarURL instead + // AvatarUrl will be removed after dropping v0.22 support + AvatarUrl string `json:"avatarUrl"` +} + +// MarshalJSON implements the [json.Marshaler] interface. +// +// @todo remove after dropping v0.22 support +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 + + return json.Marshal(au2) +} diff --git a/tools/auth/auth_test.go b/tools/auth/auth_test.go index 6d46624b..66b30e2c 100644 --- a/tools/auth/auth_test.go +++ b/tools/auth/auth_test.go @@ -6,6 +6,14 @@ import ( "github.com/pocketbase/pocketbase/tools/auth" ) +func TestProvidersCount(t *testing.T) { + expected := 25 + + if total := len(auth.Providers); total != expected { + t.Fatalf("Expected %d providers, got %d", expected, total) + } +} + func TestNewProviderByName(t *testing.T) { var err error var p auth.Provider diff --git a/tools/auth/base_provider.go b/tools/auth/base_provider.go index afda229f..fc6be6fb 100644 --- a/tools/auth/base_provider.go +++ b/tools/auth/base_provider.go @@ -9,147 +9,147 @@ import ( "golang.org/x/oauth2" ) -// baseProvider defines common fields and methods used by OAuth2 client providers. -type baseProvider struct { +// BaseProvider defines common fields and methods used by OAuth2 client providers. +type BaseProvider struct { ctx context.Context clientId string clientSecret string displayName string - redirectUrl string - authUrl string - tokenUrl string - userApiUrl string + redirectURL string + authURL string + tokenURL string + userInfoURL string scopes []string pkce bool } // Context implements Provider.Context() interface method. -func (p *baseProvider) Context() context.Context { +func (p *BaseProvider) Context() context.Context { return p.ctx } // SetContext implements Provider.SetContext() interface method. -func (p *baseProvider) SetContext(ctx context.Context) { +func (p *BaseProvider) SetContext(ctx context.Context) { p.ctx = ctx } // PKCE implements Provider.PKCE() interface method. -func (p *baseProvider) PKCE() bool { +func (p *BaseProvider) PKCE() bool { return p.pkce } // SetPKCE implements Provider.SetPKCE() interface method. -func (p *baseProvider) SetPKCE(enable bool) { +func (p *BaseProvider) SetPKCE(enable bool) { p.pkce = enable } // DisplayName implements Provider.DisplayName() interface method. -func (p *baseProvider) DisplayName() string { +func (p *BaseProvider) DisplayName() string { return p.displayName } // SetDisplayName implements Provider.SetDisplayName() interface method. -func (p *baseProvider) SetDisplayName(displayName string) { +func (p *BaseProvider) SetDisplayName(displayName string) { p.displayName = displayName } // Scopes implements Provider.Scopes() interface method. -func (p *baseProvider) Scopes() []string { +func (p *BaseProvider) Scopes() []string { return p.scopes } // SetScopes implements Provider.SetScopes() interface method. -func (p *baseProvider) SetScopes(scopes []string) { +func (p *BaseProvider) SetScopes(scopes []string) { p.scopes = scopes } // ClientId implements Provider.ClientId() interface method. -func (p *baseProvider) ClientId() string { +func (p *BaseProvider) ClientId() string { return p.clientId } // SetClientId implements Provider.SetClientId() interface method. -func (p *baseProvider) SetClientId(clientId string) { +func (p *BaseProvider) SetClientId(clientId string) { p.clientId = clientId } // ClientSecret implements Provider.ClientSecret() interface method. -func (p *baseProvider) ClientSecret() string { +func (p *BaseProvider) ClientSecret() string { return p.clientSecret } // SetClientSecret implements Provider.SetClientSecret() interface method. -func (p *baseProvider) SetClientSecret(secret string) { +func (p *BaseProvider) SetClientSecret(secret string) { p.clientSecret = secret } -// RedirectUrl implements Provider.RedirectUrl() interface method. -func (p *baseProvider) RedirectUrl() string { - return p.redirectUrl +// RedirectURL implements Provider.RedirectURL() interface method. +func (p *BaseProvider) RedirectURL() string { + return p.redirectURL } -// SetRedirectUrl implements Provider.SetRedirectUrl() interface method. -func (p *baseProvider) SetRedirectUrl(url string) { - p.redirectUrl = url +// SetRedirectURL implements Provider.SetRedirectURL() interface method. +func (p *BaseProvider) SetRedirectURL(url string) { + p.redirectURL = url } -// AuthUrl implements Provider.AuthUrl() interface method. -func (p *baseProvider) AuthUrl() string { - return p.authUrl +// AuthURL implements Provider.AuthURL() interface method. +func (p *BaseProvider) AuthURL() string { + return p.authURL } -// SetAuthUrl implements Provider.SetAuthUrl() interface method. -func (p *baseProvider) SetAuthUrl(url string) { - p.authUrl = url +// SetAuthURL implements Provider.SetAuthURL() interface method. +func (p *BaseProvider) SetAuthURL(url string) { + p.authURL = url } -// TokenUrl implements Provider.TokenUrl() interface method. -func (p *baseProvider) TokenUrl() string { - return p.tokenUrl +// TokenURL implements Provider.TokenURL() interface method. +func (p *BaseProvider) TokenURL() string { + return p.tokenURL } -// SetTokenUrl implements Provider.SetTokenUrl() interface method. -func (p *baseProvider) SetTokenUrl(url string) { - p.tokenUrl = url +// SetTokenURL implements Provider.SetTokenURL() interface method. +func (p *BaseProvider) SetTokenURL(url string) { + p.tokenURL = url } -// UserApiUrl implements Provider.UserApiUrl() interface method. -func (p *baseProvider) UserApiUrl() string { - return p.userApiUrl +// UserInfoURL implements Provider.UserInfoURL() interface method. +func (p *BaseProvider) UserInfoURL() string { + return p.userInfoURL } -// SetUserApiUrl implements Provider.SetUserApiUrl() interface method. -func (p *baseProvider) SetUserApiUrl(url string) { - p.userApiUrl = url +// SetUserInfoURL implements Provider.SetUserInfoURL() interface method. +func (p *BaseProvider) SetUserInfoURL(url string) { + p.userInfoURL = url } -// BuildAuthUrl implements Provider.BuildAuthUrl() interface method. -func (p *baseProvider) BuildAuthUrl(state string, opts ...oauth2.AuthCodeOption) string { +// BuildAuthURL implements Provider.BuildAuthURL() interface method. +func (p *BaseProvider) BuildAuthURL(state string, opts ...oauth2.AuthCodeOption) string { return p.oauth2Config().AuthCodeURL(state, opts...) } // FetchToken implements Provider.FetchToken() interface method. -func (p *baseProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { +func (p *BaseProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { return p.oauth2Config().Exchange(p.ctx, code, opts...) } // Client implements Provider.Client() interface method. -func (p *baseProvider) Client(token *oauth2.Token) *http.Client { +func (p *BaseProvider) Client(token *oauth2.Token) *http.Client { return p.oauth2Config().Client(p.ctx, token) } -// FetchRawUserData implements Provider.FetchRawUserData() interface method. -func (p *baseProvider) FetchRawUserData(token *oauth2.Token) ([]byte, error) { - req, err := http.NewRequestWithContext(p.ctx, "GET", p.userApiUrl, nil) +// FetchRawUserInfo implements Provider.FetchRawUserInfo() interface method. +func (p *BaseProvider) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) { + req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil) if err != nil { return nil, err } - return p.sendRawUserDataRequest(req, token) + return p.sendRawUserInfoRequest(req, token) } -// sendRawUserDataRequest sends the specified user data request and return its raw response body. -func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.Token) ([]byte, error) { +// sendRawUserInfoRequest sends the specified user info request and return its raw response body. +func (p *BaseProvider) sendRawUserInfoRequest(req *http.Request, token *oauth2.Token) ([]byte, error) { client := p.Client(token) res, err := client.Do(req) @@ -167,7 +167,7 @@ func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.T if res.StatusCode >= 400 { return nil, fmt.Errorf( "failed to fetch OAuth2 user profile via %s (%d):\n%s", - p.userApiUrl, + p.userInfoURL, res.StatusCode, string(result), ) @@ -177,15 +177,15 @@ func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.T } // oauth2Config constructs a oauth2.Config instance based on the provider settings. -func (p *baseProvider) oauth2Config() *oauth2.Config { +func (p *BaseProvider) oauth2Config() *oauth2.Config { return &oauth2.Config{ - RedirectURL: p.redirectUrl, + RedirectURL: p.redirectURL, ClientID: p.clientId, ClientSecret: p.clientSecret, Scopes: p.scopes, Endpoint: oauth2.Endpoint{ - AuthURL: p.authUrl, - TokenURL: p.tokenUrl, + AuthURL: p.authURL, + TokenURL: p.tokenURL, }, } } diff --git a/tools/auth/base_provider_test.go b/tools/auth/base_provider_test.go index d4e52c2c..437e234d 100644 --- a/tools/auth/base_provider_test.go +++ b/tools/auth/base_provider_test.go @@ -8,7 +8,7 @@ import ( ) func TestContext(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.Scopes() if before != nil { @@ -24,7 +24,7 @@ func TestContext(t *testing.T) { } func TestDisplayName(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.DisplayName() if before != "" { @@ -40,7 +40,7 @@ func TestDisplayName(t *testing.T) { } func TestPKCE(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.PKCE() if before != false { @@ -56,7 +56,7 @@ func TestPKCE(t *testing.T) { } func TestScopes(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.Scopes() if len(before) != 0 { @@ -72,7 +72,7 @@ func TestScopes(t *testing.T) { } func TestClientId(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.ClientId() if before != "" { @@ -88,7 +88,7 @@ func TestClientId(t *testing.T) { } func TestClientSecret(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} before := b.ClientSecret() if before != "" { @@ -103,82 +103,82 @@ func TestClientSecret(t *testing.T) { } } -func TestRedirectUrl(t *testing.T) { - b := baseProvider{} +func TestRedirectURL(t *testing.T) { + b := BaseProvider{} - before := b.RedirectUrl() + before := b.RedirectURL() if before != "" { - t.Fatalf("Expected RedirectUrl to be empty, got %v", before) + t.Fatalf("Expected RedirectURL to be empty, got %v", before) } - b.SetRedirectUrl("test") + b.SetRedirectURL("test") - after := b.RedirectUrl() + after := b.RedirectURL() if after != "test" { - t.Fatalf("Expected RedirectUrl to be 'test', got %v", after) + t.Fatalf("Expected RedirectURL to be 'test', got %v", after) } } -func TestAuthUrl(t *testing.T) { - b := baseProvider{} +func TestAuthURL(t *testing.T) { + b := BaseProvider{} - before := b.AuthUrl() + before := b.AuthURL() if before != "" { - t.Fatalf("Expected authUrl to be empty, got %v", before) + t.Fatalf("Expected authURL to be empty, got %v", before) } - b.SetAuthUrl("test") + b.SetAuthURL("test") - after := b.AuthUrl() + after := b.AuthURL() if after != "test" { - t.Fatalf("Expected authUrl to be 'test', got %v", after) + t.Fatalf("Expected authURL to be 'test', got %v", after) } } -func TestTokenUrl(t *testing.T) { - b := baseProvider{} +func TestTokenURL(t *testing.T) { + b := BaseProvider{} - before := b.TokenUrl() + before := b.TokenURL() if before != "" { - t.Fatalf("Expected tokenUrl to be empty, got %v", before) + t.Fatalf("Expected tokenURL to be empty, got %v", before) } - b.SetTokenUrl("test") + b.SetTokenURL("test") - after := b.TokenUrl() + after := b.TokenURL() if after != "test" { - t.Fatalf("Expected tokenUrl to be 'test', got %v", after) + t.Fatalf("Expected tokenURL to be 'test', got %v", after) } } -func TestUserApiUrl(t *testing.T) { - b := baseProvider{} +func TestUserInfoURL(t *testing.T) { + b := BaseProvider{} - before := b.UserApiUrl() + before := b.UserInfoURL() if before != "" { - t.Fatalf("Expected userApiUrl to be empty, got %v", before) + t.Fatalf("Expected userInfoURL to be empty, got %v", before) } - b.SetUserApiUrl("test") + b.SetUserInfoURL("test") - after := b.UserApiUrl() + after := b.UserInfoURL() if after != "test" { - t.Fatalf("Expected userApiUrl to be 'test', got %v", after) + t.Fatalf("Expected userInfoURL to be 'test', got %v", after) } } -func TestBuildAuthUrl(t *testing.T) { - b := baseProvider{ - authUrl: "authUrl_test", - tokenUrl: "tokenUrl_test", - redirectUrl: "redirectUrl_test", +func TestBuildAuthURL(t *testing.T) { + b := BaseProvider{ + authURL: "authURL_test", + tokenURL: "tokenURL_test", + redirectURL: "redirectURL_test", clientId: "clientId_test", clientSecret: "clientSecret_test", scopes: []string{"test_scope"}, } - expected := "authUrl_test?access_type=offline&client_id=clientId_test&prompt=consent&redirect_uri=redirectUrl_test&response_type=code&scope=test_scope&state=state_test" - result := b.BuildAuthUrl("state_test", oauth2.AccessTypeOffline, oauth2.ApprovalForce) + expected := "authURL_test?access_type=offline&client_id=clientId_test&prompt=consent&redirect_uri=redirectURL_test&response_type=code&scope=test_scope&state=state_test" + result := b.BuildAuthURL("state_test", oauth2.AccessTypeOffline, oauth2.ApprovalForce) if result != expected { t.Errorf("Expected auth url %q, got %q", expected, result) @@ -186,7 +186,7 @@ func TestBuildAuthUrl(t *testing.T) { } func TestClient(t *testing.T) { - b := baseProvider{} + b := BaseProvider{} result := b.Client(&oauth2.Token{}) if result == nil { @@ -195,10 +195,10 @@ func TestClient(t *testing.T) { } func TestOauth2Config(t *testing.T) { - b := baseProvider{ - authUrl: "authUrl_test", - tokenUrl: "tokenUrl_test", - redirectUrl: "redirectUrl_test", + b := BaseProvider{ + authURL: "authURL_test", + tokenURL: "tokenURL_test", + redirectURL: "redirectURL_test", clientId: "clientId_test", clientSecret: "clientSecret_test", scopes: []string{"test"}, @@ -206,8 +206,8 @@ func TestOauth2Config(t *testing.T) { result := b.oauth2Config() - if result.RedirectURL != b.RedirectUrl() { - t.Errorf("Expected redirectUrl %s, got %s", b.RedirectUrl(), result.RedirectURL) + if result.RedirectURL != b.RedirectURL() { + t.Errorf("Expected redirectURL %s, got %s", b.RedirectURL(), result.RedirectURL) } if result.ClientID != b.ClientId() { @@ -218,12 +218,12 @@ func TestOauth2Config(t *testing.T) { t.Errorf("Expected clientSecret %s, got %s", b.ClientSecret(), result.ClientSecret) } - if result.Endpoint.AuthURL != b.AuthUrl() { - t.Errorf("Expected authUrl %s, got %s", b.AuthUrl(), result.Endpoint.AuthURL) + if result.Endpoint.AuthURL != b.AuthURL() { + t.Errorf("Expected authURL %s, got %s", b.AuthURL(), result.Endpoint.AuthURL) } - if result.Endpoint.TokenURL != b.TokenUrl() { - t.Errorf("Expected authUrl %s, got %s", b.TokenUrl(), result.Endpoint.TokenURL) + if result.Endpoint.TokenURL != b.TokenURL() { + t.Errorf("Expected authURL %s, got %s", b.TokenURL(), result.Endpoint.TokenURL) } if len(result.Scopes) != len(b.Scopes()) || result.Scopes[0] != b.Scopes()[0] { diff --git a/tools/auth/bitbucket.go b/tools/auth/bitbucket.go index 6381b6cd..ca5d8776 100644 --- a/tools/auth/bitbucket.go +++ b/tools/auth/bitbucket.go @@ -10,6 +10,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameBitbucket] = wrapFactory(NewBitbucketProvider) +} + var _ Provider = (*Bitbucket)(nil) // NameBitbucket is the unique name of the Bitbucket provider. @@ -17,19 +21,19 @@ const NameBitbucket = "bitbucket" // Bitbucket is an auth provider for Bitbucket. type Bitbucket struct { - *baseProvider + BaseProvider } // NewBitbucketProvider creates a new Bitbucket provider instance with some defaults. func NewBitbucketProvider() *Bitbucket { - return &Bitbucket{&baseProvider{ + return &Bitbucket{BaseProvider{ ctx: context.Background(), displayName: "Bitbucket", pkce: false, scopes: []string{"account"}, - authUrl: "https://bitbucket.org/site/oauth2/authorize", - tokenUrl: "https://bitbucket.org/site/oauth2/access_token", - userApiUrl: "https://api.bitbucket.org/2.0/user", + authURL: "https://bitbucket.org/site/oauth2/authorize", + tokenURL: "https://bitbucket.org/site/oauth2/access_token", + userInfoURL: "https://api.bitbucket.org/2.0/user", }} } @@ -37,7 +41,7 @@ func NewBitbucketProvider() *Bitbucket { // // API reference: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get func (p *Bitbucket) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -76,7 +80,7 @@ func (p *Bitbucket) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name: extracted.DisplayName, Username: extracted.Username, Email: email, - AvatarUrl: extracted.Links.Avatar.Href, + AvatarURL: extracted.Links.Avatar.Href, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, @@ -95,7 +99,7 @@ func (p *Bitbucket) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { // // API reference: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-emails-get func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) { - response, err := p.Client(token).Get(p.userApiUrl + "/emails") + response, err := p.Client(token).Get(p.userInfoURL + "/emails") if err != nil { return "", err } diff --git a/tools/auth/discord.go b/tools/auth/discord.go index 097ff068..41e78b48 100644 --- a/tools/auth/discord.go +++ b/tools/auth/discord.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameDiscord] = wrapFactory(NewDiscordProvider) +} + var _ Provider = (*Discord)(nil) // NameDiscord is the unique name of the Discord provider. @@ -16,21 +20,21 @@ const NameDiscord string = "discord" // Discord allows authentication via Discord OAuth2. type Discord struct { - *baseProvider + BaseProvider } // NewDiscordProvider creates a new Discord provider instance with some defaults. func NewDiscordProvider() *Discord { // https://discord.com/developers/docs/topics/oauth2 // https://discord.com/developers/docs/resources/user#get-current-user - return &Discord{&baseProvider{ + return &Discord{BaseProvider{ ctx: context.Background(), displayName: "Discord", pkce: true, scopes: []string{"identify", "email"}, - authUrl: "https://discord.com/api/oauth2/authorize", - tokenUrl: "https://discord.com/api/oauth2/token", - userApiUrl: "https://discord.com/api/users/@me", + authURL: "https://discord.com/api/oauth2/authorize", + tokenURL: "https://discord.com/api/oauth2/token", + userInfoURL: "https://discord.com/api/users/@me", }} } @@ -38,7 +42,7 @@ func NewDiscordProvider() *Discord { // // API reference: https://discord.com/developers/docs/resources/user#user-object func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -62,7 +66,7 @@ func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { // Build a full avatar URL using the avatar hash provided in the API response // https://discord.com/developers/docs/reference#image-formatting - avatarUrl := fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png", extracted.Id, extracted.Avatar) + avatarURL := fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png", extracted.Id, extracted.Avatar) // Concatenate the user's username and discriminator into a single username string username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator) @@ -71,7 +75,7 @@ func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: extracted.Id, Name: username, Username: extracted.Username, - AvatarUrl: avatarUrl, + AvatarURL: avatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/facebook.go b/tools/auth/facebook.go index 5aa9c1cd..b3f08c06 100644 --- a/tools/auth/facebook.go +++ b/tools/auth/facebook.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2/facebook" ) +func init() { + Providers[NameFacebook] = wrapFactory(NewFacebookProvider) +} + var _ Provider = (*Facebook)(nil) // NameFacebook is the unique name of the Facebook provider. @@ -16,19 +20,19 @@ const NameFacebook string = "facebook" // Facebook allows authentication via Facebook OAuth2. type Facebook struct { - *baseProvider + BaseProvider } // NewFacebookProvider creates new Facebook provider instance with some defaults. func NewFacebookProvider() *Facebook { - return &Facebook{&baseProvider{ + return &Facebook{BaseProvider{ ctx: context.Background(), displayName: "Facebook", pkce: true, scopes: []string{"email"}, - authUrl: facebook.Endpoint.AuthURL, - tokenUrl: facebook.Endpoint.TokenURL, - userApiUrl: "https://graph.facebook.com/me?fields=name,email,picture.type(large)", + authURL: facebook.Endpoint.AuthURL, + tokenURL: facebook.Endpoint.TokenURL, + userInfoURL: "https://graph.facebook.com/me?fields=name,email,picture.type(large)", }} } @@ -36,7 +40,7 @@ func NewFacebookProvider() *Facebook { // // API reference: https://developers.facebook.com/docs/graph-api/reference/user/ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -62,7 +66,7 @@ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: extracted.Id, Name: extracted.Name, Email: extracted.Email, - AvatarUrl: extracted.Picture.Data.Url, + AvatarURL: extracted.Picture.Data.Url, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/gitea.go b/tools/auth/gitea.go index b60f3c4f..eb1ede76 100644 --- a/tools/auth/gitea.go +++ b/tools/auth/gitea.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameGitea] = wrapFactory(NewGiteaProvider) +} + var _ Provider = (*Gitea)(nil) // NameGitea is the unique name of the Gitea provider. @@ -16,19 +20,19 @@ const NameGitea string = "gitea" // Gitea allows authentication via Gitea OAuth2. type Gitea struct { - *baseProvider + BaseProvider } // NewGiteaProvider creates new Gitea provider instance with some defaults. func NewGiteaProvider() *Gitea { - return &Gitea{&baseProvider{ + return &Gitea{BaseProvider{ ctx: context.Background(), displayName: "Gitea", pkce: true, scopes: []string{"read:user", "user:email"}, - authUrl: "https://gitea.com/login/oauth/authorize", - tokenUrl: "https://gitea.com/login/oauth/access_token", - userApiUrl: "https://gitea.com/api/v1/user", + authURL: "https://gitea.com/login/oauth/authorize", + tokenURL: "https://gitea.com/login/oauth/access_token", + userInfoURL: "https://gitea.com/api/v1/user", }} } @@ -36,7 +40,7 @@ func NewGiteaProvider() *Gitea { // // API reference: https://try.gitea.io/api/swagger#/user/userGetCurrent func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -51,7 +55,7 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name string `json:"full_name"` Username string `json:"login"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` + AvatarURL string `json:"avatar_url"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -62,7 +66,7 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name: extracted.Name, Username: extracted.Username, Email: extracted.Email, - AvatarUrl: extracted.AvatarUrl, + AvatarURL: extracted.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/gitee.go b/tools/auth/gitee.go index fea0a34d..f851b48d 100644 --- a/tools/auth/gitee.go +++ b/tools/auth/gitee.go @@ -11,6 +11,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameGitee] = wrapFactory(NewGiteeProvider) +} + var _ Provider = (*Gitee)(nil) // NameGitee is the unique name of the Gitee provider. @@ -18,19 +22,19 @@ const NameGitee string = "gitee" // Gitee allows authentication via Gitee OAuth2. type Gitee struct { - *baseProvider + BaseProvider } // NewGiteeProvider creates new Gitee provider instance with some defaults. func NewGiteeProvider() *Gitee { - return &Gitee{&baseProvider{ + return &Gitee{BaseProvider{ ctx: context.Background(), displayName: "Gitee", pkce: true, scopes: []string{"user_info", "emails"}, - authUrl: "https://gitee.com/oauth/authorize", - tokenUrl: "https://gitee.com/oauth/token", - userApiUrl: "https://gitee.com/api/v5/user", + authURL: "https://gitee.com/oauth/authorize", + tokenURL: "https://gitee.com/oauth/token", + userInfoURL: "https://gitee.com/api/v5/user", }} } @@ -38,7 +42,7 @@ func NewGiteeProvider() *Gitee { // // API reference: https://gitee.com/api/v5/swagger#/getV5User func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -53,7 +57,7 @@ func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id int `json:"id"` Name string `json:"name"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` + AvatarURL string `json:"avatar_url"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -63,7 +67,7 @@ func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: strconv.Itoa(extracted.Id), Name: extracted.Name, Username: extracted.Login, - AvatarUrl: extracted.AvatarUrl, + AvatarURL: extracted.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/github.go b/tools/auth/github.go index e1c2014f..c9898b38 100644 --- a/tools/auth/github.go +++ b/tools/auth/github.go @@ -11,6 +11,10 @@ import ( "golang.org/x/oauth2/github" ) +func init() { + Providers[NameGithub] = wrapFactory(NewGithubProvider) +} + var _ Provider = (*Github)(nil) // NameGithub is the unique name of the Github provider. @@ -18,19 +22,19 @@ const NameGithub string = "github" // Github allows authentication via Github OAuth2. type Github struct { - *baseProvider + BaseProvider } // NewGithubProvider creates new Github provider instance with some defaults. func NewGithubProvider() *Github { - return &Github{&baseProvider{ + return &Github{BaseProvider{ ctx: context.Background(), displayName: "GitHub", pkce: true, // technically is not supported yet but it is safe as the PKCE params are just ignored scopes: []string{"read:user", "user:email"}, - authUrl: github.Endpoint.AuthURL, - tokenUrl: github.Endpoint.TokenURL, - userApiUrl: "https://api.github.com/user", + authURL: github.Endpoint.AuthURL, + tokenURL: github.Endpoint.TokenURL, + userInfoURL: "https://api.github.com/user", }} } @@ -38,7 +42,7 @@ func NewGithubProvider() *Github { // // API reference: https://docs.github.com/en/rest/reference/users#get-the-authenticated-user func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -53,7 +57,7 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id int `json:"id"` Name string `json:"name"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` + AvatarURL string `json:"avatar_url"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -64,7 +68,7 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name: extracted.Name, Username: extracted.Login, Email: extracted.Email, - AvatarUrl: extracted.AvatarUrl, + AvatarURL: extracted.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, @@ -95,7 +99,7 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) { client := p.Client(token) - response, err := client.Get(p.userApiUrl + "/emails") + response, err := client.Get(p.userInfoURL + "/emails") if err != nil { return "", err } diff --git a/tools/auth/gitlab.go b/tools/auth/gitlab.go index 09d04b1a..7ea60192 100644 --- a/tools/auth/gitlab.go +++ b/tools/auth/gitlab.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameGitlab] = wrapFactory(NewGitlabProvider) +} + var _ Provider = (*Gitlab)(nil) // NameGitlab is the unique name of the Gitlab provider. @@ -16,19 +20,19 @@ const NameGitlab string = "gitlab" // Gitlab allows authentication via Gitlab OAuth2. type Gitlab struct { - *baseProvider + BaseProvider } // NewGitlabProvider creates new Gitlab provider instance with some defaults. func NewGitlabProvider() *Gitlab { - return &Gitlab{&baseProvider{ + return &Gitlab{BaseProvider{ ctx: context.Background(), displayName: "GitLab", pkce: true, scopes: []string{"read_user"}, - authUrl: "https://gitlab.com/oauth/authorize", - tokenUrl: "https://gitlab.com/oauth/token", - userApiUrl: "https://gitlab.com/api/v4/user", + authURL: "https://gitlab.com/oauth/authorize", + tokenURL: "https://gitlab.com/oauth/token", + userInfoURL: "https://gitlab.com/api/v4/user", }} } @@ -36,7 +40,7 @@ func NewGitlabProvider() *Gitlab { // // API reference: https://docs.gitlab.com/ee/api/users.html#for-admin func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -51,7 +55,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name string `json:"name"` Username string `json:"username"` Email string `json:"email"` - AvatarUrl string `json:"avatar_url"` + AvatarURL string `json:"avatar_url"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -62,7 +66,7 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name: extracted.Name, Username: extracted.Username, Email: extracted.Email, - AvatarUrl: extracted.AvatarUrl, + AvatarURL: extracted.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/google.go b/tools/auth/google.go index 1b7d114c..f017cc4b 100644 --- a/tools/auth/google.go +++ b/tools/auth/google.go @@ -8,6 +8,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameGoogle] = wrapFactory(NewGoogleProvider) +} + var _ Provider = (*Google)(nil) // NameGoogle is the unique name of the Google provider. @@ -15,12 +19,12 @@ const NameGoogle string = "google" // Google allows authentication via Google OAuth2. type Google struct { - *baseProvider + BaseProvider } // NewGoogleProvider creates new Google provider instance with some defaults. func NewGoogleProvider() *Google { - return &Google{&baseProvider{ + return &Google{BaseProvider{ ctx: context.Background(), displayName: "Google", pkce: true, @@ -28,15 +32,15 @@ func NewGoogleProvider() *Google { "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", }, - authUrl: "https://accounts.google.com/o/oauth2/auth", - tokenUrl: "https://accounts.google.com/o/oauth2/token", - userApiUrl: "https://www.googleapis.com/oauth2/v1/userinfo", + authURL: "https://accounts.google.com/o/oauth2/auth", + tokenURL: "https://accounts.google.com/o/oauth2/token", + userInfoURL: "https://www.googleapis.com/oauth2/v1/userinfo", }} } // FetchAuthUser returns an AuthUser instance based the Google's user api. func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -60,7 +64,7 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user := &AuthUser{ Id: extracted.Id, Name: extracted.Name, - AvatarUrl: extracted.Picture, + AvatarURL: extracted.Picture, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/instagram.go b/tools/auth/instagram.go index c28c7537..1a856c93 100644 --- a/tools/auth/instagram.go +++ b/tools/auth/instagram.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2/instagram" ) +func init() { + Providers[NameInstagram] = wrapFactory(NewInstagramProvider) +} + var _ Provider = (*Instagram)(nil) // NameInstagram is the unique name of the Instagram provider. @@ -16,19 +20,19 @@ const NameInstagram string = "instagram" // Instagram allows authentication via Instagram OAuth2. type Instagram struct { - *baseProvider + BaseProvider } // NewInstagramProvider creates new Instagram provider instance with some defaults. func NewInstagramProvider() *Instagram { - return &Instagram{&baseProvider{ + return &Instagram{BaseProvider{ ctx: context.Background(), displayName: "Instagram", pkce: true, scopes: []string{"user_profile"}, - authUrl: instagram.Endpoint.AuthURL, - tokenUrl: instagram.Endpoint.TokenURL, - userApiUrl: "https://graph.instagram.com/me?fields=id,username,account_type", + authURL: instagram.Endpoint.AuthURL, + tokenURL: instagram.Endpoint.TokenURL, + userInfoURL: "https://graph.instagram.com/me?fields=id,username,account_type", }} } @@ -36,7 +40,7 @@ func NewInstagramProvider() *Instagram { // // API reference: https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#fields func (p *Instagram) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } diff --git a/tools/auth/kakao.go b/tools/auth/kakao.go index ef5e7265..1078442a 100644 --- a/tools/auth/kakao.go +++ b/tools/auth/kakao.go @@ -10,6 +10,10 @@ import ( "golang.org/x/oauth2/kakao" ) +func init() { + Providers[NameKakao] = wrapFactory(NewKakaoProvider) +} + var _ Provider = (*Kakao)(nil) // NameKakao is the unique name of the Kakao provider. @@ -17,19 +21,19 @@ const NameKakao string = "kakao" // Kakao allows authentication via Kakao OAuth2. type Kakao struct { - *baseProvider + BaseProvider } // NewKakaoProvider creates a new Kakao provider instance with some defaults. func NewKakaoProvider() *Kakao { - return &Kakao{&baseProvider{ + return &Kakao{BaseProvider{ ctx: context.Background(), displayName: "Kakao", pkce: true, scopes: []string{"account_email", "profile_nickname", "profile_image"}, - authUrl: kakao.Endpoint.AuthURL, - tokenUrl: kakao.Endpoint.TokenURL, - userApiUrl: "https://kapi.kakao.com/v2/user/me", + authURL: kakao.Endpoint.AuthURL, + tokenURL: kakao.Endpoint.TokenURL, + userInfoURL: "https://kapi.kakao.com/v2/user/me", }} } @@ -37,7 +41,7 @@ func NewKakaoProvider() *Kakao { // // API reference: https://developers.kakao.com/docs/latest/en/kakaologin/rest-api#req-user-info-response func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -51,7 +55,7 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id int `json:"id"` Profile struct { Nickname string `json:"nickname"` - ImageUrl string `json:"profile_image"` + ImageURL string `json:"profile_image"` } `json:"properties"` KakaoAccount struct { Email string `json:"email"` @@ -66,7 +70,7 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user := &AuthUser{ Id: strconv.Itoa(extracted.Id), Username: extracted.Profile.Nickname, - AvatarUrl: extracted.Profile.ImageUrl, + AvatarURL: extracted.Profile.ImageURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/livechat.go b/tools/auth/livechat.go index ee644817..2272091b 100644 --- a/tools/auth/livechat.go +++ b/tools/auth/livechat.go @@ -8,6 +8,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameLivechat] = wrapFactory(NewLivechatProvider) +} + var _ Provider = (*Livechat)(nil) // NameLivechat is the unique name of the Livechat provider. @@ -15,19 +19,19 @@ const NameLivechat = "livechat" // Livechat allows authentication via Livechat OAuth2. type Livechat struct { - *baseProvider + BaseProvider } // NewLivechatProvider creates new Livechat provider instance with some defaults. func NewLivechatProvider() *Livechat { - return &Livechat{&baseProvider{ + return &Livechat{BaseProvider{ ctx: context.Background(), displayName: "LiveChat", pkce: true, scopes: []string{}, // default scopes are specified from the provider dashboard - authUrl: "https://accounts.livechat.com/", - tokenUrl: "https://accounts.livechat.com/token", - userApiUrl: "https://accounts.livechat.com/v2/accounts/me", + authURL: "https://accounts.livechat.com/", + tokenURL: "https://accounts.livechat.com/token", + userInfoURL: "https://accounts.livechat.com/v2/accounts/me", }} } @@ -35,7 +39,7 @@ func NewLivechatProvider() *Livechat { // // API reference: https://developers.livechat.com/docs/authorization func (p *Livechat) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -50,7 +54,7 @@ func (p *Livechat) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name string `json:"name"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` - AvatarUrl string `json:"avatar_url"` + AvatarURL string `json:"avatar_url"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -59,7 +63,7 @@ func (p *Livechat) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user := &AuthUser{ Id: extracted.Id, Name: extracted.Name, - AvatarUrl: extracted.AvatarUrl, + AvatarURL: extracted.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/mailcow.go b/tools/auth/mailcow.go index 8ce5f276..7e802be7 100644 --- a/tools/auth/mailcow.go +++ b/tools/auth/mailcow.go @@ -10,6 +10,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameMailcow] = wrapFactory(NewMailcowProvider) +} + var _ Provider = (*Mailcow)(nil) // NameMailcow is the unique name of the mailcow provider. @@ -17,12 +21,12 @@ const NameMailcow string = "mailcow" // Mailcow allows authentication via mailcow OAuth2. type Mailcow struct { - *baseProvider + BaseProvider } // NewMailcowProvider creates a new mailcow provider instance with some defaults. func NewMailcowProvider() *Mailcow { - return &Mailcow{&baseProvider{ + return &Mailcow{BaseProvider{ ctx: context.Background(), displayName: "mailcow", pkce: true, @@ -34,7 +38,7 @@ func NewMailcowProvider() *Mailcow { // // API reference: https://github.com/mailcow/mailcow-dockerized/blob/master/data/web/oauth/profile.php func (p *Mailcow) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } diff --git a/tools/auth/microsoft.go b/tools/auth/microsoft.go index f9cc7c06..98732583 100644 --- a/tools/auth/microsoft.go +++ b/tools/auth/microsoft.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2/microsoft" ) +func init() { + Providers[NameMicrosoft] = wrapFactory(NewMicrosoftProvider) +} + var _ Provider = (*Microsoft)(nil) // NameMicrosoft is the unique name of the Microsoft provider. @@ -16,20 +20,20 @@ const NameMicrosoft string = "microsoft" // Microsoft allows authentication via AzureADEndpoint OAuth2. type Microsoft struct { - *baseProvider + BaseProvider } // NewMicrosoftProvider creates new Microsoft AD provider instance with some defaults. func NewMicrosoftProvider() *Microsoft { endpoints := microsoft.AzureADEndpoint("") - return &Microsoft{&baseProvider{ + return &Microsoft{BaseProvider{ ctx: context.Background(), displayName: "Microsoft", pkce: true, scopes: []string{"User.Read"}, - authUrl: endpoints.AuthURL, - tokenUrl: endpoints.TokenURL, - userApiUrl: "https://graph.microsoft.com/v1.0/me", + authURL: endpoints.AuthURL, + tokenURL: endpoints.TokenURL, + userInfoURL: "https://graph.microsoft.com/v1.0/me", }} } @@ -38,7 +42,7 @@ func NewMicrosoftProvider() *Microsoft { // API reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo // Graph explorer: https://developer.microsoft.com/en-us/graph/graph-explorer func (p *Microsoft) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } diff --git a/tools/auth/oidc.go b/tools/auth/oidc.go index c5ccf2b6..7ca09d3c 100644 --- a/tools/auth/oidc.go +++ b/tools/auth/oidc.go @@ -8,6 +8,12 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameOIDC] = wrapFactory(NewOIDCProvider) + Providers[NameOIDC+"2"] = wrapFactory(NewOIDCProvider) + Providers[NameOIDC+"3"] = wrapFactory(NewOIDCProvider) +} + var _ Provider = (*OIDC)(nil) // NameOIDC is the unique name of the OpenID Connect (OIDC) provider. @@ -15,12 +21,12 @@ const NameOIDC string = "oidc" // OIDC allows authentication via OpenID Connect (OIDC) OAuth2 provider. type OIDC struct { - *baseProvider + BaseProvider } // NewOIDCProvider creates new OpenID Connect (OIDC) provider instance with some defaults. func NewOIDCProvider() *OIDC { - return &OIDC{&baseProvider{ + return &OIDC{BaseProvider{ ctx: context.Background(), displayName: "OIDC", pkce: true, @@ -35,8 +41,10 @@ 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.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -62,7 +70,7 @@ func (p *OIDC) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: extracted.Id, Name: extracted.Name, Username: extracted.Username, - AvatarUrl: extracted.Picture, + AvatarURL: extracted.Picture, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/patreon.go b/tools/auth/patreon.go index d981d2de..b079c99a 100644 --- a/tools/auth/patreon.go +++ b/tools/auth/patreon.go @@ -8,6 +8,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NamePatreon] = wrapFactory(NewPatreonProvider) +} + var _ Provider = (*Patreon)(nil) // NamePatreon is the unique name of the Patreon provider. @@ -15,19 +19,19 @@ const NamePatreon string = "patreon" // Patreon allows authentication via Patreon OAuth2. type Patreon struct { - *baseProvider + BaseProvider } // NewPatreonProvider creates new Patreon provider instance with some defaults. func NewPatreonProvider() *Patreon { - return &Patreon{&baseProvider{ + return &Patreon{BaseProvider{ ctx: context.Background(), displayName: "Patreon", pkce: true, scopes: []string{"identity", "identity[email]"}, - authUrl: "https://www.patreon.com/oauth2/authorize", - tokenUrl: "https://www.patreon.com/api/oauth2/token", - userApiUrl: "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=full_name,email,vanity,image_url,is_email_verified", + authURL: "https://www.patreon.com/oauth2/authorize", + tokenURL: "https://www.patreon.com/api/oauth2/token", + userInfoURL: "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=full_name,email,vanity,image_url,is_email_verified", }} } @@ -37,7 +41,7 @@ func NewPatreonProvider() *Patreon { // https://docs.patreon.com/#get-api-oauth2-v2-identity // https://docs.patreon.com/#user-v2 func (p *Patreon) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -54,7 +58,7 @@ func (p *Patreon) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Email string `json:"email"` Name string `json:"full_name"` Username string `json:"vanity"` - AvatarUrl string `json:"image_url"` + AvatarURL string `json:"image_url"` IsEmailVerified bool `json:"is_email_verified"` } `json:"attributes"` } `json:"data"` @@ -67,7 +71,7 @@ func (p *Patreon) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: extracted.Data.Id, Username: extracted.Data.Attributes.Username, Name: extracted.Data.Attributes.Name, - AvatarUrl: extracted.Data.Attributes.AvatarUrl, + AvatarURL: extracted.Data.Attributes.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/planningcenter.go b/tools/auth/planningcenter.go index aaf0657f..2d834dcc 100644 --- a/tools/auth/planningcenter.go +++ b/tools/auth/planningcenter.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NamePlanningcenter] = wrapFactory(NewPlanningcenterProvider) +} + var _ Provider = (*Planningcenter)(nil) // NamePlanningcenter is the unique name of the Planningcenter provider. @@ -16,19 +20,19 @@ const NamePlanningcenter string = "planningcenter" // Planningcenter allows authentication via Planningcenter OAuth2. type Planningcenter struct { - *baseProvider + BaseProvider } // NewPlanningcenterProvider creates a new Planningcenter provider instance with some defaults. func NewPlanningcenterProvider() *Planningcenter { - return &Planningcenter{&baseProvider{ + return &Planningcenter{BaseProvider{ ctx: context.Background(), displayName: "Planning Center", pkce: true, scopes: []string{"people"}, - authUrl: "https://api.planningcenteronline.com/oauth/authorize", - tokenUrl: "https://api.planningcenteronline.com/oauth/token", - userApiUrl: "https://api.planningcenteronline.com/people/v2/me", + authURL: "https://api.planningcenteronline.com/oauth/authorize", + tokenURL: "https://api.planningcenteronline.com/oauth/token", + userInfoURL: "https://api.planningcenteronline.com/people/v2/me", }} } @@ -36,7 +40,7 @@ func NewPlanningcenterProvider() *Planningcenter { // // API reference: https://developer.planning.center/docs/#/overview/authentication func (p *Planningcenter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -52,7 +56,7 @@ func (p *Planningcenter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Attributes struct { Status string `json:"status"` Name string `json:"name"` - AvatarUrl string `json:"avatar"` + AvatarURL string `json:"avatar"` // don't map the email because users can have multiple assigned // and it's not clear if they are verified } @@ -69,7 +73,7 @@ func (p *Planningcenter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user := &AuthUser{ Id: extracted.Data.Id, Name: extracted.Data.Attributes.Name, - AvatarUrl: extracted.Data.Attributes.AvatarUrl, + AvatarURL: extracted.Data.Attributes.AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/spotify.go b/tools/auth/spotify.go index 0dcc19d5..7e6b970e 100644 --- a/tools/auth/spotify.go +++ b/tools/auth/spotify.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2/spotify" ) +func init() { + Providers[NameSpotify] = wrapFactory(NewSpotifyProvider) +} + var _ Provider = (*Spotify)(nil) // NameSpotify is the unique name of the Spotify provider. @@ -16,12 +20,12 @@ const NameSpotify string = "spotify" // Spotify allows authentication via Spotify OAuth2. type Spotify struct { - *baseProvider + BaseProvider } // NewSpotifyProvider creates a new Spotify provider instance with some defaults. func NewSpotifyProvider() *Spotify { - return &Spotify{&baseProvider{ + return &Spotify{BaseProvider{ ctx: context.Background(), displayName: "Spotify", pkce: true, @@ -30,9 +34,9 @@ func NewSpotifyProvider() *Spotify { // currently Spotify doesn't return information whether the email is verified or not // "user-read-email", }, - authUrl: spotify.Endpoint.AuthURL, - tokenUrl: spotify.Endpoint.TokenURL, - userApiUrl: "https://api.spotify.com/v1/me", + authURL: spotify.Endpoint.AuthURL, + tokenURL: spotify.Endpoint.TokenURL, + userInfoURL: "https://api.spotify.com/v1/me", }} } @@ -40,7 +44,7 @@ func NewSpotifyProvider() *Spotify { // // API reference: https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -54,7 +58,7 @@ func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id string `json:"id"` Name string `json:"display_name"` Images []struct { - Url string `json:"url"` + URL string `json:"url"` } `json:"images"` // don't map the email because per the official docs // the email field is "unverified" and there is no proof @@ -76,7 +80,7 @@ func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user.Expiry, _ = types.ParseDateTime(token.Expiry) if len(extracted.Images) > 0 { - user.AvatarUrl = extracted.Images[0].Url + user.AvatarURL = extracted.Images[0].URL } return user, nil diff --git a/tools/auth/strava.go b/tools/auth/strava.go index 6a860f0a..5cfce10a 100644 --- a/tools/auth/strava.go +++ b/tools/auth/strava.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameStrava] = wrapFactory(NewStravaProvider) +} + var _ Provider = (*Strava)(nil) // NameStrava is the unique name of the Strava provider. @@ -16,21 +20,21 @@ const NameStrava string = "strava" // Strava allows authentication via Strava OAuth2. type Strava struct { - *baseProvider + BaseProvider } // NewStravaProvider creates new Strava provider instance with some defaults. func NewStravaProvider() *Strava { - return &Strava{&baseProvider{ + return &Strava{BaseProvider{ ctx: context.Background(), displayName: "Strava", pkce: true, scopes: []string{ "profile:read_all", }, - authUrl: "https://www.strava.com/oauth/authorize", - tokenUrl: "https://www.strava.com/api/v3/oauth/token", - userApiUrl: "https://www.strava.com/api/v3/athlete", + authURL: "https://www.strava.com/oauth/authorize", + tokenURL: "https://www.strava.com/api/v3/oauth/token", + userInfoURL: "https://www.strava.com/api/v3/athlete", }} } @@ -38,7 +42,7 @@ func NewStravaProvider() *Strava { // // API reference: https://developers.strava.com/docs/authentication/ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -53,7 +57,7 @@ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { FirstName string `json:"firstname"` LastName string `json:"lastname"` Username string `json:"username"` - ProfileImageUrl string `json:"profile"` + ProfileImageURL string `json:"profile"` // At the time of writing, Strava OAuth2 doesn't support returning the user email address // Email string `json:"email"` @@ -65,7 +69,7 @@ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user := &AuthUser{ Name: extracted.FirstName + " " + extracted.LastName, Username: extracted.Username, - AvatarUrl: extracted.ProfileImageUrl, + AvatarURL: extracted.ProfileImageURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/twitch.go b/tools/auth/twitch.go index 2c211a74..73698a3d 100644 --- a/tools/auth/twitch.go +++ b/tools/auth/twitch.go @@ -11,6 +11,10 @@ import ( "golang.org/x/oauth2/twitch" ) +func init() { + Providers[NameTwitch] = wrapFactory(NewTwitchProvider) +} + var _ Provider = (*Twitch)(nil) // NameTwitch is the unique name of the Twitch provider. @@ -18,19 +22,19 @@ const NameTwitch string = "twitch" // Twitch allows authentication via Twitch OAuth2. type Twitch struct { - *baseProvider + BaseProvider } // NewTwitchProvider creates new Twitch provider instance with some defaults. func NewTwitchProvider() *Twitch { - return &Twitch{&baseProvider{ + return &Twitch{BaseProvider{ ctx: context.Background(), displayName: "Twitch", pkce: true, scopes: []string{"user:read:email"}, - authUrl: twitch.Endpoint.AuthURL, - tokenUrl: twitch.Endpoint.TokenURL, - userApiUrl: "https://api.twitch.tv/helix/users", + authURL: twitch.Endpoint.AuthURL, + tokenURL: twitch.Endpoint.TokenURL, + userInfoURL: "https://api.twitch.tv/helix/users", }} } @@ -38,7 +42,7 @@ func NewTwitchProvider() *Twitch { // // API reference: https://dev.twitch.tv/docs/api/reference#get-users func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -54,7 +58,7 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Login string `json:"login"` DisplayName string `json:"display_name"` Email string `json:"email"` - ProfileImageUrl string `json:"profile_image_url"` + ProfileImageURL string `json:"profile_image_url"` } `json:"data"` }{} if err := json.Unmarshal(data, &extracted); err != nil { @@ -70,7 +74,7 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Name: extracted.Data[0].DisplayName, Username: extracted.Data[0].Login, Email: extracted.Data[0].Email, - AvatarUrl: extracted.Data[0].ProfileImageUrl, + AvatarURL: extracted.Data[0].ProfileImageURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, @@ -81,16 +85,16 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { return user, nil } -// FetchRawUserData implements Provider.FetchRawUserData interface. +// FetchRawUserInfo implements Provider.FetchRawUserInfo interface. // // This differ from baseProvider because Twitch requires the `Client-Id` header. -func (p *Twitch) FetchRawUserData(token *oauth2.Token) ([]byte, error) { - req, err := http.NewRequest("GET", p.userApiUrl, nil) +func (p *Twitch) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) { + req, err := http.NewRequest("GET", p.userInfoURL, nil) if err != nil { return nil, err } req.Header.Set("Client-Id", p.clientId) - return p.sendRawUserDataRequest(req, token) + return p.sendRawUserInfoRequest(req, token) } diff --git a/tools/auth/twitter.go b/tools/auth/twitter.go index b7e4a7eb..60a905f8 100644 --- a/tools/auth/twitter.go +++ b/tools/auth/twitter.go @@ -8,6 +8,10 @@ import ( "golang.org/x/oauth2" ) +func init() { + Providers[NameTwitter] = wrapFactory(NewTwitterProvider) +} + var _ Provider = (*Twitter)(nil) // NameTwitter is the unique name of the Twitter provider. @@ -15,12 +19,12 @@ const NameTwitter string = "twitter" // Twitter allows authentication via Twitter OAuth2. type Twitter struct { - *baseProvider + BaseProvider } // NewTwitterProvider creates new Twitter provider instance with some defaults. func NewTwitterProvider() *Twitter { - return &Twitter{&baseProvider{ + return &Twitter{BaseProvider{ ctx: context.Background(), displayName: "Twitter", pkce: true, @@ -31,9 +35,9 @@ func NewTwitterProvider() *Twitter { // (see https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me) "tweet.read", }, - authUrl: "https://twitter.com/i/oauth2/authorize", - tokenUrl: "https://api.twitter.com/2/oauth2/token", - userApiUrl: "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url", + authURL: "https://twitter.com/i/oauth2/authorize", + tokenURL: "https://api.twitter.com/2/oauth2/token", + userInfoURL: "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url", }} } @@ -41,7 +45,7 @@ func NewTwitterProvider() *Twitter { // // API reference: https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -56,7 +60,7 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id string `json:"id"` Name string `json:"name"` Username string `json:"username"` - ProfileImageUrl string `json:"profile_image_url"` + ProfileImageURL string `json:"profile_image_url"` // NB! At the time of writing, Twitter OAuth2 doesn't support returning the user email address // (see https://twittercommunity.com/t/which-api-to-get-user-after-oauth2-authorization/162417/33) @@ -71,7 +75,7 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: extracted.Data.Id, Name: extracted.Data.Name, Username: extracted.Data.Username, - AvatarUrl: extracted.Data.ProfileImageUrl, + AvatarURL: extracted.Data.ProfileImageURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/vk.go b/tools/auth/vk.go index 622752d1..5bf51fde 100644 --- a/tools/auth/vk.go +++ b/tools/auth/vk.go @@ -13,6 +13,10 @@ import ( "golang.org/x/oauth2/vk" ) +func init() { + Providers[NameVK] = wrapFactory(NewVKProvider) +} + var _ Provider = (*VK)(nil) // NameVK is the unique name of the VK provider. @@ -20,21 +24,21 @@ const NameVK string = "vk" // VK allows authentication via VK OAuth2. type VK struct { - *baseProvider + BaseProvider } // NewVKProvider creates new VK provider instance with some defaults. // // Docs: https://dev.vk.com/api/oauth-parameters func NewVKProvider() *VK { - return &VK{&baseProvider{ + return &VK{BaseProvider{ ctx: context.Background(), displayName: "ВКонтакте", pkce: false, // VK currently doesn't support PKCE and throws an error if PKCE params are send scopes: []string{"email"}, - authUrl: vk.Endpoint.AuthURL, - tokenUrl: vk.Endpoint.TokenURL, - userApiUrl: "https://api.vk.com/method/users.get?fields=photo_max,screen_name&v=5.131", + authURL: vk.Endpoint.AuthURL, + tokenURL: vk.Endpoint.TokenURL, + userInfoURL: "https://api.vk.com/method/users.get?fields=photo_max,screen_name&v=5.131", }} } @@ -42,7 +46,7 @@ func NewVKProvider() *VK { // // API reference: https://dev.vk.com/method/users.get func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -58,7 +62,7 @@ func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"screen_name"` - AvatarUrl string `json:"photo_max"` + AvatarURL string `json:"photo_max"` } `json:"response"` }{} @@ -74,7 +78,7 @@ func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: strconv.Itoa(extracted.Response[0].Id), Name: strings.TrimSpace(extracted.Response[0].FirstName + " " + extracted.Response[0].LastName), Username: extracted.Response[0].Username, - AvatarUrl: extracted.Response[0].AvatarUrl, + AvatarURL: extracted.Response[0].AvatarURL, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, diff --git a/tools/auth/yandex.go b/tools/auth/yandex.go index 6ec1c90e..d4af1fa0 100644 --- a/tools/auth/yandex.go +++ b/tools/auth/yandex.go @@ -9,6 +9,10 @@ import ( "golang.org/x/oauth2/yandex" ) +func init() { + Providers[NameYandex] = wrapFactory(NewYandexProvider) +} + var _ Provider = (*Yandex)(nil) // NameYandex is the unique name of the Yandex provider. @@ -16,21 +20,21 @@ const NameYandex string = "yandex" // Yandex allows authentication via Yandex OAuth2. type Yandex struct { - *baseProvider + BaseProvider } // NewYandexProvider creates new Yandex provider instance with some defaults. // // Docs: https://yandex.ru/dev/id/doc/en/ func NewYandexProvider() *Yandex { - return &Yandex{&baseProvider{ + return &Yandex{BaseProvider{ ctx: context.Background(), displayName: "Yandex", pkce: true, scopes: []string{"login:email", "login:avatar", "login:info"}, - authUrl: yandex.Endpoint.AuthURL, - tokenUrl: yandex.Endpoint.TokenURL, - userApiUrl: "https://login.yandex.ru/info", + authURL: yandex.Endpoint.AuthURL, + tokenURL: yandex.Endpoint.TokenURL, + userInfoURL: "https://login.yandex.ru/info", }} } @@ -38,7 +42,7 @@ func NewYandexProvider() *Yandex { // // API reference: https://yandex.ru/dev/id/doc/en/user-information#response-format func (p *Yandex) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { - data, err := p.FetchRawUserData(token) + data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } @@ -73,7 +77,7 @@ func (p *Yandex) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user.Expiry, _ = types.ParseDateTime(token.Expiry) if !extracted.IsAvatarEmpty { - user.AvatarUrl = "https://avatars.yandex.net/get-yapic/" + extracted.AvatarId + "/islands-200" + user.AvatarURL = "https://avatars.yandex.net/get-yapic/" + extracted.AvatarId + "/islands-200" } return user, nil diff --git a/tools/cron/cron.go b/tools/cron/cron.go index 39d3627c..39f8539c 100644 --- a/tools/cron/cron.go +++ b/tools/cron/cron.go @@ -26,10 +26,9 @@ type Cron struct { ticker *time.Ticker startTimer *time.Timer jobs map[string]*job - interval time.Duration tickerDone chan bool - - sync.RWMutex + interval time.Duration + mux sync.RWMutex } // New create a new Cron struct with default tick interval of 1 minute @@ -50,10 +49,10 @@ func New() *Cron { // (it usually should be >= 1 minute). func (c *Cron) SetInterval(d time.Duration) { // update interval - c.Lock() + c.mux.Lock() wasStarted := c.ticker != nil c.interval = d - c.Unlock() + c.mux.Unlock() // restart the ticker if wasStarted { @@ -63,8 +62,8 @@ func (c *Cron) SetInterval(d time.Duration) { // SetTimezone changes the current cron tick timezone. func (c *Cron) SetTimezone(l *time.Location) { - c.Lock() - defer c.Unlock() + c.mux.Lock() + defer c.mux.Unlock() c.timezone = l } @@ -88,8 +87,8 @@ func (c *Cron) Add(jobId string, cronExpr string, run func()) error { return errors.New("failed to add new cron job: run must be non-nil function") } - c.Lock() - defer c.Unlock() + c.mux.Lock() + defer c.mux.Unlock() schedule, err := NewSchedule(cronExpr) if err != nil { @@ -106,24 +105,24 @@ func (c *Cron) Add(jobId string, cronExpr string, run func()) error { // Remove removes a single cron job by its id. func (c *Cron) Remove(jobId string) { - c.Lock() - defer c.Unlock() + c.mux.Lock() + defer c.mux.Unlock() delete(c.jobs, jobId) } // RemoveAll removes all registered cron jobs. func (c *Cron) RemoveAll() { - c.Lock() - defer c.Unlock() + c.mux.Lock() + defer c.mux.Unlock() c.jobs = map[string]*job{} } // Total returns the current total number of registered cron jobs. func (c *Cron) Total() int { - c.RLock() - defer c.RUnlock() + c.mux.RLock() + defer c.mux.RUnlock() return len(c.jobs) } @@ -132,8 +131,8 @@ func (c *Cron) Total() int { // // You can resume the ticker by calling Start(). func (c *Cron) Stop() { - c.Lock() - defer c.Unlock() + c.mux.Lock() + defer c.mux.Unlock() if c.startTimer != nil { c.startTimer.Stop() @@ -160,11 +159,11 @@ func (c *Cron) Start() { next := now.Add(c.interval).Truncate(c.interval) delay := next.Sub(now) - c.Lock() + c.mux.Lock() c.startTimer = time.AfterFunc(delay, func() { - c.Lock() + c.mux.Lock() c.ticker = time.NewTicker(c.interval) - c.Unlock() + c.mux.Unlock() // run immediately at 00 c.runDue(time.Now()) @@ -181,21 +180,21 @@ func (c *Cron) Start() { } }() }) - c.Unlock() + c.mux.Unlock() } // HasStarted checks whether the current Cron ticker has been started. func (c *Cron) HasStarted() bool { - c.RLock() - defer c.RUnlock() + c.mux.RLock() + defer c.mux.RUnlock() return c.ticker != nil } // runDue runs all registered jobs that are scheduled for the provided time. func (c *Cron) runDue(t time.Time) { - c.RLock() - defer c.RUnlock() + c.mux.RLock() + defer c.mux.RUnlock() moment := NewMoment(t.In(c.timezone)) diff --git a/tools/cron/schedule_test.go b/tools/cron/schedule_test.go index 1901968e..edc38e11 100644 --- a/tools/cron/schedule_test.go +++ b/tools/cron/schedule_test.go @@ -2,6 +2,7 @@ package cron_test import ( "encoding/json" + "fmt" "testing" "time" @@ -252,26 +253,28 @@ func TestNewSchedule(t *testing.T) { } for _, s := range scenarios { - schedule, err := cron.NewSchedule(s.cronExpr) + t.Run(s.cronExpr, func(t *testing.T) { + schedule, err := cron.NewSchedule(s.cronExpr) - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("[%s] Expected hasErr to be %v, got %v (%v)", s.cronExpr, s.expectError, hasErr, err) - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err) + } - if hasErr { - continue - } + if hasErr { + return + } - encoded, err := json.Marshal(schedule) - if err != nil { - t.Fatalf("[%s] Failed to marshalize the result schedule: %v", s.cronExpr, err) - } - encodedStr := string(encoded) + encoded, err := json.Marshal(schedule) + if err != nil { + t.Fatalf("Failed to marshalize the result schedule: %v", err) + } + encodedStr := string(encoded) - if encodedStr != s.expectSchedule { - t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.cronExpr, s.expectSchedule, encodedStr) - } + if encodedStr != s.expectSchedule { + t.Fatalf("Expected \n%s, \ngot \n%s", s.expectSchedule, encodedStr) + } + }) } } @@ -390,15 +393,17 @@ func TestScheduleIsDue(t *testing.T) { } for i, s := range scenarios { - schedule, err := cron.NewSchedule(s.cronExpr) - if err != nil { - t.Fatalf("[%d-%s] Unexpected cron error: %v", i, s.cronExpr, err) - } + t.Run(fmt.Sprintf("%d-%s", i, s.cronExpr), func(t *testing.T) { + schedule, err := cron.NewSchedule(s.cronExpr) + if err != nil { + t.Fatalf("Unexpected cron error: %v", err) + } - result := schedule.IsDue(s.moment) + result := schedule.IsDue(s.moment) - if result != s.expected { - t.Fatalf("[%d-%s] Expected %v, got %v", i, s.cronExpr, s.expected, result) - } + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) } } diff --git a/tools/dbutils/json.go b/tools/dbutils/json.go index 47afe285..69e5e26a 100644 --- a/tools/dbutils/json.go +++ b/tools/dbutils/json.go @@ -5,31 +5,31 @@ import ( "strings" ) -// JsonEach returns JSON_EACH SQLite string expression with +// JSONEach returns JSON_EACH SQLite string expression with // some normalizations for non-json columns. -func JsonEach(column string) string { +func JSONEach(column string) string { return fmt.Sprintf( `json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`, column, column, column, ) } -// JsonArrayLength returns JSON_ARRAY_LENGTH SQLite string expression +// JSONArrayLength returns JSON_ARRAY_LENGTH SQLite string expression // with some normalizations for non-json columns. // // It works with both json and non-json column values. // // Returns 0 for empty string or NULL column values. -func JsonArrayLength(column string) string { +func JSONArrayLength(column string) string { return fmt.Sprintf( `json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`, column, column, column, column, column, ) } -// JsonExtract returns a JSON_EXTRACT SQLite string expression with +// JSONExtract returns a JSON_EXTRACT SQLite string expression with // some normalizations for non-json columns. -func JsonExtract(column string, path string) string { +func JSONExtract(column string, path string) string { // prefix the path with dot if it is not starting with array notation if path != "" && !strings.HasPrefix(path, "[") { path = "." + path diff --git a/tools/dbutils/json_test.go b/tools/dbutils/json_test.go index 6088f29e..015d8cdf 100644 --- a/tools/dbutils/json_test.go +++ b/tools/dbutils/json_test.go @@ -6,8 +6,8 @@ import ( "github.com/pocketbase/pocketbase/tools/dbutils" ) -func TestJsonEach(t *testing.T) { - result := dbutils.JsonEach("a.b") +func TestJSONEach(t *testing.T) { + result := dbutils.JSONEach("a.b") expected := "json_each(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE json_array([[a.b]]) END)" @@ -16,8 +16,8 @@ func TestJsonEach(t *testing.T) { } } -func TestJsonArrayLength(t *testing.T) { - result := dbutils.JsonArrayLength("a.b") +func TestJSONArrayLength(t *testing.T) { + result := dbutils.JSONArrayLength("a.b") expected := "json_array_length(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE (CASE WHEN [[a.b]] = '' OR [[a.b]] IS NULL THEN json_array() ELSE json_array([[a.b]]) END) END)" @@ -26,7 +26,7 @@ func TestJsonArrayLength(t *testing.T) { } } -func TestJsonExtract(t *testing.T) { +func TestJSONExtract(t *testing.T) { scenarios := []struct { name string column string @@ -55,12 +55,11 @@ func TestJsonExtract(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := dbutils.JsonExtract(s.column, s.path) + result := dbutils.JSONExtract(s.column, s.path) if result != s.expected { t.Fatalf("Expected\n%v\ngot\n%v", s.expected, result) } }) } - } diff --git a/tools/filesystem/file.go b/tools/filesystem/file.go index 93b868ba..21c87212 100644 --- a/tools/filesystem/file.go +++ b/tools/filesystem/file.go @@ -27,10 +27,20 @@ type FileReader interface { // // The file could be from a local path, multipart/form-data header, etc. type File struct { - Reader FileReader - Name string - OriginalName string - Size int64 + Reader FileReader `form:"-" json:"-" xml:"-"` + Name string `form:"name" json:"name" xml:"name"` + OriginalName string `form:"originalName" json:"originalName" xml:"originalName"` + Size int64 `form:"size" json:"size" xml:"size"` +} + +// AsMap implements [core.mapExtractor] and returns a value suitable +// to be used in an API rule expression. +func (f *File) AsMap() map[string]any { + return map[string]any{ + "name": f.Name, + "originalName": f.OriginalName, + "size": f.Size, + } } // NewFileFromPath creates a new File instance from the provided local file path. @@ -79,7 +89,7 @@ func NewFileFromMultipart(mh *multipart.FileHeader) (*File, error) { return f, nil } -// NewFileFromUrl creates a new File from the provided url by +// NewFileFromURL creates a new File from the provided url by // downloading the resource and load it as BytesReader. // // Example @@ -87,8 +97,8 @@ func NewFileFromMultipart(mh *multipart.FileHeader) (*File, error) { // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // defer cancel() // -// file, err := filesystem.NewFileFromUrl(ctx, "https://example.com/image.png") -func NewFileFromUrl(ctx context.Context, url string) (*File, error) { +// file, err := filesystem.NewFileFromURL(ctx, "https://example.com/image.png") +func NewFileFromURL(ctx context.Context, url string) (*File, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err @@ -168,6 +178,8 @@ func (r *bytesReadSeekCloser) Close() error { var extInvalidCharsRegex = regexp.MustCompile(`[^\w\.\*\-\+\=\#]+`) +const randomAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" + func normalizeName(fr FileReader, name string) string { // extension // --- @@ -187,7 +199,7 @@ func normalizeName(fr FileReader, name string) string { cleanName := inflector.Snakecase(strings.TrimSuffix(name, originalExt)) if length := len(cleanName); length < 3 { // the name is too short so we concatenate an additional random part - cleanName += security.RandomString(10) + cleanName += security.RandomStringWithAlphabet(10, randomAlphabet) } else if length > 100 { // keep only the first 100 characters (it is multibyte safe after Snakecase) cleanName = cleanName[:100] @@ -196,7 +208,7 @@ func normalizeName(fr FileReader, name string) string { return fmt.Sprintf( "%s_%s%s", cleanName, - security.RandomString(10), // ensure that there is always a random part + security.RandomStringWithAlphabet(10, randomAlphabet), // ensure that there is always a random part cleanExt, ) } diff --git a/tools/filesystem/file_test.go b/tools/filesystem/file_test.go index 5940bb1c..43ad31c1 100644 --- a/tools/filesystem/file_test.go +++ b/tools/filesystem/file_test.go @@ -12,11 +12,35 @@ import ( "strings" "testing" - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/filesystem" ) +func TestFileAsMap(t *testing.T) { + file, err := filesystem.NewFileFromBytes([]byte("test"), "test123.txt") + if err != nil { + t.Fatal(err) + } + + result := file.AsMap() + + if len(result) != 3 { + t.Fatalf("Expected map with %d keys, got\n%v", 3, result) + } + + if result["size"] != int64(4) { + t.Fatalf("Expected size %d, got %#v", 4, result["size"]) + } + + if str, ok := result["name"].(string); !ok || !strings.HasPrefix(str, "test123") { + t.Fatalf("Expected name to have prefix %q, got %#v", "test123", result["name"]) + } + + if result["originalName"] != "test123.txt" { + t.Fatalf("Expected originalName %q, got %#v", "test123.txt", result["originalName"]) + } +} + func TestNewFileFromPath(t *testing.T) { testDir := createTestDir(t) defer os.RemoveAll(testDir) @@ -83,7 +107,7 @@ func TestNewFileFromMultipart(t *testing.T) { } req := httptest.NewRequest("", "/", formData) - req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + req.Header.Set("Content-Type", mp.FormDataContentType()) req.ParseMultipartForm(32 << 20) _, mh, err := req.FormFile("test") @@ -115,7 +139,7 @@ func TestNewFileFromMultipart(t *testing.T) { } } -func TestNewFileFromUrlTimeout(t *testing.T) { +func TestNewFileFromURLTimeout(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/error" { w.WriteHeader(http.StatusInternalServerError) @@ -129,7 +153,7 @@ func TestNewFileFromUrlTimeout(t *testing.T) { { ctx, cancel := context.WithCancel(context.Background()) cancel() - f, err := filesystem.NewFileFromUrl(ctx, srv.URL+"/cancel") + f, err := filesystem.NewFileFromURL(ctx, srv.URL+"/cancel") if err == nil { t.Fatal("[ctx_cancel] Expected error, got nil") } @@ -140,7 +164,7 @@ func TestNewFileFromUrlTimeout(t *testing.T) { // error response { - f, err := filesystem.NewFileFromUrl(context.Background(), srv.URL+"/error") + f, err := filesystem.NewFileFromURL(context.Background(), srv.URL+"/error") if err == nil { t.Fatal("[error_status] Expected error, got nil") } @@ -154,7 +178,7 @@ func TestNewFileFromUrlTimeout(t *testing.T) { originalName := "image_! noext" normalizedNamePattern := regexp.QuoteMeta("image_noext_") + `\w{10}` + regexp.QuoteMeta(".txt") - f, err := filesystem.NewFileFromUrl(context.Background(), srv.URL+"/"+originalName) + f, err := filesystem.NewFileFromURL(context.Background(), srv.URL+"/"+originalName) if err != nil { t.Fatalf("[valid] Unexpected error %v", err) } diff --git a/tools/filesystem/filesystem.go b/tools/filesystem/filesystem.go index b3a3bc0c..be2473e8 100644 --- a/tools/filesystem/filesystem.go +++ b/tools/filesystem/filesystem.go @@ -20,13 +20,17 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/disintegration/imaging" "github.com/gabriel-vasile/mimetype" + "github.com/pocketbase/pocketbase/tools/filesystem/internal/s3lite" "github.com/pocketbase/pocketbase/tools/list" "gocloud.dev/blob" "gocloud.dev/blob/fileblob" + "gocloud.dev/gcerrors" ) var gcpIgnoreHeaders = []string{"Accept-Encoding"} +var ErrNotFound = errors.New("blob not found") + type System struct { ctx context.Context bucket *blob.Bucket @@ -47,25 +51,23 @@ func NewS3( cred := credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") - cfg, err := config.LoadDefaultConfig(ctx, + cfg, err := config.LoadDefaultConfig( + ctx, config.WithCredentialsProvider(cred), config.WithRegion(region), - config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { - // ensure that the endpoint has url scheme for - // backward compatibility with v1 of the aws sdk - prefixedEndpoint := endpoint - if !strings.Contains(endpoint, "://") { - prefixedEndpoint = "https://" + endpoint - } - - return aws.Endpoint{URL: prefixedEndpoint, SigningRegion: region}, nil - })), ) if err != nil { return nil, err } client := s3.NewFromConfig(cfg, func(o *s3.Options) { + // ensure that the endpoint has url scheme for + // backward compatibility with v1 of the aws sdk + if !strings.Contains(endpoint, "://") { + endpoint = "https://" + endpoint + } + o.BaseEndpoint = aws.String(endpoint) + o.UsePathStyle = s3ForcePathStyle // Google Cloud Storage alters the Accept-Encoding header, @@ -76,7 +78,7 @@ func NewS3( } }) - bucket, err := OpenBucketV2(ctx, client, bucketName, nil) + bucket, err := s3lite.OpenBucketV2(ctx, client, bucketName, nil) if err != nil { return nil, err } @@ -116,32 +118,59 @@ func (s *System) Close() error { } // Exists checks if file with fileKey path exists or not. +// +// If the file doesn't exist returns false and ErrNotFound. func (s *System) Exists(fileKey string) (bool, error) { - return s.bucket.Exists(s.ctx, fileKey) + exists, err := s.bucket.Exists(s.ctx, fileKey) + + if gcerrors.Code(err) == gcerrors.NotFound { + err = ErrNotFound + } + + return exists, err } // Attributes returns the attributes for the file with fileKey path. +// +// If the file doesn't exist it returns ErrNotFound. func (s *System) Attributes(fileKey string) (*blob.Attributes, error) { - return s.bucket.Attributes(s.ctx, fileKey) + attrs, err := s.bucket.Attributes(s.ctx, fileKey) + + if gcerrors.Code(err) == gcerrors.NotFound { + err = ErrNotFound + } + + return attrs, err } // GetFile returns a file content reader for the given fileKey. // -// NB! Make sure to call `Close()` after you are done working with it. +// NB! Make sure to call Close() on the file after you are done working with it. +// +// If the file doesn't exist returns ErrNotFound. func (s *System) GetFile(fileKey string) (*blob.Reader, error) { br, err := s.bucket.NewReader(s.ctx, fileKey, nil) - if err != nil { - return nil, err + + if gcerrors.Code(err) == gcerrors.NotFound { + err = ErrNotFound } - return br, nil + return br, err } // Copy copies the file stored at srcKey to dstKey. // +// If srcKey file doesn't exist, it returns ErrNotFound. +// // If dstKey file already exists, it is overwritten. func (s *System) Copy(srcKey, dstKey string) error { - return s.bucket.Copy(s.ctx, dstKey, srcKey, nil) + err := s.bucket.Copy(s.ctx, dstKey, srcKey, nil) + + if gcerrors.Code(err) == gcerrors.NotFound { + err = ErrNotFound + } + + return err } // List returns a flat list with info for all files under the specified prefix. @@ -178,14 +207,13 @@ func (s *System) Upload(content []byte, fileKey string) error { } if _, err := w.Write(content); err != nil { - w.Close() - return err + return errors.Join(err, w.Close()) } return w.Close() } -// UploadFile uploads the provided multipart file to the fileKey location. +// UploadFile uploads the provided File to the fileKey location. func (s *System) UploadFile(file *File, fileKey string) error { f, err := file.Reader.Open() if err != nil { @@ -270,8 +298,16 @@ func (s *System) UploadMultipart(fh *multipart.FileHeader, fileKey string) error } // Delete deletes stored file at fileKey location. +// +// If the file doesn't exist returns ErrNotFound. func (s *System) Delete(fileKey string) error { - return s.bucket.Delete(s.ctx, fileKey) + err := s.bucket.Delete(s.ctx, fileKey) + + if gcerrors.Code(err) == gcerrors.NotFound { + return ErrNotFound + } + + return err } // DeletePrefix deletes everything starting with the specified prefix. @@ -345,6 +381,26 @@ func (s *System) DeletePrefix(prefix string) []error { return failed } +// Checks if the provided dir prefix doesn't have any files. +// +// A trailing slash will be appended to a non-empty dir string argument +// to ensure that the checked prefix is a "directory". +// +// Returns "false" in case the has at least one file, otherwise - "true". +func (s *System) IsEmptyDir(dir string) bool { + if dir != "" && !strings.HasSuffix(dir, "/") { + dir += "/" + } + + iter := s.bucket.List(&blob.ListOptions{ + Prefix: dir, + }) + + _, err := iter.Next(s.ctx) + + return err == io.EOF +} + var inlineServeContentTypes = []string{ // image "image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/x-icon", "image/bmp", @@ -371,8 +427,11 @@ const forceAttachmentParam = "download" // // If the `download` query parameter is used the file will be always served for // download no matter of its type (aka. with "Content-Disposition: attachment"). +// +// Internally this method uses [http.ServeContent] so Range requests, +// If-Match, If-Unmodified-Since, etc. headers are handled transparently. func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey string, name string) error { - br, readErr := s.bucket.NewReader(s.ctx, fileKey, nil) + br, readErr := s.GetFile(fileKey) if readErr != nil { return readErr } @@ -444,7 +503,7 @@ func (s *System) CreateThumb(originalKey string, thumbKey, thumbSize string) err } // fetch the original - r, readErr := s.bucket.NewReader(s.ctx, originalKey, nil) + r, readErr := s.GetFile(originalKey) if readErr != nil { return readErr } diff --git a/tools/filesystem/filesystem_test.go b/tools/filesystem/filesystem_test.go index 57e880ca..c618b186 100644 --- a/tools/filesystem/filesystem_test.go +++ b/tools/filesystem/filesystem_test.go @@ -2,6 +2,7 @@ package filesystem_test import ( "bytes" + "errors" "image" "image/png" "mime/multipart" @@ -19,11 +20,11 @@ func TestFileSystemExists(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() scenarios := []struct { file string @@ -35,12 +36,18 @@ func TestFileSystemExists(t *testing.T) { {"image.png", true}, } - for i, scenario := range scenarios { - exists, _ := fs.Exists(scenario.file) + for _, s := range scenarios { + t.Run(s.file, func(t *testing.T) { + exists, err := fsys.Exists(s.file) - if exists != scenario.exists { - t.Errorf("(%d) Expected %v, got %v", i, scenario.exists, exists) - } + if err != nil { + t.Fatal(err) + } + + if exists != s.exists { + t.Fatalf("Expected exists %v, got %v", s.exists, exists) + } + }) } } @@ -48,11 +55,11 @@ func TestFileSystemAttributes(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() scenarios := []struct { file string @@ -65,20 +72,24 @@ func TestFileSystemAttributes(t *testing.T) { {"image.png", false, "image/png"}, } - for i, scenario := range scenarios { - attr, err := fs.Attributes(scenario.file) + for _, s := range scenarios { + t.Run(s.file, func(t *testing.T) { + attr, err := fsys.Attributes(s.file) - if err == nil && scenario.expectError { - t.Errorf("(%d) Expected error, got nil", i) - } + hasErr := err != nil - if err != nil && !scenario.expectError { - t.Errorf("(%d) Expected nil, got error, %v", i, err) - } + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } - if err == nil && attr.ContentType != scenario.expectContentType { - t.Errorf("(%d) Expected attr.ContentType to be %q, got %q", i, scenario.expectContentType, attr.ContentType) - } + if hasErr && !errors.Is(err, filesystem.ErrNotFound) { + t.Fatalf("Expected ErrNotFound err, got %q", err) + } + + if !hasErr && attr.ContentType != s.expectContentType { + t.Fatalf("Expected attr.ContentType to be %q, got %q", s.expectContentType, attr.ContentType) + } + }) } } @@ -86,17 +97,17 @@ func TestFileSystemDelete(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() - if err := fs.Delete("missing.txt"); err == nil { - t.Fatal("Expected error, got nil") + if err := fsys.Delete("missing.txt"); err == nil || !errors.Is(err, filesystem.ErrNotFound) { + t.Fatalf("Expected ErrNotFound error, got %v", err) } - if err := fs.Delete("image.png"); err != nil { + if err := fsys.Delete("image.png"); err != nil { t.Fatalf("Expected nil, got error %v", err) } } @@ -105,29 +116,29 @@ func TestFileSystemDeletePrefixWithoutTrailingSlash(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() - if errs := fs.DeletePrefix(""); len(errs) == 0 { + if errs := fsys.DeletePrefix(""); len(errs) == 0 { t.Fatal("Expected error, got nil", errs) } - if errs := fs.DeletePrefix("missing"); len(errs) != 0 { + if errs := fsys.DeletePrefix("missing"); len(errs) != 0 { t.Fatalf("Not existing prefix shouldn't error, got %v", errs) } - if errs := fs.DeletePrefix("test"); len(errs) != 0 { + if errs := fsys.DeletePrefix("test"); len(errs) != 0 { t.Fatalf("Expected nil, got errors %v", errs) } // ensure that the test/* files are deleted - if exists, _ := fs.Exists("test/sub1.txt"); exists { + if exists, _ := fsys.Exists("test/sub1.txt"); exists { t.Fatalf("Expected test/sub1.txt to be deleted") } - if exists, _ := fs.Exists("test/sub2.txt"); exists { + if exists, _ := fsys.Exists("test/sub2.txt"); exists { t.Fatalf("Expected test/sub2.txt to be deleted") } @@ -141,25 +152,25 @@ func TestFileSystemDeletePrefixWithTrailingSlash(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() - if errs := fs.DeletePrefix("missing/"); len(errs) != 0 { + if errs := fsys.DeletePrefix("missing/"); len(errs) != 0 { t.Fatalf("Not existing prefix shouldn't error, got %v", errs) } - if errs := fs.DeletePrefix("test/"); len(errs) != 0 { + if errs := fsys.DeletePrefix("test/"); len(errs) != 0 { t.Fatalf("Expected nil, got errors %v", errs) } // ensure that the test/* files are deleted - if exists, _ := fs.Exists("test/sub1.txt"); exists { + if exists, _ := fsys.Exists("test/sub1.txt"); exists { t.Fatalf("Expected test/sub1.txt to be deleted") } - if exists, _ := fs.Exists("test/sub2.txt"); exists { + if exists, _ := fsys.Exists("test/sub2.txt"); exists { t.Fatalf("Expected test/sub2.txt to be deleted") } @@ -169,6 +180,41 @@ func TestFileSystemDeletePrefixWithTrailingSlash(t *testing.T) { } } +func TestFileSystemIsEmptyDir(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fsys, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fsys.Close() + + scenarios := []struct { + dir string + expected bool + }{ + {"", false}, // special case that shouldn't be suffixed with delimiter to search for any files within the bucket + {"/", true}, + {"missing", true}, + {"missing/", true}, + {"test", false}, + {"test/", false}, + {"empty", true}, + {"empty/", true}, + } + + for _, s := range scenarios { + t.Run(s.dir, func(t *testing.T) { + result := fsys.IsEmptyDir(s.dir) + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + func TestFileSystemUploadMultipart(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) @@ -193,24 +239,24 @@ func TestFileSystemUploadMultipart(t *testing.T) { defer file.Close() // --- - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() fileKey := "newdir/newkey.txt" - uploadErr := fs.UploadMultipart(fh, fileKey) + uploadErr := fsys.UploadMultipart(fh, fileKey) if uploadErr != nil { t.Fatal(uploadErr) } - if exists, _ := fs.Exists(fileKey); !exists { + if exists, _ := fsys.Exists(fileKey); !exists { t.Fatalf("Expected %q to exist", fileKey) } - attrs, err := fs.Attributes(fileKey) + attrs, err := fsys.Attributes(fileKey) if err != nil { t.Fatalf("Failed to fetch file attributes: %v", err) } @@ -223,11 +269,11 @@ func TestFileSystemUploadFile(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() fileKey := "newdir/newkey.txt" @@ -238,16 +284,16 @@ func TestFileSystemUploadFile(t *testing.T) { file.OriginalName = "test.txt" - uploadErr := fs.UploadFile(file, fileKey) + uploadErr := fsys.UploadFile(file, fileKey) if uploadErr != nil { t.Fatal(uploadErr) } - if exists, _ := fs.Exists(fileKey); !exists { + if exists, _ := fsys.Exists(fileKey); !exists { t.Fatalf("Expected %q to exist", fileKey) } - attrs, err := fs.Attributes(fileKey) + attrs, err := fsys.Attributes(fileKey) if err != nil { t.Fatalf("Failed to fetch file attributes: %v", err) } @@ -260,20 +306,20 @@ func TestFileSystemUpload(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() fileKey := "newdir/newkey.txt" - uploadErr := fs.Upload([]byte("demo"), fileKey) + uploadErr := fsys.Upload([]byte("demo"), fileKey) if uploadErr != nil { t.Fatal(uploadErr) } - if exists, _ := fs.Exists(fileKey); !exists { + if exists, _ := fsys.Exists(fileKey); !exists { t.Fatalf("Expected %s to exist", fileKey) } } @@ -282,11 +328,11 @@ func TestFileSystemServe(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() csp := "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox" cacheControl := "max-age=2592000, stale-while-revalidate=86400" @@ -409,39 +455,41 @@ func TestFileSystemServe(t *testing.T) { } for _, s := range scenarios { - res := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) + t.Run(s.path, func(t *testing.T) { + res := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) - query := req.URL.Query() - for k, v := range s.query { - query.Set(k, v) - } - req.URL.RawQuery = query.Encode() - - for k, v := range s.headers { - res.Header().Set(k, v) - } - - err := fs.Serve(res, req, s.path, s.name) - hasErr := err != nil - - if hasErr != s.expectError { - t.Errorf("(%s) Expected hasError %v, got %v (%v)", s.path, s.expectError, hasErr, err) - continue - } - - if s.expectError { - continue - } - - result := res.Result() - - for hName, hValue := range s.expectHeaders { - v := result.Header.Get(hName) - if v != hValue { - t.Errorf("(%s) Expected value %q for header %q, got %q", s.path, hValue, hName, v) + query := req.URL.Query() + for k, v := range s.query { + query.Set(k, v) } - } + req.URL.RawQuery = query.Encode() + + for k, v := range s.headers { + res.Header().Set(k, v) + } + + err := fsys.Serve(res, req, s.path, s.name) + hasErr := err != nil + + if hasErr != s.expectError { + t.Fatalf("Expected hasError %v, got %v (%v)", s.expectError, hasErr, err) + } + + if s.expectError { + return + } + + result := res.Result() + defer result.Body.Close() + + for hName, hValue := range s.expectHeaders { + v := result.Header.Get(hName) + if v != hValue { + t.Errorf("Expected value %q for header %q, got %q", hValue, hName, v) + } + } + }) } } @@ -449,20 +497,38 @@ func TestFileSystemGetFile(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() - f, err := fs.GetFile("image.png") - if err != nil { - t.Fatal(err) + scenarios := []struct { + file string + expectError bool + }{ + {"missing.png", true}, + {"image.png", false}, } - defer f.Close() - if f == nil { - t.Fatal("File is supposed to be found") + for _, s := range scenarios { + t.Run(s.file, func(t *testing.T) { + f, err := fsys.GetFile(s.file) + + hasErr := err != nil + + if !hasErr { + defer f.Close() + } + + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } + + if hasErr && !errors.Is(err, filesystem.ErrNotFound) { + t.Fatalf("Expected ErrNotFound error, got %v", err) + } + }) } } @@ -470,25 +536,26 @@ func TestFileSystemCopy(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() src := "image.png" dst := "image.png_copy" // copy missing file - if err := fs.Copy(dst, src); err == nil { + if err := fsys.Copy(dst, src); err == nil { t.Fatalf("Expected to fail copying %q to %q, got nil", dst, src) } // copy existing file - if err := fs.Copy(src, dst); err != nil { + if err := fsys.Copy(src, dst); err != nil { t.Fatalf("Failed to copy %q to %q: %v", src, dst, err) } - f, err := fs.GetFile(dst) + f, err := fsys.GetFile(dst) + //nolint defer f.Close() if err != nil { t.Fatalf("Missing copied file %q: %v", dst, err) @@ -502,11 +569,11 @@ func TestFileSystemList(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() scenarios := []struct { prefix string @@ -537,7 +604,7 @@ func TestFileSystemList(t *testing.T) { } for _, s := range scenarios { - objs, err := fs.List(s.prefix) + objs, err := fsys.List(s.prefix) if err != nil { t.Fatalf("[%s] %v", s.prefix, err) } @@ -563,17 +630,17 @@ func TestFileSystemServeSingleRange(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Add("Range", "bytes=0-20") - if err := fs.Serve(res, req, "image.png", "image.png"); err != nil { + if err := fsys.Serve(res, req, "image.png", "image.png"); err != nil { t.Fatal(err) } @@ -597,17 +664,17 @@ func TestFileSystemServeMultiRange(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Add("Range", "bytes=0-20, 25-30") - if err := fs.Serve(res, req, "image.png", "image.png"); err != nil { + if err := fsys.Serve(res, req, "image.png", "image.png"); err != nil { t.Fatal(err) } @@ -626,11 +693,11 @@ func TestFileSystemCreateThumb(t *testing.T) { dir := createTestDir(t) defer os.RemoveAll(dir) - fs, err := filesystem.NewLocal(dir) + fsys, err := filesystem.NewLocal(dir) if err != nil { t.Fatal(err) } - defer fs.Close() + defer fsys.Close() scenarios := []struct { file string @@ -651,7 +718,7 @@ func TestFileSystemCreateThumb(t *testing.T) { } for i, scenario := range scenarios { - err := fs.CreateThumb(scenario.file, scenario.thumb, "100x100") + err := fsys.CreateThumb(scenario.file, scenario.thumb, "100x100") hasErr := err != nil if hasErr != scenario.expectError { @@ -663,7 +730,7 @@ func TestFileSystemCreateThumb(t *testing.T) { continue } - if exists, _ := fs.Exists(scenario.thumb); !exists { + if exists, _ := fsys.Exists(scenario.thumb); !exists { t.Errorf("(%d) Couldn't find %q thumb", i, scenario.thumb) } } @@ -677,6 +744,10 @@ func createTestDir(t *testing.T) string { t.Fatal(err) } + if err := os.MkdirAll(filepath.Join(dir, "empty"), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "test"), os.ModePerm); err != nil { t.Fatal(err) } diff --git a/tools/filesystem/s3_trimmed.go b/tools/filesystem/internal/s3lite/s3lite.go similarity index 82% rename from tools/filesystem/s3_trimmed.go rename to tools/filesystem/internal/s3lite/s3lite.go index a50e8658..926d7133 100644 --- a/tools/filesystem/s3_trimmed.go +++ b/tools/filesystem/internal/s3lite/s3lite.go @@ -66,7 +66,7 @@ // (V1) *s3.PutObjectInput; (V2) *s3v2.PutObjectInput, when Options.Method == http.MethodPut, or // (V1) *s3.DeleteObjectInput; (V2) [not supported] when Options.Method == http.MethodDelete -package filesystem +package s3lite import ( "context" @@ -82,7 +82,6 @@ import ( "strings" awsv2 "github.com/aws/aws-sdk-go-v2/aws" - awsv2cfg "github.com/aws/aws-sdk-go-v2/config" s3managerv2 "github.com/aws/aws-sdk-go-v2/feature/s3/manager" s3v2 "github.com/aws/aws-sdk-go-v2/service/s3" typesv2 "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -244,116 +243,8 @@ func URLUnescape(s string) string { // ------------------------------------------------------------------- -// UseV2 returns true iff the URL parameters indicate that the provider -// should use the AWS SDK v2. -// -// "awssdk=v1" will force V1. -// "awssdk=v2" will force V2. -// No "awssdk" parameter (or any other value) will return the default, currently V1. -// Note that the default may change in the future. -func UseV2(q url.Values) bool { - if values, ok := q["awssdk"]; ok { - if values[0] == "v2" || values[0] == "V2" { - return true - } - } - return false -} - -// NewDefaultV2Config returns a aws.Config for AWS SDK v2, using the default options. -func NewDefaultV2Config(ctx context.Context) (awsv2.Config, error) { - return awsv2cfg.LoadDefaultConfig(ctx) -} - -// V2ConfigFromURLParams returns an aws.Config for AWS SDK v2 initialized based on the URL -// parameters in q. It is intended to be used by URLOpeners for AWS services if -// UseV2 returns true. -// -// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws#Config -// -// It returns an error if q contains any unknown query parameters; callers -// should remove any query parameters they know about from q before calling -// V2ConfigFromURLParams. -// -// The following query options are supported: -// - region: The AWS region for requests; sets WithRegion. -// - profile: The shared config profile to use; sets SharedConfigProfile. -// - endpoint: The AWS service endpoint to send HTTP request. -func V2ConfigFromURLParams(ctx context.Context, q url.Values) (awsv2.Config, error) { - var opts []func(*awsv2cfg.LoadOptions) error - for param, values := range q { - value := values[0] - switch param { - case "region": - opts = append(opts, awsv2cfg.WithRegion(value)) - case "endpoint": - customResolver := awsv2.EndpointResolverWithOptionsFunc( - func(service, region string, options ...interface{}) (awsv2.Endpoint, error) { - return awsv2.Endpoint{ - PartitionID: "aws", - URL: value, - SigningRegion: region, - }, nil - }) - opts = append(opts, awsv2cfg.WithEndpointResolverWithOptions(customResolver)) - case "profile": - opts = append(opts, awsv2cfg.WithSharedConfigProfile(value)) - case "awssdk": - // ignore, should be handled before this - default: - return awsv2.Config{}, fmt.Errorf("unknown query parameter %q", param) - } - } - return awsv2cfg.LoadDefaultConfig(ctx, opts...) -} - -// ------------------------------------------------------------------- - const defaultPageSize = 1000 -func init() { - blob.DefaultURLMux().RegisterBucket(Scheme, new(urlSessionOpener)) -} - -type urlSessionOpener struct{} - -func (o *urlSessionOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { - opener := &URLOpener{UseV2: true} - return opener.OpenBucketURL(ctx, u) -} - -// Scheme is the URL scheme s3blob registers its URLOpener under on -// blob.DefaultMux. -const Scheme = "s3" - -// URLOpener opens S3 URLs like "s3://mybucket". -// -// The URL host is used as the bucket name. -// -// Use "awssdk=v1" to force using AWS SDK v1, "awssdk=v2" to force using AWS SDK v2, -// or anything else to accept the default. -// -// For V1, see gocloud.dev/aws/ConfigFromURLParams for supported query parameters -// for overriding the aws.Session from the URL. -// For V2, see gocloud.dev/aws/V2ConfigFromURLParams. -type URLOpener struct { - // UseV2 indicates whether the AWS SDK V2 should be used. - UseV2 bool - - // Options specifies the options to pass to OpenBucket. - Options Options -} - -// OpenBucketURL opens a blob.Bucket based on u. -func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { - cfg, err := V2ConfigFromURLParams(ctx, u.Query()) - if err != nil { - return nil, fmt.Errorf("open bucket %v: %v", u, err) - } - clientV2 := s3v2.NewFromConfig(cfg) - return OpenBucketV2(ctx, clientV2, u.Host, &o.Options) -} - // Options sets options for constructing a *blob.Bucket backed by fileblob. type Options struct { // UseLegacyList forces the use of ListObjects instead of ListObjectsV2. @@ -676,64 +567,6 @@ func (b *bucket) listObjectsV2(ctx context.Context, in *s3v2.ListObjectsV2Input, }, nil } -// func (b *bucket) listObjects(ctx context.Context, in *s3.ListObjectsV2Input, opts *driver.ListOptions) (*s3.ListObjectsV2Output, error) { -// if !b.useLegacyList { -// if opts.BeforeList != nil { -// asFunc := func(i interface{}) bool { -// if p, ok := i.(**s3.ListObjectsV2Input); ok { -// *p = in -// return true -// } -// return false -// } -// if err := opts.BeforeList(asFunc); err != nil { -// return nil, err -// } -// } -// return b.client.ListObjectsV2WithContext(ctx, in) -// } - -// // Use the legacy ListObjects request. -// legacyIn := &s3.ListObjectsInput{ -// Bucket: in.Bucket, -// Delimiter: in.Delimiter, -// EncodingType: in.EncodingType, -// Marker: in.ContinuationToken, -// MaxKeys: in.MaxKeys, -// Prefix: in.Prefix, -// RequestPayer: in.RequestPayer, -// } -// if opts.BeforeList != nil { -// asFunc := func(i interface{}) bool { -// p, ok := i.(**s3.ListObjectsInput) -// if !ok { -// return false -// } -// *p = legacyIn -// return true -// } -// if err := opts.BeforeList(asFunc); err != nil { -// return nil, err -// } -// } -// legacyResp, err := b.client.ListObjectsWithContext(ctx, legacyIn) -// if err != nil { -// return nil, err -// } - -// var nextContinuationToken *string -// if legacyResp.NextMarker != nil { -// nextContinuationToken = legacyResp.NextMarker -// } else if awsv2.ToBool(legacyResp.IsTruncated) { -// nextContinuationToken = awsv2.String(awsv2.ToString(legacyResp.Contents[len(legacyResp.Contents)-1].Key)) -// } -// return &s3.ListObjectsV2Output{ -// CommonPrefixes: legacyResp.CommonPrefixes, -// Contents: legacyResp.Contents, -// NextContinuationToken: nextContinuationToken, -// }, nil -// } - // As implements driver.As. func (b *bucket) As(i interface{}) bool { p, ok := i.(**s3v2.Client) diff --git a/tools/hook/event.go b/tools/hook/event.go new file mode 100644 index 00000000..12bceecc --- /dev/null +++ b/tools/hook/event.go @@ -0,0 +1,45 @@ +package hook + +// Resolver defines a common interface for a Hook event (see [Event]). +type Resolver interface { + // Next triggers the next handler in the hook's chain (if any). + Next() error + + // note: kept only for the generic interface; may get removed in the future + nextFunc() func() error + setNextFunc(f func() error) +} + +var _ Resolver = (*Event)(nil) + +// Event implements [Resolver] and it is intended to be used as a base +// Hook event that you can embed in your custom typed event structs. +// +// Example: +// +// type CustomEvent struct { +// hook.Event +// +// SomeField int +// } +type Event struct { + next func() error +} + +// Next calls the next hook handler. +func (e *Event) Next() error { + if e.next != nil { + return e.next() + } + return nil +} + +// nextFunc returns the function that Next calls. +func (e *Event) nextFunc() func() error { + return e.next +} + +// setNextFunc sets the function that Next calls. +func (e *Event) setNextFunc(f func() error) { + e.next = f +} diff --git a/tools/hook/event_test.go b/tools/hook/event_test.go new file mode 100644 index 00000000..89b15efe --- /dev/null +++ b/tools/hook/event_test.go @@ -0,0 +1,29 @@ +package hook + +import "testing" + +func TestEventNext(t *testing.T) { + calls := 0 + + e := Event{} + + if e.nextFunc() != nil { + t.Fatalf("Expected nextFunc to be nil") + } + + e.setNextFunc(func() error { + calls++ + return nil + }) + + if e.nextFunc() == nil { + t.Fatalf("Expected nextFunc to be non-nil") + } + + e.Next() + e.Next() + + if calls != 2 { + t.Fatalf("Expected %d calls, got %d", 2, calls) + } +} diff --git a/tools/hook/hook.go b/tools/hook/hook.go index eb6780ee..de15a07d 100644 --- a/tools/hook/hook.go +++ b/tools/hook/hook.go @@ -1,126 +1,179 @@ package hook import ( - "errors" - "fmt" + "sort" "sync" "github.com/pocketbase/pocketbase/tools/security" ) -var StopPropagation = errors.New("Event hook propagation stopped") +// HandlerFunc defines a hook handler function. +type HandlerFunc[T Resolver] func(e T) error -// Handler defines a hook handler function. -type Handler[T any] func(e T) error +// Handler defines a single Hook handler. +// Multiple handlers can share the same id. +// If Id is not explicitly set it will be autogenerated by Hook.Add and Hook.AddHandler. +type Handler[T Resolver] struct { + // Func defines the handler function to execute. + // + // Note that users need to call e.Next() in order to proceed with + // the execution of the hook chain. + Func HandlerFunc[T] -// handlerPair defines a pair of string id and Handler. -type handlerPair[T any] struct { - id string - handler Handler[T] + // Id is the unique identifier of the handler. + // + // It could be used later to remove the handler from a hook via [Hook.Remove]. + // + // If missing, an autogenerated value will be assigned when adding + // the handler to a hook. + Id string + + // Priority allows changing the default exec priority of the handler within a hook. + // + // If 0, the handler will be executed in the same order it was registered. + Priority int } -// Hook defines a concurrent safe structure for handling event hooks -// (aka. callbacks propagation). -type Hook[T any] struct { - mux sync.RWMutex - handlers []*handlerPair[T] -} - -// PreAdd registers a new handler to the hook by prepending it to the existing queue. +// Hook defines a generic concurrent safe structure for managing event hooks. // -// Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id). -func (h *Hook[T]) PreAdd(fn Handler[T]) string { - h.mux.Lock() - defer h.mux.Unlock() - - id := generateHookId() - - // minimize allocations by shifting the slice - h.handlers = append(h.handlers, nil) - copy(h.handlers[1:], h.handlers) - h.handlers[0] = &handlerPair[T]{id, fn} - - return id -} - -// Add registers a new handler to the hook by appending it to the existing queue. +// When using custom a event it must embed the base [hook.Event]. // -// Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id). -func (h *Hook[T]) Add(fn Handler[T]) string { - h.mux.Lock() - defer h.mux.Unlock() - - id := generateHookId() - - h.handlers = append(h.handlers, &handlerPair[T]{id, fn}) - - return id +// Example: +// +// type CustomEvent struct { +// hook.Event +// SomeField int +// } +// +// h := Hook[*CustomEvent]{} +// +// h.BindFunc(func(e *CustomEvent) error { +// println(e.SomeField) +// +// return e.Next() +// }) +// +// h.Trigger(&CustomEvent{ SomeField: 123 }) +type Hook[T Resolver] struct { + handlers []*Handler[T] + mu sync.RWMutex } -// Remove removes a single hook handler by its id. -func (h *Hook[T]) Remove(id string) { - h.mux.Lock() - defer h.mux.Unlock() +// Bind registers the provided handler to the current hooks queue. +// +// If handler.Id is empty it is updated with autogenerated value. +// +// If a handler from the current hook list has Id matching handler.Id +// then the old handler is replaced with the new one. +func (h *Hook[T]) Bind(handler *Handler[T]) string { + h.mu.Lock() + defer h.mu.Unlock() + + var exists bool + + if handler.Id == "" { + handler.Id = generateHookId() + + // ensure that it doesn't exist + DUPLICATE_CHECK: + for _, existing := range h.handlers { + if existing.Id == handler.Id { + handler.Id = generateHookId() + goto DUPLICATE_CHECK + } + } + } else { + // replace existing + for i, existing := range h.handlers { + if existing.Id == handler.Id { + h.handlers[i] = handler + exists = true + break + } + } + } + + // append new + if !exists { + h.handlers = append(h.handlers, handler) + } + + // sort handlers by Priority, preserving the original order of equal items + sort.SliceStable(h.handlers, func(i, j int) bool { + return h.handlers[i].Priority < h.handlers[j].Priority + }) + + return handler.Id +} + +// BindFunc is similar to Bind but registers a new handler from just the provided function. +// +// 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 HandlerFunc[T]) string { + return h.Bind(&Handler[T]{Func: fn}) +} + +// Unbind removes a single hook handler by its id. +func (h *Hook[T]) Unbind(id string) { + h.mu.Lock() + defer h.mu.Unlock() for i := len(h.handlers) - 1; i >= 0; i-- { - if h.handlers[i].id == id { + if h.handlers[i].Id == id { h.handlers = append(h.handlers[:i], h.handlers[i+1:]...) - return + break // for now stop on the first occurrence since we don't allow handlers with duplicated ids } } } -// RemoveAll removes all registered handlers. -func (h *Hook[T]) RemoveAll() { - h.mux.Lock() - defer h.mux.Unlock() +// UnbindAll removes all registered handlers. +func (h *Hook[T]) UnbindAll() { + h.mu.Lock() + defer h.mu.Unlock() h.handlers = nil } +// Length returns to total number of registered hook handlers. +func (h *Hook[T]) Length() int { + h.mu.RLock() + defer h.mu.RUnlock() + + return len(h.handlers) +} + // Trigger executes all registered hook handlers one by one -// with the specified `data` as an argument. +// with the specified event as an argument. // // Optionally, this method allows also to register additional one off // handlers that will be temporary appended to the handlers queue. // -// The execution stops when: -// - hook.StopPropagation is returned in one of the handlers -// - any non-nil error is returned in one of the handlers -func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error { - h.mux.RLock() +// NB! Each hook handler must call event.Next() in order the hook chain to proceed. +func (h *Hook[T]) Trigger(event T, oneOffHandlers ...HandlerFunc[T]) error { + h.mu.RLock() + handlers := make([]HandlerFunc[T], 0, len(h.handlers)+len(oneOffHandlers)) + for _, handler := range h.handlers { + handlers = append(handlers, handler.Func) + } + handlers = append(handlers, oneOffHandlers...) + h.mu.RUnlock() - handlers := make([]*handlerPair[T], 0, len(h.handlers)+len(oneOffHandlers)) - handlers = append(handlers, h.handlers...) + event.setNextFunc(nil) // reset in case the event is being reused - // append the one off handlers - for i, oneOff := range oneOffHandlers { - handlers = append(handlers, &handlerPair[T]{ - id: fmt.Sprintf("@%d", i), - handler: oneOff, + for i := len(handlers) - 1; i >= 0; i-- { + i := i + old := event.nextFunc() + event.setNextFunc(func() error { + event.setNextFunc(old) + return handlers[i](event) }) } - // unlock is not deferred to avoid deadlocks in case Trigger - // is called recursively by the handlers - h.mux.RUnlock() - - for _, item := range handlers { - err := item.handler(data) - if err == nil { - continue - } - - if errors.Is(err, StopPropagation) { - return nil - } - - return err - } - - return nil + return event.Next() } func generateHookId() string { - return security.PseudorandomString(8) + return security.PseudorandomString(20) } diff --git a/tools/hook/hook_test.go b/tools/hook/hook_test.go index a3ab688c..50d596d2 100644 --- a/tools/hook/hook_test.go +++ b/tools/hook/hook_test.go @@ -5,175 +5,157 @@ import ( "testing" ) -func TestHookAddAndPreAdd(t *testing.T) { - h := Hook[int]{} +func TestHookAddHandlerAndAdd(t *testing.T) { + calls := "" - if total := len(h.handlers); total != 0 { - t.Fatalf("Expected no handlers, found %d", total) + h := Hook[*Event]{} + + h.BindFunc(func(e *Event) error { calls += "1"; return e.Next() }) + h.BindFunc(func(e *Event) error { calls += "2"; return e.Next() }) + h3Id := h.BindFunc(func(e *Event) error { calls += "3"; return e.Next() }) + h.Bind(&Handler[*Event]{ + Id: h3Id, // should replace 3 + Func: func(e *Event) error { calls += "3'"; return e.Next() }, + }) + h.Bind(&Handler[*Event]{ + Func: func(e *Event) error { calls += "4"; return e.Next() }, + Priority: -2, + }) + h.Bind(&Handler[*Event]{ + Func: func(e *Event) error { calls += "5"; return e.Next() }, + Priority: -1, + }) + h.Bind(&Handler[*Event]{ + Func: func(e *Event) error { calls += "6"; return e.Next() }, + }) + h.Bind(&Handler[*Event]{ + Func: func(e *Event) error { calls += "7"; e.Next(); return errors.New("test") }, // error shouldn't stop the chain + }) + + h.Trigger( + &Event{}, + func(e *Event) error { calls += "8"; return e.Next() }, + func(e *Event) error { calls += "9"; return nil }, // skip next + func(e *Event) error { calls += "10"; return e.Next() }, + ) + + if total := len(h.handlers); total != 7 { + t.Fatalf("Expected %d handlers, found %d", 7, total) } - triggerSequence := "" + expectedCalls := "45123'6789" - f1 := func(data int) error { triggerSequence += "f1"; return nil } - f2 := func(data int) error { triggerSequence += "f2"; return nil } - f3 := func(data int) error { triggerSequence += "f3"; return nil } - f4 := func(data int) error { triggerSequence += "f4"; return nil } - - h.Add(f1) - h.Add(f2) - h.PreAdd(f3) - h.PreAdd(f4) - h.Trigger(1) - - if total := len(h.handlers); total != 4 { - t.Fatalf("Expected %d handlers, found %d", 4, total) - } - - expectedTriggerSequence := "f4f3f1f2" - - if triggerSequence != expectedTriggerSequence { - t.Fatalf("Expected trigger sequence %s, got %s", expectedTriggerSequence, triggerSequence) + if calls != expectedCalls { + t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls) } } -func TestHookRemove(t *testing.T) { - h := Hook[int]{} +func TestHookLength(t *testing.T) { + h := Hook[*Event]{} - h1Called := false - h2Called := false + if l := h.Length(); l != 0 { + t.Fatalf("Expected 0 hook handlers, got %d", l) + } - id1 := h.Add(func(data int) error { h1Called = true; return nil }) - h.Add(func(data int) error { h2Called = true; return nil }) + h.BindFunc(func(e *Event) error { return e.Next() }) + h.BindFunc(func(e *Event) error { return e.Next() }) - h.Remove("missing") // should do nothing and not panic + if l := h.Length(); l != 2 { + t.Fatalf("Expected 2 hook handlers, got %d", l) + } +} + +func TestHookUnbind(t *testing.T) { + h := Hook[*Event]{} + + calls := "" + + id1 := h.BindFunc(func(e *Event) error { calls += "1"; return e.Next() }) + h.BindFunc(func(e *Event) error { calls += "2"; return e.Next() }) + h.Bind(&Handler[*Event]{ + Func: func(e *Event) error { calls += "3"; return e.Next() }, + }) + + h.Unbind("missing") // should do nothing and not panic + + if total := len(h.handlers); total != 3 { + t.Fatalf("Expected %d handlers, got %d", 3, total) + } + + h.Unbind(id1) if total := len(h.handlers); total != 2 { t.Fatalf("Expected %d handlers, got %d", 2, total) } - h.Remove(id1) - - if total := len(h.handlers); total != 1 { - t.Fatalf("Expected %d handlers, got %d", 1, total) - } - - if err := h.Trigger(1); err != nil { + err := h.Trigger(&Event{}, func(e *Event) error { calls += "4"; return e.Next() }) + if err != nil { t.Fatal(err) } - if h1Called { - t.Fatalf("Expected hook 1 to be removed and not called") - } + expectedCalls := "234" - if !h2Called { - t.Fatalf("Expected hook 2 to be called") + if calls != expectedCalls { + t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls) } } -func TestHookRemoveAll(t *testing.T) { - h := Hook[int]{} +func TestHookUnbindAll(t *testing.T) { + h := Hook[*Event]{} - h.RemoveAll() // should do nothing and not panic + h.UnbindAll() // should do nothing and not panic - h.Add(func(data int) error { return nil }) - h.Add(func(data int) error { return nil }) + h.BindFunc(func(e *Event) error { return nil }) + h.BindFunc(func(e *Event) error { return nil }) if total := len(h.handlers); total != 2 { - t.Fatalf("Expected 2 handlers before RemoveAll, found %d", total) + t.Fatalf("Expected %d handlers before UnbindAll, found %d", 2, total) } - h.RemoveAll() + h.UnbindAll() if total := len(h.handlers); total != 0 { - t.Fatalf("Expected no handlers after RemoveAll, found %d", total) + t.Fatalf("Expected no handlers after UnbindAll, found %d", total) } } -func TestHookTrigger(t *testing.T) { - err1 := errors.New("demo") - err2 := errors.New("demo") +func TestHookTriggerErrorPropagation(t *testing.T) { + err := errors.New("test") scenarios := []struct { - handlers []Handler[int] + name string + handlers []HandlerFunc[*Event] expectedError error }{ { - []Handler[int]{ - func(data int) error { return nil }, - func(data int) error { return nil }, + "without error", + []HandlerFunc[*Event]{ + func(e *Event) error { return e.Next() }, + func(e *Event) error { return e.Next() }, }, nil, }, { - []Handler[int]{ - func(data int) error { return nil }, - func(data int) error { return err1 }, - func(data int) error { return err2 }, + "with error", + []HandlerFunc[*Event]{ + func(e *Event) error { return e.Next() }, + func(e *Event) error { e.Next(); return err }, + func(e *Event) error { return e.Next() }, }, - err1, + err, }, } - for i, scenario := range scenarios { - h := Hook[int]{} - for _, handler := range scenario.handlers { - h.Add(handler) - } - result := h.Trigger(1) - if result != scenario.expectedError { - t.Fatalf("(%d) Expected %v, got %v", i, scenario.expectedError, result) - } - } -} - -func TestHookTriggerStopPropagation(t *testing.T) { - called1 := false - f1 := func(data int) error { called1 = true; return nil } - - called2 := false - f2 := func(data int) error { called2 = true; return nil } - - called3 := false - f3 := func(data int) error { called3 = true; return nil } - - called4 := false - f4 := func(data int) error { called4 = true; return StopPropagation } - - called5 := false - f5 := func(data int) error { called5 = true; return nil } - - called6 := false - f6 := func(data int) error { called6 = true; return nil } - - h := Hook[int]{} - h.Add(f1) - h.Add(f2) - - result := h.Trigger(123, f3, f4, f5, f6) - - if result != nil { - t.Fatalf("Expected nil after StopPropagation, got %v", result) - } - - // ensure that the trigger handler were not persisted - if total := len(h.handlers); total != 2 { - t.Fatalf("Expected 2 handlers, found %d", total) - } - - scenarios := []struct { - called bool - expected bool - }{ - {called1, true}, - {called2, true}, - {called3, true}, - {called4, true}, // StopPropagation - {called5, false}, - {called6, false}, - } - for i, scenario := range scenarios { - if scenario.called != scenario.expected { - t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, scenario.called) - } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + h := Hook[*Event]{} + for _, handler := range s.handlers { + h.BindFunc(handler) + } + result := h.Trigger(&Event{}) + if result != s.expectedError { + t.Fatalf("Expected %v, got %v", s.expectedError, result) + } + }) } } diff --git a/tools/hook/tagged.go b/tools/hook/tagged.go index f0906f3f..77413861 100644 --- a/tools/hook/tagged.go +++ b/tools/hook/tagged.go @@ -7,6 +7,8 @@ import ( // Tagger defines an interface for event data structs that support tags/groups/categories/etc. // Usually used together with TaggedHook. type Tagger interface { + Resolver + Tags() []string } @@ -33,12 +35,14 @@ type TaggedHook[T Tagger] struct { // CanTriggerOn checks if the current TaggedHook can be triggered with // the provided event data tags. -func (h *TaggedHook[T]) CanTriggerOn(tags []string) bool { +// +// It returns always true if the hook doens't have any tags. +func (h *TaggedHook[T]) CanTriggerOn(tagsToCheck []string) bool { if len(h.tags) == 0 { return true // match all } - for _, t := range tags { + for _, t := range tagsToCheck { if list.ExistInSlice(t, h.tags) { return true } @@ -47,28 +51,34 @@ func (h *TaggedHook[T]) CanTriggerOn(tags []string) bool { return false } -// PreAdd registers a new handler to the hook by prepending it to the existing queue. +// Bind registers the provided handler to the current hooks queue. // -// The fn handler will be called only if the event data tags satisfy h.CanTriggerOn. -func (h *TaggedHook[T]) PreAdd(fn Handler[T]) string { - return h.mainHook.PreAdd(func(e T) error { +// 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]) Bind(handler *Handler[T]) string { + fn := handler.Func + + handler.Func = func(e T) error { if h.CanTriggerOn(e.Tags()) { return fn(e) } - return nil - }) + return e.Next() + } + + return h.mainHook.Bind(handler) } -// Add registers a new handler to the hook by appending it to the existing queue. +// BindFunc registers a new handler with the specified function. // -// The fn handler will be called only if the event data tags satisfy h.CanTriggerOn. -func (h *TaggedHook[T]) Add(fn Handler[T]) string { - return h.mainHook.Add(func(e T) error { +// 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 HandlerFunc[T]) string { + return h.mainHook.BindFunc(func(e T) error { if h.CanTriggerOn(e.Tags()) { return fn(e) } - return nil + return e.Next() }) } diff --git a/tools/hook/tagged_test.go b/tools/hook/tagged_test.go index 84b2fa44..7fa4ce34 100644 --- a/tools/hook/tagged_test.go +++ b/tools/hook/tagged_test.go @@ -1,69 +1,84 @@ package hook -import "testing" +import ( + "strings" + "testing" +) -type mockTagsData struct { +type mockTagsEvent struct { + Event tags []string } -func (m mockTagsData) Tags() []string { +func (m mockTagsEvent) Tags() []string { return m.tags } func TestTaggedHook(t *testing.T) { - triggerSequence := "" + calls := "" - base := &Hook[mockTagsData]{} - base.Add(func(data mockTagsData) error { triggerSequence += "f0"; return nil }) + base := &Hook[*mockTagsEvent]{} + base.BindFunc(func(e *mockTagsEvent) error { calls += "f0"; return e.Next() }) hA := NewTaggedHook(base) - hA.Add(func(data mockTagsData) error { triggerSequence += "a1"; return nil }) - hA.PreAdd(func(data mockTagsData) error { triggerSequence += "a2"; return nil }) + hA.BindFunc(func(e *mockTagsEvent) error { calls += "a1"; return e.Next() }) + hA.Bind(&Handler[*mockTagsEvent]{ + Func: func(e *mockTagsEvent) error { calls += "a2"; return e.Next() }, + Priority: -1, + }) hB := NewTaggedHook(base, "b1", "b2") - hB.Add(func(data mockTagsData) error { triggerSequence += "b1"; return nil }) - hB.PreAdd(func(data mockTagsData) error { triggerSequence += "b2"; return nil }) + hB.BindFunc(func(e *mockTagsEvent) error { calls += "b1"; return e.Next() }) + hB.Bind(&Handler[*mockTagsEvent]{ + Func: func(e *mockTagsEvent) error { calls += "b2"; return e.Next() }, + Priority: -2, + }) hC := NewTaggedHook(base, "c1", "c2") - hC.Add(func(data mockTagsData) error { triggerSequence += "c1"; return nil }) - hC.PreAdd(func(data mockTagsData) error { triggerSequence += "c2"; return nil }) + hC.BindFunc(func(e *mockTagsEvent) error { calls += "c1"; return e.Next() }) + hC.Bind(&Handler[*mockTagsEvent]{ + Func: func(e *mockTagsEvent) error { calls += "c2"; return e.Next() }, + Priority: -3, + }) scenarios := []struct { - data mockTagsData - expectedSequence string + event *mockTagsEvent + expectedCalls string }{ { - mockTagsData{}, + &mockTagsEvent{}, "a2f0a1", }, { - mockTagsData{[]string{"missing"}}, + &mockTagsEvent{tags: []string{"missing"}}, "a2f0a1", }, { - mockTagsData{[]string{"b2"}}, + &mockTagsEvent{tags: []string{"b2"}}, "b2a2f0a1b1", }, { - mockTagsData{[]string{"c1"}}, + &mockTagsEvent{tags: []string{"c1"}}, "c2a2f0a1c1", }, { - mockTagsData{[]string{"b1", "c2"}}, + &mockTagsEvent{tags: []string{"b1", "c2"}}, "c2b2a2f0a1b1c1", }, } - for i, s := range scenarios { - triggerSequence = "" // reset + for _, s := range scenarios { + t.Run(strings.Join(s.event.tags, "_"), func(t *testing.T) { + calls = "" // reset - err := hA.Trigger(s.data) - if err != nil { - t.Fatalf("[%d] Unexpected trigger error: %v", i, err) - } + err := base.Trigger(s.event) + if err != nil { + t.Fatalf("Unexpected trigger error: %v", err) + } - if triggerSequence != s.expectedSequence { - t.Fatalf("[%d] Expected trigger sequence %s, got %s", i, s.expectedSequence, triggerSequence) - } + if calls != s.expectedCalls { + t.Fatalf("Expected calls sequence %q, got %q", s.expectedCalls, calls) + } + }) } } diff --git a/tools/inflector/inflector_test.go b/tools/inflector/inflector_test.go index a4d21223..fef9f1df 100644 --- a/tools/inflector/inflector_test.go +++ b/tools/inflector/inflector_test.go @@ -1,6 +1,7 @@ package inflector_test import ( + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/inflector" @@ -18,10 +19,13 @@ func TestUcFirst(t *testing.T) { {"test test2", "Test test2"}, } - for i, scenario := range scenarios { - if result := inflector.UcFirst(scenario.val); result != scenario.expected { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) - } + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + result := inflector.UcFirst(s.val) + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } @@ -42,10 +46,13 @@ func TestColumnify(t *testing.T) { {"test1--test2", "test1--test2"}, } - for i, scenario := range scenarios { - if result := inflector.Columnify(scenario.val); result != scenario.expected { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) - } + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + result := inflector.Columnify(s.val) + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } @@ -67,10 +74,13 @@ func TestSentenize(t *testing.T) { {"hello world?", "Hello world?"}, } - for i, scenario := range scenarios { - if result := inflector.Sentenize(scenario.val); result != scenario.expected { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) - } + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + result := inflector.Sentenize(s.val) + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } @@ -89,21 +99,19 @@ func TestSanitize(t *testing.T) { {"abcABC", `[A-Z`, "", true}, // invalid pattern } - for i, scenario := range scenarios { - result, err := inflector.Sanitize(scenario.val, scenario.pattern) - hasErr := err != nil + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + result, err := inflector.Sanitize(s.val, s.pattern) + hasErr := err != nil - if scenario.expectErr != hasErr { - if scenario.expectErr { - t.Errorf("(%d) Expected error, got nil", i) - } else { - t.Errorf("(%d) Didn't expect error, got", err) + if s.expectErr != hasErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectErr, hasErr, err) } - } - if result != scenario.expected { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) - } + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } @@ -126,9 +134,12 @@ func TestSnakecase(t *testing.T) { {"testABR", "test_abr"}, } - for i, scenario := range scenarios { - if result := inflector.Snakecase(scenario.val); result != scenario.expected { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) - } + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) { + result := inflector.Snakecase(s.val) + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } diff --git a/tools/list/list.go b/tools/list/list.go index 049fbdde..aff00597 100644 --- a/tools/list/list.go +++ b/tools/list/list.go @@ -39,7 +39,7 @@ func ExistInSlice[T comparable](item T, list []T) bool { // ExistInSliceWithRegex checks whether a string exists in a slice // either by direct match, or by a regular expression (eg. `^\w+$`). // -// _Note: Only list items starting with '^' and ending with '$' are treated as regular expressions!_ +// Note: Only list items starting with '^' and ending with '$' are treated as regular expressions! func ExistInSliceWithRegex(str string, list []string) bool { for _, field := range list { isRegex := strings.HasPrefix(field, "^") && strings.HasSuffix(field, "$") @@ -64,7 +64,7 @@ func ExistInSliceWithRegex(str string, list []string) bool { // (the limit size is arbitrary and it is there to prevent the cache growing too big) // // @todo consider replacing with TTL or LRU type cache - cachedPatterns.SetIfLessThanLimit(field, pattern, 5000) + cachedPatterns.SetIfLessThanLimit(field, pattern, 500) } if pattern != nil && pattern.MatchString(str) { @@ -129,7 +129,7 @@ func ToUniqueStringSlice(value any) (result []string) { // just add the string as single array element result = append(result, val) } - case json.Marshaler: // eg. JsonArray + case json.Marshaler: // eg. JSONArray raw, _ := val.MarshalJSON() _ = json.Unmarshal(raw, &result) default: @@ -138,3 +138,26 @@ func ToUniqueStringSlice(value any) (result []string) { return NonzeroUniques(result) } + +// ToChunks splits list into chunks. +// +// Zero or negative chunkSize argument is normalized to 1. +// +// See https://go.dev/wiki/SliceTricks#batching-with-minimal-allocation. +func ToChunks[T any](list []T, chunkSize int) [][]T { + if chunkSize <= 0 { + chunkSize = 1 + } + + chunks := make([][]T, 0, (len(list)+chunkSize-1)/chunkSize) + + if len(list) == 0 { + return chunks + } + + for chunkSize < len(list) { + list, chunks = list[chunkSize:], append(chunks, list[0:chunkSize:chunkSize]) + } + + return append(chunks, list) +} diff --git a/tools/list/list_test.go b/tools/list/list_test.go index 4270335e..d169a75f 100644 --- a/tools/list/list_test.go +++ b/tools/list/list_test.go @@ -2,6 +2,7 @@ package list_test import ( "encoding/json" + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/list" @@ -42,18 +43,20 @@ func TestSubtractSliceString(t *testing.T) { } for i, s := range scenarios { - result := list.SubtractSlice(s.base, s.subtract) + t.Run(fmt.Sprintf("%d_%s", i, s.expected), func(t *testing.T) { + result := list.SubtractSlice(s.base, s.subtract) - raw, err := json.Marshal(result) - if err != nil { - t.Fatalf("(%d) Failed to serialize: %v", i, err) - } + raw, err := json.Marshal(result) + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } - strResult := string(raw) + strResult := string(raw) - if strResult != s.expected { - t.Fatalf("(%d) Expected %v, got %v", i, s.expected, strResult) - } + if strResult != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, strResult) + } + }) } } @@ -91,18 +94,20 @@ func TestSubtractSliceInt(t *testing.T) { } for i, s := range scenarios { - result := list.SubtractSlice(s.base, s.subtract) + t.Run(fmt.Sprintf("%d_%s", i, s.expected), func(t *testing.T) { + result := list.SubtractSlice(s.base, s.subtract) - raw, err := json.Marshal(result) - if err != nil { - t.Fatalf("(%d) Failed to serialize: %v", i, err) - } + raw, err := json.Marshal(result) + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } - strResult := string(raw) + strResult := string(raw) - if strResult != s.expected { - t.Fatalf("(%d) Expected %v, got %v", i, s.expected, strResult) - } + if strResult != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, strResult) + } + }) } } @@ -120,15 +125,13 @@ func TestExistInSliceString(t *testing.T) { {"test", []string{"1", "2", "test"}, true}, } - for i, scenario := range scenarios { - result := list.ExistInSlice(scenario.item, scenario.list) - if result != scenario.expected { - if scenario.expected { - t.Errorf("(%d) Expected to exist in the list", i) - } else { - t.Errorf("(%d) Expected NOT to exist in the list", i) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.item), func(t *testing.T) { + result := list.ExistInSlice(s.item, s.list) + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) } - } + }) } } @@ -146,15 +149,13 @@ func TestExistInSliceInt(t *testing.T) { {-1, []int{0, -1, -2, -3, -4}, true}, } - for i, scenario := range scenarios { - result := list.ExistInSlice(scenario.item, scenario.list) - if result != scenario.expected { - if scenario.expected { - t.Errorf("(%d) Expected to exist in the list", i) - } else { - t.Errorf("(%d) Expected NOT to exist in the list", i) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%d", i, s.item), func(t *testing.T) { + result := list.ExistInSlice(s.item, s.list) + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) } - } + }) } } @@ -177,15 +178,13 @@ func TestExistInSliceWithRegex(t *testing.T) { {"!?@test", []string{`^\W+$`, "test"}, false}, } - for i, scenario := range scenarios { - result := list.ExistInSliceWithRegex(scenario.item, scenario.list) - if result != scenario.expected { - if scenario.expected { - t.Errorf("(%d) Expected the string to exist in the list", i) - } else { - t.Errorf("(%d) Expected the string NOT to exist in the list", i) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.item), func(t *testing.T) { + result := list.ExistInSliceWithRegex(s.item, s.list) + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) } - } + }) } } @@ -196,21 +195,23 @@ func TestToInterfaceSlice(t *testing.T) { {[]string{}}, {[]string{""}}, {[]string{"1", "test"}}, - {[]string{"test1", "test2", "test3"}}, + {[]string{"test1", "test1", "test2", "test3"}}, } - for i, scenario := range scenarios { - result := list.ToInterfaceSlice(scenario.items) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.items), func(t *testing.T) { + result := list.ToInterfaceSlice(s.items) - if len(result) != len(scenario.items) { - t.Errorf("(%d) Result list length doesn't match with the original list", i) - } - - for j, v := range result { - if v != scenario.items[j] { - t.Errorf("(%d:%d) Result list item should match with the original list item", i, j) + if len(result) != len(s.items) { + t.Fatalf("Expected length %d, got %d", len(s.items), len(result)) } - } + + for j, v := range result { + if v != s.items[j] { + t.Fatalf("Result list item doesn't match with the original list item, got %v VS %v", v, s.items[j]) + } + } + }) } } @@ -225,18 +226,20 @@ func TestNonzeroUniquesString(t *testing.T) { {[]string{"test1", "", "test2", "Test2", "test1", "test3"}, []string{"test1", "test2", "Test2", "test3"}}, } - for i, scenario := range scenarios { - result := list.NonzeroUniques(scenario.items) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.items), func(t *testing.T) { + result := list.NonzeroUniques(s.items) - if len(result) != len(scenario.expected) { - t.Errorf("(%d) Result list length doesn't match with the expected list", i) - } - - for j, v := range result { - if v != scenario.expected[j] { - t.Errorf("(%d:%d) Result list item should match with the expected list item", i, j) + if len(result) != len(s.expected) { + t.Fatalf("Expected length %d, got %d", len(s.expected), len(result)) } - } + + for j, v := range result { + if v != s.expected[j] { + t.Fatalf("Result list item doesn't match with the expected list item, got %v VS %v", v, s.expected[j]) + } + } + }) } } @@ -254,20 +257,54 @@ func TestToUniqueStringSlice(t *testing.T) { {[]any{0, 1, "test", ""}, []string{"0", "1", "test"}}, {[]string{"test1", "test2", "test1"}, []string{"test1", "test2"}}, {`["test1", "test2", "test2"]`, []string{"test1", "test2"}}, - {types.JsonArray[string]{"test1", "test2", "test1"}, []string{"test1", "test2"}}, + {types.JSONArray[string]{"test1", "test2", "test1"}, []string{"test1", "test2"}}, } - for i, scenario := range scenarios { - result := list.ToUniqueStringSlice(scenario.value) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + result := list.ToUniqueStringSlice(s.value) - if len(result) != len(scenario.expected) { - t.Errorf("(%d) Result list length doesn't match with the expected list", i) - } - - for j, v := range result { - if v != scenario.expected[j] { - t.Errorf("(%d:%d) Result list item should match with the expected list item", i, j) + if len(result) != len(s.expected) { + t.Fatalf("Expected length %d, got %d", len(s.expected), len(result)) } - } + + for j, v := range result { + if v != s.expected[j] { + t.Fatalf("Result list item doesn't match with the expected list item, got %v vs %v", v, s.expected[j]) + } + } + }) + } +} + +func TestToChunks(t *testing.T) { + scenarios := []struct { + items []any + chunkSize int + expected string + }{ + {nil, 2, "[]"}, + {[]any{}, 2, "[]"}, + {[]any{1, 2, 3, 4}, -1, "[[1],[2],[3],[4]]"}, + {[]any{1, 2, 3, 4}, 0, "[[1],[2],[3],[4]]"}, + {[]any{1, 2, 3, 4}, 2, "[[1,2],[3,4]]"}, + {[]any{1, 2, 3, 4, 5}, 2, "[[1,2],[3,4],[5]]"}, + {[]any{1, 2, 3, 4, 5}, 10, "[[1,2,3,4,5]]"}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.items), func(t *testing.T) { + result := list.ToChunks(s.items, s.chunkSize) + + raw, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, rawStr) + } + }) } } diff --git a/tools/logger/batch_handler.go b/tools/logger/batch_handler.go index 5ee25a05..e189e418 100644 --- a/tools/logger/batch_handler.go +++ b/tools/logger/batch_handler.go @@ -2,9 +2,11 @@ package logger import ( "context" + "errors" "log/slog" "sync" + validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/tools/types" ) @@ -160,7 +162,6 @@ func (h *BatchHandler) Handle(ctx context.Context, r slog.Record) error { if err := h.resolveAttr(data, a); err != nil { return false } - return true }) @@ -168,7 +169,7 @@ func (h *BatchHandler) Handle(ctx context.Context, r slog.Record) error { Time: r.Time, Level: r.Level, Message: r.Message, - Data: types.JsonMap(data), + Data: types.JSONMap[any](data), } if h.options.BeforeAddFunc != nil && !h.options.BeforeAddFunc(ctx, log) { @@ -251,11 +252,23 @@ func (h *BatchHandler) resolveAttr(data map[string]any, attr slog.Attr) error { data[attr.Key] = groupData } default: - v := attr.Value.Any() - - if err, ok := v.(error); ok { - data[attr.Key] = err.Error() - } else { + switch v := attr.Value.Any().(type) { + case validation.Errors: + data[attr.Key] = map[string]any{ + "data": v, + "raw": v.Error(), + } + case error: + var ve validation.Errors + if errors.As(v, &ve) { + data[attr.Key] = map[string]any{ + "data": ve, + "raw": v.Error(), + } + } else { + data[attr.Key] = v.Error() + } + default: data[attr.Key] = v } } diff --git a/tools/logger/batch_handler_test.go b/tools/logger/batch_handler_test.go index f73652e9..bd958336 100644 --- a/tools/logger/batch_handler_test.go +++ b/tools/logger/batch_handler_test.go @@ -181,11 +181,8 @@ func TestBatchHandlerHandle(t *testing.T) { BeforeAddFunc: func(_ context.Context, log *Log) bool { beforeLogs = append(beforeLogs, log) - if log.Message == "test2" { - return false // skip test2 log - } - - return true + // skip test2 log + return log.Message != "test2" }, WriteFunc: func(_ context.Context, logs []*Log) error { writeLogs = logs diff --git a/tools/logger/log.go b/tools/logger/log.go index 6a9b506d..e017ff18 100644 --- a/tools/logger/log.go +++ b/tools/logger/log.go @@ -11,7 +11,7 @@ import ( // preformatted JSON map. type Log struct { Time time.Time + Data types.JSONMap[any] Message string Level slog.Level - Data types.JsonMap } diff --git a/tools/mailer/html2text_test.go b/tools/mailer/html2text_test.go index eb8f4d44..d6304d23 100644 --- a/tools/mailer/html2text_test.go +++ b/tools/mailer/html2text_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestHtml2Text(t *testing.T) { +func TestHTML2Text(t *testing.T) { scenarios := []struct { html string expected string diff --git a/tools/mailer/mailer.go b/tools/mailer/mailer.go index b64da898..1b3c9ed7 100644 --- a/tools/mailer/mailer.go +++ b/tools/mailer/mailer.go @@ -3,6 +3,8 @@ package mailer import ( "io" "net/mail" + + "github.com/pocketbase/pocketbase/tools/hook" ) // Message defines a generic email message struct. @@ -24,6 +26,16 @@ type Mailer interface { Send(message *Message) error } +// SendInterceptor is optional interface for registering mail send hooks. +type SendInterceptor interface { + OnSend() *hook.Hook[*SendEvent] +} + +type SendEvent struct { + hook.Event + Message *Message +} + // addressesToStrings converts the provided address to a list of serialized RFC 5322 strings. // // To export only the email part of mail.Address, you can set withName to false. diff --git a/tools/mailer/sendmail.go b/tools/mailer/sendmail.go index 6826a036..b9e9c4ea 100644 --- a/tools/mailer/sendmail.go +++ b/tools/mailer/sendmail.go @@ -7,6 +7,8 @@ import ( "net/http" "os/exec" "strings" + + "github.com/pocketbase/pocketbase/tools/hook" ) var _ Mailer = (*Sendmail)(nil) @@ -16,10 +18,29 @@ var _ Mailer = (*Sendmail)(nil) // // This client is usually recommended only for development and testing. type Sendmail struct { + onSend *hook.Hook[*SendEvent] } -// Send implements `mailer.Mailer` interface. +// OnSend implements [mailer.SendInterceptor] interface. +func (c *Sendmail) OnSend() *hook.Hook[*SendEvent] { + if c.onSend == nil { + c.onSend = &hook.Hook[*SendEvent]{} + } + return c.onSend +} + +// Send implements [mailer.Mailer] interface. func (c *Sendmail) Send(m *Message) error { + if c.onSend != nil { + return c.onSend.Trigger(&SendEvent{Message: m}, func(e *SendEvent) error { + return c.send(e.Message) + }) + } + + return c.send(m) +} + +func (c *Sendmail) send(m *Message) error { toAddresses := addressesToStrings(m.To, false) headers := make(http.Header) @@ -74,5 +95,5 @@ func findSendmailPath() (string, error) { } } - return "", errors.New("failed to locate a sendmail executable path") + return "", errors.New("Failed to locate a sendmail executable path.") } diff --git a/tools/mailer/smtp.go b/tools/mailer/smtp.go index 8c97b4aa..8bf25ed6 100644 --- a/tools/mailer/smtp.go +++ b/tools/mailer/smtp.go @@ -7,43 +7,27 @@ import ( "strings" "github.com/domodwyer/mailyak/v3" + "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/security" ) -var _ Mailer = (*SmtpClient)(nil) +var _ Mailer = (*SMTPClient)(nil) const ( - SmtpAuthPlain = "PLAIN" - SmtpAuthLogin = "LOGIN" + SMTPAuthPlain = "PLAIN" + SMTPAuthLogin = "LOGIN" ) -// Deprecated: Use directly the SmtpClient struct literal. -// -// NewSmtpClient creates new SmtpClient with the provided configuration. -func NewSmtpClient( - host string, - port int, - username string, - password string, - tls bool, -) *SmtpClient { - return &SmtpClient{ - Host: host, - Port: port, - Username: username, - Password: password, - Tls: tls, - } -} - -// SmtpClient defines a SMTP mail client structure that implements +// SMTPClient defines a SMTP mail client structure that implements // `mailer.Mailer` interface. -type SmtpClient struct { - Host string +type SMTPClient struct { + onSend *hook.Hook[*SendEvent] + + TLS bool Port int + Host string Username string Password string - Tls bool // SMTP auth method to use // (if not explicitly set, defaults to "PLAIN") @@ -56,12 +40,30 @@ type SmtpClient struct { LocalName string } -// Send implements `mailer.Mailer` interface. -func (c *SmtpClient) Send(m *Message) error { +// OnSend implements [mailer.SendInterceptor] interface. +func (c *SMTPClient) OnSend() *hook.Hook[*SendEvent] { + if c.onSend == nil { + c.onSend = &hook.Hook[*SendEvent]{} + } + return c.onSend +} + +// Send implements [mailer.Mailer] interface. +func (c *SMTPClient) Send(m *Message) error { + if c.onSend != nil { + return c.onSend.Trigger(&SendEvent{Message: m}, func(e *SendEvent) error { + return c.send(e.Message) + }) + } + + return c.send(m) +} + +func (c *SMTPClient) send(m *Message) error { var smtpAuth smtp.Auth if c.Username != "" || c.Password != "" { switch c.AuthMethod { - case SmtpAuthLogin: + case SMTPAuthLogin: smtpAuth = &smtpLoginAuth{c.Username, c.Password} default: smtpAuth = smtp.PlainAuth("", c.Username, c.Password, c.Host) @@ -70,7 +72,7 @@ func (c *SmtpClient) Send(m *Message) error { // create mail instance var yak *mailyak.MailYak - if c.Tls { + if c.TLS { var tlsErr error yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil) if tlsErr != nil { diff --git a/tools/mailer/smtp_test.go b/tools/mailer/smtp_test.go index 072ae14d..49797d5b 100644 --- a/tools/mailer/smtp_test.go +++ b/tools/mailer/smtp_test.go @@ -56,24 +56,26 @@ func TestLoginAuthStart(t *testing.T) { } for _, s := range scenarios { - method, resp, err := auth.Start(s.serverInfo) + t.Run(s.name, func(t *testing.T) { + method, resp, err := auth.Start(s.serverInfo) - hasErr := err != nil - if hasErr != s.expectError { - t.Fatalf("[%s] Expected hasErr %v, got %v", s.name, s.expectError, hasErr) - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr) + } - if hasErr { - continue - } + if hasErr { + return + } - if len(resp) != 0 { - t.Fatalf("[%s] Expected empty data response, got %v", s.name, resp) - } + if len(resp) != 0 { + t.Fatalf("Expected empty data response, got %v", resp) + } - if method != "LOGIN" { - t.Fatalf("[%s] Expected LOGIN, got %v", s.name, method) - } + if method != "LOGIN" { + t.Fatalf("Expected LOGIN, got %v", method) + } + }) } } diff --git a/tools/migrate/list_test.go b/tools/migrate/list_test.go deleted file mode 100644 index 5030e882..00000000 --- a/tools/migrate/list_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package migrate - -import ( - "testing" -) - -func TestMigrationsList(t *testing.T) { - l := MigrationsList{} - - l.Register(nil, nil, "3_test.go") - l.Register(nil, nil, "1_test.go") - l.Register(nil, nil, "2_test.go") - l.Register(nil, nil /* auto detect file name */) - - expected := []string{ - "1_test.go", - "2_test.go", - "3_test.go", - "list_test.go", - } - - items := l.Items() - if len(items) != len(expected) { - t.Fatalf("Expected %d items, got %d: \n%#v", len(expected), len(items), items) - } - - for i, name := range expected { - item := l.Item(i) - if item.File != name { - t.Fatalf("Expected name %s for index %d, got %s", name, i, item.File) - } - } -} diff --git a/tools/migrate/runner.go b/tools/migrate/runner.go deleted file mode 100644 index 0e23c191..00000000 --- a/tools/migrate/runner.go +++ /dev/null @@ -1,275 +0,0 @@ -package migrate - -import ( - "fmt" - "strings" - "time" - - "github.com/AlecAivazis/survey/v2" - "github.com/fatih/color" - "github.com/pocketbase/dbx" - "github.com/spf13/cast" -) - -const DefaultMigrationsTable = "_migrations" - -// Runner defines a simple struct for managing the execution of db migrations. -type Runner struct { - db *dbx.DB - migrationsList MigrationsList - tableName string -} - -// NewRunner creates and initializes a new db migrations Runner instance. -func NewRunner(db *dbx.DB, migrationsList MigrationsList) (*Runner, error) { - runner := &Runner{ - db: db, - migrationsList: migrationsList, - tableName: DefaultMigrationsTable, - } - - if err := runner.createMigrationsTable(); err != nil { - return nil, err - } - - return runner, nil -} - -// Run interactively executes the current runner with the provided args. -// -// The following commands are supported: -// - up - applies all migrations -// - down [n] - reverts the last n applied migrations -func (r *Runner) Run(args ...string) error { - cmd := "up" - if len(args) > 0 { - cmd = args[0] - } - - switch cmd { - case "up": - applied, err := r.Up() - if err != nil { - return err - } - - if len(applied) == 0 { - color.Green("No new migrations to apply.") - } else { - for _, file := range applied { - color.Green("Applied %s", file) - } - } - - return nil - case "down": - toRevertCount := 1 - if len(args) > 1 { - toRevertCount = cast.ToInt(args[1]) - if toRevertCount < 0 { - // revert all applied migrations - toRevertCount = len(r.migrationsList.Items()) - } - } - - names, err := r.lastAppliedMigrations(toRevertCount) - if err != nil { - return err - } - - confirm := false - prompt := &survey.Confirm{ - Message: fmt.Sprintf( - "\n%v\nDo you really want to revert the last %d applied migration(s)?", - strings.Join(names, "\n"), - toRevertCount, - ), - } - survey.AskOne(prompt, &confirm) - if !confirm { - fmt.Println("The command has been cancelled") - return nil - } - - reverted, err := r.Down(toRevertCount) - if err != nil { - return err - } - - if len(reverted) == 0 { - color.Green("No migrations to revert.") - } else { - for _, file := range reverted { - color.Green("Reverted %s", file) - } - } - - return nil - case "history-sync": - if err := r.removeMissingAppliedMigrations(); err != nil { - return err - } - - color.Green("The %s table was synced with the available migrations.", r.tableName) - return nil - default: - return fmt.Errorf("Unsupported command: %q\n", cmd) - } -} - -// Up executes all unapplied migrations for the provided runner. -// -// On success returns list with the applied migrations file names. -func (r *Runner) Up() ([]string, error) { - applied := []string{} - - err := r.db.Transactional(func(tx *dbx.Tx) error { - for _, m := range r.migrationsList.Items() { - // skip applied - if r.isMigrationApplied(tx, m.File) { - continue - } - - // ignore empty Up action - if m.Up != nil { - if err := m.Up(tx); err != nil { - return fmt.Errorf("Failed to apply migration %s: %w", m.File, err) - } - } - - if err := r.saveAppliedMigration(tx, m.File); err != nil { - return fmt.Errorf("Failed to save applied migration info for %s: %w", m.File, err) - } - - applied = append(applied, m.File) - } - - return nil - }) - - if err != nil { - return nil, err - } - return applied, nil -} - -// Down reverts the last `toRevertCount` applied migrations -// (in the order they were applied). -// -// On success returns list with the reverted migrations file names. -func (r *Runner) Down(toRevertCount int) ([]string, error) { - reverted := make([]string, 0, toRevertCount) - - names, appliedErr := r.lastAppliedMigrations(toRevertCount) - if appliedErr != nil { - return nil, appliedErr - } - - err := r.db.Transactional(func(tx *dbx.Tx) error { - for _, name := range names { - for _, m := range r.migrationsList.Items() { - if m.File != name { - continue - } - - // revert limit reached - if toRevertCount-len(reverted) <= 0 { - return nil - } - - // ignore empty Down action - if m.Down != nil { - if err := m.Down(tx); err != nil { - return fmt.Errorf("Failed to revert migration %s: %w", m.File, err) - } - } - - if err := r.saveRevertedMigration(tx, m.File); err != nil { - return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err) - } - - reverted = append(reverted, m.File) - } - } - - return nil - }) - - if err != nil { - return nil, err - } - - return reverted, nil -} - -func (r *Runner) createMigrationsTable() error { - rawQuery := fmt.Sprintf( - "CREATE TABLE IF NOT EXISTS %v (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)", - r.db.QuoteTableName(r.tableName), - ) - - _, err := r.db.NewQuery(rawQuery).Execute() - - return err -} - -func (r *Runner) isMigrationApplied(tx dbx.Builder, file string) bool { - var exists bool - - err := tx.Select("count(*)"). - From(r.tableName). - Where(dbx.HashExp{"file": file}). - Limit(1). - Row(&exists) - - return err == nil && exists -} - -func (r *Runner) saveAppliedMigration(tx dbx.Builder, file string) error { - _, err := tx.Insert(r.tableName, dbx.Params{ - "file": file, - "applied": time.Now().UnixMicro(), - }).Execute() - - return err -} - -func (r *Runner) saveRevertedMigration(tx dbx.Builder, file string) error { - _, err := tx.Delete(r.tableName, dbx.HashExp{"file": file}).Execute() - - return err -} - -func (r *Runner) lastAppliedMigrations(limit int) ([]string, error) { - var files = make([]string, 0, limit) - - err := r.db.Select("file"). - From(r.tableName). - Where(dbx.Not(dbx.HashExp{"applied": nil})). - // unify microseconds and seconds applied time for backward compatibility - OrderBy("substr(applied||'0000000000000000', 0, 17) DESC"). - AndOrderBy("file DESC"). - Limit(int64(limit)). - Column(&files) - - if err != nil { - return nil, err - } - - return files, nil -} - -func (r *Runner) removeMissingAppliedMigrations() error { - loadedMigrations := r.migrationsList.Items() - - names := make([]any, len(loadedMigrations)) - for i, migration := range loadedMigrations { - names[i] = migration.File - } - - _, err := r.db.Delete(r.tableName, dbx.Not(dbx.HashExp{ - "file": names, - })).Execute() - - return err -} diff --git a/tools/migrate/runner_test.go b/tools/migrate/runner_test.go deleted file mode 100644 index 935199f6..00000000 --- a/tools/migrate/runner_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package migrate - -import ( - "context" - "database/sql" - "encoding/json" - "testing" - "time" - - "github.com/pocketbase/dbx" - "github.com/pocketbase/pocketbase/tools/list" - _ "modernc.org/sqlite" -) - -func TestNewRunner(t *testing.T) { - testDB, err := createTestDB() - if err != nil { - t.Fatal(err) - } - defer testDB.Close() - - l := MigrationsList{} - l.Register(nil, nil, "1_test.go") - l.Register(nil, nil, "2_test.go") - l.Register(nil, nil, "3_test.go") - - r, err := NewRunner(testDB.DB, l) - if err != nil { - t.Fatal(err) - } - - if len(r.migrationsList.Items()) != len(l.Items()) { - t.Fatalf("Expected the same migrations list to be assigned, got \n%#v", r.migrationsList) - } - - expectedQueries := []string{ - "CREATE TABLE IF NOT EXISTS `_migrations` (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)", - } - if len(expectedQueries) != len(testDB.CalledQueries) { - t.Fatalf("Expected %d queries, got %d: \n%v", len(expectedQueries), len(testDB.CalledQueries), testDB.CalledQueries) - } - for _, q := range expectedQueries { - if !list.ExistInSlice(q, testDB.CalledQueries) { - t.Fatalf("Query %s was not found in \n%v", q, testDB.CalledQueries) - } - } -} - -func TestRunnerUpAndDown(t *testing.T) { - testDB, err := createTestDB() - if err != nil { - t.Fatal(err) - } - defer testDB.Close() - - callsOrder := []string{} - - l := MigrationsList{} - l.Register(func(db dbx.Builder) error { - callsOrder = append(callsOrder, "up2") - return nil - }, func(db dbx.Builder) error { - callsOrder = append(callsOrder, "down2") - return nil - }, "2_test") - l.Register(func(db dbx.Builder) error { - callsOrder = append(callsOrder, "up3") - return nil - }, func(db dbx.Builder) error { - callsOrder = append(callsOrder, "down3") - return nil - }, "3_test") - l.Register(func(db dbx.Builder) error { - callsOrder = append(callsOrder, "up1") - return nil - }, func(db dbx.Builder) error { - callsOrder = append(callsOrder, "down1") - return nil - }, "1_test") - - r, err := NewRunner(testDB.DB, l) - if err != nil { - t.Fatal(err) - } - - // simulate partially out-of-order run migration - r.saveAppliedMigration(testDB, "2_test") - - // --------------------------------------------------------------- - // Up() - // --------------------------------------------------------------- - - if _, err := r.Up(); err != nil { - t.Fatal(err) - } - - expectedUpCallsOrder := `["up1","up3"]` // skip up2 since it was applied previously - - upCallsOrder, err := json.Marshal(callsOrder) - if err != nil { - t.Fatal(err) - } - - if v := string(upCallsOrder); v != expectedUpCallsOrder { - t.Fatalf("Expected Up() calls order %s, got %s", expectedUpCallsOrder, upCallsOrder) - } - - // --------------------------------------------------------------- - - // reset callsOrder - callsOrder = []string{} - - // simulate unrun migration - r.migrationsList.Register(nil, func(db dbx.Builder) error { - callsOrder = append(callsOrder, "down4") - return nil - }, "4_test") - - // --------------------------------------------------------------- - - // --------------------------------------------------------------- - // Down() - // --------------------------------------------------------------- - - if _, err := r.Down(2); err != nil { - t.Fatal(err) - } - - expectedDownCallsOrder := `["down3","down1"]` // revert in the applied order - - downCallsOrder, err := json.Marshal(callsOrder) - if err != nil { - t.Fatal(err) - } - - if v := string(downCallsOrder); v != expectedDownCallsOrder { - t.Fatalf("Expected Down() calls order %s, got %s", expectedDownCallsOrder, downCallsOrder) - } -} - -func TestHistorySync(t *testing.T) { - testDB, err := createTestDB() - if err != nil { - t.Fatal(err) - } - defer testDB.Close() - - // mock migrations history - l := MigrationsList{} - l.Register(func(db dbx.Builder) error { - return nil - }, func(db dbx.Builder) error { - return nil - }, "1_test") - l.Register(func(db dbx.Builder) error { - return nil - }, func(db dbx.Builder) error { - return nil - }, "2_test") - l.Register(func(db dbx.Builder) error { - return nil - }, func(db dbx.Builder) error { - return nil - }, "3_test") - - r, err := NewRunner(testDB.DB, l) - if err != nil { - t.Fatalf("Failed to initialize the runner: %v", err) - } - - if _, err := r.Up(); err != nil { - t.Fatalf("Failed to apply the mock migrations: %v", err) - } - - if !r.isMigrationApplied(testDB.DB, "2_test") { - t.Fatalf("Expected 2_test migration to be applied") - } - - // mock deleted migrations - r.migrationsList.list = []*Migration{r.migrationsList.list[0], r.migrationsList.list[2]} - - if err := r.removeMissingAppliedMigrations(); err != nil { - t.Fatalf("Failed to remove missing applied migrations: %v", err) - } - - if r.isMigrationApplied(testDB.DB, "2_test") { - t.Fatalf("Expected 2_test migration to NOT be applied") - } -} - -// ------------------------------------------------------------------- -// Helpers -// ------------------------------------------------------------------- - -type testDB struct { - *dbx.DB - CalledQueries []string -} - -// NB! Don't forget to call `db.Close()` at the end of the test. -func createTestDB() (*testDB, error) { - sqlDB, err := sql.Open("sqlite", ":memory:") - if err != nil { - return nil, err - } - - db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")} - db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { - db.CalledQueries = append(db.CalledQueries, sql) - } - db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { - db.CalledQueries = append(db.CalledQueries, sql) - } - - return &db, nil -} diff --git a/tools/rest/excerpt_modifier.go b/tools/picker/excerpt_modifier.go similarity index 93% rename from tools/rest/excerpt_modifier.go rename to tools/picker/excerpt_modifier.go index 07da1a06..c17269aa 100644 --- a/tools/rest/excerpt_modifier.go +++ b/tools/picker/excerpt_modifier.go @@ -1,4 +1,4 @@ -package rest +package picker import ( "errors" @@ -10,6 +10,12 @@ import ( "golang.org/x/net/html" ) +func init() { + Modifiers["excerpt"] = func(args ...string) (Modifier, error) { + return newExcerptModifier(args...) + } +} + var whitespaceRegex = regexp.MustCompile(`\s+`) var excludeTags = []string{ @@ -24,7 +30,7 @@ var inlineTags = []string{ "strong", "strike", "sub", "sup", "time", } -var _ FieldModifier = (*excerptModifier)(nil) +var _ Modifier = (*excerptModifier)(nil) type excerptModifier struct { max int // approximate max excerpt length @@ -59,7 +65,7 @@ func newExcerptModifier(args ...string) (*excerptModifier, error) { return &excerptModifier{max, withEllipsis}, nil } -// Modify implements the [FieldModifier.Modify] interface method. +// Modify implements the [Modifier.Modify] interface method. // // It returns a plain text excerpt/short-description from a formatted // html string (non-string values are kept untouched). diff --git a/tools/rest/excerpt_modifier_test.go b/tools/picker/excerpt_modifier_test.go similarity index 99% rename from tools/rest/excerpt_modifier_test.go rename to tools/picker/excerpt_modifier_test.go index 47a87a66..d71927b1 100644 --- a/tools/rest/excerpt_modifier_test.go +++ b/tools/picker/excerpt_modifier_test.go @@ -1,4 +1,4 @@ -package rest +package picker import ( "fmt" diff --git a/tools/picker/modifiers.go b/tools/picker/modifiers.go new file mode 100644 index 00000000..6572e0c4 --- /dev/null +++ b/tools/picker/modifiers.go @@ -0,0 +1,41 @@ +package picker + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/tools/tokenizer" +) + +var Modifiers = map[string]ModifierFactoryFunc{} + +type ModifierFactoryFunc func(args ...string) (Modifier, error) + +type Modifier interface { + // Modify executes the modifier and returns a new modified value. + Modify(value any) (any, error) +} + +func initModifer(rawModifier string) (Modifier, error) { + t := tokenizer.NewFromString(rawModifier) + t.Separators('(', ')', ',', ' ') + t.IgnoreParenthesis(true) + + parts, err := t.ScanAll() + if err != nil { + return nil, err + } + + if len(parts) == 0 { + return nil, fmt.Errorf("invalid or empty modifier expression %q", rawModifier) + } + + name := parts[0] + args := parts[1:] + + factory, ok := Modifiers[name] + if !ok { + return nil, fmt.Errorf("missing or invalid modifier %q", name) + } + + return factory(args...) +} diff --git a/tools/rest/json_serializer.go b/tools/picker/pick.go similarity index 50% rename from tools/rest/json_serializer.go rename to tools/picker/pick.go index 6c2dfd01..2cd23aa5 100644 --- a/tools/rest/json_serializer.go +++ b/tools/picker/pick.go @@ -1,76 +1,25 @@ -package rest +package picker import ( "encoding/json" - "fmt" "strings" - // Experimental! - // - // Need more tests before replacing encoding/json entirely. - // Test also encoding/json/v2 once released (see https://github.com/golang/go/discussions/63397) - goccy "github.com/goccy/go-json" - - "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/tokenizer" ) -type FieldModifier interface { - // Modify executes the modifier and returns a new modified value. - Modify(value any) (any, error) -} - -// Serializer represents custom REST JSON serializer based on echo.DefaultJSONSerializer, -// with support for additional generic response data transformation (eg. fields picker). -type Serializer struct { - echo.DefaultJSONSerializer - - FieldsParam string -} - -// Serialize converts an interface into a json and writes it to the response. +// Pick converts data into a []any, map[string]any, etc. (using json marshal->unmarshal) +// containing only the fields from the parsed rawFields expression. // -// It also provides a generic response data fields picker via the FieldsParam query parameter (default to "fields"). -// -// Note: for the places where it is safe, the std encoding/json is replaced -// with goccy due to its slightly better Unmarshal/Marshal performance. -func (s *Serializer) Serialize(c echo.Context, i any, indent string) error { - fieldsParam := s.FieldsParam - if fieldsParam == "" { - fieldsParam = "fields" - } - - statusCode := c.Response().Status - - rawFields := c.QueryParam(fieldsParam) - if rawFields == "" || statusCode < 200 || statusCode > 299 { - return s.DefaultJSONSerializer.Serialize(c, i, indent) - } - - decoded, err := PickFields(i, rawFields) - if err != nil { - return err - } - - enc := goccy.NewEncoder(c.Response()) - if indent != "" { - enc.SetIndent("", indent) - } - - return enc.Encode(decoded) -} - -// PickFields parses the provided fields string expression and -// returns a new subset of data with only the requested fields. -// -// Fields transformations with modifiers are also supported (see initModifer()). +// rawFields is a comma separated string of the fields to include. +// Nested fields should be listed with dot-notation. +// Fields value modifiers are also supported using the `:modifier(args)` format (see Modifiers). // // Example: // // data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}} -// PickFields(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}} -func PickFields(data any, rawFields string) (any, error) { +// Pick(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}} +func Pick(data any, rawFields string) (any, error) { parsedFields, err := parseFields(rawFields) if err != nil { return nil, err @@ -82,18 +31,18 @@ func PickFields(data any, rawFields string) (any, error) { // // @todo research other approaches to avoid the double serialization // --- - encoded, err := json.Marshal(data) // use the std json since goccy has several bugs reported with struct marshaling and it is not safe + encoded, err := json.Marshal(data) if err != nil { return nil, err } var decoded any - if err := goccy.Unmarshal(encoded, &decoded); err != nil { + if err := json.Unmarshal(encoded, &decoded); err != nil { return nil, err } // --- - // special cases to preserve the same fields format when used with single item or array data. + // special cases to preserve the same fields format when used with single item or search results data. var isSearchResult bool switch data.(type) { case search.Result, *search.Result: @@ -111,7 +60,7 @@ func PickFields(data any, rawFields string) (any, error) { return decoded, nil } -func parseFields(rawFields string) (map[string]FieldModifier, error) { +func parseFields(rawFields string) (map[string]Modifier, error) { t := tokenizer.NewFromString(rawFields) fields, err := t.ScanAll() @@ -119,7 +68,7 @@ func parseFields(rawFields string) (map[string]FieldModifier, error) { return nil, err } - result := make(map[string]FieldModifier, len(fields)) + result := make(map[string]Modifier, len(fields)) for _, f := range fields { parts := strings.SplitN(strings.TrimSpace(f), ":", 2) @@ -138,36 +87,7 @@ func parseFields(rawFields string) (map[string]FieldModifier, error) { return result, nil } -func initModifer(rawModifier string) (FieldModifier, error) { - t := tokenizer.NewFromString(rawModifier) - t.Separators('(', ')', ',', ' ') - t.IgnoreParenthesis(true) - - parts, err := t.ScanAll() - if err != nil { - return nil, err - } - - if len(parts) == 0 { - return nil, fmt.Errorf("invalid or empty modifier expression %q", rawModifier) - } - - name := parts[0] - args := parts[1:] - - switch name { - case "excerpt": - m, err := newExcerptModifier(args...) - if err != nil { - return nil, fmt.Errorf("invalid excerpt modifier: %w", err) - } - return m, nil - } - - return nil, fmt.Errorf("missing or invalid modifier %q", name) -} - -func pickParsedFields(data any, fields map[string]FieldModifier) error { +func pickParsedFields(data any, fields map[string]Modifier) error { switch v := data.(type) { case map[string]any: pickMapFields(v, fields) @@ -196,7 +116,7 @@ func pickParsedFields(data any, fields map[string]FieldModifier) error { return nil } -func pickMapFields(data map[string]any, fields map[string]FieldModifier) error { +func pickMapFields(data map[string]any, fields map[string]Modifier) error { if len(fields) == 0 { return nil // nothing to pick } @@ -221,7 +141,7 @@ func pickMapFields(data map[string]any, fields map[string]FieldModifier) error { DataLoop: for k := range data { - matchingFields := make(map[string]FieldModifier, len(fields)) + matchingFields := make(map[string]Modifier, len(fields)) for f, m := range fields { if strings.HasPrefix(f+".", k+".") { matchingFields[f] = m diff --git a/tools/rest/json_serializer_test.go b/tools/picker/pick_test.go similarity index 69% rename from tools/rest/json_serializer_test.go rename to tools/picker/pick_test.go index 63dcfc76..95add588 100644 --- a/tools/rest/json_serializer_test.go +++ b/tools/picker/pick_test.go @@ -1,135 +1,13 @@ -package rest_test +package picker_test import ( "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" "testing" - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/picker" "github.com/pocketbase/pocketbase/tools/search" ) -func TestSerialize(t *testing.T) { - scenarios := []struct { - name string - serializer rest.Serializer - statusCode int - data any - query string - expected string - }{ - { - "empty query", - rest.Serializer{}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "", - `{"a":1,"b":2,"c":"test"}`, - }, - { - "empty fields", - rest.Serializer{}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=", - `{"a":1,"b":2,"c":"test"}`, - }, - { - "missing fields", - rest.Serializer{}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=missing", - `{}`, - }, - { - ">299 response", - rest.Serializer{}, - 300, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=missing", - `{"a":1,"b":2,"c":"test"}`, - }, - { - "<200 response", - rest.Serializer{}, - 199, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=missing", - `{"a":1,"b":2,"c":"test"}`, - }, - { - "non map response", - rest.Serializer{}, - 200, - "test", - "fields=a,b,test", - `"test"`, - }, - { - "non slice of map response", - rest.Serializer{}, - 200, - []any{"a", "b", "test"}, - "fields=a,test", - `["a","b","test"]`, - }, - { - "map with no matching field", - rest.Serializer{}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=missing", // test individual fields trim - `{}`, - }, - { - "map with existing and missing fields", - rest.Serializer{}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "fields=a, c ,missing", // test individual fields trim - `{"a":1,"c":"test"}`, - }, - { - "custom fields param", - rest.Serializer{FieldsParam: "custom"}, - 200, - map[string]any{"a": 1, "b": 2, "c": "test"}, - "custom=a, c ,missing", // test individual fields trim - `{"a":1,"c":"test"}`, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.URL.RawQuery = s.query - rec := httptest.NewRecorder() - - e := echo.New() - c := e.NewContext(req, rec) - c.Response().Status = s.statusCode - - if err := s.serializer.Serialize(c, s.data, ""); err != nil { - t.Fatalf("Serialize failure: %v", err) - } - - rawBody, err := io.ReadAll(rec.Result().Body) - if err != nil { - t.Fatalf("Failed to read request body: %v", err) - } - - if v := strings.TrimSpace(string(rawBody)); v != s.expected { - t.Fatalf("Expected body\n%v \ngot \n%v", s.expected, v) - } - }) - } -} - func TestPickFields(t *testing.T) { scenarios := []struct { name string @@ -374,7 +252,7 @@ func TestPickFields(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result, err := rest.PickFields(s.data, s.fields) + result, err := picker.Pick(s.data, s.fields) hasErr := err != nil if hasErr != s.expectError { diff --git a/tools/rest/multi_binder.go b/tools/rest/multi_binder.go deleted file mode 100644 index 6fcc2e29..00000000 --- a/tools/rest/multi_binder.go +++ /dev/null @@ -1,170 +0,0 @@ -package rest - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "reflect" - "strings" - - "github.com/labstack/echo/v5" - "github.com/spf13/cast" -) - -// MultipartJsonKey is the key for the special multipart/form-data -// handling allowing reading serialized json payload without normalization. -const MultipartJsonKey string = "@jsonPayload" - -// MultiBinder is similar to [echo.DefaultBinder] but uses slightly different -// application/json and multipart/form-data bind methods to accommodate better -// the PocketBase router needs. -type MultiBinder struct{} - -// Bind implements the [Binder.Bind] method. -// -// Bind is almost identical to [echo.DefaultBinder.Bind] but uses the -// [rest.BindBody] function for binding the request body. -func (b *MultiBinder) Bind(c echo.Context, i interface{}) (err error) { - if err := echo.BindPathParams(c, i); err != nil { - return err - } - - // Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body. - // For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues. - method := c.Request().Method - if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead { - if err = echo.BindQueryParams(c, i); err != nil { - return err - } - } - - return BindBody(c, i) -} - -// BindBody binds request body content to i. -// -// This is similar to `echo.BindBody()`, but for JSON requests uses -// custom json reader that **copies** the request body, allowing multiple reads. -func BindBody(c echo.Context, i any) error { - req := c.Request() - if req.ContentLength == 0 { - return nil - } - - ctype := req.Header.Get(echo.HeaderContentType) - switch { - case strings.HasPrefix(ctype, echo.MIMEApplicationJSON): - err := CopyJsonBody(c.Request(), i) - if err != nil { - return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error()) - } - return nil - case strings.HasPrefix(ctype, echo.MIMEApplicationForm), strings.HasPrefix(ctype, echo.MIMEMultipartForm): - return bindFormData(c, i) - } - - // fallback to the default binder - return echo.BindBody(c, i) -} - -// CopyJsonBody reads the request body into i by -// creating a copy of `r.Body` to allow multiple reads. -func CopyJsonBody(r *http.Request, i any) error { - body := r.Body - - // this usually shouldn't be needed because the Server calls close - // for us but we are changing the request body with a new reader - defer body.Close() - - limitReader := io.LimitReader(body, DefaultMaxMemory) - - bodyBytes, readErr := io.ReadAll(limitReader) - if readErr != nil { - return readErr - } - - err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(i) - - // set new body reader - r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - - return err -} - -// Custom multipart/form-data binder that implements an additional handling like -// loading a serialized json payload or properly scan array values when a map destination is used. -func bindFormData(c echo.Context, i any) error { - if i == nil { - return nil - } - - values, err := c.FormValues() - if err != nil { - return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error()) - } - - if len(values) == 0 { - return nil - } - - // special case to allow submitting json without normalization - // alongside the other multipart/form-data values - jsonPayloadValues := values[MultipartJsonKey] - for _, payload := range jsonPayloadValues { - json.Unmarshal([]byte(payload), i) - } - - rt := reflect.TypeOf(i).Elem() - - // map - if rt.Kind() == reflect.Map { - rv := reflect.ValueOf(i).Elem() - - for k, v := range values { - if k == MultipartJsonKey { - continue - } - - if total := len(v); total == 1 { - rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalizeMultipartValue(v[0]))) - } else { - normalized := make([]any, total) - for i, vItem := range v { - normalized[i] = normalizeMultipartValue(vItem) - } - rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalized)) - } - } - - return nil - } - - // anything else - return echo.BindBody(c, i) -} - -// 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 -func normalizeMultipartValue(raw string) any { - switch raw { - case "": - return raw - case "true": - return true - case "false": - return false - default: - if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') { - if v, err := cast.ToFloat64E(raw); err == nil { - return v - } - } - - return raw - } -} diff --git a/tools/rest/multi_binder_test.go b/tools/rest/multi_binder_test.go deleted file mode 100644 index 24732eca..00000000 --- a/tools/rest/multi_binder_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package rest_test - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/labstack/echo/v5" - "github.com/pocketbase/pocketbase/tools/rest" -) - -func TestMultiBinderBind(t *testing.T) { - binder := rest.MultiBinder{} - - req := httptest.NewRequest(http.MethodGet, "/test?query=123", strings.NewReader(`{"body":"456"}`)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - - rec := httptest.NewRecorder() - - e := echo.New() - e.Any("/:name", func(c echo.Context) error { - // bind twice to ensure that the json body reader copy is invoked - for i := 0; i < 2; i++ { - data := struct { - Name string `param:"name"` - Query string `query:"query"` - Body string `form:"body"` - }{} - - if err := binder.Bind(c, &data); err != nil { - t.Fatal(err) - } - - if data.Name != "test" { - t.Fatalf("Expected Name %q, got %q", "test", data.Name) - } - - if data.Query != "123" { - t.Fatalf("Expected Query %q, got %q", "123", data.Query) - } - - if data.Body != "456" { - t.Fatalf("Expected Body %q, got %q", "456", data.Body) - } - } - - return nil - }) - e.ServeHTTP(rec, req) -} - -func TestBindBody(t *testing.T) { - scenarios := []struct { - body io.Reader - contentType string - expectBody string - expectError bool - }{ - { - strings.NewReader(""), - echo.MIMEApplicationJSON, - `{}`, - false, - }, - { - strings.NewReader(`{"test":"invalid`), - echo.MIMEApplicationJSON, - `{}`, - true, - }, - { - strings.NewReader(`{"test":123}`), - echo.MIMEApplicationJSON, - `{"test":123}`, - false, - }, - { - strings.NewReader( - url.Values{ - "string": []string{"str"}, - "stings": []string{"str1", "str2", ""}, - "number": []string{"-123"}, - "numbers": []string{"123", "456.789"}, - "bool": []string{"true"}, - "bools": []string{"true", "false"}, - rest.MultipartJsonKey: []string{`invalid`, `{"a":123}`, `{"b":456}`}, - }.Encode(), - ), - echo.MIMEApplicationForm, - `{"a":123,"b":456,"bool":true,"bools":[true,false],"number":-123,"numbers":[123,456.789],"stings":["str1","str2",""],"string":"str"}`, - false, - }, - } - - for i, scenario := range scenarios { - e := echo.New() - req := httptest.NewRequest(http.MethodPost, "/", scenario.body) - req.Header.Set(echo.HeaderContentType, scenario.contentType) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - data := map[string]any{} - err := rest.BindBody(c, &data) - - hasErr := err != nil - if hasErr != scenario.expectError { - t.Errorf("[%d] Expected hasErr %v, got %v", i, scenario.expectError, hasErr) - } - - rawBody, err := json.Marshal(data) - if err != nil { - t.Errorf("[%d] Failed to marshal binded body: %v", i, err) - } - - if scenario.expectBody != string(rawBody) { - t.Errorf("[%d] Expected body \n%s, \ngot \n%s", i, scenario.expectBody, rawBody) - } - } -} - -func TestCopyJsonBody(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{"test":"test123"}`)) - - // simulate multiple reads from the same request - result1 := map[string]string{} - rest.CopyJsonBody(req, &result1) - result2 := map[string]string{} - rest.CopyJsonBody(req, &result2) - - if len(result1) == 0 { - t.Error("Expected result1 to be filled") - } - - if len(result2) == 0 { - t.Error("Expected result2 to be filled") - } - - if v, ok := result1["test"]; !ok || v != "test123" { - t.Errorf("Expected result1.test to be %q, got %q", "test123", v) - } - - if v, ok := result2["test"]; !ok || v != "test123" { - t.Errorf("Expected result2.test to be %q, got %q", "test123", v) - } -} diff --git a/tools/rest/uploaded_file.go b/tools/rest/uploaded_file.go deleted file mode 100644 index c7d2f6bc..00000000 --- a/tools/rest/uploaded_file.go +++ /dev/null @@ -1,39 +0,0 @@ -package rest - -import ( - "net/http" - - "github.com/pocketbase/pocketbase/tools/filesystem" -) - -// DefaultMaxMemory defines the default max memory bytes that -// will be used when parsing a form request body. -const DefaultMaxMemory = 32 << 20 // 32mb - -// FindUploadedFiles extracts all form files of "key" from a http request -// and returns a slice with filesystem.File instances (if any). -func FindUploadedFiles(r *http.Request, key string) ([]*filesystem.File, error) { - if r.MultipartForm == nil { - err := r.ParseMultipartForm(DefaultMaxMemory) - if err != nil { - return nil, err - } - } - - if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File[key]) == 0 { - return nil, http.ErrMissingFile - } - - result := make([]*filesystem.File, 0, len(r.MultipartForm.File[key])) - - for _, fh := range r.MultipartForm.File[key] { - file, err := filesystem.NewFileFromMultipart(fh) - if err != nil { - return nil, err - } - - result = append(result, file) - } - - return result, nil -} diff --git a/tools/rest/uploaded_file_test.go b/tools/rest/uploaded_file_test.go deleted file mode 100644 index 156e280d..00000000 --- a/tools/rest/uploaded_file_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package rest_test - -import ( - "bytes" - "mime/multipart" - "net/http" - "net/http/httptest" - "regexp" - "strings" - "testing" - - "github.com/pocketbase/pocketbase/tools/rest" -) - -func TestFindUploadedFiles(t *testing.T) { - scenarios := []struct { - filename string - expectedPattern string - }{ - {"ab.png", `^ab\w{10}_\w{10}\.png$`}, - {"test", `^test_\w{10}\.txt$`}, - {"a b c d!@$.j!@$pg", `^a_b_c_d_\w{10}\.jpg$`}, - {strings.Repeat("a", 150), `^a{100}_\w{10}\.txt$`}, - } - - for i, s := range scenarios { - // create multipart form file body - body := new(bytes.Buffer) - mp := multipart.NewWriter(body) - w, err := mp.CreateFormFile("test", s.filename) - if err != nil { - t.Fatal(err) - } - w.Write([]byte("test")) - mp.Close() - // --- - - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Add("Content-Type", mp.FormDataContentType()) - - result, err := rest.FindUploadedFiles(req, "test") - if err != nil { - t.Fatal(err) - } - - if len(result) != 1 { - t.Errorf("[%d] Expected 1 file, got %d", i, len(result)) - } - - if result[0].Size != 4 { - t.Errorf("[%d] Expected the file size to be 4 bytes, got %d", i, result[0].Size) - } - - pattern, err := regexp.Compile(s.expectedPattern) - if err != nil { - t.Errorf("[%d] Invalid filename pattern %q: %v", i, s.expectedPattern, err) - } - if !pattern.MatchString(result[0].Name) { - t.Fatalf("Expected filename to match %s, got filename %s", s.expectedPattern, result[0].Name) - } - } -} - -func TestFindUploadedFilesMissing(t *testing.T) { - body := new(bytes.Buffer) - mp := multipart.NewWriter(body) - mp.Close() - - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Add("Content-Type", mp.FormDataContentType()) - - result, err := rest.FindUploadedFiles(req, "test") - if err == nil { - t.Error("Expected error, got nil") - } - - if result != nil { - t.Errorf("Expected result to be nil, got %v", result) - } -} diff --git a/tools/rest/url.go b/tools/rest/url.go deleted file mode 100644 index 87dd6b21..00000000 --- a/tools/rest/url.go +++ /dev/null @@ -1,29 +0,0 @@ -package rest - -import ( - "net/url" - "path" - "strings" -) - -// NormalizeUrl removes duplicated slashes from a url path. -func NormalizeUrl(originalUrl string) (string, error) { - u, err := url.Parse(originalUrl) - if err != nil { - return "", err - } - - hasSlash := strings.HasSuffix(u.Path, "/") - - // clean up path by removing duplicated / - u.Path = path.Clean(u.Path) - u.RawPath = path.Clean(u.RawPath) - - // restore original trailing slash - if hasSlash && !strings.HasSuffix(u.Path, "/") { - u.Path += "/" - u.RawPath += "/" - } - - return u.String(), nil -} diff --git a/tools/rest/url_test.go b/tools/rest/url_test.go deleted file mode 100644 index 091d5bc4..00000000 --- a/tools/rest/url_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package rest_test - -import ( - "testing" - - "github.com/pocketbase/pocketbase/tools/rest" -) - -func TestNormalizeUrl(t *testing.T) { - scenarios := []struct { - url string - expectError bool - expectUrl string - }{ - {":/", true, ""}, - {"./", false, "./"}, - {"../../test////", false, "../../test/"}, - {"/a/b/c", false, "/a/b/c"}, - {"a/////b//c/", false, "a/b/c/"}, - {"/a/////b//c", false, "/a/b/c"}, - {"///a/b/c", false, "/a/b/c"}, - {"//a/b/c", false, "//a/b/c"}, // preserve "auto-schema" - {"http://a/b/c", false, "http://a/b/c"}, - {"a//bc?test=1//dd", false, "a/bc?test=1//dd"}, // only the path is normalized - {"a//bc?test=1#12///3", false, "a/bc?test=1#12///3"}, // only the path is normalized - } - - for i, s := range scenarios { - result, err := rest.NormalizeUrl(s.url) - - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v", i, s.expectError, hasErr) - } - - if result != s.expectUrl { - t.Errorf("(%d) Expected url %q, got %q", i, s.expectUrl, result) - } - } -} diff --git a/tools/router/error.go b/tools/router/error.go new file mode 100644 index 00000000..2c437cdc --- /dev/null +++ b/tools/router/error.go @@ -0,0 +1,231 @@ +package router + +import ( + "database/sql" + "errors" + "io/fs" + "net/http" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/inflector" +) + +// SafeErrorItem defines a common error interface for a printable public safe error. +type SafeErrorItem interface { + // Code represents a fixed unique identifier of the error (usually used as translation key). + Code() string + + // Error is the default English human readable error message that will be returned. + Error() string +} + +// SafeErrorParamsResolver defines an optional interface for specifying dynamic error parameters. +type SafeErrorParamsResolver interface { + // Params defines a map with dynamic parameters to return as part of the public safe error view. + Params() map[string]any +} + +// SafeErrorResolver defines an error interface for resolving the public safe error fields. +type SafeErrorResolver interface { + // Resolve allows modifying and returning a new public safe error data map. + Resolve(errData map[string]any) any +} + +// ApiError defines the struct for a basic api error response. +type ApiError struct { + rawData any + + Data map[string]any `json:"data"` + Message string `json:"message"` + Status int `json:"status"` +} + +// Error makes it compatible with the `error` interface. +func (e *ApiError) Error() string { + return e.Message +} + +// RawData returns the unformatted error data (could be an internal error, text, etc.) +func (e *ApiError) RawData() any { + return e.rawData +} + +// Is reports whether the current ApiError wraps the target. +func (e *ApiError) Is(target error) bool { + err, ok := e.rawData.(error) + if ok { + return errors.Is(err, target) + } + + apiErr, ok := target.(*ApiError) + + return ok && e == apiErr +} + +// NewNotFoundError creates and returns 404 ApiError. +func NewNotFoundError(message string, rawErrData any) *ApiError { + if message == "" { + message = "The requested resource wasn't found." + } + + return NewApiError(http.StatusNotFound, message, rawErrData) +} + +// NewBadRequestError creates and returns 400 ApiError. +func NewBadRequestError(message string, rawErrData any) *ApiError { + if message == "" { + message = "Something went wrong while processing your request." + } + + return NewApiError(http.StatusBadRequest, message, rawErrData) +} + +// NewForbiddenError creates and returns 403 ApiError. +func NewForbiddenError(message string, rawErrData any) *ApiError { + if message == "" { + message = "You are not allowed to perform this request." + } + + return NewApiError(http.StatusForbidden, message, rawErrData) +} + +// NewUnauthorizedError creates and returns 401 ApiError. +func NewUnauthorizedError(message string, rawErrData any) *ApiError { + if message == "" { + message = "Missing or invalid authentication." + } + + return NewApiError(http.StatusUnauthorized, message, rawErrData) +} + +// NewInternalServerError creates and returns 500 ApiError. +func NewInternalServerError(message string, rawErrData any) *ApiError { + if message == "" { + message = "Something went wrong while processing your request." + } + + return NewApiError(http.StatusInternalServerError, message, rawErrData) +} + +func NewTooManyRequestsError(message string, rawErrData any) *ApiError { + if message == "" { + message = "Too Many Requests." + } + + return NewApiError(http.StatusTooManyRequests, message, rawErrData) +} + +// NewApiError creates and returns new normalized ApiError instance. +func NewApiError(status int, message string, rawErrData any) *ApiError { + if message == "" { + message = http.StatusText(status) + } + + return &ApiError{ + rawData: rawErrData, + Data: safeErrorsData(rawErrData), + Status: status, + Message: strings.TrimSpace(inflector.Sentenize(message)), + } +} + +// ToApiError wraps err into ApiError instance (if not already). +func ToApiError(err error) *ApiError { + var apiErr *ApiError + + if !errors.As(err, &apiErr) { + // no ApiError found -> assign a generic one + if errors.Is(err, sql.ErrNoRows) || errors.Is(err, fs.ErrNotExist) { + apiErr = NewNotFoundError("", err) + } else { + apiErr = NewBadRequestError("", err) + } + } + + return apiErr +} + +// ------------------------------------------------------------------- + +func safeErrorsData(data any) map[string]any { + switch v := data.(type) { + case validation.Errors: + return resolveSafeErrorsData(v) + case error: + validationErrors := validation.Errors{} + if errors.As(v, &validationErrors) { + return resolveSafeErrorsData(validationErrors) + } + return map[string]any{} // not nil to ensure that is json serialized as object + case map[string]validation.Error: + return resolveSafeErrorsData(v) + case map[string]SafeErrorItem: + return resolveSafeErrorsData(v) + case map[string]error: + return resolveSafeErrorsData(v) + case map[string]string: + return resolveSafeErrorsData(v) + case map[string]any: + return resolveSafeErrorsData(v) + default: + return map[string]any{} // not nil to ensure that is json serialized as object + } +} + +func resolveSafeErrorsData[T any](data map[string]T) map[string]any { + result := map[string]any{} + + for name, err := range data { + if isNestedError(err) { + result[name] = safeErrorsData(err) + } else { + result[name] = resolveSafeErrorItem(err) + } + } + + return result +} + +func isNestedError(err any) bool { + switch err.(type) { + case validation.Errors, + map[string]validation.Error, + map[string]SafeErrorItem, + map[string]error, + map[string]string, + map[string]any: + return true + } + + return false +} + +// resolveSafeErrorItem extracts from each validation error its +// public safe error code and message. +func resolveSafeErrorItem(err any) any { + data := map[string]any{} + + if obj, ok := err.(SafeErrorItem); ok { + // extract the specific error code and message + data["code"] = obj.Code() + data["message"] = inflector.Sentenize(obj.Error()) + } else { + // fallback to the default public safe values + data["code"] = "validation_invalid_value" + data["message"] = "Invalid value." + } + + if s, ok := err.(SafeErrorParamsResolver); ok { + params := s.Params() + if len(params) > 0 { + data["params"] = params + } + } + + if s, ok := err.(SafeErrorResolver); ok { + return s.Resolve(data) + } + + return data +} diff --git a/tools/router/error_test.go b/tools/router/error_test.go new file mode 100644 index 00000000..8ced8932 --- /dev/null +++ b/tools/router/error_test.go @@ -0,0 +1,358 @@ +package router_test + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "io/fs" + "strconv" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/router" +) + +func TestNewApiErrorWithRawData(t *testing.T) { + t.Parallel() + + e := router.NewApiError( + 300, + "message_test", + "rawData_test", + ) + + result, _ := json.Marshal(e) + expected := `{"data":{},"message":"Message_test.","status":300}` + + if string(result) != expected { + t.Errorf("Expected\n%v\ngot\n%v", expected, string(result)) + } + + if e.Error() != "Message_test." { + t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) + } + + if e.RawData() != "rawData_test" { + t.Errorf("Expected rawData\n%v\ngot\n%v", "rawData_test", e.RawData()) + } +} + +func TestNewApiErrorWithValidationData(t *testing.T) { + t.Parallel() + + e := router.NewApiError( + 300, + "message_test", + map[string]any{ + "err1": errors.New("test error"), // should be normalized + "err2": validation.ErrRequired, + "err3": validation.Errors{ + "err3.1": errors.New("test error"), // should be normalized + "err3.2": validation.ErrRequired, + "err3.3": validation.Errors{ + "err3.3.1": validation.ErrRequired, + }, + }, + "err4": &mockSafeErrorItem{}, + "err5": map[string]error{ + "err5.1": validation.ErrRequired, + }, + }, + ) + + result, _ := json.Marshal(e) + expected := `{"data":{"err1":{"code":"validation_invalid_value","message":"Invalid value."},"err2":{"code":"validation_required","message":"Cannot be blank."},"err3":{"err3.1":{"code":"validation_invalid_value","message":"Invalid value."},"err3.2":{"code":"validation_required","message":"Cannot be blank."},"err3.3":{"err3.3.1":{"code":"validation_required","message":"Cannot be blank."}}},"err4":{"code":"mock_code","message":"Mock_error.","mock_resolve":123},"err5":{"err5.1":{"code":"validation_required","message":"Cannot be blank."}}},"message":"Message_test.","status":300}` + + if string(result) != expected { + t.Errorf("Expected \n%v, \ngot \n%v", expected, string(result)) + } + + if e.Error() != "Message_test." { + t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) + } + + if e.RawData() == nil { + t.Error("Expected non-nil rawData") + } +} + +func TestNewNotFoundError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"The requested resource wasn't found.","status":404}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":404}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"data":{"err1":{"code":"test_code","message":"Test_message."}},"message":"Demo.","status":404}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewNotFoundError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestNewBadRequestError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"Something went wrong while processing your request.","status":400}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":400}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"data":{"err1":{"code":"test_code","message":"Test_message."}},"message":"Demo.","status":400}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewBadRequestError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestNewForbiddenError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"You are not allowed to perform this request.","status":403}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":403}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"data":{"err1":{"code":"test_code","message":"Test_message."}},"message":"Demo.","status":403}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewForbiddenError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestNewUnauthorizedError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"Missing or invalid authentication.","status":401}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":401}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"data":{"err1":{"code":"test_code","message":"Test_message."}},"message":"Demo.","status":401}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewUnauthorizedError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestNewInternalServerError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"Something went wrong while processing your request.","status":500}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":500}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message")}, `{"data":{"err1":{"code":"test_code","message":"Test_message."}},"message":"Demo.","status":500}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewInternalServerError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestNewTooManyRequestsError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"data":{},"message":"Too Many Requests.","status":429}`}, + {"demo", "rawData_test", `{"data":{},"message":"Demo.","status":429}`}, + {"demo", validation.Errors{"err1": validation.NewError("test_code", "test_message").SetParams(map[string]any{"test": 123})}, `{"data":{"err1":{"code":"test_code","message":"Test_message.","params":{"test":123}}},"message":"Demo.","status":429}`}, + } + + for i, s := range scenarios { + t.Run(strconv.Itoa(i), func(t *testing.T) { + e := router.NewTooManyRequestsError(s.message, s.data) + result, _ := json.Marshal(e) + + if str := string(result); str != s.expected { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str) + } + }) + } +} + +func TestApiErrorIs(t *testing.T) { + t.Parallel() + + err0 := router.NewInternalServerError("", nil) + err1 := router.NewInternalServerError("", nil) + err2 := errors.New("test") + err3 := fmt.Errorf("wrapped: %w", err0) + + scenarios := []struct { + name string + err error + target error + expected bool + }{ + { + "nil error", + err0, + nil, + false, + }, + { + "non ApiError", + err0, + err1, + false, + }, + { + "different ApiError", + err0, + err2, + false, + }, + { + "same ApiError", + err0, + err0, + true, + }, + { + "wrapped ApiError", + err3, + err0, + true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + is := errors.Is(s.err, s.target) + + if is != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, is) + } + }) + } +} + +func TestToApiError(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + err error + expected string + }{ + { + "regular error", + errors.New("test"), + `{"data":{},"message":"Something went wrong while processing your request.","status":400}`, + }, + { + "fs.ErrNotExist", + fs.ErrNotExist, + `{"data":{},"message":"The requested resource wasn't found.","status":404}`, + }, + { + "sql.ErrNoRows", + sql.ErrNoRows, + `{"data":{},"message":"The requested resource wasn't found.","status":404}`, + }, + { + "ApiError", + router.NewForbiddenError("test", nil), + `{"data":{},"message":"Test.","status":403}`, + }, + { + "wrapped ApiError", + fmt.Errorf("wrapped: %w", router.NewForbiddenError("test", nil)), + `{"data":{},"message":"Test.","status":403}`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + raw, err := json.Marshal(router.ToApiError(s.err)) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != s.expected { + t.Fatalf("Expected error\n%vgot\n%v", s.expected, rawStr) + } + }) + } +} + +// ------------------------------------------------------------------- + +var ( + _ router.SafeErrorItem = (*mockSafeErrorItem)(nil) + _ router.SafeErrorResolver = (*mockSafeErrorItem)(nil) +) + +type mockSafeErrorItem struct { +} + +func (m *mockSafeErrorItem) Code() string { + return "mock_code" +} + +func (m *mockSafeErrorItem) Error() string { + return "mock_error" +} + +func (m *mockSafeErrorItem) Resolve(errData map[string]any) any { + errData["mock_resolve"] = 123 + return errData +} diff --git a/tools/router/event.go b/tools/router/event.go new file mode 100644 index 00000000..d4e51b12 --- /dev/null +++ b/tools/router/event.go @@ -0,0 +1,369 @@ +package router + +import ( + "encoding/json" + "encoding/xml" + "errors" + "io" + "io/fs" + "net" + "net/http" + "net/netip" + "path/filepath" + "strings" + + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/picker" + "github.com/pocketbase/pocketbase/tools/store" +) + +var ErrUnsupportedContentType = NewBadRequestError("Unsupported Content-Type", nil) +var ErrInvalidRedirectStatusCode = NewInternalServerError("Invalid redirect status code", nil) +var ErrFileNotFound = NewNotFoundError("File not found", nil) + +const IndexPage = "index.html" + +// Event specifies based Route handler event that is usually intended +// to be embedded as part of a custom event struct. +// +// NB! It is expected that the Response and Request fields are always set. +type Event struct { + Response http.ResponseWriter + Request *http.Request + + hook.Event + + data store.Store[any] +} + +// RWUnwrapper specifies that an http.ResponseWriter could be "unwrapped" +// (usually used with [http.ResponseController]). +type RWUnwrapper interface { + Unwrap() http.ResponseWriter +} + +// Written reports whether the current response has already been written. +// +// This method always returns false if e.ResponseWritter doesn't implement the WriteTracker interface +// (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). +func (e *Event) Written() bool { + written, _ := getWritten(e.Response) + return written +} + +// Status reports the status code of the current response. +// +// This method always returns 0 if e.Response doesn't implement the StatusTracker interface +// (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). +func (e *Event) Status() int { + status, _ := getStatus(e.Response) + return status +} + +// Flush flushes buffered data to the current response. +// +// Returns [http.ErrNotSupported] if e.Response doesn't implement the [http.Flusher] interface +// (all router package handlers receives a ResponseWritter that implements it unless explicitly replaced with a custom one). +func (e *Event) Flush() error { + return http.NewResponseController(e.Response).Flush() +} + +// IsTLS reports whether the connection on which the request was received is TLS. +func (e *Event) IsTLS() bool { + return e.Request.TLS != nil +} + +// SetCookie is an alias for [http.SetCookie]. +// +// SetCookie adds a Set-Cookie header to the current response's headers. +// The provided cookie must have a valid Name. +// Invalid cookies may be silently dropped. +func (e *Event) SetCookie(cookie *http.Cookie) { + http.SetCookie(e.Response, cookie) +} + +// RemoteIP returns the IP address of the client that sent the request. +// +// IPv6 addresses are returned expanded. +// For example, "2001:db8::1" becomes "2001:0db8:0000:0000:0000:0000:0000:0001". +// +// Note that if you are behind reverse proxy(ies), this method returns +// the IP of the last connecting proxy. +func (e *Event) RemoteIP() string { + ip, _, _ := net.SplitHostPort(e.Request.RemoteAddr) + parsed, _ := netip.ParseAddr(ip) + return parsed.StringExpanded() +} + +// UnsafeRealIP returns the "real" client IP from common proxy headers +// OR fallbacks to the RemoteIP if none is found. +// +// NB! The returned IP value could be anything and it shouldn't be trusted if not behind a trusted reverse proxy! +func (e *Event) UnsafeRealIP() string { + if ip := e.Request.Header.Get("CF-Connecting-IP"); ip != "" { + return ip + } + + if ip := e.Request.Header.Get("Fly-Client-IP"); ip != "" { + return ip + } + + if ip := e.Request.Header.Get("X-Real-IP"); ip != "" { + return ip + } + + if ipsList := e.Request.Header.Get("X-Forwarded-For"); ipsList != "" { + // extract the first non-empty leftmost-ish ip + ips := strings.Split(ipsList, ",") + for _, ip := range ips { + ip = strings.TrimSpace(ip) + if ip != "" { + return ip + } + } + } + + return e.RemoteIP() +} + +// Store +// ------------------------------------------------------------------- + +// Get retrieves single value from the current event data store. +func (e *Event) Get(key string) any { + return e.data.Get(key) +} + +// GetAll returns a copy of the current event data store. +func (e *Event) GetAll() map[string]any { + return e.data.GetAll() +} + +// Set saves single value into the current event data store. +func (e *Event) Set(key string, value any) { + e.data.Set(key, value) +} + +// SetAll saves all items from m into the current event data store. +func (e *Event) SetAll(m map[string]any) { + for k, v := range m { + e.Set(k, v) + } +} + +// Response writers +// ------------------------------------------------------------------- + +const headerContentType = "Content-Type" + +func (e *Event) setResponseHeaderIfEmpty(key, value string) { + header := e.Response.Header() + if header.Get(key) == "" { + header.Set(key, value) + } +} + +// String writes a plain string response. +func (e *Event) String(status int, data string) error { + e.setResponseHeaderIfEmpty(headerContentType, "text/plain; charset=utf-8") + e.Response.WriteHeader(status) + _, err := e.Response.Write([]byte(data)) + return err +} + +// HTML writes an HTML response. +func (e *Event) HTML(status int, data string) error { + e.setResponseHeaderIfEmpty(headerContentType, "text/html; charset=utf-8") + e.Response.WriteHeader(status) + _, err := e.Response.Write([]byte(data)) + return err +} + +const jsonFieldsParam = "fields" + +// JSON writes a JSON response. +// +// It also provides a generic response data fields picker if the "fields" query parameter is set. +func (e *Event) JSON(status int, data any) error { + e.setResponseHeaderIfEmpty(headerContentType, "application/json") + e.Response.WriteHeader(status) + + rawFields := e.Request.URL.Query().Get(jsonFieldsParam) + + // error response or no fields to pick + if rawFields == "" || status < 200 || status > 299 { + return json.NewEncoder(e.Response).Encode(data) + } + + // pick only the requested fields + modified, err := picker.Pick(data, rawFields) + if err != nil { + return err + } + + return json.NewEncoder(e.Response).Encode(modified) +} + +// XML writes an XML response. +// It automatically prepends the generic [xml.Header] string to the response. +func (e *Event) XML(status int, data any) error { + e.setResponseHeaderIfEmpty(headerContentType, "application/xml; charset=utf-8") + e.Response.WriteHeader(status) + if _, err := e.Response.Write([]byte(xml.Header)); err != nil { + return err + } + return xml.NewEncoder(e.Response).Encode(data) +} + +// Stream streams the specified reader into the response. +func (e *Event) Stream(status int, contentType string, reader io.Reader) error { + e.Response.Header().Set(headerContentType, contentType) + e.Response.WriteHeader(status) + _, err := io.Copy(e.Response, reader) + return err +} + +// FileFS serves the specified filename from fsys. +// +// It is similar to [echo.FileFS] for consistency with earlier versions. +func (e *Event) FileFS(fsys fs.FS, filename string) error { + f, err := fsys.Open(filename) + if err != nil { + return ErrFileNotFound + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return err + } + + // if it is a directory try to open its index.html file + if fi.IsDir() { + filename = filepath.ToSlash(filepath.Join(filename, IndexPage)) + f, err = fsys.Open(filename) + if err != nil { + return ErrFileNotFound + } + defer f.Close() + + fi, err = f.Stat() + if err != nil { + return err + } + } + + ff, ok := f.(io.ReadSeeker) + if !ok { + return errors.New("[FileFS] file does not implement io.ReadSeeker") + } + + http.ServeContent(e.Response, e.Request, fi.Name(), fi.ModTime(), ff) + + return nil +} + +// NoContent writes a response with no body (ex. 204). +func (e *Event) NoContent(status int) error { + e.Response.WriteHeader(status) + return nil +} + +// Redirect writes a redirect response to the specified url. +// The status code must be in between 300 – 399 range. +func (e *Event) Redirect(status int, url string) error { + if status < 300 || status > 399 { + return ErrInvalidRedirectStatusCode + } + e.Response.Header().Set("Location", url) + e.Response.WriteHeader(status) + return nil +} + +// ApiError helpers +// ------------------------------------------------------------------- + +func (e *Event) Error(status int, message string, errData any) *ApiError { + return NewApiError(status, message, errData) +} + +func (e *Event) BadRequestError(message string, errData any) *ApiError { + return NewBadRequestError(message, errData) +} + +func (e *Event) NotFoundError(message string, errData any) *ApiError { + return NewNotFoundError(message, errData) +} + +func (e *Event) ForbiddenError(message string, errData any) *ApiError { + return NewForbiddenError(message, errData) +} + +func (e *Event) UnauthorizedError(message string, errData any) *ApiError { + return NewUnauthorizedError(message, errData) +} + +func (e *Event) TooManyRequestsError(message string, errData any) *ApiError { + return NewTooManyRequestsError(message, errData) +} + +func (e *Event) InternalServerError(message string, errData any) *ApiError { + return NewInternalServerError(message, errData) +} + +// Binders +// ------------------------------------------------------------------- + +const DefaultMaxMemory = 32 << 20 // 32mb + +// Supports the following content-types: +// +// - application/json +// - multipart/form-data +// - application/x-www-form-urlencoded +// - text/xml, application/xml +func (e *Event) BindBody(dst any) error { + if e.Request.ContentLength == 0 { + return nil + } + + contentType := e.Request.Header.Get(headerContentType) + + if strings.HasPrefix(contentType, "application/json") { + dec := json.NewDecoder(e.Request.Body) + err := dec.Decode(dst) + if err == nil { + // manually call Reread because single call of json.Decoder.Decode() + // doesn't ensure that the entire body is a valid json string + // and it is not guaranteed that it will reach EOF to trigger the reread reset + // (ex. in case of trailing spaces or invalid trailing parts like: `{"test":1},something`) + if body, ok := e.Request.Body.(Rereader); ok { + body.Reread() + } + } + return err + } + + if strings.HasPrefix(contentType, "multipart/form-data") { + if err := e.Request.ParseMultipartForm(DefaultMaxMemory); err != nil { + return err + } + + return UnmarshalRequestData(e.Request.Form, dst, "", "") + } + + if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + if err := e.Request.ParseForm(); err != nil { + return err + } + + return UnmarshalRequestData(e.Request.Form, dst, "", "") + } + + if strings.HasPrefix(contentType, "text/xml") || + strings.HasPrefix(contentType, "application/xml") { + return xml.NewDecoder(e.Request.Body).Decode(dst) + } + + return ErrUnsupportedContentType +} diff --git a/tools/router/event_test.go b/tools/router/event_test.go new file mode 100644 index 00000000..ba464bdc --- /dev/null +++ b/tools/router/event_test.go @@ -0,0 +1,924 @@ +package router_test + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/router" +) + +type unwrapTester struct { + http.ResponseWriter +} + +func (ut unwrapTester) Unwrap() http.ResponseWriter { + return ut.ResponseWriter +} + +func TestEventWritten(t *testing.T) { + t.Parallel() + + res1 := httptest.NewRecorder() + + res2 := httptest.NewRecorder() + res2.Write([]byte("test")) + + res3 := &router.ResponseWriter{ResponseWriter: unwrapTester{httptest.NewRecorder()}} + + res4 := &router.ResponseWriter{ResponseWriter: unwrapTester{httptest.NewRecorder()}} + res4.Write([]byte("test")) + + scenarios := []struct { + name string + response http.ResponseWriter + expected bool + }{ + { + name: "non-written non-WriteTracker", + response: res1, + expected: false, + }, + { + name: "written non-WriteTracker", + response: res2, + expected: false, + }, + { + name: "non-written WriteTracker", + response: res3, + expected: false, + }, + { + name: "written WriteTracker", + response: res4, + expected: true, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + event := router.Event{ + Response: s.response, + } + + result := event.Written() + + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestEventStatus(t *testing.T) { + t.Parallel() + + res1 := httptest.NewRecorder() + + res2 := httptest.NewRecorder() + res2.WriteHeader(123) + + res3 := &router.ResponseWriter{ResponseWriter: unwrapTester{httptest.NewRecorder()}} + + res4 := &router.ResponseWriter{ResponseWriter: unwrapTester{httptest.NewRecorder()}} + res4.WriteHeader(123) + + scenarios := []struct { + name string + response http.ResponseWriter + expected int + }{ + { + name: "non-written non-StatusTracker", + response: res1, + expected: 0, + }, + { + name: "written non-StatusTracker", + response: res2, + expected: 0, + }, + { + name: "non-written StatusTracker", + response: res3, + expected: 0, + }, + { + name: "written StatusTracker", + response: res4, + expected: 123, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + event := router.Event{ + Response: s.response, + } + + result := event.Status() + + if result != s.expected { + t.Fatalf("Expected %d, got %d", s.expected, result) + } + }) + } +} + +func TestEventIsTLS(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + + event := router.Event{Request: req} + + // without TLS + if event.IsTLS() { + t.Fatalf("Expected IsTLS false") + } + + // dummy TLS state + req.TLS = new(tls.ConnectionState) + + // with TLS + if !event.IsTLS() { + t.Fatalf("Expected IsTLS true") + } +} + +func TestEventSetCookie(t *testing.T) { + t.Parallel() + + event := router.Event{ + Response: httptest.NewRecorder(), + } + + cookie := event.Response.Header().Get("set-cookie") + if cookie != "" { + t.Fatalf("Expected empty cookie string, got %q", cookie) + } + + event.SetCookie(&http.Cookie{Name: "test", Value: "a"}) + + expected := "test=a" + + cookie = event.Response.Header().Get("set-cookie") + if cookie != expected { + t.Fatalf("Expected cookie %q, got %q", expected, cookie) + } +} + +func TestEventRemoteIP(t *testing.T) { + t.Parallel() + + scenarios := []struct { + remoteAddr string + expected string + }{ + {"", "invalid IP"}, + {"1.2.3.4", "invalid IP"}, + {"1.2.3.4:8090", "1.2.3.4"}, + {"[0000:0000:0000:0000:0000:0000:0000:0002]:80", "0000:0000:0000:0000:0000:0000:0000:0002"}, + {"[::2]:80", "0000:0000:0000:0000:0000:0000:0000:0002"}, // should always return the expanded version + } + + for _, s := range scenarios { + t.Run(s.remoteAddr, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + req.RemoteAddr = s.remoteAddr + + event := router.Event{Request: req} + + ip := event.RemoteIP() + + if ip != s.expected { + t.Fatalf("Expected IP %q, got %q", s.expected, ip) + } + }) + } +} + +func TestEventUnsafeRealIP(t *testing.T) { + t.Parallel() + + scenarios := []struct { + headers map[string]string + expected string + }{ + {nil, "1.2.3.4"}, + { + map[string]string{"CF-Connecting-IP": "test"}, + "test", + }, + { + map[string]string{"Fly-Client-IP": "test"}, + "test", + }, + { + map[string]string{"X-Real-IP": "test"}, + "test", + }, + { + map[string]string{"X-Forwarded-For": "test1,test2,test3"}, + "test1", + }, + } + + for i, s := range scenarios { + keys := make([]string, 0, len(s.headers)) + for h := range s.headers { + keys = append(keys, h) + } + + testName := strings.Join(keys, "_") + if testName == "" { + testName = "no_headers" + strconv.Itoa(i) + } + + t.Run(testName, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + req.RemoteAddr = "1.2.3.4:80" // fallback + + for k, v := range s.headers { + req.Header.Set(k, v) + } + + event := router.Event{Request: req} + + ip := event.UnsafeRealIP() + + if ip != s.expected { + t.Fatalf("Expected IP %q, got %q", s.expected, ip) + } + }) + } +} + +func TestEventSetGet(t *testing.T) { + event := router.Event{} + + // get before any set (ensures that doesn't panic) + if v := event.Get("test"); v != nil { + t.Fatalf("Expected nil value, got %v", v) + } + + event.Set("a", 123) + event.Set("b", 456) + + scenarios := []struct { + key string + expected any + }{ + {"", nil}, + {"missing", nil}, + {"a", 123}, + {"b", 456}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.key), func(t *testing.T) { + result := event.Get(s.key) + if result != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, result) + } + }) + } +} + +func TestEventSetAllGetAll(t *testing.T) { + data := map[string]any{ + "a": 123, + "b": 456, + } + rawData, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + event := router.Event{} + event.SetAll(data) + + // modify the data to ensure that the map was shallow coppied + data["c"] = 789 + + result := event.GetAll() + rawResult, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + if len(rawResult) == 0 || !bytes.Equal(rawData, rawResult) { + t.Fatalf("Expected\n%v\ngot\n%v", rawData, rawResult) + } +} + +func TestEventString(t *testing.T) { + scenarios := []testResponseWriteScenario[string]{ + { + name: "no explicit content-type", + status: 123, + headers: nil, + body: "test", + expectedStatus: 123, + expectedHeaders: map[string]string{"content-type": "text/plain; charset=utf-8"}, + expectedBody: "test", + }, + { + name: "with explicit content-type", + status: 123, + headers: map[string]string{"content-type": "text/test"}, + body: "test", + expectedStatus: 123, + expectedHeaders: map[string]string{"content-type": "text/test"}, + expectedBody: "test", + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.String(s.status, s.body) + }) + } +} + +func TestEventHTML(t *testing.T) { + scenarios := []testResponseWriteScenario[string]{ + { + name: "no explicit content-type", + status: 123, + headers: nil, + body: "test", + expectedStatus: 123, + expectedHeaders: map[string]string{"content-type": "text/html; charset=utf-8"}, + expectedBody: "test", + }, + { + name: "with explicit content-type", + status: 123, + headers: map[string]string{"content-type": "text/test"}, + body: "test", + expectedStatus: 123, + expectedHeaders: map[string]string{"content-type": "text/test"}, + expectedBody: "test", + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.HTML(s.status, s.body) + }) + } +} + +func TestEventJSON(t *testing.T) { + body := map[string]any{"a": 123, "b": 456, "c": "test"} + expectedPickedBody := `{"a":123,"c":"test"}` + "\n" + expectedFullBody := `{"a":123,"b":456,"c":"test"}` + "\n" + + scenarios := []testResponseWriteScenario[any]{ + { + name: "no explicit content-type", + status: 200, + headers: nil, + body: body, + expectedStatus: 200, + expectedHeaders: map[string]string{"content-type": "application/json"}, + expectedBody: expectedPickedBody, + }, + { + name: "with explicit content-type (200)", + status: 200, + headers: map[string]string{"content-type": "application/test"}, + body: body, + expectedStatus: 200, + expectedHeaders: map[string]string{"content-type": "application/test"}, + expectedBody: expectedPickedBody, + }, + { + name: "with explicit content-type (400)", // no fields picker + status: 400, + headers: map[string]string{"content-type": "application/test"}, + body: body, + expectedStatus: 400, + expectedHeaders: map[string]string{"content-type": "application/test"}, + expectedBody: expectedFullBody, + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + e.Request.URL.RawQuery = "fields=a,c" // ensures that the picker is invoked + return e.JSON(s.status, s.body) + }) + } +} + +func TestEventXML(t *testing.T) { + scenarios := []testResponseWriteScenario[string]{ + { + name: "no explicit content-type", + status: 234, + headers: nil, + body: "test", + expectedStatus: 234, + expectedHeaders: map[string]string{"content-type": "application/xml; charset=utf-8"}, + expectedBody: xml.Header + "test", + }, + { + name: "with explicit content-type", + status: 234, + headers: map[string]string{"content-type": "text/test"}, + body: "test", + expectedStatus: 234, + expectedHeaders: map[string]string{"content-type": "text/test"}, + expectedBody: xml.Header + "test", + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.XML(s.status, s.body) + }) + } +} + +func TestEventStream(t *testing.T) { + scenarios := []testResponseWriteScenario[string]{ + { + name: "stream", + status: 234, + headers: map[string]string{"content-type": "text/test"}, + body: "test", + expectedStatus: 234, + expectedHeaders: map[string]string{"content-type": "text/test"}, + expectedBody: "test", + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.Stream(s.status, s.headers["content-type"], strings.NewReader(s.body)) + }) + } +} + +func TestEventNoContent(t *testing.T) { + s := testResponseWriteScenario[any]{ + name: "no content", + status: 234, + headers: map[string]string{"content-type": "text/test"}, + body: nil, + expectedStatus: 234, + expectedHeaders: map[string]string{"content-type": "text/test"}, + expectedBody: "", + } + + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.NoContent(s.status) + }) +} + +func TestEventFlush(t *testing.T) { + rec := httptest.NewRecorder() + + event := &router.Event{ + Response: unwrapTester{&router.ResponseWriter{ResponseWriter: rec}}, + } + event.Response.Write([]byte("test")) + event.Flush() + + if !rec.Flushed { + t.Fatal("Expected response to be flushed") + } +} + +func TestEventRedirect(t *testing.T) { + scenarios := []testResponseWriteScenario[any]{ + { + name: "non-30x status", + status: 200, + expectedStatus: 200, + expectedError: router.ErrInvalidRedirectStatusCode, + }, + { + name: "30x status", + status: 302, + headers: map[string]string{"location": "test"}, // should be overwritten with the argument + expectedStatus: 302, + expectedHeaders: map[string]string{"location": "example"}, + }, + } + + for _, s := range scenarios { + testEventResponseWrite(t, s, func(e *router.Event) error { + return e.Redirect(s.status, "example") + }) + } +} + +func TestEventFileFS(t *testing.T) { + // stub test files + // --- + dir, err := os.MkdirTemp("", "EventFileFS") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + err = os.WriteFile(filepath.Join(dir, "index.html"), []byte("index"), 0644) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(filepath.Join(dir, "test.txt"), []byte("test"), 0644) + if err != nil { + t.Fatal(err) + } + + // create sub directory with an index.html file inside it + err = os.MkdirAll(filepath.Join(dir, "sub1"), os.ModePerm) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(dir, "sub1", "index.html"), []byte("sub1 index"), 0644) + if err != nil { + t.Fatal(err) + } + + err = os.MkdirAll(filepath.Join(dir, "sub2"), os.ModePerm) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(dir, "sub2", "test.txt"), []byte("sub2 test"), 0644) + if err != nil { + t.Fatal(err) + } + // --- + + scenarios := []struct { + name string + path string + expected string + }{ + {"missing file", "", ""}, + {"root with no explicit file", "", ""}, + {"root with explicit file", "test.txt", "test"}, + {"sub dir with no explicit file", "sub1", "sub1 index"}, + {"sub dir with no explicit file (no index.html)", "sub2", ""}, + {"sub dir explicit file", "sub2/test.txt", "sub2 test"}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + + event := &router.Event{ + Request: req, + Response: rec, + } + + err = event.FileFS(os.DirFS(dir), s.path) + + hasErr := err != nil + expectErr := s.expected == "" + if hasErr != expectErr { + t.Fatalf("Expected hasErr %v, got %v (%v)", expectErr, hasErr, err) + } + + result := rec.Result() + + raw, err := io.ReadAll(result.Body) + result.Body.Close() + if err != nil { + t.Fatal(err) + } + + if string(raw) != s.expected { + t.Fatalf("Expected body\n%s\ngot\n%s", s.expected, raw) + } + + // ensure that the proper file headers are added + // (aka. http.ServeContent is invoked) + length, _ := strconv.Atoi(result.Header.Get("content-length")) + if length != len(s.expected) { + t.Fatalf("Expected Content-Length %d, got %d", len(s.expected), length) + } + }) + } +} + +func TestEventError(t *testing.T) { + err := new(router.Event).Error(123, "message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":123}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventBadRequestError(t *testing.T) { + err := new(router.Event).BadRequestError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":400}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventNotFoundError(t *testing.T) { + err := new(router.Event).NotFoundError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":404}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventForbiddenError(t *testing.T) { + err := new(router.Event).ForbiddenError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":403}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventUnauthorizedError(t *testing.T) { + err := new(router.Event).UnauthorizedError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":401}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventTooManyRequestsError(t *testing.T) { + err := new(router.Event).TooManyRequestsError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":429}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventInternalServerError(t *testing.T) { + err := new(router.Event).InternalServerError("message_test", map[string]any{"a": validation.Required, "b": "test"}) + + result, _ := json.Marshal(err) + expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":500}` + + if string(result) != expected { + t.Errorf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestEventBindBody(t *testing.T) { + type testDstStruct struct { + A int `json:"a" xml:"a" form:"a"` + B int `json:"b" xml:"b" form:"b"` + C string `json:"c" xml:"c" form:"c"` + } + + emptyDst := `{"a":0,"b":0,"c":""}` + + queryDst := `a=123&b=-456&c=test` + + xmlDst := ` + + + 123 + -456 + test + + ` + + jsonDst := `{"a":123,"b":-456,"c":"test"}` + + // multipart + mpBody := &bytes.Buffer{} + mpWriter := multipart.NewWriter(mpBody) + mpWriter.WriteField("@jsonPayload", `{"a":123}`) + mpWriter.WriteField("b", "-456") + mpWriter.WriteField("c", "test") + if err := mpWriter.Close(); err != nil { + t.Fatal(err) + } + + scenarios := []struct { + contentType string + body io.Reader + expectDst string + expectError bool + }{ + { + contentType: "", + body: strings.NewReader(jsonDst), + expectDst: emptyDst, + expectError: true, + }, + { + contentType: "application/rtf", // unsupported + body: strings.NewReader(jsonDst), + expectDst: emptyDst, + expectError: true, + }, + // empty body + { + contentType: "application/json;charset=emptybody", + body: strings.NewReader(""), + expectDst: emptyDst, + }, + // json + { + contentType: "application/json", + body: strings.NewReader(jsonDst), + expectDst: jsonDst, + }, + { + contentType: "application/json;charset=abc", + body: strings.NewReader(jsonDst), + expectDst: jsonDst, + }, + // xml + { + contentType: "text/xml", + body: strings.NewReader(xmlDst), + expectDst: jsonDst, + }, + { + contentType: "text/xml;charset=abc", + body: strings.NewReader(xmlDst), + expectDst: jsonDst, + }, + { + contentType: "application/xml", + body: strings.NewReader(xmlDst), + expectDst: jsonDst, + }, + { + contentType: "application/xml;charset=abc", + body: strings.NewReader(xmlDst), + expectDst: jsonDst, + }, + // x-www-form-urlencoded + { + contentType: "application/x-www-form-urlencoded", + body: strings.NewReader(queryDst), + expectDst: jsonDst, + }, + { + contentType: "application/x-www-form-urlencoded;charset=abc", + body: strings.NewReader(queryDst), + expectDst: jsonDst, + }, + // multipart + { + contentType: mpWriter.FormDataContentType(), + body: mpBody, + expectDst: jsonDst, + }, + } + + for _, s := range scenarios { + t.Run(s.contentType, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "/", s.body) + if err != nil { + t.Fatal(err) + } + req.Header.Add("content-type", s.contentType) + + event := &router.Event{Request: req} + + dst := testDstStruct{} + + err = event.BindBody(&dst) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + dstRaw, err := json.Marshal(dst) + if err != nil { + t.Fatal(err) + } + + if string(dstRaw) != s.expectDst { + t.Fatalf("Expected dst\n%s\ngot\n%s", s.expectDst, dstRaw) + } + }) + } +} + +// ------------------------------------------------------------------- + +type testResponseWriteScenario[T any] struct { + name string + status int + headers map[string]string + body T + expectedStatus int + expectedHeaders map[string]string + expectedBody string + expectedError error +} + +func testEventResponseWrite[T any]( + t *testing.T, + scenario testResponseWriteScenario[T], + writeFunc func(e *router.Event) error, +) { + t.Run(scenario.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + event := &router.Event{ + Request: req, + Response: &router.ResponseWriter{ResponseWriter: rec}, + } + + for k, v := range scenario.headers { + event.Response.Header().Add(k, v) + } + + err = writeFunc(event) + if (scenario.expectedError != nil || err != nil) && !errors.Is(err, scenario.expectedError) { + t.Fatalf("Expected error %v, got %v", scenario.expectedError, err) + } + + result := rec.Result() + + if result.StatusCode != scenario.expectedStatus { + t.Fatalf("Expected status code %d, got %d", scenario.expectedStatus, result.StatusCode) + } + + resultBody, err := io.ReadAll(result.Body) + result.Body.Close() + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + resultBody, err = json.Marshal(string(resultBody)) + if err != nil { + t.Fatal(err) + } + + expectedBody, err := json.Marshal(scenario.expectedBody) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(resultBody, expectedBody) { + t.Fatalf("Expected body\n%s\ngot\n%s", expectedBody, resultBody) + } + + for k, ev := range scenario.expectedHeaders { + if v := result.Header.Get(k); v != ev { + t.Fatalf("Expected %q header to be %q, got %q", k, ev, v) + } + } + }) +} diff --git a/tools/router/group.go b/tools/router/group.go new file mode 100644 index 00000000..55f24f00 --- /dev/null +++ b/tools/router/group.go @@ -0,0 +1,226 @@ +package router + +import ( + "net/http" + "regexp" + "strings" + + "github.com/pocketbase/pocketbase/tools/hook" +) + +// (note: the struct is named RouterGroup instead of Group so that it can +// be embedded in the Router without conflicting with the Group method) + +// RouterGroup represents a collection of routes and other sub groups +// that share common pattern prefix and middlewares. +type RouterGroup[T hook.Resolver] struct { + excludedMiddlewares map[string]struct{} + children []any // Route or RouterGroup + + Prefix string + Middlewares []*hook.Handler[T] +} + +// Group creates and register a new child Group into the current one +// with the specified prefix. +// +// The prefix follows the standard Go net/http ServeMux pattern format ("[HOST]/[PATH]") +// and will be concatenated recursively into the final route path, meaning that +// only the root level group could have HOST as part of the prefix. +// +// Returns the newly created group to allow chaining and registering +// sub-routes and group specific middlewares. +func (group *RouterGroup[T]) Group(prefix string) *RouterGroup[T] { + newGroup := &RouterGroup[T]{} + newGroup.Prefix = prefix + + group.children = append(group.children, newGroup) + + return newGroup +} + +// BindFunc registers one or multiple middleware functions to the current group. +// +// The registered middleware functions are "anonymous" and with default priority, +// 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 ...hook.HandlerFunc[T]) *RouterGroup[T] { + for _, m := range middlewareFuncs { + group.Middlewares = append(group.Middlewares, &hook.Handler[T]{Func: m}) + } + + return group +} + +// Bind registers one or multiple middleware handlers to the current group. +func (group *RouterGroup[T]) Bind(middlewares ...*hook.Handler[T]) *RouterGroup[T] { + group.Middlewares = append(group.Middlewares, middlewares...) + + // unmark the newly added middlewares in case they were previously "excluded" + if group.excludedMiddlewares != nil { + for _, m := range middlewares { + if m.Id != "" { + delete(group.excludedMiddlewares, m.Id) + } + } + } + + return group +} + +// Unbind removes one or more middlewares with the specified id(s) +// from the current group and its children (if any). +// +// Anonymous middlewares are not removable, aka. this method does nothing +// if the middleware id is an empty string. +func (group *RouterGroup[T]) Unbind(middlewareIds ...string) *RouterGroup[T] { + for _, middlewareId := range middlewareIds { + if middlewareId == "" { + continue + } + + // remove from the group middlwares + for i := len(group.Middlewares) - 1; i >= 0; i-- { + if group.Middlewares[i].Id == middlewareId { + group.Middlewares = append(group.Middlewares[:i], group.Middlewares[i+1:]...) + } + } + + // remove from the group children + for i := len(group.children) - 1; i >= 0; i-- { + switch v := group.children[i].(type) { + case *RouterGroup[T]: + v.Unbind(middlewareId) + case *Route[T]: + v.Unbind(middlewareId) + } + } + + // add to the exclude list + if group.excludedMiddlewares == nil { + group.excludedMiddlewares = map[string]struct{}{} + } + group.excludedMiddlewares[middlewareId] = struct{}{} + } + + return group +} + +// Route registers a single route into the current group. +// +// Note that the final route path will be the concatenation of all parent groups prefixes + the route path. +// The path follows the standard Go net/http ServeMux format ("[HOST]/[PATH]"), +// 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 hook.HandlerFunc[T]) *Route[T] { + route := &Route[T]{ + Method: method, + Path: path, + Action: action, + } + + group.children = append(group.children, route) + + 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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *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 hook.HandlerFunc[T]) *Route[T] { + return group.Route(http.MethodOptions, path, action) +} + +// HasRoute checks whether the specified route pattern (method + path) +// is registered in the current group or its children. +// +// This could be useful to conditionally register and checks for routes +// in order prevent panic on duplicated routes. +// +// Note that routes with anonymous and named wildcard placeholder are treated as equal, +// aka. "GET /abc/" is considered the same as "GET /abc/{something...}". +func (group *RouterGroup[T]) HasRoute(method string, path string) bool { + pattern := path + if method != "" { + pattern = strings.ToUpper(method) + " " + pattern + } + + return group.hasRoute(pattern, nil) +} + +func (group *RouterGroup[T]) hasRoute(pattern string, parents []*RouterGroup[T]) bool { + for _, child := range group.children { + switch v := child.(type) { + case *RouterGroup[T]: + if v.hasRoute(pattern, append(parents, group)) { + return true + } + case *Route[T]: + var result string + + if v.Method != "" { + result += v.Method + " " + } + + // add parent groups prefixes + for _, p := range parents { + result += p.Prefix + } + + // add current group prefix + result += group.Prefix + + // add current route path + result += v.Path + + if result == pattern || // direct match + // compares without the named wildcard, aka. /abc/{test...} is equal to /abc/ + stripWildcard(result) == stripWildcard(pattern) { + return true + } + } + } + return false +} + +var wildcardPlaceholderRegex = regexp.MustCompile(`/{.+\.\.\.}$`) + +func stripWildcard(pattern string) string { + return wildcardPlaceholderRegex.ReplaceAllString(pattern, "/") +} diff --git a/tools/router/group_test.go b/tools/router/group_test.go new file mode 100644 index 00000000..1b655af6 --- /dev/null +++ b/tools/router/group_test.go @@ -0,0 +1,425 @@ +package router + +import ( + "errors" + "fmt" + "net/http" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/tools/hook" +) + +func TestRouterGroupGroup(t *testing.T) { + t.Parallel() + + g0 := RouterGroup[*Event]{} + + g1 := g0.Group("test1") + g2 := g0.Group("test2") + + if total := len(g0.children); total != 2 { + t.Fatalf("Expected %d child groups, got %d", 2, total) + } + + if g1.Prefix != "test1" { + t.Fatalf("Expected g1 with prefix %q, got %q", "test1", g1.Prefix) + } + if g2.Prefix != "test2" { + t.Fatalf("Expected g2 with prefix %q, got %q", "test2", g2.Prefix) + } +} + +func TestRouterGroupBindFunc(t *testing.T) { + t.Parallel() + + g := RouterGroup[*Event]{} + + calls := "" + + // append one function + g.BindFunc(func(e *Event) error { + calls += "a" + return nil + }) + + // append multiple functions + g.BindFunc( + func(e *Event) error { + calls += "b" + return nil + }, + func(e *Event) error { + calls += "c" + return nil + }, + ) + + if total := len(g.Middlewares); total != 3 { + t.Fatalf("Expected %d middlewares, got %v", 3, total) + } + + for _, h := range g.Middlewares { + _ = h.Func(nil) + } + + if calls != "abc" { + t.Fatalf("Expected calls sequence %q, got %q", "abc", calls) + } +} + +func TestRouterGroupBind(t *testing.T) { + t.Parallel() + + g := RouterGroup[*Event]{ + // mock excluded middlewares to check whether the entry will be deleted + excludedMiddlewares: map[string]struct{}{"test2": {}}, + } + + calls := "" + + // append one handler + g.Bind(&hook.Handler[*Event]{ + Func: func(e *Event) error { + calls += "a" + return nil + }, + }) + + // append multiple handlers + g.Bind( + &hook.Handler[*Event]{ + Id: "test1", + Func: func(e *Event) error { + calls += "b" + return nil + }, + }, + &hook.Handler[*Event]{ + Id: "test2", + Func: func(e *Event) error { + calls += "c" + return nil + }, + }, + ) + + if total := len(g.Middlewares); total != 3 { + t.Fatalf("Expected %d middlewares, got %v", 3, total) + } + + for _, h := range g.Middlewares { + _ = h.Func(nil) + } + + if calls != "abc" { + t.Fatalf("Expected calls %q, got %q", "abc", calls) + } + + // ensures that the previously excluded middleware was removed + if len(g.excludedMiddlewares) != 0 { + t.Fatalf("Expected test2 to be removed from the excludedMiddlewares list, got %v", g.excludedMiddlewares) + } +} + +func TestRouterGroupUnbind(t *testing.T) { + t.Parallel() + + g := RouterGroup[*Event]{} + + calls := "" + + // anonymous middlewares + g.Bind(&hook.Handler[*Event]{ + Func: func(e *Event) error { + calls += "a" + return nil // unused value + }, + }) + + // middlewares with id + g.Bind(&hook.Handler[*Event]{ + Id: "test1", + Func: func(e *Event) error { + calls += "b" + return nil // unused value + }, + }) + g.Bind(&hook.Handler[*Event]{ + Id: "test2", + Func: func(e *Event) error { + calls += "c" + return nil // unused value + }, + }) + g.Bind(&hook.Handler[*Event]{ + Id: "test3", + Func: func(e *Event) error { + calls += "d" + return nil // unused value + }, + }) + + // remove + g.Unbind("") // should be no-op + g.Unbind("test1", "test3") + + if total := len(g.Middlewares); total != 2 { + t.Fatalf("Expected %d middlewares, got %v", 2, total) + } + + for _, h := range g.Middlewares { + if err := h.Func(nil); err != nil { + continue + } + } + + if calls != "ac" { + t.Fatalf("Expected calls %q, got %q", "ac", calls) + } + + // ensure that the ids were added in the exclude list + excluded := []string{"test1", "test3"} + if len(g.excludedMiddlewares) != len(excluded) { + t.Fatalf("Expected excludes %v, got %v", excluded, g.excludedMiddlewares) + } + for id := range g.excludedMiddlewares { + if !slices.Contains(excluded, id) { + t.Fatalf("Expected %q to be marked as excluded", id) + } + } +} + +func TestRouterGroupRoute(t *testing.T) { + t.Parallel() + + group := RouterGroup[*Event]{} + + sub := group.Group("sub") + + var called bool + route := group.Route(http.MethodPost, "/test", func(e *Event) error { + called = true + return nil + }) + + // ensure that the route was registered only to the main one + // --- + if len(sub.children) != 0 { + t.Fatalf("Expected no sub children, got %d", len(sub.children)) + } + + if len(group.children) != 2 { + t.Fatalf("Expected %d group children, got %d", 2, len(group.children)) + } + // --- + + // check the registered route + // --- + if route != group.children[1] { + t.Fatalf("Expected group children %v, got %v", route, group.children[1]) + } + + if route.Method != http.MethodPost { + t.Fatalf("Expected route method %q, got %q", http.MethodPost, route.Method) + } + + if route.Path != "/test" { + t.Fatalf("Expected route path %q, got %q", "/test", route.Path) + } + + route.Action(nil) + if !called { + t.Fatal("Expected route action to be called") + } +} + +func TestRouterGroupRouteAliases(t *testing.T) { + t.Parallel() + + group := RouterGroup[*Event]{} + + testErr := errors.New("test") + + testAction := func(e *Event) error { + return testErr + } + + scenarios := []struct { + route *Route[*Event] + expectMethod string + expectPath string + }{ + { + group.Any("/test", testAction), + "", + "/test", + }, + { + group.GET("/test", testAction), + http.MethodGet, + "/test", + }, + { + group.POST("/test", testAction), + http.MethodPost, + "/test", + }, + { + group.DELETE("/test", testAction), + http.MethodDelete, + "/test", + }, + { + group.PATCH("/test", testAction), + http.MethodPatch, + "/test", + }, + { + group.PUT("/test", testAction), + http.MethodPut, + "/test", + }, + { + group.HEAD("/test", testAction), + http.MethodHead, + "/test", + }, + { + group.OPTIONS("/test", testAction), + http.MethodOptions, + "/test", + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s_%s", i, s.expectMethod, s.expectPath), func(t *testing.T) { + if s.route.Method != s.expectMethod { + t.Fatalf("Expected method %q, got %q", s.expectMethod, s.route.Method) + } + + if s.route.Path != s.expectPath { + t.Fatalf("Expected path %q, got %q", s.expectPath, s.route.Path) + } + + if err := s.route.Action(nil); !errors.Is(err, testErr) { + t.Fatal("Expected test action") + } + }) + } +} + +func TestRouterGroupHasRoute(t *testing.T) { + t.Parallel() + + group := RouterGroup[*Event]{} + + group.Any("/any", nil) + + group.GET("/base", nil) + group.DELETE("/base", nil) + + sub := group.Group("/sub1") + sub.GET("/a", nil) + sub.POST("/a", nil) + + sub2 := sub.Group("/sub2") + sub2.GET("/b", nil) + sub2.GET("/b/{test}", nil) + + // special cases to test the normalizations + group.GET("/c/", nil) // the same as /c/{test...} + group.GET("/d/{test...}", nil) // the same as /d/ + + scenarios := []struct { + method string + path string + expected bool + }{ + { + http.MethodGet, + "", + false, + }, + { + "", + "/any", + true, + }, + { + http.MethodPost, + "/base", + false, + }, + { + http.MethodGet, + "/base", + true, + }, + { + http.MethodDelete, + "/base", + true, + }, + { + http.MethodGet, + "/sub1", + false, + }, + { + http.MethodGet, + "/sub1/a", + true, + }, + { + http.MethodPost, + "/sub1/a", + true, + }, + { + http.MethodDelete, + "/sub1/a", + false, + }, + { + http.MethodGet, + "/sub2/b", + false, + }, + { + http.MethodGet, + "/sub1/sub2/b", + true, + }, + { + http.MethodGet, + "/sub1/sub2/b/{test}", + true, + }, + { + http.MethodGet, + "/sub1/sub2/b/{test2}", + false, + }, + { + http.MethodGet, + "/c/{test...}", + true, + }, + { + http.MethodGet, + "/d/", + true, + }, + } + + for _, s := range scenarios { + t.Run(s.method+"_"+s.path, func(t *testing.T) { + has := group.HasRoute(s.method, s.path) + + if has != s.expected { + t.Fatalf("Expected %v, got %v", s.expected, has) + } + }) + } +} diff --git a/tools/router/rereadable_read_closer.go b/tools/router/rereadable_read_closer.go new file mode 100644 index 00000000..0e5f5d8a --- /dev/null +++ b/tools/router/rereadable_read_closer.go @@ -0,0 +1,60 @@ +package router + +import ( + "bytes" + "io" +) + +var ( + _ io.ReadCloser = (*RereadableReadCloser)(nil) + _ Rereader = (*RereadableReadCloser)(nil) +) + +// Rereader defines an interface for rewindable readers. +type Rereader interface { + Reread() +} + +// RereadableReadCloser defines a wrapper around a io.ReadCloser reader +// allowing to read the original reader multiple times. +type RereadableReadCloser struct { + io.ReadCloser + + copy *bytes.Buffer + active io.Reader +} + +// Read implements the standard io.Reader interface. +// +// It reads up to len(b) bytes into b and at at the same time writes +// the read data into an internal bytes buffer. +// +// On EOF the r is "rewinded" to allow reading from r multiple times. +func (r *RereadableReadCloser) Read(b []byte) (int, error) { + if r.active == nil { + if r.copy == nil { + r.copy = &bytes.Buffer{} + } + r.active = io.TeeReader(r.ReadCloser, r.copy) + } + + n, err := r.active.Read(b) + if err == io.EOF { + r.Reread() + } + + return n, err +} + +// Reread satisfies the [Rereader] interface and resets the r internal state to allow rereads. +// +// note: not named Reset to avoid conflicts with other reader interfaces. +func (r *RereadableReadCloser) Reread() { + if r.copy == nil || r.copy.Len() == 0 { + return // nothing to reset or it has been already reset + } + + oldCopy := r.copy + r.copy = &bytes.Buffer{} + r.active = io.TeeReader(oldCopy, r.copy) +} diff --git a/tools/router/rereadable_read_closer_test.go b/tools/router/rereadable_read_closer_test.go new file mode 100644 index 00000000..2334ee68 --- /dev/null +++ b/tools/router/rereadable_read_closer_test.go @@ -0,0 +1,28 @@ +package router_test + +import ( + "io" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/tools/router" +) + +func TestRereadableReadCloser(t *testing.T) { + content := "test" + + rereadable := &router.RereadableReadCloser{ + ReadCloser: io.NopCloser(strings.NewReader(content)), + } + + // read multiple times + for i := 0; i < 3; i++ { + result, err := io.ReadAll(rereadable) + if err != nil { + t.Fatalf("[read:%d] %v", i, err) + } + if str := string(result); str != content { + t.Fatalf("[read:%d] Expected %q, got %q", i, content, result) + } + } +} diff --git a/tools/router/route.go b/tools/router/route.go new file mode 100644 index 00000000..2bb3e568 --- /dev/null +++ b/tools/router/route.go @@ -0,0 +1,73 @@ +package router + +import "github.com/pocketbase/pocketbase/tools/hook" + +type Route[T hook.Resolver] struct { + excludedMiddlewares map[string]struct{} + + Action hook.HandlerFunc[T] + Method string + Path string + Middlewares []*hook.Handler[T] +} + +// BindFunc registers one or multiple middleware functions to the current route. +// +// The registered middleware functions are "anonymous" and with default priority, +// 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 ...hook.HandlerFunc[T]) *Route[T] { + for _, m := range middlewareFuncs { + route.Middlewares = append(route.Middlewares, &hook.Handler[T]{Func: m}) + } + + return route +} + +// Bind registers one or multiple middleware handlers to the current route. +func (route *Route[T]) Bind(middlewares ...*hook.Handler[T]) *Route[T] { + route.Middlewares = append(route.Middlewares, middlewares...) + + // unmark the newly added middlewares in case they were previously "excluded" + if route.excludedMiddlewares != nil { + for _, m := range middlewares { + if m.Id != "" { + delete(route.excludedMiddlewares, m.Id) + } + } + } + + return route +} + +// Unbind removes one or more middlewares with the specified id(s) from the current route. +// +// It also adds the removed middleware ids to an exclude list so that they could be skipped from +// the execution chain in case the middleware is registered in a parent group. +// +// Anonymous middlewares are considered non-removable, aka. this method +// does nothing if the middleware id is an empty string. +func (route *Route[T]) Unbind(middlewareIds ...string) *Route[T] { + for _, middlewareId := range middlewareIds { + if middlewareId == "" { + continue + } + + // remove from the route's middlewares + for i := len(route.Middlewares) - 1; i >= 0; i-- { + if route.Middlewares[i].Id == middlewareId { + route.Middlewares = append(route.Middlewares[:i], route.Middlewares[i+1:]...) + } + } + + // add to the exclude list + if route.excludedMiddlewares == nil { + route.excludedMiddlewares = map[string]struct{}{} + } + route.excludedMiddlewares[middlewareId] = struct{}{} + } + + return route +} diff --git a/tools/router/route_test.go b/tools/router/route_test.go new file mode 100644 index 00000000..ef6e5416 --- /dev/null +++ b/tools/router/route_test.go @@ -0,0 +1,168 @@ +package router + +import ( + "slices" + "testing" + + "github.com/pocketbase/pocketbase/tools/hook" +) + +func TestRouteBindFunc(t *testing.T) { + t.Parallel() + + r := Route[*Event]{} + + calls := "" + + // append one function + r.BindFunc(func(e *Event) error { + calls += "a" + return nil + }) + + // append multiple functions + r.BindFunc( + func(e *Event) error { + calls += "b" + return nil + }, + func(e *Event) error { + calls += "c" + return nil + }, + ) + + if total := len(r.Middlewares); total != 3 { + t.Fatalf("Expected %d middlewares, got %v", 3, total) + } + + for _, h := range r.Middlewares { + _ = h.Func(nil) + } + + if calls != "abc" { + t.Fatalf("Expected calls sequence %q, got %q", "abc", calls) + } +} + +func TestRouteBind(t *testing.T) { + t.Parallel() + + r := Route[*Event]{ + // mock excluded middlewares to check whether the entry will be deleted + excludedMiddlewares: map[string]struct{}{"test2": {}}, + } + + calls := "" + + // append one handler + r.Bind(&hook.Handler[*Event]{ + Func: func(e *Event) error { + calls += "a" + return nil + }, + }) + + // append multiple handlers + r.Bind( + &hook.Handler[*Event]{ + Id: "test1", + Func: func(e *Event) error { + calls += "b" + return nil + }, + }, + &hook.Handler[*Event]{ + Id: "test2", + Func: func(e *Event) error { + calls += "c" + return nil + }, + }, + ) + + if total := len(r.Middlewares); total != 3 { + t.Fatalf("Expected %d middlewares, got %v", 3, total) + } + + for _, h := range r.Middlewares { + _ = h.Func(nil) + } + + if calls != "abc" { + t.Fatalf("Expected calls %q, got %q", "abc", calls) + } + + // ensures that the previously excluded middleware was removed + if len(r.excludedMiddlewares) != 0 { + t.Fatalf("Expected test2 to be removed from the excludedMiddlewares list, got %v", r.excludedMiddlewares) + } +} + +func TestRouteUnbind(t *testing.T) { + t.Parallel() + + r := Route[*Event]{} + + calls := "" + + // anonymous middlewares + r.Bind(&hook.Handler[*Event]{ + Func: func(e *Event) error { + calls += "a" + return nil // unused value + }, + }) + + // middlewares with id + r.Bind(&hook.Handler[*Event]{ + Id: "test1", + Func: func(e *Event) error { + calls += "b" + return nil // unused value + }, + }) + r.Bind(&hook.Handler[*Event]{ + Id: "test2", + Func: func(e *Event) error { + calls += "c" + return nil // unused value + }, + }) + r.Bind(&hook.Handler[*Event]{ + Id: "test3", + Func: func(e *Event) error { + calls += "d" + return nil // unused value + }, + }) + + // remove + r.Unbind("") // should be no-op + r.Unbind("test1", "test3") + + if total := len(r.Middlewares); total != 2 { + t.Fatalf("Expected %d middlewares, got %v", 2, total) + } + + for _, h := range r.Middlewares { + if err := h.Func(nil); err != nil { + continue + } + } + + if calls != "ac" { + t.Fatalf("Expected calls %q, got %q", "ac", calls) + } + + // ensure that the id was added in the exclude list + excluded := []string{"test1", "test3"} + if len(r.excludedMiddlewares) != len(excluded) { + t.Fatalf("Expected excludes %v, got %v", excluded, r.excludedMiddlewares) + } + for id := range r.excludedMiddlewares { + if !slices.Contains(excluded, id) { + t.Fatalf("Expected %q to be marked as excluded", id) + } + } +} diff --git a/tools/router/router.go b/tools/router/router.go new file mode 100644 index 00000000..8a9051da --- /dev/null +++ b/tools/router/router.go @@ -0,0 +1,362 @@ +package router + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "runtime" + + "github.com/pocketbase/pocketbase/tools/hook" +) + +type EventCleanupFunc func() + +// EventFactoryFunc defines the function responsible for creating a Route specific event +// based on the provided request handler ServeHTTP data. +// +// Optionally return a clean up function that will be invoked right after the route execution. +type EventFactoryFunc[T hook.Resolver] func(w http.ResponseWriter, r *http.Request) (T, EventCleanupFunc) + +// Router defines a thin wrapper around the standard Go [http.ServeMux] by +// adding support for routing sub-groups, middlewares and other common utils. +// +// Example: +// +// r := NewRouter[*MyEvent](eventFactory) +// +// // middlewares +// r.BindFunc(m1, m2) +// +// // routes +// r.GET("/test", handler1) +// +// // sub-routers/groups +// api := r.Group("/api") +// api.GET("/admins", handler2) +// +// // generate a http.ServeMux instance based on the router configurations +// mux, _ := r.BuildMux() +// +// http.ListenAndServe("localhost:8090", mux) +type Router[T hook.Resolver] struct { + *RouterGroup[T] + + eventFactory EventFactoryFunc[T] +} + +// NewRouter creates a new Router instance with the provided event factory function. +func NewRouter[T hook.Resolver](eventFactory EventFactoryFunc[T]) *Router[T] { + return &Router[T]{ + RouterGroup: &RouterGroup[T]{}, + eventFactory: eventFactory, + } +} + +// BuildMux constructs a new mux [http.Handler] instance from the current router configurations. +func (r *Router[T]) BuildMux() (http.Handler, error) { + // Note that some of the default std Go handlers like the [http.NotFoundHandler] + // cannot be currently extended and requires defining a custom "catch-all" route + // so that the group middlewares could be executed. + // + // https://github.com/golang/go/issues/65648 + if !r.HasRoute("", "/") { + r.Route("", "/", func(e T) error { + return NewNotFoundError("", nil) + }) + } + + mux := http.NewServeMux() + + if err := r.loadMux(mux, r.RouterGroup, nil); err != nil { + return nil, err + } + + return mux, nil +} + +func (r *Router[T]) loadMux(mux *http.ServeMux, group *RouterGroup[T], parents []*RouterGroup[T]) error { + for _, child := range group.children { + switch v := child.(type) { + case *RouterGroup[T]: + if err := r.loadMux(mux, v, append(parents, group)); err != nil { + return err + } + case *Route[T]: + routeHook := &hook.Hook[T]{} + + var pattern string + + if v.Method != "" { + pattern = v.Method + " " + } + + // add parent groups middlewares + for _, p := range parents { + pattern += p.Prefix + for _, h := range p.Middlewares { + if _, ok := p.excludedMiddlewares[h.Id]; !ok { + if _, ok = group.excludedMiddlewares[h.Id]; !ok { + if _, ok = v.excludedMiddlewares[h.Id]; !ok { + routeHook.Bind(h) + } + } + } + } + } + + // add current groups middlewares + pattern += group.Prefix + for _, h := range group.Middlewares { + if _, ok := group.excludedMiddlewares[h.Id]; !ok { + if _, ok = v.excludedMiddlewares[h.Id]; !ok { + routeHook.Bind(h) + } + } + } + + // add current route middlewares + pattern += v.Path + for _, h := range v.Middlewares { + if _, ok := v.excludedMiddlewares[h.Id]; !ok { + routeHook.Bind(h) + } + } + + // 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} + + // wrap the request body to allow multiple reads + req.Body = &RereadableReadCloser{ReadCloser: req.Body} + + event, cleanupFunc := r.eventFactory(resp, req) + + // trigger the handler hook chain + err := routeHook.Trigger(event, v.Action) + if err != nil { + ErrorHandler(resp, req, err) + } + + if cleanupFunc != nil { + cleanupFunc() + } + }) + default: + return errors.New("invalid Group item type") + } + } + + 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 + } + + if ok, _ := getWritten(resp); ok { + return // a response was already written (aka. already handled) + } + + header := resp.Header() + if header.Get("Content-Type") == "" { + header.Set("Content-Type", "application/json") + } + + apiErr := ToApiError(err) + + resp.WriteHeader(apiErr.Status) + + if req.Method != http.MethodHead { + if jsonErr := json.NewEncoder(resp).Encode(apiErr); jsonErr != nil { + log.Println(jsonErr) // truly rare case, log to stderr only for dev purposes + } + } +} + +// ------------------------------------------------------------------- + +type WriteTracker interface { + // Written reports whether a write operation has occurred. + Written() bool +} + +type StatusTracker interface { + // Status reports the written response status code. + Status() int +} + +type flushErrorer interface { + FlushError() error +} + +var ( + _ WriteTracker = (*ResponseWriter)(nil) + _ StatusTracker = (*ResponseWriter)(nil) + _ http.Flusher = (*ResponseWriter)(nil) + _ http.Hijacker = (*ResponseWriter)(nil) + _ http.Pusher = (*ResponseWriter)(nil) + _ io.ReaderFrom = (*ResponseWriter)(nil) + _ flushErrorer = (*ResponseWriter)(nil) +) + +// ResponseWriter wraps a http.ResponseWriter to track its write state. +type ResponseWriter struct { + http.ResponseWriter + + written bool + status int +} + +func (rw *ResponseWriter) WriteHeader(status int) { + if rw.written { + return + } + + rw.written = true + rw.status = status + rw.ResponseWriter.WriteHeader(status) +} + +func (rw *ResponseWriter) Write(b []byte) (int, error) { + if !rw.written { + rw.WriteHeader(http.StatusOK) + } + + return rw.ResponseWriter.Write(b) +} + +// Written implements [WriteTracker] and returns whether the current response body has been already written. +func (rw *ResponseWriter) Written() bool { + return rw.written +} + +// Written implements [StatusTracker] and returns the written status code of the current response. +func (rw *ResponseWriter) Status() int { + return rw.status +} + +// Flush implements [http.Flusher] and allows an HTTP handler to flush buffered data to the client. +// This method is no-op if the wrapped writer doesn't support it. +func (rw *ResponseWriter) Flush() { + _ = rw.FlushError() +} + +// FlushError is similar to [Flush] but returns [http.ErrNotSupported] +// if the wrapped writer doesn't support it. +func (rw *ResponseWriter) FlushError() error { + err := http.NewResponseController(rw.ResponseWriter).Flush() + if err == nil || !errors.Is(err, http.ErrNotSupported) { + rw.written = true + } + return err +} + +// Hijack implements [http.Hijacker] and allows an HTTP handler to take over the current connection. +func (rw *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return http.NewResponseController(rw.ResponseWriter).Hijack() +} + +// Pusher implements [http.Pusher] to indicate HTTP/2 server push support. +func (rw *ResponseWriter) Push(target string, opts *http.PushOptions) error { + w := rw.ResponseWriter + for { + switch p := w.(type) { + case http.Pusher: + return p.Push(target, opts) + case RWUnwrapper: + w = p.Unwrap() + default: + return http.ErrNotSupported + } + } +} + +// ReaderFrom implements [io.ReaderFrom] by checking if the underlying writer supports it. +// Otherwise calls [io.Copy]. +func (rw *ResponseWriter) ReadFrom(r io.Reader) (n int64, err error) { + if !rw.written { + rw.WriteHeader(http.StatusOK) + } + + w := rw.ResponseWriter + for { + switch rf := w.(type) { + case io.ReaderFrom: + return rf.ReadFrom(r) + case RWUnwrapper: + w = rf.Unwrap() + default: + return io.Copy(rw.ResponseWriter, r) + } + } +} + +// Unwrap returns the underlying ResponseWritter instance (usually used by [http.ResponseController]). +func (rw *ResponseWriter) Unwrap() http.ResponseWriter { + return rw.ResponseWriter +} + +func getWritten(rw http.ResponseWriter) (bool, error) { + for { + switch w := rw.(type) { + case WriteTracker: + return w.Written(), nil + case RWUnwrapper: + rw = w.Unwrap() + default: + return false, http.ErrNotSupported + } + } +} + +func getStatus(rw http.ResponseWriter) (int, error) { + for { + switch w := rw.(type) { + case StatusTracker: + return w.Status(), nil + case RWUnwrapper: + rw = w.Unwrap() + default: + return 0, http.ErrNotSupported + } + } +} diff --git a/tools/router/router_test.go b/tools/router/router_test.go new file mode 100644 index 00000000..82021c54 --- /dev/null +++ b/tools/router/router_test.go @@ -0,0 +1,258 @@ +package router_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/router" +) + +func TestRouter(t *testing.T) { + calls := "" + + r := router.NewRouter(func(w http.ResponseWriter, r *http.Request) (*router.Event, router.EventCleanupFunc) { + return &router.Event{ + Response: w, + Request: r, + }, + func() { + calls += ":cleanup" + } + }) + + r.BindFunc(func(e *router.Event) error { + calls += "root_m:" + + err := e.Next() + + if err != nil { + calls += "/error" + } + + return err + }) + + r.Any("/any", func(e *router.Event) error { + calls += "/any" + return nil + }) + + r.GET("/a", func(e *router.Event) error { + calls += "/a" + return nil + }) + + g1 := r.Group("/a/b").BindFunc(func(e *router.Event) error { + calls += "a_b_group_m:" + return e.Next() + }) + g1.GET("/1", func(e *router.Event) error { + calls += "/1_get" + return nil + }).BindFunc(func(e *router.Event) error { + calls += "1_get_m:" + return e.Next() + }) + g1.POST("/1", func(e *router.Event) error { + calls += "/1_post" + return nil + }) + g1.GET("/{param}", func(e *router.Event) error { + 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 { + t.Fatal(err) + } + + ts := httptest.NewServer(mux) + defer ts.Close() + + client := ts.Client() + + scenarios := []struct { + method string + path string + calls string + }{ + {http.MethodGet, "/any", "root_m:/any:cleanup"}, + {http.MethodOptions, "/any", "root_m:/any:cleanup"}, + {http.MethodPatch, "/any", "root_m:/any:cleanup"}, + {http.MethodPut, "/any", "root_m:/any:cleanup"}, + {http.MethodPost, "/any", "root_m:/any:cleanup"}, + {http.MethodDelete, "/any", "root_m:/any:cleanup"}, + // --- + {http.MethodPost, "/a", "root_m:/error:cleanup"}, // missing + {http.MethodGet, "/a", "root_m:/a:cleanup"}, + {http.MethodHead, "/a", "root_m:/a:cleanup"}, // auto registered with the GET + {http.MethodGet, "/a/b/1", "root_m:a_b_group_m:1_get_m:/1_get:cleanup"}, + {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 { + t.Run(s.method+"_"+s.path, func(t *testing.T) { + calls = "" // reset + + req, err := http.NewRequest(s.method, ts.URL+s.path, nil) + if err != nil { + t.Fatal(err) + } + + _, err = client.Do(req) + if err != nil { + t.Fatal(err) + } + + if calls != s.calls { + t.Fatalf("Expected calls\n%q\ngot\n%q", s.calls, calls) + } + }) + } +} + +func TestRouterUnbind(t *testing.T) { + calls := "" + + r := router.NewRouter(func(w http.ResponseWriter, r *http.Request) (*router.Event, router.EventCleanupFunc) { + return &router.Event{ + Response: w, + Request: r, + }, + func() { + calls += ":cleanup" + } + }) + r.Bind(&hook.Handler[*router.Event]{ + Id: "root_1", + Func: func(e *router.Event) error { + calls += "root_1:" + return e.Next() + }, + }) + r.Bind(&hook.Handler[*router.Event]{ + Id: "root_2", + Func: func(e *router.Event) error { + calls += "root_2:" + return e.Next() + }, + }) + r.Bind(&hook.Handler[*router.Event]{ + Id: "root_3", + Func: func(e *router.Event) error { + calls += "root_3:" + return e.Next() + }, + }) + r.GET("/action", func(e *router.Event) error { + calls += "root_action" + return nil + }).Unbind("root_1") + + ga := r.Group("/group_a") + ga.Unbind("root_1") + ga.Bind(&hook.Handler[*router.Event]{ + Id: "group_a_1", + Func: func(e *router.Event) error { + calls += "group_a_1:" + return e.Next() + }, + }) + ga.Bind(&hook.Handler[*router.Event]{ + Id: "group_a_2", + Func: func(e *router.Event) error { + calls += "group_a_2:" + return e.Next() + }, + }) + ga.Bind(&hook.Handler[*router.Event]{ + Id: "group_a_3", + Func: func(e *router.Event) error { + calls += "group_a_3:" + return e.Next() + }, + }) + ga.GET("/action", func(e *router.Event) error { + calls += "group_a_action" + return nil + }).Unbind("root_2", "group_b_1", "group_a_1") + + gb := r.Group("/group_b") + gb.Unbind("root_2") + gb.Bind(&hook.Handler[*router.Event]{ + Id: "group_b_1", + Func: func(e *router.Event) error { + calls += "group_b_1:" + return e.Next() + }, + }) + gb.Bind(&hook.Handler[*router.Event]{ + Id: "group_b_2", + Func: func(e *router.Event) error { + calls += "group_b_2:" + return e.Next() + }, + }) + gb.Bind(&hook.Handler[*router.Event]{ + Id: "group_b_3", + Func: func(e *router.Event) error { + calls += "group_b_3:" + return e.Next() + }, + }) + gb.GET("/action", func(e *router.Event) error { + calls += "group_b_action" + return nil + }).Unbind("group_b_3", "group_a_3", "root_3") + + mux, err := r.BuildMux() + if err != nil { + t.Fatal(err) + } + + ts := httptest.NewServer(mux) + defer ts.Close() + + client := ts.Client() + + scenarios := []struct { + method string + path string + calls string + }{ + {http.MethodGet, "/action", "root_2:root_3:root_action:cleanup"}, + {http.MethodGet, "/group_a/action", "root_3:group_a_2:group_a_3:group_a_action:cleanup"}, + {http.MethodGet, "/group_b/action", "root_1:group_b_1:group_b_2:group_b_action:cleanup"}, + } + + for _, s := range scenarios { + t.Run(s.method+"_"+s.path, func(t *testing.T) { + calls = "" // reset + + req, err := http.NewRequest(s.method, ts.URL+s.path, nil) + if err != nil { + t.Fatal(err) + } + + _, err = client.Do(req) + if err != nil { + t.Fatal(err) + } + + if calls != s.calls { + t.Fatalf("Expected calls\n%q\ngot\n%q", s.calls, calls) + } + }) + } +} diff --git a/tools/router/unmarshal_request_data.go b/tools/router/unmarshal_request_data.go new file mode 100644 index 00000000..04d3abb3 --- /dev/null +++ b/tools/router/unmarshal_request_data.go @@ -0,0 +1,330 @@ +package router + +import ( + "encoding" + "encoding/json" + "errors" + "reflect" + "strconv" +) + +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + +// JSONPayloadKey is the key for the special UnmarshalRequestData case +// used for reading serialized json payload without normalization. +const JSONPayloadKey string = "@jsonPayload" + +// UnmarshalRequestData unmarshals url.Values type of data (query, multipart/form-data, etc.) into dst. +// +// dst must be a pointer to a map[string]any or struct. +// +// If dst is a map[string]any, each data value will be inferred and +// converted to its bool, numeric, or string equivalent value +// (refer to inferValue() for the exact rules). +// +// If dst is a struct, the following field types are supported: +// - bool +// - string +// - int, int8, int16, int32, int64 +// - uint, uint8, uint16, uint32, uint64 +// - float32, float64 +// - serialized json string if submitted under the special "@jsonPayload" key +// - encoding.TextUnmarshaler +// - pointer and slice variations of the above primitives (ex. *string, []string, *[]string []*string, etc.) +// - named/anonymous struct fields +// Dot-notation is used to target nested fields, ex. "nestedStructField.title". +// - embedded struct fields +// The embedded struct fields are treated by default as if they were defined in their parent struct. +// If the embedded struct has a tag matching structTagKey then to set its fields the data keys must be prefixed with that tag +// similar to the regular nested struct fields. +// +// structTagKey and structPrefix are used only when dst is a struct. +// +// structTagKey represents the tag to use to match a data entry with a struct field (defaults to "form"). +// If the struct field doesn't have the structTagKey tag, then the exported struct field name will be used as it is. +// +// structPrefix could be provided if all of the data keys are prefixed with a common string +// and you want the struct field to match only the value without the structPrefix +// (ex. for "user.name", "user.email" data keys and structPrefix "user", it will match "name" and "email" struct fields). +// +// Note that while the method was inspired by binders from echo, gorrila/schema, ozzo-routing +// and other similar common routing packages, it is not intended to be a drop-in replacement. +// +// @todo Consider adding support for dot-notation keys, in addition to the prefix, (ex. parent.child.title) to express nested object keys. +func UnmarshalRequestData(data map[string][]string, dst any, structTagKey string, structPrefix string) error { + if len(data) == 0 { + return nil // nothing to unmarshal + } + + dstValue := reflect.ValueOf(dst) + if dstValue.Kind() != reflect.Pointer { + return errors.New("dst must be a pointer") + } + + dstValue = dereference(dstValue) + + dstType := dstValue.Type() + + switch dstType.Kind() { + case reflect.Map: // map[string]any + if dstType.Elem().Kind() != reflect.Interface { + return errors.New("dst map value type must be any/interface{}") + } + + for k, v := range data { + if k == JSONPayloadKey { + continue // unmarshalled separately + } + + total := len(v) + + if total == 1 { + dstValue.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(inferValue(v[0]))) + } else { + normalized := make([]any, total) + for i, vItem := range v { + normalized[i] = inferValue(vItem) + } + dstValue.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalized)) + } + } + case reflect.Struct: + // set a default tag key + if structTagKey == "" { + structTagKey = "form" + } + + err := unmarshalInStructValue(data, dstValue, structTagKey, structPrefix) + if err != nil { + return err + } + default: + return errors.New("dst must be a map[string]any or struct") + } + + // @jsonPayload + // + // Special case to scan serialized json string without + // normalization alongside the other data values + // --------------------------------------------------------------- + jsonPayloadValues := data[JSONPayloadKey] + for _, payload := range jsonPayloadValues { + if err := json.Unmarshal([]byte(payload), dst); err != nil { + return err + } + } + + return nil +} + +// unmarshalInStructValue unmarshals data into the provided struct reflect.Value fields. +func unmarshalInStructValue( + data map[string][]string, + dstStructValue reflect.Value, + structTagKey string, + structPrefix string, +) error { + dstStructType := dstStructValue.Type() + + for i := 0; i < dstStructValue.NumField(); i++ { + fieldType := dstStructType.Field(i) + + tag := fieldType.Tag.Get(structTagKey) + + if tag == "-" || (!fieldType.Anonymous && !fieldType.IsExported()) { + continue // disabled or unexported non-anonymous struct field + } + + fieldValue := dereference(dstStructValue.Field(i)) + + ft := fieldType.Type + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + + isSlice := ft.Kind() == reflect.Slice + if isSlice { + ft = ft.Elem() + } + + name := tag + if name == "" && !fieldType.Anonymous { + name = fieldType.Name + } + if name != "" && structPrefix != "" { + name = structPrefix + "." + name + } + + // (*)encoding.TextUnmarshaler field + // --- + if ft.Implements(textUnmarshalerType) || reflect.PointerTo(ft).Implements(textUnmarshalerType) { + values, ok := data[name] + if !ok || len(values) == 0 || !fieldValue.CanSet() { + continue // no value to load or the field cannot be set + } + + if isSlice { + n := len(values) + slice := reflect.MakeSlice(fieldValue.Type(), n, n) + for i, v := range values { + unmarshaler, ok := dereference(slice.Index(i)).Addr().Interface().(encoding.TextUnmarshaler) + if ok { + if err := unmarshaler.UnmarshalText([]byte(v)); err != nil { + return err + } + } + } + fieldValue.Set(slice) + } else { + unmarshaler, ok := fieldValue.Addr().Interface().(encoding.TextUnmarshaler) + if ok { + if err := unmarshaler.UnmarshalText([]byte(values[0])); err != nil { + return err + } + } + } + continue + } + + // "regular" field + // --- + if ft.Kind() != reflect.Struct { + values, ok := data[name] + if !ok || len(values) == 0 || !fieldValue.CanSet() { + continue // no value to load + } + + if isSlice { + n := len(values) + slice := reflect.MakeSlice(fieldValue.Type(), n, n) + for i, v := range values { + if err := setRegularReflectedValue(dereference(slice.Index(i)), v); err != nil { + return err + } + } + fieldValue.Set(slice) + } else { + if err := setRegularReflectedValue(fieldValue, values[0]); err != nil { + return err + } + } + continue + } + + // structs (embedded or nested) + // --- + // slice of structs + if isSlice { + // populating slice of structs is not supported at the moment + // because the filling rules are ambiguous + continue + } + + if tag != "" { + structPrefix = tag + } else { + structPrefix = name // name is empty for anonymous structs -> no prefix + } + + if err := unmarshalInStructValue(data, fieldValue, structTagKey, structPrefix); err != nil { + return err + } + } + + return nil +} + +// dereference returns the underlying value v points to. +func dereference(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + // initialize with a new value and continue searching + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + return v +} + +// setRegularReflectedValue sets and casts value into rv. +func setRegularReflectedValue(rv reflect.Value, value string) error { + switch rv.Kind() { + case reflect.String: + rv.SetString(value) + case reflect.Bool: + if value == "" { + value = "f" + } + + v, err := strconv.ParseBool(value) + if err != nil { + return err + } + + rv.SetBool(v) + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + if value == "" { + value = "0" + } + + v, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + + rv.SetInt(v) + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + if value == "" { + value = "0" + } + + v, err := strconv.ParseUint(value, 0, 64) + if err != nil { + return err + } + + rv.SetUint(v) + case reflect.Float32, reflect.Float64: + if value == "" { + value = "0" + } + + v, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + + rv.SetFloat(v) + default: + return errors.New("unknown value type " + rv.Kind().String()) + } + + return nil +} + +// 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 +func inferValue(raw string) any { + switch raw { + case "": + return raw + case "true": + return true + case "false": + return false + default: + // try to convert to number + if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') { + v, err := strconv.ParseFloat(raw, 64) + if err == nil { + return v + } + } + + return raw + } +} diff --git a/tools/router/unmarshal_request_data_test.go b/tools/router/unmarshal_request_data_test.go new file mode 100644 index 00000000..e04b0282 --- /dev/null +++ b/tools/router/unmarshal_request_data_test.go @@ -0,0 +1,450 @@ +package router_test + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + "github.com/pocketbase/pocketbase/tools/router" +) + +func pointer[T any](val T) *T { + return &val +} + +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"}, + "bool1": {"true"}, + "bool2": {"true", "false"}, + "mixed": {"true", "123", "test"}, + "@jsonPayload": {`{"json_a":null,"json_b":123}`, `{"json_c":[1,2,3]}`}, + } + + structData := map[string][]string{ + "stringTag": {"a", "b"}, + "StringPtr": {"b"}, + "StringSlice": {"a", "b", "c", ""}, + "stringSlicePtrTag": {"d", "e"}, + "StringSliceOfPtr": {"f", "g"}, + + "boolTag": {"true"}, + "BoolPtr": {"true"}, + "BoolSlice": {"true", "false", ""}, + "boolSlicePtrTag": {"false", "false", "true"}, + "BoolSliceOfPtr": {"false", "true", "false"}, + + "int8Tag": {"-1", "2"}, + "Int8Ptr": {"3"}, + "Int8Slice": {"4", "5", ""}, + "int8SlicePtrTag": {"5", "6"}, + "Int8SliceOfPtr": {"7", "8"}, + + "int16Tag": {"-1", "2"}, + "Int16Ptr": {"3"}, + "Int16Slice": {"4", "5", ""}, + "int16SlicePtrTag": {"5", "6"}, + "Int16SliceOfPtr": {"7", "8"}, + + "int32Tag": {"-1", "2"}, + "Int32Ptr": {"3"}, + "Int32Slice": {"4", "5", ""}, + "int32SlicePtrTag": {"5", "6"}, + "Int32SliceOfPtr": {"7", "8"}, + + "int64Tag": {"-1", "2"}, + "Int64Ptr": {"3"}, + "Int64Slice": {"4", "5", ""}, + "int64SlicePtrTag": {"5", "6"}, + "Int64SliceOfPtr": {"7", "8"}, + + "intTag": {"-1", "2"}, + "IntPtr": {"3"}, + "IntSlice": {"4", "5", ""}, + "intSlicePtrTag": {"5", "6"}, + "IntSliceOfPtr": {"7", "8"}, + + "uint8Tag": {"1", "2"}, + "Uint8Ptr": {"3"}, + "Uint8Slice": {"4", "5", ""}, + "uint8SlicePtrTag": {"5", "6"}, + "Uint8SliceOfPtr": {"7", "8"}, + + "uint16Tag": {"1", "2"}, + "Uint16Ptr": {"3"}, + "Uint16Slice": {"4", "5", ""}, + "uint16SlicePtrTag": {"5", "6"}, + "Uint16SliceOfPtr": {"7", "8"}, + + "uint32Tag": {"1", "2"}, + "Uint32Ptr": {"3"}, + "Uint32Slice": {"4", "5", ""}, + "uint32SlicePtrTag": {"5", "6"}, + "Uint32SliceOfPtr": {"7", "8"}, + + "uint64Tag": {"1", "2"}, + "Uint64Ptr": {"3"}, + "Uint64Slice": {"4", "5", ""}, + "uint64SlicePtrTag": {"5", "6"}, + "Uint64SliceOfPtr": {"7", "8"}, + + "uintTag": {"1", "2"}, + "UintPtr": {"3"}, + "UintSlice": {"4", "5", ""}, + "uintSlicePtrTag": {"5", "6"}, + "UintSliceOfPtr": {"7", "8"}, + + "float32Tag": {"-1.2"}, + "Float32Ptr": {"1.5", "2.0"}, + "Float32Slice": {"1", "2.3", "-0.3", ""}, + "float32SlicePtrTag": {"-1.3", "3"}, + "Float32SliceOfPtr": {"0", "1.2"}, + + "float64Tag": {"-1.2"}, + "Float64Ptr": {"1.5", "2.0"}, + "Float64Slice": {"1", "2.3", "-0.3", ""}, + "float64SlicePtrTag": {"-1.3", "3"}, + "Float64SliceOfPtr": {"0", "1.2"}, + + "timeTag": {"2009-11-10T15:00:00Z"}, + "TimePtr": {"2009-11-10T14:00:00Z", "2009-11-10T15:00:00Z"}, + "TimeSlice": {"2009-11-10T14:00:00Z", "2009-11-10T15:00:00Z"}, + "timeSlicePtrTag": {"2009-11-10T15:00:00Z", "2009-11-10T16:00:00Z"}, + "TimeSliceOfPtr": {"2009-11-10T17:00:00Z", "2009-11-10T18:00:00Z"}, + + // @jsonPayload fields + "@jsonPayload": { + `{"payloadA":"test", "shouldBeIgnored": "abc"}`, + `{"payloadB":[1,2,3], "payloadC":true}`, + }, + + // unexported fields or `-` tags + "unexperted": {"test"}, + "SkipExported": {"test"}, + "unexportedStructFieldWithoutTag.Name": {"test"}, + "unexportedStruct.Name": {"test"}, + + // structs + "StructWithoutTag.Name": {"test1"}, + "exportedStruct.Name": {"test2"}, + + // embedded + "embed_name": {"test3"}, + "embed2.embed_name2": {"test4"}, + } + + type embed1 struct { + Name string `form:"embed_name" json:"embed_name"` + } + + type embed2 struct { + Name string `form:"embed_name2" json:"embed_name2"` + } + + //nolint + type TestStruct struct { + String string `form:"stringTag" query:"stringTag2"` + StringPtr *string + StringSlice []string + StringSlicePtr *[]string `form:"stringSlicePtrTag"` + StringSliceOfPtr []*string + + Bool bool `form:"boolTag" query:"boolTag2"` + BoolPtr *bool + BoolSlice []bool + BoolSlicePtr *[]bool `form:"boolSlicePtrTag"` + BoolSliceOfPtr []*bool + + Int8 int8 `form:"int8Tag" query:"int8Tag2"` + Int8Ptr *int8 + Int8Slice []int8 + Int8SlicePtr *[]int8 `form:"int8SlicePtrTag"` + Int8SliceOfPtr []*int8 + + Int16 int16 `form:"int16Tag" query:"int16Tag2"` + Int16Ptr *int16 + Int16Slice []int16 + Int16SlicePtr *[]int16 `form:"int16SlicePtrTag"` + Int16SliceOfPtr []*int16 + + Int32 int32 `form:"int32Tag" query:"int32Tag2"` + Int32Ptr *int32 + Int32Slice []int32 + Int32SlicePtr *[]int32 `form:"int32SlicePtrTag"` + Int32SliceOfPtr []*int32 + + Int64 int64 `form:"int64Tag" query:"int64Tag2"` + Int64Ptr *int64 + Int64Slice []int64 + Int64SlicePtr *[]int64 `form:"int64SlicePtrTag"` + Int64SliceOfPtr []*int64 + + Int int `form:"intTag" query:"intTag2"` + IntPtr *int + IntSlice []int + IntSlicePtr *[]int `form:"intSlicePtrTag"` + IntSliceOfPtr []*int + + Uint8 uint8 `form:"uint8Tag" query:"uint8Tag2"` + Uint8Ptr *uint8 + Uint8Slice []uint8 + Uint8SlicePtr *[]uint8 `form:"uint8SlicePtrTag"` + Uint8SliceOfPtr []*uint8 + + Uint16 uint16 `form:"uint16Tag" query:"uint16Tag2"` + Uint16Ptr *uint16 + Uint16Slice []uint16 + Uint16SlicePtr *[]uint16 `form:"uint16SlicePtrTag"` + Uint16SliceOfPtr []*uint16 + + Uint32 uint32 `form:"uint32Tag" query:"uint32Tag2"` + Uint32Ptr *uint32 + Uint32Slice []uint32 + Uint32SlicePtr *[]uint32 `form:"uint32SlicePtrTag"` + Uint32SliceOfPtr []*uint32 + + Uint64 uint64 `form:"uint64Tag" query:"uint64Tag2"` + Uint64Ptr *uint64 + Uint64Slice []uint64 + Uint64SlicePtr *[]uint64 `form:"uint64SlicePtrTag"` + Uint64SliceOfPtr []*uint64 + + Uint uint `form:"uintTag" query:"uintTag2"` + UintPtr *uint + UintSlice []uint + UintSlicePtr *[]uint `form:"uintSlicePtrTag"` + UintSliceOfPtr []*uint + + Float32 float32 `form:"float32Tag" query:"float32Tag2"` + Float32Ptr *float32 + Float32Slice []float32 + Float32SlicePtr *[]float32 `form:"float32SlicePtrTag"` + Float32SliceOfPtr []*float32 + + Float64 float64 `form:"float64Tag" query:"float64Tag2"` + Float64Ptr *float64 + Float64Slice []float64 + Float64SlicePtr *[]float64 `form:"float64SlicePtrTag"` + Float64SliceOfPtr []*float64 + + // encoding.TextUnmarshaler + Time time.Time `form:"timeTag" query:"timeTag2"` + TimePtr *time.Time + TimeSlice []time.Time + TimeSlicePtr *[]time.Time `form:"timeSlicePtrTag"` + TimeSliceOfPtr []*time.Time + + // @jsonPayload fields + JSONPayloadA string `form:"shouldBeIgnored" json:"payloadA"` + JSONPayloadB []int `json:"payloadB"` + JSONPayloadC bool `json:"-"` + + // unexported fields or `-` tags + unexported string + SkipExported string `form:"-"` + unexportedStructFieldWithoutTag struct { + Name string `json:"unexportedStructFieldWithoutTag_name"` + } + unexportedStructFieldWithTag struct { + Name string `json:"unexportedStructFieldWithTag_name"` + } `form:"unexportedStruct"` + + // structs + StructWithoutTag struct { + Name string `json:"StructWithoutTag_name"` + } + StructWithTag struct { + Name string `json:"StructWithTag_name"` + } `form:"exportedStruct"` + + // embedded + embed1 + embed2 `form:"embed2"` + } + + scenarios := []struct { + name string + data map[string][]string + dst any + tag string + prefix string + error bool + result string + }{ + { + name: "nil data", + data: nil, + dst: pointer(map[string]any{}), + error: false, + result: `{}`, + }, + { + name: "non-pointer map[string]any", + data: mapData, + dst: map[string]any{}, + error: true, + }, + { + name: "unsupported *map[string]string", + data: mapData, + dst: pointer(map[string]string{}), + error: true, + }, + { + name: "unsupported *map[string][]string", + data: mapData, + dst: pointer(map[string][]string{}), + error: true, + }, + { + 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"]}`, + }, + { + name: "valid pointer struct (all fields)", + data: structData, + dst: &TestStruct{}, + result: `{"String":"a","StringPtr":"b","StringSlice":["a","b","c",""],"StringSlicePtr":["d","e"],"StringSliceOfPtr":["f","g"],"Bool":true,"BoolPtr":true,"BoolSlice":[true,false,false],"BoolSlicePtr":[false,false,true],"BoolSliceOfPtr":[false,true,false],"Int8":-1,"Int8Ptr":3,"Int8Slice":[4,5,0],"Int8SlicePtr":[5,6],"Int8SliceOfPtr":[7,8],"Int16":-1,"Int16Ptr":3,"Int16Slice":[4,5,0],"Int16SlicePtr":[5,6],"Int16SliceOfPtr":[7,8],"Int32":-1,"Int32Ptr":3,"Int32Slice":[4,5,0],"Int32SlicePtr":[5,6],"Int32SliceOfPtr":[7,8],"Int64":-1,"Int64Ptr":3,"Int64Slice":[4,5,0],"Int64SlicePtr":[5,6],"Int64SliceOfPtr":[7,8],"Int":-1,"IntPtr":3,"IntSlice":[4,5,0],"IntSlicePtr":[5,6],"IntSliceOfPtr":[7,8],"Uint8":1,"Uint8Ptr":3,"Uint8Slice":"BAUA","Uint8SlicePtr":"BQY=","Uint8SliceOfPtr":[7,8],"Uint16":1,"Uint16Ptr":3,"Uint16Slice":[4,5,0],"Uint16SlicePtr":[5,6],"Uint16SliceOfPtr":[7,8],"Uint32":1,"Uint32Ptr":3,"Uint32Slice":[4,5,0],"Uint32SlicePtr":[5,6],"Uint32SliceOfPtr":[7,8],"Uint64":1,"Uint64Ptr":3,"Uint64Slice":[4,5,0],"Uint64SlicePtr":[5,6],"Uint64SliceOfPtr":[7,8],"Uint":1,"UintPtr":3,"UintSlice":[4,5,0],"UintSlicePtr":[5,6],"UintSliceOfPtr":[7,8],"Float32":-1.2,"Float32Ptr":1.5,"Float32Slice":[1,2.3,-0.3,0],"Float32SlicePtr":[-1.3,3],"Float32SliceOfPtr":[0,1.2],"Float64":-1.2,"Float64Ptr":1.5,"Float64Slice":[1,2.3,-0.3,0],"Float64SlicePtr":[-1.3,3],"Float64SliceOfPtr":[0,1.2],"Time":"2009-11-10T15:00:00Z","TimePtr":"2009-11-10T14:00:00Z","TimeSlice":["2009-11-10T14:00:00Z","2009-11-10T15:00:00Z"],"TimeSlicePtr":["2009-11-10T15:00:00Z","2009-11-10T16:00:00Z"],"TimeSliceOfPtr":["2009-11-10T17:00:00Z","2009-11-10T18:00:00Z"],"payloadA":"test","payloadB":[1,2,3],"SkipExported":"","StructWithoutTag":{"StructWithoutTag_name":"test1"},"StructWithTag":{"StructWithTag_name":"test2"},"embed_name":"test3","embed_name2":"test4"}`, + }, + { + name: "non-pointer struct", + data: structData, + dst: TestStruct{}, + error: true, + }, + { + name: "invalid struct uint value", + data: map[string][]string{"uintTag": {"-1"}}, + dst: &TestStruct{}, + error: true, + }, + { + name: "invalid struct int value", + data: map[string][]string{"intTag": {"abc"}}, + dst: &TestStruct{}, + error: true, + }, + { + name: "invalid struct bool value", + data: map[string][]string{"boolTag": {"abc"}}, + dst: &TestStruct{}, + error: true, + }, + { + name: "invalid struct float value", + data: map[string][]string{"float64Tag": {"abc"}}, + dst: &TestStruct{}, + error: true, + }, + { + name: "invalid struct TextUnmarshaler value", + data: map[string][]string{"timeTag": {"123"}}, + dst: &TestStruct{}, + error: true, + }, + { + name: "custom tagKey", + data: map[string][]string{ + "tag1": {"a"}, + "tag2": {"b"}, + "tag3": {"c"}, + "Item": {"d"}, + }, + dst: &struct { + Item string `form:"tag1" query:"tag2" json:"tag2"` + }{}, + tag: "query", + result: `{"tag2":"b"}`, + }, + { + name: "custom prefix", + data: map[string][]string{ + "test.A": {"1"}, + "A": {"2"}, + "test.alias": {"3"}, + }, + dst: &struct { + A string + B string `form:"alias"` + }{}, + prefix: "test", + result: `{"A":"1","B":"3"}`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + err := router.UnmarshalRequestData(s.data, s.dst, s.tag, s.prefix) + + hasErr := err != nil + if hasErr != s.error { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.error, hasErr, err) + } + + if hasErr { + return + } + + raw, err := json.Marshal(s.dst) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(raw, []byte(s.result)) { + t.Fatalf("Expected dst \n%s\ngot\n%s", s.result, raw) + } + }) + } +} + +// note: extra unexported checks in addition to the above test as there +// is no easy way to print nested structs with all their fields. +func TestUnmarshalRequestDataUnexportedFields(t *testing.T) { + t.Parallel() + + //nolint:all + type TestStruct struct { + Exported string + + unexported string + // to ensure that the reflection doesn't take tags with higher priority than the exported state + unexportedWithTag string `form:"unexportedWithTag" json:"unexportedWithTag"` + } + + dst := &TestStruct{} + + err := router.UnmarshalRequestData(map[string][]string{ + "Exported": {"test"}, // just for reference + + "Unexported": {"test"}, + "unexported": {"test"}, + "UnexportedWithTag": {"test"}, + "unexportedWithTag": {"test"}, + }, dst, "", "") + + if err != nil { + t.Fatal(err) + } + + if dst.Exported != "test" { + t.Fatalf("Expected the Exported field to be %q, got %q", "test", dst.Exported) + } + + if dst.unexported != "" { + t.Fatalf("Expected the unexported field to remain empty, got %q", dst.unexported) + } + + if dst.unexportedWithTag != "" { + t.Fatalf("Expected the unexportedWithTag field to remain empty, got %q", dst.unexportedWithTag) + } +} diff --git a/tools/routine/routine.go b/tools/routine/routine.go index a18f8559..afddd30f 100644 --- a/tools/routine/routine.go +++ b/tools/routine/routine.go @@ -6,9 +6,9 @@ import ( "sync" ) -// FireAndForget executes `f()` in a new go routine and auto recovers if panic. +// FireAndForget executes f() in a new go routine and auto recovers if panic. // -// **Note:** Use this only if you are not interested in the result of `f()` +// **Note:** Use this only if you are not interested in the result of f() // and don't want to block the parent go routine. func FireAndForget(f func(), wg ...*sync.WaitGroup) { if len(wg) > 0 && wg[0] != nil { diff --git a/tools/search/filter.go b/tools/search/filter.go index 04f720b3..cbbd04b2 100644 --- a/tools/search/filter.go +++ b/tools/search/filter.go @@ -64,9 +64,10 @@ func (f FilterData) BuildExpr( } } - if parsedFilterData.Has(raw) { - return buildParsedFilterExpr(parsedFilterData.Get(raw), fieldResolver) + if data, ok := parsedFilterData.GetOk(raw); ok { + return buildParsedFilterExpr(data, fieldResolver) } + data, err := fexpr.Parse(raw) if err != nil { // depending on the users demand we may allow empty expressions @@ -78,9 +79,11 @@ func (f FilterData) BuildExpr( return nil, err } + // store in cache // (the limit size is arbitrary and it is there to prevent the cache growing too big) parsedFilterData.SetIfLessThanLimit(raw, data, 500) + return buildParsedFilterExpr(data, fieldResolver) } @@ -431,6 +434,8 @@ func mergeParams(params ...dbx.Params) dbx.Params { return result } +// @todo consider adding support for custom single character wildcard +// // wrapLikeParams wraps each provided param value string with `%` // if the param doesn't contain an explicit wildcard (`%`) character already. func wrapLikeParams(params dbx.Params) dbx.Params { diff --git a/tools/search/provider.go b/tools/search/provider.go index 8ce21733..17ae72b3 100644 --- a/tools/search/provider.go +++ b/tools/search/provider.go @@ -5,16 +5,20 @@ import ( "math" "net/url" "strconv" + "strings" "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/inflector" "golang.org/x/sync/errgroup" ) // DefaultPerPage specifies the default returned search result items. const DefaultPerPage int = 30 +// @todo consider making it configurable +// // MaxPerPage specifies the maximum allowed search result items returned in a single page. -const MaxPerPage int = 500 +const MaxPerPage int = 1000 // url search query params const ( @@ -27,23 +31,23 @@ const ( // Result defines the returned search result structure. type Result struct { + Items any `json:"items"` Page int `json:"page"` PerPage int `json:"perPage"` TotalItems int `json:"totalItems"` TotalPages int `json:"totalPages"` - Items any `json:"items"` } // Provider represents a single configured search provider instance. type Provider struct { fieldResolver FieldResolver query *dbx.SelectQuery - skipTotal bool countCol string - page int - perPage int sort []SortField filter []FilterData + page int + perPage int + skipTotal bool } // NewProvider creates and returns a new search provider. @@ -208,6 +212,14 @@ func (s *Provider) Exec(items any) (*Result, error) { return nil, err } if expr != "" { + // ensure that _rowid_ expressions are always prefixed with the first FROM table + if sortField.Name == rowidSortKey && !strings.Contains(expr, ".") { + queryInfo := modelsQuery.Info() + if len(queryInfo.From) > 0 { + expr = "[[" + inflector.Columnify(queryInfo.From[0]) + "]]." + expr + } + } + modelsQuery.AndOrderBy(expr) } } diff --git a/tools/search/provider_test.go b/tools/search/provider_test.go index 2e815b7b..e4756ff1 100644 --- a/tools/search/provider_test.go +++ b/tools/search/provider_test.go @@ -180,38 +180,39 @@ func TestProviderParse(t *testing.T) { } for i, s := range scenarios { - r := &testFieldResolver{} - p := NewProvider(r). - Page(initialPage). - PerPage(initialPerPage). - Sort(initialSort). - Filter(initialFilter) + t.Run(fmt.Sprintf("%d_%s", i, s.query), func(t *testing.T) { + r := &testFieldResolver{} + p := NewProvider(r). + Page(initialPage). + PerPage(initialPerPage). + Sort(initialSort). + Filter(initialFilter) - err := p.Parse(s.query) + err := p.Parse(s.query) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } - if p.page != s.expectPage { - t.Errorf("(%d) Expected page %v, got %v", i, s.expectPage, p.page) - } + if p.page != s.expectPage { + t.Fatalf("Expected page %v, got %v", s.expectPage, p.page) + } - if p.perPage != s.expectPerPage { - t.Errorf("(%d) Expected perPage %v, got %v", i, s.expectPerPage, p.perPage) - } + if p.perPage != s.expectPerPage { + t.Fatalf("Expected perPage %v, got %v", s.expectPerPage, p.perPage) + } - encodedSort, _ := json.Marshal(p.sort) - if string(encodedSort) != s.expectSort { - t.Errorf("(%d) Expected sort %v, got \n%v", i, s.expectSort, string(encodedSort)) - } + encodedSort, _ := json.Marshal(p.sort) + if string(encodedSort) != s.expectSort { + t.Fatalf("Expected sort %v, got \n%v", s.expectSort, string(encodedSort)) + } - encodedFilter, _ := json.Marshal(p.filter) - if string(encodedFilter) != s.expectFilter { - t.Errorf("(%d) Expected filter %v, got \n%v", i, s.expectFilter, string(encodedFilter)) - } + encodedFilter, _ := json.Marshal(p.filter) + if string(encodedFilter) != s.expectFilter { + t.Fatalf("Expected filter %v, got \n%v", s.expectFilter, string(encodedFilter)) + } + }) } } @@ -256,7 +257,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{}, false, false, - `{"page":1,"perPage":10,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}],"page":1,"perPage":10,"totalItems":2,"totalPages":1}`, []string{ "SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)", "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10", @@ -270,7 +271,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{}, false, false, - `{"page":10,"perPage":30,"totalItems":2,"totalPages":1,"items":[]}`, + `{"items":[],"page":10,"perPage":30,"totalItems":2,"totalPages":1}`, []string{ "SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)", "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30 OFFSET 270", @@ -306,7 +307,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{"test2 != null", "test1 >= 2"}, false, false, - `{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1}`, []string{ "SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 IS NOT '' AND test2 IS NOT NULL)))) AND (test1 >= 2)", "SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 IS NOT '' AND test2 IS NOT NULL)))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT " + fmt.Sprint(MaxPerPage), @@ -320,7 +321,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{"test2 != null", "test1 >= 2"}, true, false, - `{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":-1,"totalPages":-1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":-1,"totalPages":-1}`, []string{ "SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 IS NOT '' AND test2 IS NOT NULL)))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT " + fmt.Sprint(MaxPerPage), }, @@ -333,7 +334,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{"test3 != ''"}, false, false, - `{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`, + `{"items":[],"page":1,"perPage":10,"totalItems":0,"totalPages":0}`, []string{ "SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 IS NOT '' AND test3 IS NOT NULL)))", "SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 IS NOT '' AND test3 IS NOT NULL))) ORDER BY `test1` ASC, `test3` ASC LIMIT 10", @@ -347,7 +348,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{"test3 != ''"}, true, false, - `{"page":1,"perPage":10,"totalItems":-1,"totalPages":-1,"items":[]}`, + `{"items":[],"page":1,"perPage":10,"totalItems":-1,"totalPages":-1}`, []string{ "SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 IS NOT '' AND test3 IS NOT NULL))) ORDER BY `test1` ASC, `test3` ASC LIMIT 10", }, @@ -360,7 +361,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{}, false, false, - `{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":2,"perPage":1,"totalItems":2,"totalPages":2}`, []string{ "SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)", "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1", @@ -374,7 +375,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { []FilterData{}, true, false, - `{"page":2,"perPage":1,"totalItems":-1,"totalPages":-1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":2,"perPage":1,"totalItems":-1,"totalPages":-1}`, []string{ "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1", }, @@ -449,7 +450,7 @@ func TestProviderParseAndExec(t *testing.T) { "no extra query params (aka. use the provider presets)", "", false, - `{"page":2,"perPage":123,"totalItems":2,"totalPages":1,"items":[]}`, + `{"items":[],"page":2,"perPage":123,"totalItems":2,"totalPages":1}`, }, { "invalid query", @@ -491,62 +492,63 @@ func TestProviderParseAndExec(t *testing.T) { "page > existing", "page=3&perPage=9999", false, - `{"page":3,"perPage":500,"totalItems":2,"totalPages":1,"items":[]}`, + `{"items":[],"page":3,"perPage":1000,"totalItems":2,"totalPages":1}`, }, { "valid query params", "page=1&perPage=9999&filter=test1>1&sort=-test2,test3", false, - `{"page":1,"perPage":500,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":1,"perPage":1000,"totalItems":1,"totalPages":1}`, }, { "valid query params with skipTotal=1", "page=1&perPage=9999&filter=test1>1&sort=-test2,test3&skipTotal=1", false, - `{"page":1,"perPage":500,"totalItems":-1,"totalPages":-1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + `{"items":[{"test1":2,"test2":"test2.2","test3":""}],"page":1,"perPage":1000,"totalItems":-1,"totalPages":-1}`, }, } for _, s := range scenarios { - testDB.CalledQueries = []string{} // reset + t.Run(s.name, func(t *testing.T) { + testDB.CalledQueries = []string{} // reset - testResolver := &testFieldResolver{} - provider := NewProvider(testResolver). - Query(query). - Page(2). - PerPage(123). - Sort([]SortField{{"test2", SortAsc}}). - Filter([]FilterData{"test1 > 0"}) + testResolver := &testFieldResolver{} + provider := NewProvider(testResolver). + Query(query). + Page(2). + PerPage(123). + Sort([]SortField{{"test2", SortAsc}}). + Filter([]FilterData{"test1 > 0"}) - result, err := provider.ParseAndExec(s.queryString, &[]testTableStruct{}) + result, err := provider.ParseAndExec(s.queryString, &[]testTableStruct{}) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) - continue - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } - if hasErr { - continue - } + if hasErr { + return + } - if testResolver.UpdateQueryCalls != 1 { - t.Errorf("[%s] Expected resolver.Update to be called %d, got %d", s.name, 1, testResolver.UpdateQueryCalls) - } + if testResolver.UpdateQueryCalls != 1 { + t.Fatalf("Expected resolver.Update to be called %d, got %d", 1, testResolver.UpdateQueryCalls) + } - expectedQueries := 2 - if provider.skipTotal { - expectedQueries = 1 - } + expectedQueries := 2 + if provider.skipTotal { + expectedQueries = 1 + } - if len(testDB.CalledQueries) != expectedQueries { - t.Errorf("[%s] Expected %d db queries, got %d: \n%v", s.name, expectedQueries, len(testDB.CalledQueries), testDB.CalledQueries) - } + if len(testDB.CalledQueries) != expectedQueries { + t.Fatalf("Expected %d db queries, got %d: \n%v", expectedQueries, len(testDB.CalledQueries), testDB.CalledQueries) + } - encoded, _ := json.Marshal(result) - if string(encoded) != s.expectResult { - t.Errorf("[%s] Expected result %v, got \n%v", s.name, s.expectResult, string(encoded)) - } + encoded, _ := json.Marshal(result) + if string(encoded) != s.expectResult { + t.Fatalf("Expected result \n%v\ngot\n%v", s.expectResult, string(encoded)) + } + }) } } diff --git a/tools/search/simple_field_resolver.go b/tools/search/simple_field_resolver.go index bfd96ada..09a1a651 100644 --- a/tools/search/simple_field_resolver.go +++ b/tools/search/simple_field_resolver.go @@ -76,7 +76,7 @@ func (r *SimpleFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { // Returns error if `field` is not in `r.allowedFields`. func (r *SimpleFieldResolver) Resolve(field string) (*ResolverResult, error) { if !list.ExistInSliceWithRegex(field, r.allowedFields) { - return nil, fmt.Errorf("failed to resolve field %q", field) + return nil, fmt.Errorf("Failed to resolve field %q.", field) } parts := strings.Split(field, ".") diff --git a/tools/search/simple_field_resolver_test.go b/tools/search/simple_field_resolver_test.go index 0bb2814a..c5543e08 100644 --- a/tools/search/simple_field_resolver_test.go +++ b/tools/search/simple_field_resolver_test.go @@ -1,6 +1,7 @@ package search_test import ( + "fmt" "testing" "github.com/pocketbase/dbx" @@ -23,22 +24,22 @@ func TestSimpleFieldResolverUpdateQuery(t *testing.T) { } for i, s := range scenarios { - db := dbx.NewFromDB(nil, "") - query := db.Select("id").From("test") + t.Run(fmt.Sprintf("%d_%s", i, s.fieldName), func(t *testing.T) { + db := dbx.NewFromDB(nil, "") + query := db.Select("id").From("test") - r.Resolve(s.fieldName) + r.Resolve(s.fieldName) - if err := r.UpdateQuery(nil); err != nil { - t.Errorf("(%d) UpdateQuery failed with error %v", i, err) - continue - } + if err := r.UpdateQuery(nil); err != nil { + t.Fatalf("UpdateQuery failed with error %v", err) + } - rawQuery := query.Build().SQL() - // rawQuery := s.expectQuery + rawQuery := query.Build().SQL() - if rawQuery != s.expectQuery { - t.Errorf("(%d) Expected query %v, got \n%v", i, s.expectQuery, rawQuery) - } + if rawQuery != s.expectQuery { + t.Fatalf("Expected query %v, got \n%v", s.expectQuery, rawQuery) + } + }) } } @@ -62,25 +63,25 @@ func TestSimpleFieldResolverResolve(t *testing.T) { } for i, s := range scenarios { - r, err := r.Resolve(s.fieldName) + t.Run(fmt.Sprintf("%d_%s", i, s.fieldName), func(t *testing.T) { + r, err := r.Resolve(s.fieldName) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } - if hasErr { - continue - } + if hasErr { + return + } - if r.Identifier != s.expectName { - t.Errorf("(%d) Expected r.Identifier %q, got %q", i, s.expectName, r.Identifier) - } + if r.Identifier != s.expectName { + t.Fatalf("Expected r.Identifier %q, got %q", s.expectName, r.Identifier) + } - // params should be empty - if len(r.Params) != 0 { - t.Errorf("(%d) Expected 0 r.Params, got %v", i, r.Params) - } + if len(r.Params) != 0 { + t.Fatalf("r.Params should be empty, got %v", r.Params) + } + }) } } diff --git a/tools/search/sort.go b/tools/search/sort.go index 7dd28804..bd600d13 100644 --- a/tools/search/sort.go +++ b/tools/search/sort.go @@ -5,7 +5,10 @@ import ( "strings" ) -const randomSortKey string = "@random" +const ( + randomSortKey string = "@random" + rowidSortKey string = "@rowid" +) // sort field directions const ( @@ -26,6 +29,11 @@ func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) { return "RANDOM()", nil } + // special case for the builtin SQLite rowid column + if s.Name == rowidSortKey { + return fmt.Sprintf("[[_rowid_]] %s", s.Direction), nil + } + result, err := fieldResolver.Resolve(s.Name) // invalidate empty fields and non-column identifiers diff --git a/tools/search/sort_test.go b/tools/search/sort_test.go index d6bb6b11..1c23cc45 100644 --- a/tools/search/sort_test.go +++ b/tools/search/sort_test.go @@ -2,6 +2,7 @@ package search_test import ( "encoding/json" + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/search" @@ -29,27 +30,30 @@ func TestSortFieldBuildExpr(t *testing.T) { {search.SortField{"test1", search.SortDesc}, false, "[[test1]] DESC"}, // special @random field (ignore direction) {search.SortField{"@random", search.SortDesc}, false, "RANDOM()"}, + // special _rowid_ field + {search.SortField{"@rowid", search.SortDesc}, false, "[[_rowid_]] DESC"}, } - for i, s := range scenarios { - result, err := s.sortField.BuildExpr(resolver) + for _, s := range scenarios { + t.Run(fmt.Sprintf("%s_%s", s.sortField.Name, s.sortField.Name), func(t *testing.T) { + result, err := s.sortField.BuildExpr(resolver) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) - continue - } + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } - if result != s.expectExpression { - t.Errorf("(%d) Expected expression %v, got %v", i, s.expectExpression, result) - } + if result != s.expectExpression { + t.Fatalf("Expected expression %v, got %v", s.expectExpression, result) + } + }) } } func TestParseSortFromString(t *testing.T) { scenarios := []struct { - value string - expectedJson string + value string + expected string }{ {"", `[{"name":"","direction":"ASC"}]`}, {"test", `[{"name":"test","direction":"ASC"}]`}, @@ -57,14 +61,18 @@ func TestParseSortFromString(t *testing.T) { {"-test", `[{"name":"test","direction":"DESC"}]`}, {"test1,-test2,+test3", `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"DESC"},{"name":"test3","direction":"ASC"}]`}, {"@random,-test", `[{"name":"@random","direction":"ASC"},{"name":"test","direction":"DESC"}]`}, + {"-@rowid,-test", `[{"name":"@rowid","direction":"DESC"},{"name":"test","direction":"DESC"}]`}, } - for i, s := range scenarios { - result := search.ParseSortFromString(s.value) - encoded, _ := json.Marshal(result) + for _, s := range scenarios { + t.Run(s.value, func(t *testing.T) { + result := search.ParseSortFromString(s.value) + encoded, _ := json.Marshal(result) + encodedStr := string(encoded) - if string(encoded) != s.expectedJson { - t.Errorf("(%d) Expected expression %v, got %v", i, s.expectedJson, string(encoded)) - } + if encodedStr != s.expected { + t.Fatalf("Expected expression %s, got %s", s.expected, encodedStr) + } + }) } } diff --git a/tools/security/encrypt_test.go b/tools/security/encrypt_test.go index ed26e23a..19316721 100644 --- a/tools/security/encrypt_test.go +++ b/tools/security/encrypt_test.go @@ -1,6 +1,7 @@ package security_test import ( + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/security" @@ -17,30 +18,30 @@ func TestEncrypt(t *testing.T) { {"123", "abcdabcdabcdabcdabcdabcdabcdabcd", false}, } - for i, scenario := range scenarios { - result, err := security.Encrypt([]byte(scenario.data), scenario.key) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.data), func(t *testing.T) { + result, err := security.Encrypt([]byte(s.data), s.key) - if scenario.expectError && err == nil { - t.Errorf("(%d) Expected error got nil", i) - } - if !scenario.expectError && err != nil { - t.Errorf("(%d) Expected nil got error %v", i, err) - } + hasErr := err != nil - if scenario.expectError && result != "" { - t.Errorf("(%d) Expected empty string, got %q", i, result) - } - if !scenario.expectError && result == "" { - t.Errorf("(%d) Expected non empty encrypted result string", i) - } - - // try to decrypt - if result != "" { - decrypted, _ := security.Decrypt(result, scenario.key) - if string(decrypted) != scenario.data { - t.Errorf("(%d) Expected decrypted value to match with the data input, got %q", i, decrypted) + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } - } + + if hasErr { + if result != "" { + t.Fatalf("Expected empty Encrypt result on error, got %q", result) + } + + return + } + + // try to decrypt + decrypted, err := security.Decrypt(result, s.key) + if err != nil || string(decrypted) != s.data { + t.Fatalf("Expected decrypted value to match with the data input, got %q (%v)", decrypted, err) + } + }) } } @@ -57,19 +58,23 @@ func TestDecrypt(t *testing.T) { {"8kcEqilvv+YKYcfnSr0aSC54gmnQCsB02SaB8ATlnA==", "abcdabcdabcdabcdabcdabcdabcdabcd", false, "123"}, } - for i, scenario := range scenarios { - result, err := security.Decrypt(scenario.cipher, scenario.key) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.key), func(t *testing.T) { + result, err := security.Decrypt(s.cipher, s.key) - if scenario.expectError && err == nil { - t.Errorf("(%d) Expected error got nil", i) - } - if !scenario.expectError && err != nil { - t.Errorf("(%d) Expected nil got error %v", i, err) - } + hasErr := err != nil - resultStr := string(result) - if resultStr != scenario.expectedData { - t.Errorf("(%d) Expected %q, got %q", i, scenario.expectedData, resultStr) - } + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + if str := string(result); str != s.expectedData { + t.Fatalf("Expected %q, got %q", s.expectedData, str) + } + }) } } diff --git a/tools/security/jwt.go b/tools/security/jwt.go index 78faf7d9..039755ba 100644 --- a/tools/security/jwt.go +++ b/tools/security/jwt.go @@ -4,6 +4,7 @@ import ( "errors" "time" + // @todo update to v5 "github.com/golang-jwt/jwt/v4" ) @@ -43,11 +44,9 @@ func ParseJWT(token string, verificationKey string) (jwt.MapClaims, error) { } // NewJWT generates and returns new HS256 signed JWT. -func NewJWT(payload jwt.MapClaims, signingKey string, secondsDuration int64) (string, error) { - seconds := time.Duration(secondsDuration) * time.Second - +func NewJWT(payload jwt.MapClaims, signingKey string, duration time.Duration) (string, error) { claims := jwt.MapClaims{ - "exp": time.Now().Add(seconds).Unix(), + "exp": time.Now().Add(duration).Unix(), } for k, v := range payload { @@ -56,11 +55,3 @@ func NewJWT(payload jwt.MapClaims, signingKey string, secondsDuration int64) (st return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(signingKey)) } - -// Deprecated: -// Consider replacing with NewJWT(). -// -// NewToken is a legacy alias for NewJWT that generates a HS256 signed JWT. -func NewToken(payload jwt.MapClaims, signingKey string, secondsDuration int64) (string, error) { - return NewJWT(payload, signingKey, secondsDuration) -} diff --git a/tools/security/jwt_test.go b/tools/security/jwt_test.go index 785862ac..bc1b7210 100644 --- a/tools/security/jwt_test.go +++ b/tools/security/jwt_test.go @@ -1,7 +1,10 @@ package security_test import ( + "fmt" + "strconv" "testing" + "time" "github.com/golang-jwt/jwt/v4" "github.com/pocketbase/pocketbase/tools/security" @@ -102,26 +105,30 @@ func TestParseJWT(t *testing.T) { }, } - for i, scenario := range scenarios { - result, err := security.ParseJWT(scenario.token, scenario.secret) - if scenario.expectError && err == nil { - t.Errorf("(%d) Expected error got nil", i) - } - if !scenario.expectError && err != nil { - t.Errorf("(%d) Expected nil got error %v", i, err) - } - if len(result) != len(scenario.expectClaims) { - t.Errorf("(%d) Expected %v got %v", i, scenario.expectClaims, result) - } - for k, v := range scenario.expectClaims { - v2, ok := result[k] - if !ok { - t.Errorf("(%d) Missing expected claim %q", i, k) + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.token), func(t *testing.T) { + result, err := security.ParseJWT(s.token, s.secret) + + hasErr := err != nil + + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } - if v != v2 { - t.Errorf("(%d) Expected %v for %q claim, got %v", i, v, k, v2) + + if len(result) != len(s.expectClaims) { + t.Fatalf("Expected %v claims got %v", s.expectClaims, result) } - } + + for k, v := range s.expectClaims { + v2, ok := result[k] + if !ok { + t.Fatalf("Missing expected claim %q", k) + } + if v != v2 { + t.Fatalf("Expected %v for %q claim, got %v", v, k, v2) + } + } + }) } } @@ -129,51 +136,51 @@ func TestNewJWT(t *testing.T) { scenarios := []struct { claims jwt.MapClaims key string - duration int64 + duration time.Duration expectError bool }{ // empty, zero duration {jwt.MapClaims{}, "", 0, true}, // empty, 10 seconds duration - {jwt.MapClaims{}, "", 10, false}, + {jwt.MapClaims{}, "", 10 * time.Second, false}, // non-empty, 10 seconds duration - {jwt.MapClaims{"name": "test"}, "test", 10, false}, + {jwt.MapClaims{"name": "test"}, "test", 10 * time.Second, false}, } for i, scenario := range scenarios { - token, tokenErr := security.NewJWT(scenario.claims, scenario.key, scenario.duration) - if tokenErr != nil { - t.Errorf("(%d) Expected NewJWT to succeed, got error %v", i, tokenErr) - continue - } - - claims, parseErr := security.ParseJWT(token, scenario.key) - - hasParseErr := parseErr != nil - if hasParseErr != scenario.expectError { - t.Errorf("(%d) Expected hasParseErr to be %v, got %v (%v)", i, scenario.expectError, hasParseErr, parseErr) - continue - } - - if scenario.expectError { - continue - } - - if _, ok := claims["exp"]; !ok { - t.Errorf("(%d) Missing required claim exp, got %v", i, claims) - } - - // clear exp claim to match with the scenario ones - delete(claims, "exp") - - if len(claims) != len(scenario.claims) { - t.Errorf("(%d) Expected %v claims, got %v", i, scenario.claims, claims) - } - - for j, k := range claims { - if claims[j] != scenario.claims[j] { - t.Errorf("(%d) Expected %v for %q claim, got %v", i, claims[j], k, scenario.claims[j]) + t.Run(strconv.Itoa(i), func(t *testing.T) { + token, tokenErr := security.NewJWT(scenario.claims, scenario.key, scenario.duration) + if tokenErr != nil { + t.Fatalf("Expected NewJWT to succeed, got error %v", tokenErr) } - } + + claims, parseErr := security.ParseJWT(token, scenario.key) + + hasParseErr := parseErr != nil + if hasParseErr != scenario.expectError { + t.Fatalf("Expected hasParseErr to be %v, got %v (%v)", scenario.expectError, hasParseErr, parseErr) + } + + if scenario.expectError { + return + } + + if _, ok := claims["exp"]; !ok { + t.Fatalf("Missing required claim exp, got %v", claims) + } + + // clear exp claim to match with the scenario ones + delete(claims, "exp") + + if len(claims) != len(scenario.claims) { + t.Fatalf("Expected %v claims, got %v", scenario.claims, claims) + } + + for j, k := range claims { + if claims[j] != scenario.claims[j] { + t.Fatalf("Expected %v for %q claim, got %v", claims[j], k, scenario.claims[j]) + } + } + }) } } diff --git a/tools/security/random.go b/tools/security/random.go index 10663d46..cc00a2dc 100644 --- a/tools/security/random.go +++ b/tools/security/random.go @@ -3,16 +3,11 @@ package security import ( cryptoRand "crypto/rand" "math/big" - mathRand "math/rand" - "time" + mathRand "math/rand" // @todo replace with rand/v2? ) const defaultRandomAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" -func init() { - mathRand.Seed(time.Now().UnixNano()) -} - // RandomString generates a cryptographically random string with the specified length. // // The generated string matches [A-Za-z0-9]+ and it's transparent to URL-encoding. diff --git a/tools/security/random_by_regex.go b/tools/security/random_by_regex.go new file mode 100644 index 00000000..41cdaff2 --- /dev/null +++ b/tools/security/random_by_regex.go @@ -0,0 +1,152 @@ +package security + +import ( + cryptoRand "crypto/rand" + "fmt" + "math/big" + "regexp/syntax" + "strings" +) + +const defaultMaxRepeat = 6 + +var anyCharNotNLPairs = []rune{'A', 'Z', 'a', 'z', '0', '9'} + +// RandomStringByRegex generates a random string matching the regex pattern. +// If optFlags is not set, fallbacks to [syntax.Perl]. +// +// NB! While the source of the randomness comes from [crypto/rand] this method +// is not recommended to be used on its own in critical secure contexts because +// the generated length could vary too much on the used pattern and may not be +// as secure as simply calling [security.RandomString]. +// If you still insist on using it for such purposes, consider at least +// a large enough minimum length for the generated string, e.g. `[a-z0-9]{30}`. +// +// This function is inspired by github.com/pipe01/revregexp, github.com/lucasjones/reggen and other similar packages. +func RandomStringByRegex(pattern string, optFlags ...syntax.Flags) (string, error) { + var flags syntax.Flags + if len(optFlags) == 0 { + flags = syntax.Perl + } else { + for _, f := range optFlags { + flags |= f + } + } + + r, err := syntax.Parse(pattern, flags) + if err != nil { + return "", err + } + + var sb = new(strings.Builder) + + err = writeRandomStringByRegex(r, sb) + if err != nil { + return "", err + } + + return sb.String(), nil +} + +func writeRandomStringByRegex(r *syntax.Regexp, sb *strings.Builder) error { + // https://pkg.go.dev/regexp/syntax#Op + switch r.Op { + case syntax.OpCharClass: + c, err := randomRuneFromPairs(r.Rune) + if err != nil { + return err + } + _, err = sb.WriteRune(c) + return err + case syntax.OpAnyChar, syntax.OpAnyCharNotNL: + c, err := randomRuneFromPairs(anyCharNotNLPairs) + if err != nil { + return err + } + _, err = sb.WriteRune(c) + return err + case syntax.OpAlternate: + idx, err := randomNumber(len(r.Sub)) + if err != nil { + return err + } + return writeRandomStringByRegex(r.Sub[idx], sb) + case syntax.OpConcat: + var err error + for _, sub := range r.Sub { + err = writeRandomStringByRegex(sub, sb) + if err != nil { + break + } + } + return err + case syntax.OpRepeat: + return repeatRandomStringByRegex(r.Sub[0], sb, r.Min, r.Max) + case syntax.OpQuest: + return repeatRandomStringByRegex(r.Sub[0], sb, 0, 1) + case syntax.OpPlus: + return repeatRandomStringByRegex(r.Sub[0], sb, 1, -1) + case syntax.OpStar: + return repeatRandomStringByRegex(r.Sub[0], sb, 0, -1) + case syntax.OpCapture: + return writeRandomStringByRegex(r.Sub[0], sb) + case syntax.OpLiteral: + _, err := sb.WriteString(string(r.Rune)) + return err + default: + return fmt.Errorf("unsupported pattern operator %d", r.Op) + } +} + +func repeatRandomStringByRegex(r *syntax.Regexp, sb *strings.Builder, min int, max int) error { + if max < 0 { + max = defaultMaxRepeat + } + + if max < min { + max = min + } + + n := min + if max != min { + randRange, err := randomNumber(max - min) + if err != nil { + return err + } + n += randRange + } + + var err error + for i := 0; i < n; i++ { + err = writeRandomStringByRegex(r, sb) + if err != nil { + return err + } + } + + return nil +} + +func randomRuneFromPairs(pairs []rune) (rune, error) { + idx, err := randomNumber(len(pairs) / 2) + if err != nil { + return 0, err + } + + return randomRuneFromRange(pairs[idx*2], pairs[idx*2+1]) +} + +func randomRuneFromRange(min rune, max rune) (rune, error) { + offset, err := randomNumber(int(max - min + 1)) + if err != nil { + return min, err + } + + return min + rune(offset), nil +} + +func randomNumber(maxSoft int) (int, error) { + randRange, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(maxSoft))) + + return int(randRange.Int64()), err +} diff --git a/tools/security/random_by_regex_test.go b/tools/security/random_by_regex_test.go new file mode 100644 index 00000000..cac7ae10 --- /dev/null +++ b/tools/security/random_by_regex_test.go @@ -0,0 +1,66 @@ +package security_test + +import ( + "fmt" + "regexp" + "regexp/syntax" + "slices" + "testing" + + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestRandomStringByRegex(t *testing.T) { + generated := []string{} + + scenarios := []struct { + pattern string + flags []syntax.Flags + expectError bool + }{ + {``, nil, true}, + {`test`, nil, false}, + {`\d+`, []syntax.Flags{syntax.POSIX}, true}, + {`\d+`, nil, false}, + {`\d*`, nil, false}, + {`\d{1,10}`, nil, false}, + {`\d{3}`, nil, false}, + {`\d{0,}-abc`, nil, false}, + {`[a-zA-Z]*`, nil, false}, + {`[^a-zA-Z]{5,30}`, nil, false}, + {`\w+_abc`, nil, false}, + {`[a-zA-Z_]*`, nil, false}, + {`[2-9]{5}-\w+`, nil, false}, + {`(a|b|c)`, nil, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%q", i, s.pattern), func(t *testing.T) { + str, err := security.RandomStringByRegex(s.pattern, s.flags...) + + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) + } + + if hasErr { + return + } + + r, err := regexp.Compile(s.pattern) + if err != nil { + t.Fatal(err) + } + + if !r.Match([]byte(str)) { + t.Fatalf("Expected %q to match pattern %v", str, s.pattern) + } + + if slices.Contains(generated, str) { + t.Fatalf("The generated string %q already exists in\n%v", str, generated) + } + + generated = append(generated, str) + }) + } +} diff --git a/tools/store/store.go b/tools/store/store.go index 9dd8c364..f9c19d9f 100644 --- a/tools/store/store.go +++ b/tools/store/store.go @@ -1,11 +1,18 @@ package store -import "sync" +import ( + "encoding/json" + "sync" +) + +// @todo remove after https://github.com/golang/go/issues/20135 +const ShrinkThreshold = 200 // the number is arbitrary chosen // Store defines a concurrent safe in memory key-value data store. type Store[T any] struct { - data map[string]T - mux sync.RWMutex + data map[string]T + mu sync.RWMutex + deleted int64 } // New creates a new Store[T] instance with a shallow copy of the provided data (if any). @@ -20,8 +27,8 @@ func New[T any](data map[string]T) *Store[T] { // Reset clears the store and replaces the store data with a // shallow copy of the provided newData. func (s *Store[T]) Reset(newData map[string]T) { - s.mux.Lock() - defer s.mux.Unlock() + s.mu.Lock() + defer s.mu.Unlock() if len(newData) > 0 { s.data = make(map[string]T, len(newData)) @@ -31,38 +38,50 @@ func (s *Store[T]) Reset(newData map[string]T) { } else { s.data = make(map[string]T) } + + s.deleted = 0 } // Length returns the current number of elements in the store. func (s *Store[T]) Length() int { - s.mux.RLock() - defer s.mux.RUnlock() + s.mu.RLock() + defer s.mu.RUnlock() return len(s.data) } // RemoveAll removes all the existing store entries. func (s *Store[T]) RemoveAll() { - s.mux.Lock() - defer s.mux.Unlock() - - s.data = make(map[string]T) + s.Reset(nil) } // Remove removes a single entry from the store. // // Remove does nothing if key doesn't exist in the store. func (s *Store[T]) Remove(key string) { - s.mux.Lock() - defer s.mux.Unlock() + s.mu.Lock() + defer s.mu.Unlock() delete(s.data, key) + s.deleted++ + + // reassign to a new map so that the old one can be gc-ed because it doesn't shrink + // + // @todo remove after https://github.com/golang/go/issues/20135 + if s.deleted >= ShrinkThreshold { + newData := make(map[string]T, len(s.data)) + for k, v := range s.data { + newData[k] = v + } + s.data = newData + s.deleted = 0 + } } // Has checks if element with the specified key exist or not. func (s *Store[T]) Has(key string) bool { - s.mux.RLock() - defer s.mux.RUnlock() + s.mu.RLock() + defer s.mu.RUnlock() _, ok := s.data[key] @@ -73,16 +92,26 @@ func (s *Store[T]) Has(key string) bool { // // If key is not set, the zero T value is returned. func (s *Store[T]) Get(key string) T { - s.mux.RLock() - defer s.mux.RUnlock() + s.mu.RLock() + defer s.mu.RUnlock() return s.data[key] } +// GetOk is similar to Get but returns also a boolean indicating whether the key exists or not. +func (s *Store[T]) GetOk(key string) (T, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + v, ok := s.data[key] + + return v, ok +} + // GetAll returns a shallow copy of the current store data. func (s *Store[T]) GetAll() map[string]T { - s.mux.RLock() - defer s.mux.RUnlock() + s.mu.RLock() + defer s.mu.RUnlock() var clone = make(map[string]T, len(s.data)) @@ -93,10 +122,24 @@ func (s *Store[T]) GetAll() map[string]T { return clone } +// Values returns a slice with all of the current store values. +func (s *Store[T]) Values() []T { + s.mu.RLock() + defer s.mu.RUnlock() + + var values = make([]T, 0, len(s.data)) + + for _, v := range s.data { + values = append(values, v) + } + + return values +} + // Set sets (or overwrite if already exist) a new value for key. func (s *Store[T]) Set(key string, value T) { - s.mux.Lock() - defer s.mux.Unlock() + s.mu.Lock() + defer s.mu.Unlock() if s.data == nil { s.data = make(map[string]T) @@ -105,16 +148,34 @@ func (s *Store[T]) Set(key string, value T) { s.data[key] = value } +// 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) + } + + v, ok := s.data[key] + if !ok { + v = setFunc() + s.data[key] = v + } + + return v +} + // SetIfLessThanLimit sets (or overwrite if already exist) a new value for key. // // This method is similar to Set() but **it will skip adding new elements** // to the store if the store length has reached the specified limit. // false is returned if maxAllowedElements limit is reached. func (s *Store[T]) SetIfLessThanLimit(key string, value T, maxAllowedElements int) bool { - s.mux.Lock() - defer s.mux.Unlock() + s.mu.Lock() + defer s.mu.Unlock() - // init map if not already if s.data == nil { s.data = make(map[string]T) } @@ -132,3 +193,33 @@ func (s *Store[T]) SetIfLessThanLimit(key string, value T, maxAllowedElements in return true } + +// UnmarshalJSON implements [json.Unmarshaler] and imports the +// provided JSON data into the store. +// +// The store entries that match with the ones from the data will be overwritten with the new value. +func (s *Store[T]) UnmarshalJSON(data []byte) error { + raw := map[string]T{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + if s.data == nil { + s.data = make(map[string]T) + } + + for k, v := range raw { + s.data[k] = v + } + + return nil +} + +// MarshalJSON implements [json.Marshaler] and export the current +// store data into valid JSON. +func (s *Store[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(s.GetAll()) +} diff --git a/tools/store/store_test.go b/tools/store/store_test.go index 55ebaa65..96e807a3 100644 --- a/tools/store/store_test.go +++ b/tools/store/store_test.go @@ -3,6 +3,8 @@ package store_test import ( "bytes" "encoding/json" + "slices" + "strconv" "testing" "github.com/pocketbase/pocketbase/tools/store" @@ -137,11 +139,41 @@ func TestGet(t *testing.T) { {"missing", 0}, // should auto fallback to the zero value } - for i, scenario := range scenarios { - val := s.Get(scenario.key) - if val != scenario.expect { - t.Errorf("(%d) Expected %v, got %v", i, scenario.expect, val) - } + for _, scenario := range scenarios { + t.Run(scenario.key, func(t *testing.T) { + val := s.Get(scenario.key) + if val != scenario.expect { + t.Fatalf("Expected %v, got %v", scenario.expect, val) + } + }) + } +} + +func TestGetOk(t *testing.T) { + s := store.New(map[string]int{"test1": 0, "test2": 1}) + + scenarios := []struct { + key string + expectValue int + expectOk bool + }{ + {"test1", 0, true}, + {"test2", 1, true}, + {"missing", 0, false}, // should auto fallback to the zero value + } + + for _, scenario := range scenarios { + t.Run(scenario.key, func(t *testing.T) { + val, ok := s.GetOk(scenario.key) + + if ok != scenario.expectOk { + t.Fatalf("Expected ok %v, got %v", scenario.expectOk, ok) + } + + if val != scenario.expectValue { + t.Fatalf("Expected %v, got %v", scenario.expectValue, val) + } + }) } } @@ -173,6 +205,27 @@ func TestGetAll(t *testing.T) { } } +func TestValues(t *testing.T) { + data := map[string]int{ + "a": 1, + "b": 2, + } + + values := store.New(data).Values() + + expected := []int{1, 2} + + if len(values) != len(expected) { + t.Fatalf("Expected %d values, got %d", len(expected), len(values)) + } + + for _, v := range expected { + if !slices.Contains(values, v) { + t.Fatalf("Missing value %v in\n%v", v, values) + } + } +} + func TestSet(t *testing.T) { s := store.Store[int]{} @@ -196,6 +249,37 @@ func TestSet(t *testing.T) { } } +func TestGetOrSet(t *testing.T) { + s := store.New(map[string]int{ + "test1": 0, + "test2": 1, + "test3": 3, + }) + + scenarios := []struct { + key string + value int + expected int + }{ + {"test2", 20, 1}, + {"test3", 2, 3}, + {"test_new", 20, 20}, + {"test_new", 50, 20}, // should return the previously inserted value + } + + for _, scenario := range scenarios { + t.Run(scenario.key, func(t *testing.T) { + result := s.GetOrSet(scenario.key, func() int { + return scenario.value + }) + + if result != scenario.expected { + t.Fatalf("Expected %v, got %v", scenario.expected, result) + } + }) + } +} + func TestSetIfLessThanLimit(t *testing.T) { s := store.Store[int]{} @@ -230,3 +314,77 @@ func TestSetIfLessThanLimit(t *testing.T) { } } } + +func TestUnmarshalJSON(t *testing.T) { + s := store.Store[string]{} + s.Set("b", "old") // should be overwritten + s.Set("c", "test3") // ensures that the old values are not removed + + raw := []byte(`{"a":"test1", "b":"test2"}`) + if err := json.Unmarshal(raw, &s); err != nil { + t.Fatal(err) + } + + if v := s.Get("a"); v != "test1" { + t.Fatalf("Expected store.a to be %q, got %q", "test1", v) + } + + if v := s.Get("b"); v != "test2" { + t.Fatalf("Expected store.b to be %q, got %q", "test2", v) + } + + if v := s.Get("c"); v != "test3" { + t.Fatalf("Expected store.c to be %q, got %q", "test3", v) + } +} + +func TestMarshalJSON(t *testing.T) { + s := &store.Store[string]{} + s.Set("a", "test1") + s.Set("b", "test2") + + expected := []byte(`{"a":"test1", "b":"test2"}`) + + result, err := json.Marshal(s) + if err != nil { + t.Fatal(err) + } + + if bytes.Equal(result, expected) { + t.Fatalf("Expected\n%s\ngot\n%s", expected, result) + } +} + +func TestShrink(t *testing.T) { + s := &store.Store[int]{} + + total := 1000 + + for i := 0; i < total; i++ { + s.Set(strconv.Itoa(i), i) + } + + if s.Length() != total { + t.Fatalf("Expected %d items, got %d", total, s.Length()) + } + + // trigger map "shrink" + for i := 0; i < store.ShrinkThreshold; i++ { + s.Remove(strconv.Itoa(i)) + } + + // ensure that after the deletion, the new map was copied properly + if s.Length() != total-store.ShrinkThreshold { + t.Fatalf("Expected %d items, got %d", total-store.ShrinkThreshold, s.Length()) + } + + for k := range s.GetAll() { + kInt, err := strconv.Atoi(k) + if err != nil { + t.Fatalf("failed to convert %s into int: %v", k, err) + } + if kInt < store.ShrinkThreshold { + t.Fatalf("Key %q should have been deleted", k) + } + } +} diff --git a/tools/subscriptions/broker.go b/tools/subscriptions/broker.go index 296efad6..3b080a3c 100644 --- a/tools/subscriptions/broker.go +++ b/tools/subscriptions/broker.go @@ -2,47 +2,41 @@ package subscriptions import ( "fmt" - "sync" + + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/store" ) // Broker defines a struct for managing subscriptions clients. type Broker struct { - clients map[string]Client - mux sync.RWMutex + store *store.Store[Client] } // NewBroker initializes and returns a new Broker instance. func NewBroker() *Broker { return &Broker{ - clients: make(map[string]Client), + store: store.New[Client](nil), } } // Clients returns a shallow copy of all registered clients indexed // with their connection id. func (b *Broker) Clients() map[string]Client { - b.mux.RLock() - defer b.mux.RUnlock() + return b.store.GetAll() +} - copy := make(map[string]Client, len(b.clients)) - - for id, c := range b.clients { - copy[id] = c - } - - return copy +// ChunkedClients splits the current clients into a chunked slice. +func (b *Broker) ChunkedClients(chunkSize int) [][]Client { + return list.ToChunks(b.store.Values(), chunkSize) } // ClientById finds a registered client by its id. // // Returns non-nil error when client with clientId is not registered. func (b *Broker) ClientById(clientId string) (Client, error) { - b.mux.RLock() - defer b.mux.RUnlock() - - client, ok := b.clients[clientId] + client, ok := b.store.GetOk(clientId) if !ok { - return nil, fmt.Errorf("No client associated with connection ID %q", clientId) + return nil, fmt.Errorf("no client associated with connection ID %q", clientId) } return client, nil @@ -50,21 +44,17 @@ func (b *Broker) ClientById(clientId string) (Client, error) { // Register adds a new client to the broker instance. func (b *Broker) Register(client Client) { - b.mux.Lock() - defer b.mux.Unlock() - - b.clients[client.Id()] = client + b.store.Set(client.Id(), client) } // Unregister removes a single client by its id. // // If client with clientId doesn't exist, this method does nothing. func (b *Broker) Unregister(clientId string) { - b.mux.Lock() - defer b.mux.Unlock() - - if client, ok := b.clients[clientId]; ok { - client.Discard() - delete(b.clients, clientId) + client := b.store.Get(clientId) + if client == nil { + return } + client.Discard() + b.store.Remove(clientId) } diff --git a/tools/subscriptions/broker_test.go b/tools/subscriptions/broker_test.go index d01d290f..9c5d1ed8 100644 --- a/tools/subscriptions/broker_test.go +++ b/tools/subscriptions/broker_test.go @@ -36,6 +36,32 @@ func TestClients(t *testing.T) { } } +func TestChunkedClients(t *testing.T) { + b := subscriptions.NewBroker() + + chunks := b.ChunkedClients(2) + if total := len(chunks); total != 0 { + t.Fatalf("Expected %d chunks, got %d", 0, total) + } + + b.Register(subscriptions.NewDefaultClient()) + b.Register(subscriptions.NewDefaultClient()) + b.Register(subscriptions.NewDefaultClient()) + + chunks = b.ChunkedClients(2) + if total := len(chunks); total != 2 { + t.Fatalf("Expected %d chunks, got %d", 2, total) + } + + if total := len(chunks[0]); total != 2 { + t.Fatalf("Expected the first chunk to have 2 clients, got %d", total) + } + + if total := len(chunks[1]); total != 1 { + t.Fatalf("Expected the second chunk to have 1 client, got %d", total) + } +} + func TestClientById(t *testing.T) { b := subscriptions.NewBroker() diff --git a/tools/subscriptions/client.go b/tools/subscriptions/client.go index 644b95c7..d4d3430e 100644 --- a/tools/subscriptions/client.go +++ b/tools/subscriptions/client.go @@ -22,11 +22,8 @@ type Message struct { // SubscriptionOptions defines the request options (query params, headers, etc.) // for a single subscription topic. type SubscriptionOptions struct { - // @todo after the requests handling refactoring consider - // changing to map[string]string or map[string][]string - - Query map[string]any `json:"query"` - Headers map[string]any `json:"headers"` + Query map[string]string `json:"query"` + Headers map[string]string `json:"headers"` } // Client is an interface for a generic subscription client. @@ -168,25 +165,33 @@ func (c *DefaultClient) Subscribe(subs ...string) { } // extract subscription options (if any) - options := SubscriptionOptions{} + rawOptions := struct { + // note: any instead of string to minimize the breaking changes with earlier versions + Query map[string]any `json:"query"` + Headers map[string]any `json:"headers"` + }{} u, err := url.Parse(s) if err == nil { - rawOptions := u.Query().Get(optionsParam) - if rawOptions != "" { - json.Unmarshal([]byte(rawOptions), &options) + raw := u.Query().Get(optionsParam) + if raw != "" { + json.Unmarshal([]byte(raw), &rawOptions) } } + options := SubscriptionOptions{ + Query: make(map[string]string, len(rawOptions.Query)), + Headers: make(map[string]string, len(rawOptions.Headers)), + } + // normalize query // (currently only single string values are supported for consistency with the default routes handling) - for k, v := range options.Query { + for k, v := range rawOptions.Query { options.Query[k] = cast.ToString(v) } // normalize headers name and values, eg. "X-Token" is converted to "x_token" // (currently only single string values are supported for consistency with the default routes handling) - for k, v := range options.Headers { - delete(options.Headers, k) + for k, v := range rawOptions.Headers { options.Headers[inflector.Snakecase(k)] = cast.ToString(v) } diff --git a/tools/subscriptions/client_test.go b/tools/subscriptions/client_test.go index c479c440..1f5a9163 100644 --- a/tools/subscriptions/client_test.go +++ b/tools/subscriptions/client_test.go @@ -126,7 +126,7 @@ func TestSubscribeOptions(t *testing.T) { name string expectedOptions string }{ - {sub1, `{"query":null,"headers":null}`}, + {sub1, `{"query":{},"headers":{}}`}, {sub2, `{"query":{"name":"123"},"headers":{"x_token":"456"}}`}, } @@ -144,7 +144,7 @@ func TestSubscribeOptions(t *testing.T) { rawStr := string(rawBytes) if rawStr != s.expectedOptions { - t.Fatalf("Expected options \n%v \ngot \n%v", s.expectedOptions, rawStr) + t.Fatalf("Expected options\n%v\ngot\n%v", s.expectedOptions, rawStr) } }) } diff --git a/tools/template/registry.go b/tools/template/registry.go index c80b3391..57af4b29 100644 --- a/tools/template/registry.go +++ b/tools/template/registry.go @@ -6,19 +6,19 @@ // // Example: // -// registry := template.NewRegistry() +// registry := template.NewRegistry() // -// html1, err := registry.LoadFiles( -// // the files set wil be parsed only once and then cached -// "layout.html", -// "content.html", -// ).Render(map[string]any{"name": "John"}) +// html1, err := registry.LoadFiles( +// // the files set wil be parsed only once and then cached +// "layout.html", +// "content.html", +// ).Render(map[string]any{"name": "John"}) // -// html2, err := registry.LoadFiles( -// // reuse the already parsed and cached files set -// "layout.html", -// "content.html", -// ).Render(map[string]any{"name": "Jane"}) +// html2, err := registry.LoadFiles( +// // reuse the already parsed and cached files set +// "layout.html", +// "content.html", +// ).Render(map[string]any{"name": "Jane"}) package template import ( @@ -64,12 +64,12 @@ type Registry struct { // // Example: // -// r.AddFuncs(map[string]any{ -// "toUpper": func(str string) string { -// return strings.ToUppser(str) -// }, -// ... -// }) +// r.AddFuncs(map[string]any{ +// "toUpper": func(str string) string { +// return strings.ToUppser(str) +// }, +// ... +// }) func (r *Registry) AddFuncs(funcs map[string]any) *Registry { for name, f := range funcs { r.funcs[name] = f diff --git a/tools/tokenizer/tokenizer.go b/tools/tokenizer/tokenizer.go index f7111a27..6fdfb836 100644 --- a/tools/tokenizer/tokenizer.go +++ b/tools/tokenizer/tokenizer.go @@ -168,22 +168,6 @@ func (t *Tokenizer) readToken() (string, error) { return strings.Trim(buf.String(), t.trimCutset), nil } -// readWhiteSpaces consumes all contiguous whitespace runes. -func (t *Tokenizer) readWhiteSpaces() { - for { - ch := t.read() - - if ch == eof { - break - } - - if !t.isWhitespaceRune(ch) { - t.unread() - break - } - } -} - // read reads the next rune from the buffered reader. // Returns the `rune(0)` if an error or `io.EOF` occurs. func (t *Tokenizer) read() rune { @@ -225,17 +209,6 @@ func (t *Tokenizer) isSeperatorRune(ch rune) bool { return false } -// isWhitespaceRune checks if a rune is a space character (eg. space, tab, new line). -func (t *Tokenizer) isWhitespaceRune(ch rune) bool { - for _, c := range whitespaceChars { - if c == ch { - return true - } - } - - return false -} - // isQuoteRune checks if a rune is a quote. func (t *Tokenizer) isQuoteRune(ch rune) bool { return ch == '\'' || ch == '"' || ch == '`' diff --git a/tools/tokenizer/tokenizer_test.go b/tools/tokenizer/tokenizer_test.go index 801ba946..c9aab596 100644 --- a/tools/tokenizer/tokenizer_test.go +++ b/tools/tokenizer/tokenizer_test.go @@ -159,8 +159,8 @@ func TestScanAll(t *testing.T) { }, }, { - name: "keep separators", - content: `a, b, c, d e, "a,b, c ", (123, 456)`, + name: "keep separators", + content: `a, b, c, d e, "a,b, c ", (123, 456)`, separators: []rune{',', ' '}, // the space should be removed from the cutset keepSeparator: true, keepEmptyTokens: true, diff --git a/tools/types/datetime.go b/tools/types/datetime.go index 5db96fc5..26820391 100644 --- a/tools/types/datetime.go +++ b/tools/types/datetime.go @@ -35,6 +35,59 @@ func (d DateTime) Time() time.Time { return d.t } +// Add returns a new DateTime based on the current DateTime + the specified duration. +func (d DateTime) Add(duration time.Duration) DateTime { + d.t = d.t.Add(duration) + return d +} + +// Sub returns a [time.Duration] by subtracting the specified DateTime from the current one. +// +// If the result exceeds the maximum (or minimum) value that can be stored in a [time.Duration], +// the maximum (or minimum) duration will be returned. +func (d DateTime) Sub(u DateTime) time.Duration { + return d.Time().Sub(u.Time()) +} + +// AddDate returns a new DateTime based on the current one + duration. +// +// It follows the same rules as [time.AddDate]. +func (d DateTime) AddDate(years, months, days int) DateTime { + d.t = d.t.AddDate(years, months, days) + return d +} + +// After reports whether the current DateTime instance is after u. +func (d DateTime) After(u DateTime) bool { + return d.Time().After(u.Time()) +} + +// Before reports whether the current DateTime instance is before u. +func (d DateTime) Before(u DateTime) bool { + return d.Time().Before(u.Time()) +} + +// Compare compares the current DateTime instance with u. +// If the current instance is before u, it returns -1. +// If the current instance is after u, it returns +1. +// If they're the same, it returns 0. +func (d DateTime) Compare(u DateTime) int { + return d.Time().Compare(u.Time()) +} + +// Equal reports whether the current DateTime and u represent the same time instant. +// Two DateTime can be equal even if they are in different locations. +// For example, 6:00 +0200 and 4:00 UTC are Equal. +func (d DateTime) Equal(u DateTime) bool { + return d.Time().Equal(u.Time()) +} + +// Unix returns the current DateTime as a Unix time, aka. +// the number of seconds elapsed since January 1, 1970 UTC. +func (d DateTime) Unix() int64 { + return d.Time().Unix() +} + // IsZero checks whether the current DateTime instance has zero time value. func (d DateTime) IsZero() bool { return d.Time().IsZero() diff --git a/tools/types/datetime_test.go b/tools/types/datetime_test.go index 60cbe5f8..0d6e38b1 100644 --- a/tools/types/datetime_test.go +++ b/tools/types/datetime_test.go @@ -1,6 +1,7 @@ package types_test import ( + "fmt" "strings" "testing" "time" @@ -41,15 +42,16 @@ func TestParseDateTime(t *testing.T) { } for i, s := range scenarios { - dt, err := types.ParseDateTime(s.value) - if err != nil { - t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err) - continue - } + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + dt, err := types.ParseDateTime(s.value) + if err != nil { + t.Fatalf("Failed to parse %v: %v", s.value, err) + } - if dt.String() != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) - } + if dt.String() != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, dt.String()) + } + }) } } @@ -69,7 +71,210 @@ func TestDateTimeTime(t *testing.T) { result := dt.Time() if !expected.Equal(result) { - t.Errorf("Expected time %v, got %v", expected, result) + t.Fatalf("Expected time %v, got %v", expected, result) + } +} + +func TestDateTimeAdd(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + + d2 := d1.Add(1 * time.Hour) + + if d1.String() != "2024-01-01 10:00:00.123Z" { + t.Fatalf("Expected d1 to remain unchanged, got %s", d1.String()) + } + + expected := "2024-01-01 11:00:00.123Z" + if d2.String() != expected { + t.Fatalf("Expected d2 %s, got %s", expected, d2.String()) + } +} + +func TestDateTimeSub(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d2, _ := types.ParseDateTime("2024-01-01 10:30:00.123Z") + + result := d2.Sub(d1) + + if result.Minutes() != 30 { + t.Fatalf("Expected %v minutes diff, got %v", 30, result.Minutes()) + } +} + +func TestDateTimeAddDate(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + + d2 := d1.AddDate(1, 2, 3) + + if d1.String() != "2024-01-01 10:00:00.123Z" { + t.Fatalf("Expected d1 to remain unchanged, got %s", d1.String()) + } + + expected := "2025-03-04 10:00:00.123Z" + if d2.String() != expected { + t.Fatalf("Expected d2 %s, got %s", expected, d2.String()) + } +} + +func TestDateTimeAfter(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d2, _ := types.ParseDateTime("2024-01-02 10:00:00.123Z") + d3, _ := types.ParseDateTime("2024-01-03 10:00:00.123Z") + + scenarios := []struct { + a types.DateTime + b types.DateTime + expect bool + }{ + // d1 + {d1, d1, false}, + {d1, d2, false}, + {d1, d3, false}, + // d2 + {d2, d1, true}, + {d2, d2, false}, + {d2, d3, false}, + // d3 + {d3, d1, true}, + {d3, d2, true}, + {d3, d3, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("after_%d", i), func(t *testing.T) { + if v := s.a.After(s.b); v != s.expect { + t.Fatalf("Expected %v, got %v", s.expect, v) + } + }) + } +} + +func TestDateTimeBefore(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d2, _ := types.ParseDateTime("2024-01-02 10:00:00.123Z") + d3, _ := types.ParseDateTime("2024-01-03 10:00:00.123Z") + + scenarios := []struct { + a types.DateTime + b types.DateTime + expect bool + }{ + // d1 + {d1, d1, false}, + {d1, d2, true}, + {d1, d3, true}, + // d2 + {d2, d1, false}, + {d2, d2, false}, + {d2, d3, true}, + // d3 + {d3, d1, false}, + {d3, d2, false}, + {d3, d3, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("before_%d", i), func(t *testing.T) { + if v := s.a.Before(s.b); v != s.expect { + t.Fatalf("Expected %v, got %v", s.expect, v) + } + }) + } +} + +func TestDateTimeCompare(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d2, _ := types.ParseDateTime("2024-01-02 10:00:00.123Z") + d3, _ := types.ParseDateTime("2024-01-03 10:00:00.123Z") + + scenarios := []struct { + a types.DateTime + b types.DateTime + expect int + }{ + // d1 + {d1, d1, 0}, + {d1, d2, -1}, + {d1, d3, -1}, + // d2 + {d2, d1, 1}, + {d2, d2, 0}, + {d2, d3, -1}, + // d3 + {d3, d1, 1}, + {d3, d2, 1}, + {d3, d3, 0}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("compare_%d", i), func(t *testing.T) { + if v := s.a.Compare(s.b); v != s.expect { + t.Fatalf("Expected %v, got %v", s.expect, v) + } + }) + } +} + +func TestDateTimeEqual(t *testing.T) { + t.Parallel() + + d1, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d2, _ := types.ParseDateTime("2024-01-01 10:00:00.123Z") + d3, _ := types.ParseDateTime("2024-01-01 10:00:00.124Z") + + scenarios := []struct { + a types.DateTime + b types.DateTime + expect bool + }{ + {d1, d1, true}, + {d1, d2, true}, + {d1, d3, false}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("equal_%d", i), func(t *testing.T) { + if v := s.a.Equal(s.b); v != s.expect { + t.Fatalf("Expected %v, got %v", s.expect, v) + } + }) + } +} + +func TestDateTimeUnix(t *testing.T) { + scenarios := []struct { + date string + expected int64 + }{ + {"", -62135596800}, + {"2022-01-01 11:23:45.678Z", 1641036225}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.date), func(t *testing.T) { + dt, err := types.ParseDateTime(s.date) + if err != nil { + t.Fatal(err) + } + + v := dt.Unix() + + if v != s.expected { + t.Fatalf("Expected %d, got %d", s.expected, v) + } + }) } } @@ -108,19 +313,21 @@ func TestDateTimeMarshalJSON(t *testing.T) { } for i, s := range scenarios { - dt, err := types.ParseDateTime(s.date) - if err != nil { - t.Errorf("(%d) %v", i, err) - } + t.Run(fmt.Sprintf("%d_%s", i, s.date), func(t *testing.T) { + dt, err := types.ParseDateTime(s.date) + if err != nil { + t.Fatal(err) + } - result, err := dt.MarshalJSON() - if err != nil { - t.Errorf("(%d) %v", i, err) - } + result, err := dt.MarshalJSON() + if err != nil { + t.Fatal(err) + } - if string(result) != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result)) - } + if string(result) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, string(result)) + } + }) } } @@ -137,12 +344,14 @@ func TestDateTimeUnmarshalJSON(t *testing.T) { } for i, s := range scenarios { - dt := types.DateTime{} - dt.UnmarshalJSON([]byte(s.date)) + t.Run(fmt.Sprintf("%d_%s", i, s.date), func(t *testing.T) { + dt := types.DateTime{} + dt.UnmarshalJSON([]byte(s.date)) - if dt.String() != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) - } + if dt.String() != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, dt.String()) + } + }) } } @@ -159,16 +368,18 @@ func TestDateTimeValue(t *testing.T) { } for i, s := range scenarios { - dt, _ := types.ParseDateTime(s.value) - result, err := dt.Value() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } + t.Run(fmt.Sprintf("%d_%s", i, s.value), func(t *testing.T) { + dt, _ := types.ParseDateTime(s.value) - if result != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, result) - } + result, err := dt.Value() + if err != nil { + t.Fatal(err) + } + + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } @@ -190,16 +401,17 @@ func TestDateTimeScan(t *testing.T) { } for i, s := range scenarios { - dt := types.DateTime{} + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + dt := types.DateTime{} - err := dt.Scan(s.value) - if err != nil { - t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err) - continue - } + err := dt.Scan(s.value) + if err != nil { + t.Fatalf("Failed to parse %v: %v", s.value, err) + } - if !strings.Contains(dt.String(), s.expected) { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) - } + if !strings.Contains(dt.String(), s.expected) { + t.Fatalf("Expected %q, got %q", s.expected, dt.String()) + } + }) } } diff --git a/tools/types/json_array.go b/tools/types/json_array.go index b06f116e..4c01d3a2 100644 --- a/tools/types/json_array.go +++ b/tools/types/json_array.go @@ -6,32 +6,38 @@ import ( "fmt" ) -// JsonArray defines a slice that is safe for json and db read/write. -type JsonArray[T any] []T +// JSONArray defines a slice that is safe for json and db read/write. +type JSONArray[T any] []T // internal alias to prevent recursion during marshalization. -type jsonArrayAlias[T any] JsonArray[T] +type jsonArrayAlias[T any] JSONArray[T] // MarshalJSON implements the [json.Marshaler] interface. -func (m JsonArray[T]) MarshalJSON() ([]byte, error) { +func (m JSONArray[T]) MarshalJSON() ([]byte, error) { // initialize an empty map to ensure that `[]` is returned as json if m == nil { - m = JsonArray[T]{} + m = JSONArray[T]{} } return json.Marshal(jsonArrayAlias[T](m)) } +// String returns the string representation of the current json array. +func (m JSONArray[T]) String() string { + v, _ := m.MarshalJSON() + return string(v) +} + // Value implements the [driver.Valuer] interface. -func (m JsonArray[T]) Value() (driver.Value, error) { +func (m JSONArray[T]) Value() (driver.Value, error) { data, err := json.Marshal(m) return string(data), err } // Scan implements [sql.Scanner] interface to scan the provided value -// into the current JsonArray[T] instance. -func (m *JsonArray[T]) Scan(value any) error { +// into the current JSONArray[T] instance. +func (m *JSONArray[T]) Scan(value any) error { var data []byte switch v := value.(type) { case nil: @@ -41,7 +47,7 @@ func (m *JsonArray[T]) Scan(value any) error { case string: data = []byte(v) default: - return fmt.Errorf("failed to unmarshal JsonArray value: %q", value) + return fmt.Errorf("Failed to unmarshal JSONArray value: %q.", value) } if len(data) == 0 { diff --git a/tools/types/json_array_test.go b/tools/types/json_array_test.go index 9ab4e5a4..27aa16fe 100644 --- a/tools/types/json_array_test.go +++ b/tools/types/json_array_test.go @@ -3,64 +3,92 @@ package types_test import ( "database/sql/driver" "encoding/json" + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/types" ) -func TestJsonArrayMarshalJSON(t *testing.T) { +func TestJSONArrayMarshalJSON(t *testing.T) { scenarios := []struct { json json.Marshaler expected string }{ - {new(types.JsonArray[any]), "[]"}, - {types.JsonArray[any]{}, `[]`}, - {types.JsonArray[int]{1, 2, 3}, `[1,2,3]`}, - {types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, - {types.JsonArray[any]{1, "test"}, `[1,"test"]`}, + {new(types.JSONArray[any]), "[]"}, + {types.JSONArray[any]{}, `[]`}, + {types.JSONArray[int]{1, 2, 3}, `[1,2,3]`}, + {types.JSONArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, + {types.JSONArray[any]{1, "test"}, `[1,"test"]`}, } for i, s := range scenarios { - result, err := s.json.MarshalJSON() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - if string(result) != s.expected { - t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result)) - } + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result, err := s.json.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + if string(result) != s.expected { + t.Fatalf("Expected %s, got %s", s.expected, result) + } + }) } } -func TestJsonArrayValue(t *testing.T) { +func TestJSONArrayString(t *testing.T) { + scenarios := []struct { + json fmt.Stringer + expected string + }{ + {new(types.JSONArray[any]), "[]"}, + {types.JSONArray[any]{}, `[]`}, + {types.JSONArray[int]{1, 2, 3}, `[1,2,3]`}, + {types.JSONArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, + {types.JSONArray[any]{1, "test"}, `[1,"test"]`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result := s.json.String() + + if result != s.expected { + t.Fatalf("Expected\n%s\ngot\n%s", s.expected, result) + } + }) + } +} + +func TestJSONArrayValue(t *testing.T) { scenarios := []struct { json driver.Valuer expected driver.Value }{ - {new(types.JsonArray[any]), `[]`}, - {types.JsonArray[any]{}, `[]`}, - {types.JsonArray[int]{1, 2, 3}, `[1,2,3]`}, - {types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, - {types.JsonArray[any]{1, "test"}, `[1,"test"]`}, + {new(types.JSONArray[any]), `[]`}, + {types.JSONArray[any]{}, `[]`}, + {types.JSONArray[int]{1, 2, 3}, `[1,2,3]`}, + {types.JSONArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, + {types.JSONArray[any]{1, "test"}, `[1,"test"]`}, } for i, s := range scenarios { - result, err := s.json.Value() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - if result != s.expected { - t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) - } + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result, err := s.json.Value() + if err != nil { + t.Fatal(err) + } + + if result != s.expected { + t.Fatalf("Expected %s, got %#v", s.expected, result) + } + }) } } -func TestJsonArrayScan(t *testing.T) { +func TestJSONArrayScan(t *testing.T) { scenarios := []struct { value any expectError bool - expectJson string + expectJSON string }{ {``, false, `[]`}, {[]byte{}, false, `[]`}, @@ -78,7 +106,7 @@ func TestJsonArrayScan(t *testing.T) { } for i, s := range scenarios { - arr := types.JsonArray[any]{} + arr := types.JSONArray[any]{} scanErr := arr.Scan(s.value) hasErr := scanErr != nil @@ -89,8 +117,8 @@ func TestJsonArrayScan(t *testing.T) { result, _ := arr.MarshalJSON() - if string(result) != s.expectJson { - t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) + if string(result) != s.expectJSON { + t.Errorf("(%d) Expected %s, got %v", i, s.expectJSON, string(result)) } } } diff --git a/tools/types/json_map.go b/tools/types/json_map.go index a94ad229..87b0fc84 100644 --- a/tools/types/json_map.go +++ b/tools/types/json_map.go @@ -6,47 +6,53 @@ import ( "fmt" ) -// JsonMap defines a map that is safe for json and db read/write. -type JsonMap map[string]any +// JSONMap defines a map that is safe for json and db read/write. +type JSONMap[T any] map[string]T // MarshalJSON implements the [json.Marshaler] interface. -func (m JsonMap) MarshalJSON() ([]byte, error) { - type alias JsonMap // prevent recursion +func (m JSONMap[T]) MarshalJSON() ([]byte, error) { + type alias JSONMap[T] // prevent recursion // initialize an empty map to ensure that `{}` is returned as json if m == nil { - m = JsonMap{} + m = JSONMap[T]{} } return json.Marshal(alias(m)) } -// Get retrieves a single value from the current JsonMap. +// String returns the string representation of the current json map. +func (m JSONMap[T]) String() string { + v, _ := m.MarshalJSON() + return string(v) +} + +// Get retrieves a single value from the current JSONMap[T]. // // This helper was added primarily to assist the goja integration since custom map types // don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). -func (m JsonMap) Get(key string) any { +func (m JSONMap[T]) Get(key string) T { return m[key] } -// Set sets a single value in the current JsonMap. +// Set sets a single value in the current JSONMap[T]. // // This helper was added primarily to assist the goja integration since custom map types // don't have direct access to the map keys (https://pkg.go.dev/github.com/dop251/goja#hdr-Maps_with_methods). -func (m JsonMap) Set(key string, value any) { +func (m JSONMap[T]) Set(key string, value T) { m[key] = value } // Value implements the [driver.Valuer] interface. -func (m JsonMap) Value() (driver.Value, error) { +func (m JSONMap[T]) Value() (driver.Value, error) { data, err := json.Marshal(m) return string(data), err } // Scan implements [sql.Scanner] interface to scan the provided value -// into the current `JsonMap` instance. -func (m *JsonMap) Scan(value any) error { +// into the current JSONMap[T] instance. +func (m *JSONMap[T]) Scan(value any) error { var data []byte switch v := value.(type) { case nil: @@ -56,7 +62,7 @@ func (m *JsonMap) Scan(value any) error { case string: data = []byte(v) default: - return fmt.Errorf("failed to unmarshal JsonMap value: %q", value) + return fmt.Errorf("failed to unmarshal JSONMap[T] value: %q", value) } if len(data) == 0 { diff --git a/tools/types/json_map_test.go b/tools/types/json_map_test.go index 1c396df6..56ea95aa 100644 --- a/tools/types/json_map_test.go +++ b/tools/types/json_map_test.go @@ -2,54 +2,81 @@ package types_test import ( "database/sql/driver" + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/types" ) -func TestJsonMapMarshalJSON(t *testing.T) { +func TestJSONMapMarshalJSON(t *testing.T) { scenarios := []struct { - json types.JsonMap + json types.JSONMap[any] expected string }{ {nil, "{}"}, - {types.JsonMap{}, `{}`}, - {types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, - {types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, + {types.JSONMap[any]{}, `{}`}, + {types.JSONMap[any]{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, + {types.JSONMap[any]{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, } for i, s := range scenarios { - result, err := s.json.MarshalJSON() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - if string(result) != s.expected { - t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result)) - } + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result, err := s.json.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + if string(result) != s.expected { + t.Fatalf("Expected\n%s\ngot\n%s", s.expected, result) + } + }) } } -func TestJsonMapGet(t *testing.T) { +func TestJSONMapMarshalString(t *testing.T) { scenarios := []struct { - json types.JsonMap + json types.JSONMap[any] + expected string + }{ + {nil, "{}"}, + {types.JSONMap[any]{}, `{}`}, + {types.JSONMap[any]{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, + {types.JSONMap[any]{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result := s.json.String() + + if result != s.expected { + t.Fatalf("Expected\n%s\ngot\n%s", s.expected, result) + } + }) + } +} + +func TestJSONMapGet(t *testing.T) { + scenarios := []struct { + json types.JSONMap[any] key string expected any }{ {nil, "test", nil}, - {types.JsonMap{"test": 123}, "test", 123}, - {types.JsonMap{"test": 123}, "missing", nil}, + {types.JSONMap[any]{"test": 123}, "test", 123}, + {types.JSONMap[any]{"test": 123}, "missing", nil}, } for i, s := range scenarios { - result := s.json.Get(s.key) - if result != s.expected { - t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) - } + t.Run(fmt.Sprintf("%d_%s", i, s.key), func(t *testing.T) { + result := s.json.Get(s.key) + if result != s.expected { + t.Fatalf("Expected %s, got %#v", s.expected, result) + } + }) } } -func TestJsonMapSet(t *testing.T) { +func TestJSONMapSet(t *testing.T) { scenarios := []struct { key string value any @@ -60,44 +87,48 @@ func TestJsonMapSet(t *testing.T) { } for i, s := range scenarios { - j := types.JsonMap{} + t.Run(fmt.Sprintf("%d_%s", i, s.key), func(t *testing.T) { + j := types.JSONMap[any]{} - j.Set(s.key, s.value) + j.Set(s.key, s.value) - if v := j[s.key]; v != s.value { - t.Errorf("(%d) Expected %s, got %v", i, s.value, v) - } + if v := j[s.key]; v != s.value { + t.Fatalf("Expected %s, got %#v", s.value, v) + } + }) } } -func TestJsonMapValue(t *testing.T) { +func TestJSONMapValue(t *testing.T) { scenarios := []struct { - json types.JsonMap + json types.JSONMap[any] expected driver.Value }{ {nil, `{}`}, - {types.JsonMap{}, `{}`}, - {types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, - {types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, + {types.JSONMap[any]{}, `{}`}, + {types.JSONMap[any]{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, + {types.JSONMap[any]{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, } for i, s := range scenarios { - result, err := s.json.Value() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - if result != s.expected { - t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) - } + t.Run(fmt.Sprintf("%d_%#v", i, s.expected), func(t *testing.T) { + result, err := s.json.Value() + if err != nil { + t.Fatal(err) + } + + if result != s.expected { + t.Fatalf("Expected %s, got %#v", s.expected, result) + } + }) } } -func TestJsonArrayMapScan(t *testing.T) { +func TestJSONArrayMapScan(t *testing.T) { scenarios := []struct { value any expectError bool - expectJson string + expectJSON string }{ {``, false, `{}`}, {nil, false, `{}`}, @@ -114,19 +145,20 @@ func TestJsonArrayMapScan(t *testing.T) { } for i, s := range scenarios { - arr := types.JsonMap{} - scanErr := arr.Scan(s.value) + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + arr := types.JSONMap[any]{} + scanErr := arr.Scan(s.value) - hasErr := scanErr != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr) - continue - } + hasErr := scanErr != nil + if hasErr != s.expectError { + t.Fatalf("Expected %v, got %v (%v)", s.expectError, hasErr, scanErr) + } - result, _ := arr.MarshalJSON() + result, _ := arr.MarshalJSON() - if string(result) != s.expectJson { - t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) - } + if string(result) != s.expectJSON { + t.Fatalf("Expected %s, got %s", s.expectJSON, result) + } + }) } } diff --git a/tools/types/json_raw.go b/tools/types/json_raw.go index 670f2994..8ed58c2a 100644 --- a/tools/types/json_raw.go +++ b/tools/types/json_raw.go @@ -6,24 +6,25 @@ import ( "errors" ) -// JsonRaw defines a json value type that is safe for db read/write. -type JsonRaw []byte +// JSONRaw defines a json value type that is safe for db read/write. +type JSONRaw []byte -// ParseJsonRaw creates a new JsonRaw instance from the provided value -// (could be JsonRaw, int, float, string, []byte, etc.). -func ParseJsonRaw(value any) (JsonRaw, error) { - result := JsonRaw{} +// ParseJSONRaw creates a new JSONRaw instance from the provided value +// (could be JSONRaw, int, float, string, []byte, etc.). +func ParseJSONRaw(value any) (JSONRaw, error) { + result := JSONRaw{} err := result.Scan(value) return result, err } -// String returns the current JsonRaw instance as a json encoded string. -func (j JsonRaw) String() string { - return string(j) +// String returns the current JSONRaw instance as a json encoded string. +func (j JSONRaw) String() string { + raw, _ := j.MarshalJSON() + return string(raw) } // MarshalJSON implements the [json.Marshaler] interface. -func (j JsonRaw) MarshalJSON() ([]byte, error) { +func (j JSONRaw) MarshalJSON() ([]byte, error) { if len(j) == 0 { return []byte("null"), nil } @@ -32,9 +33,9 @@ func (j JsonRaw) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements the [json.Unmarshaler] interface. -func (j *JsonRaw) UnmarshalJSON(b []byte) error { +func (j *JSONRaw) UnmarshalJSON(b []byte) error { if j == nil { - return errors.New("JsonRaw: UnmarshalJSON on nil pointer") + return errors.New("JSONRaw: UnmarshalJSON on nil pointer") } *j = append((*j)[0:0], b...) @@ -43,7 +44,7 @@ func (j *JsonRaw) UnmarshalJSON(b []byte) error { } // Value implements the [driver.Valuer] interface. -func (j JsonRaw) Value() (driver.Value, error) { +func (j JSONRaw) Value() (driver.Value, error) { if len(j) == 0 { return nil, nil } @@ -52,8 +53,8 @@ func (j JsonRaw) Value() (driver.Value, error) { } // Scan implements [sql.Scanner] interface to scan the provided value -// into the current JsonRaw instance. -func (j *JsonRaw) Scan(value any) error { +// into the current JSONRaw instance. +func (j *JSONRaw) Scan(value any) error { var data []byte switch v := value.(type) { @@ -67,7 +68,7 @@ func (j *JsonRaw) Scan(value any) error { if v != "" { data = []byte(v) } - case JsonRaw: + case JSONRaw: if len(v) != 0 { data = []byte(v) } diff --git a/tools/types/json_raw_test.go b/tools/types/json_raw_test.go index 6683b3ff..31c8eb25 100644 --- a/tools/types/json_raw_test.go +++ b/tools/types/json_raw_test.go @@ -2,21 +2,22 @@ package types_test import ( "database/sql/driver" + "fmt" "testing" "github.com/pocketbase/pocketbase/tools/types" ) -func TestParseJsonRaw(t *testing.T) { +func TestParseJSONRaw(t *testing.T) { scenarios := []struct { value any expectError bool - expectJson string + expectJSON string }{ {nil, false, `null`}, {``, false, `null`}, {[]byte{}, false, `null`}, - {types.JsonRaw{}, false, `null`}, + {types.JSONRaw{}, false, `null`}, {`{}`, false, `{}`}, {`[]`, false, `[]`}, {123, false, `123`}, @@ -30,70 +31,75 @@ func TestParseJsonRaw(t *testing.T) { } for i, s := range scenarios { - raw, parseErr := types.ParseJsonRaw(s.value) - hasErr := parseErr != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, parseErr) - continue - } + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + raw, parseErr := types.ParseJSONRaw(s.value) - result, _ := raw.MarshalJSON() + hasErr := parseErr != nil + if hasErr != s.expectError { + t.Fatalf("Expected %v, got %v (%v)", s.expectError, hasErr, parseErr) + } - if string(result) != s.expectJson { - t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) - } + result, _ := raw.MarshalJSON() + + if string(result) != s.expectJSON { + t.Fatalf("Expected %s, got %s", s.expectJSON, string(result)) + } + }) } } -func TestJsonRawString(t *testing.T) { +func TestJSONRawString(t *testing.T) { scenarios := []struct { - json types.JsonRaw - expected string - }{ - {nil, ``}, - {types.JsonRaw{}, ``}, - {types.JsonRaw([]byte(`123`)), `123`}, - {types.JsonRaw(`{"demo":123}`), `{"demo":123}`}, - } - - for i, s := range scenarios { - result := s.json.String() - if result != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, result) - } - } -} - -func TestJsonRawMarshalJSON(t *testing.T) { - scenarios := []struct { - json types.JsonRaw + json types.JSONRaw expected string }{ {nil, `null`}, - {types.JsonRaw{}, `null`}, - {types.JsonRaw([]byte(`123`)), `123`}, - {types.JsonRaw(`{"demo":123}`), `{"demo":123}`}, + {types.JSONRaw{}, `null`}, + {types.JSONRaw([]byte(`123`)), `123`}, + {types.JSONRaw(`{"demo":123}`), `{"demo":123}`}, } for i, s := range scenarios { - result, err := s.json.MarshalJSON() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - - if string(result) != s.expected { - t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result)) - } + t.Run(fmt.Sprintf("%d_%s", i, s.expected), func(t *testing.T) { + result := s.json.String() + if result != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, result) + } + }) } } -func TestJsonRawUnmarshalJSON(t *testing.T) { +func TestJSONRawMarshalJSON(t *testing.T) { + scenarios := []struct { + json types.JSONRaw + expected string + }{ + {nil, `null`}, + {types.JSONRaw{}, `null`}, + {types.JSONRaw([]byte(`123`)), `123`}, + {types.JSONRaw(`{"demo":123}`), `{"demo":123}`}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("%d_%s", i, s.expected), func(t *testing.T) { + result, err := s.json.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + if string(result) != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, string(result)) + } + }) + } +} + +func TestJSONRawUnmarshalJSON(t *testing.T) { scenarios := []struct { json []byte expectString string }{ - {nil, ""}, + {nil, `null`}, {[]byte{0, 1, 2}, "\x00\x01\x02"}, {[]byte("123"), "123"}, {[]byte("test"), "test"}, @@ -101,53 +107,57 @@ func TestJsonRawUnmarshalJSON(t *testing.T) { } for i, s := range scenarios { - raw := types.JsonRaw{} - err := raw.UnmarshalJSON(s.json) - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } + t.Run(fmt.Sprintf("%d_%s", i, s.expectString), func(t *testing.T) { + raw := types.JSONRaw{} - if raw.String() != s.expectString { - t.Errorf("(%d) Expected %q, got %q", i, s.expectString, raw.String()) - } + err := raw.UnmarshalJSON(s.json) + if err != nil { + t.Fatal(err) + } + + if raw.String() != s.expectString { + t.Fatalf("Expected %q, got %q", s.expectString, raw.String()) + } + }) } } -func TestJsonRawValue(t *testing.T) { +func TestJSONRawValue(t *testing.T) { scenarios := []struct { - json types.JsonRaw + json types.JSONRaw expected driver.Value }{ {nil, nil}, - {types.JsonRaw{}, nil}, - {types.JsonRaw(``), nil}, - {types.JsonRaw(`test`), `test`}, + {types.JSONRaw{}, nil}, + {types.JSONRaw(``), nil}, + {types.JSONRaw(`test`), `test`}, } for i, s := range scenarios { - result, err := s.json.Value() - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - if result != s.expected { - t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) - } + t.Run(fmt.Sprintf("%d_%#v", i, s.json), func(t *testing.T) { + result, err := s.json.Value() + if err != nil { + t.Fatal(err) + } + + if result != s.expected { + t.Fatalf("Expected %s, got %v", s.expected, result) + } + }) } } -func TestJsonRawScan(t *testing.T) { +func TestJSONRawScan(t *testing.T) { scenarios := []struct { value any expectError bool - expectJson string + expectJSON string }{ {nil, false, `null`}, {``, false, `null`}, {[]byte{}, false, `null`}, - {types.JsonRaw{}, false, `null`}, - {types.JsonRaw(`test`), false, `test`}, + {types.JSONRaw{}, false, `null`}, + {types.JSONRaw(`test`), false, `test`}, {`{}`, false, `{}`}, {`[]`, false, `[]`}, {123, false, `123`}, @@ -161,18 +171,19 @@ func TestJsonRawScan(t *testing.T) { } for i, s := range scenarios { - raw := types.JsonRaw{} - scanErr := raw.Scan(s.value) - hasErr := scanErr != nil - if hasErr != s.expectError { - t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr) - continue - } + t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) { + raw := types.JSONRaw{} + scanErr := raw.Scan(s.value) + hasErr := scanErr != nil + if hasErr != s.expectError { + t.Fatalf("Expected %v, got %v (%v)", s.expectError, hasErr, scanErr) + } - result, _ := raw.MarshalJSON() + result, _ := raw.MarshalJSON() - if string(result) != s.expectJson { - t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) - } + if string(result) != s.expectJSON { + t.Fatalf("Expected %s, got %v", s.expectJSON, string(result)) + } + }) } } diff --git a/ui/.env b/ui/.env index a0c75d6e..f54d509a 100644 --- a/ui/.env +++ b/ui/.env @@ -1,6 +1,8 @@ # all environments should start with 'PB_' prefix PB_BACKEND_URL = "../" -PB_INSTALLER_PARAM = "installer" +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_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters/" PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling/" @@ -9,4 +11,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.22.21" +PB_VERSION = "v0.23.0-rc" diff --git a/ui/README.md b/ui/README.md index f7836146..854eea88 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,7 +1,7 @@ -PocketBase Admin dashboard UI +PocketBase Superuser dashboard UI ====================================================================== -This is the PocketBase Admin dashboard UI (built with Svelte and Vite). +This is the PocketBase Superuser dashboard UI (built with Svelte and Vite). Although it could be used independently, it is mainly intended to be embedded as part of a PocketBase app executable (hence the `embed.go` file). diff --git a/ui/dist/assets/AuthMethodsDocs-DkjR8bbt.js b/ui/dist/assets/AuthMethodsDocs-DkjR8bbt.js new file mode 100644 index 00000000..55c732ca --- /dev/null +++ b/ui/dist/assets/AuthMethodsDocs-DkjR8bbt.js @@ -0,0 +1,33 @@ +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-B-F-pko3.js";import{F as Ee}from"./FieldsQueryParam-CW6KZfgu.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 PocketBase from 'pocketbase'; + + const pb = new PocketBase('${n[3]}'); + + ... + + const result = await pb.collection('${(ke=n[0])==null?void 0:ke.name}').listAuthMethods(); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${n[3]}'); + + ... + + 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;ee[8].code;for(let e=0;eParam Type Description',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=` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${e[3]}'); + + ... + + const result = await pb.collection('${(ve=e[0])==null?void 0:ve.name}').listAuthMethods(); + `),t&9&&(r.dart=` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${e[3]}'); + + ... + + 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;tl(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}; diff --git a/ui/dist/assets/AuthMethodsDocs-Dsno-hdt.js b/ui/dist/assets/AuthMethodsDocs-Dsno-hdt.js deleted file mode 100644 index 26f9dc73..00000000 --- a/ui/dist/assets/AuthMethodsDocs-Dsno-hdt.js +++ /dev/null @@ -1,64 +0,0 @@ -import{S as Se,i as ye,s as Ae,O as G,e as c,v as w,b as k,c as se,f as p,g as d,h as a,m as ae,w as U,P as ve,Q as Te,k as je,R as Be,n as Oe,t as W,a as V,o as u,d as ne,C as Fe,A as Qe,q as L,r as Ne,N as qe}from"./index-Bp3jGQ0J.js";import{S as He}from"./SdkTabs-DxNNd6Sw.js";import{F as Ke}from"./FieldsQueryParam-zDO3HzQv.js";function Ce(n,l,o){const s=n.slice();return s[5]=l[o],s}function Pe(n,l,o){const s=n.slice();return s[5]=l[o],s}function $e(n,l){let o,s=l[5].code+"",_,f,i,h;function m(){return l[4](l[5])}return{key:n,first:null,c(){o=c("button"),_=w(s),f=k(),p(o,"class","tab-item"),L(o,"active",l[1]===l[5].code),this.first=o},m(v,C){d(v,o,C),a(o,_),a(o,f),i||(h=Ne(o,"click",m),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&U(_,s),C&6&&L(o,"active",l[1]===l[5].code)},d(v){v&&u(o),i=!1,h()}}}function Me(n,l){let o,s,_,f;return s=new qe({props:{content:l[5].body}}),{key:n,first:null,c(){o=c("div"),se(s.$$.fragment),_=k(),p(o,"class","tab-item"),L(o,"active",l[1]===l[5].code),this.first=o},m(i,h){d(i,o,h),ae(s,o,null),a(o,_),f=!0},p(i,h){l=i;const m={};h&4&&(m.content=l[5].body),s.$set(m),(!f||h&6)&&L(o,"active",l[1]===l[5].code)},i(i){f||(W(s.$$.fragment,i),f=!0)},o(i){V(s.$$.fragment,i),f=!1},d(i){i&&u(o),ne(s)}}}function ze(n){var be,ke;let l,o,s=n[0].name+"",_,f,i,h,m,v,C,q=n[0].name+"",E,ie,I,P,J,T,Y,$,H,ce,K,j,re,R,z=n[0].name+"",X,de,Z,B,x,M,ee,ue,te,A,le,O,oe,S,F,g=[],he=new Map,me,Q,b=[],fe=new Map,y;P=new He({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${n[3]}'); - - ... - - const result = await pb.collection('${(be=n[0])==null?void 0:be.name}').listAuthMethods(); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${n[3]}'); - - ... - - final result = await pb.collection('${(ke=n[0])==null?void 0:ke.name}').listAuthMethods(); - `}}),A=new Ke({});let D=G(n[2]);const pe=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description',ue=k(),te=c("tbody"),se(A.$$.fragment),le=k(),O=c("div"),O.textContent="Responses",oe=k(),S=c("div"),F=c("div");for(let e=0;eo(1,f=m.code);return n.$$set=m=>{"collection"in m&&o(0,_=m.collection)},o(3,s=Fe.getApiExampleUrl(Qe.baseUrl)),o(2,i=[{code:200,body:` - { - "usernamePassword": true, - "emailPassword": true, - "authProviders": [ - { - "name": "github", - "state": "3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd", - "codeVerifier": "KxFDWz1B3fxscCDJ_9gHQhLuh__ie7", - "codeChallenge": "NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE=", - "codeChallengeMethod": "S256", - "authUrl": "https://github.com/login/oauth/authorize?client_id=demo&code_challenge=NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE%3D&code_challenge_method=S256&response_type=code&scope=user&state=3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd&redirect_uri=" - }, - { - "name": "gitlab", - "state": "NeQSbtO5cShr_mk5__3CUukiMnymeb", - "codeVerifier": "ahTFHOgua8mkvPAlIBGwCUJbWKR_xi", - "codeChallenge": "O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg=", - "codeChallengeMethod": "S256", - "authUrl": "https://gitlab.com/oauth/authorize?client_id=demo&code_challenge=O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg%3D&code_challenge_method=S256&response_type=code&scope=read_user&state=NeQSbtO5cShr_mk5__3CUukiMnymeb&redirect_uri=" - }, - { - "name": "google", - "state": "zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ", - "codeVerifier": "t3CmO5VObGzdXqieakvR_fpjiW0zdO", - "codeChallenge": "KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk=", - "codeChallengeMethod": "S256", - "authUrl": "https://accounts.google.com/o/oauth2/auth?client_id=demo&code_challenge=KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk%3D&code_challenge_method=S256&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ&redirect_uri=" - } - ] - } - `}]),[_,f,i,s,h]}class Ve extends Se{constructor(l){super(),ye(this,l,De,ze,Ae,{collection:0})}}export{Ve as default}; diff --git a/ui/dist/assets/AuthRefreshDocs-1UxU_c6D.js b/ui/dist/assets/AuthRefreshDocs-1UxU_c6D.js deleted file mode 100644 index a64d0a20..00000000 --- a/ui/dist/assets/AuthRefreshDocs-1UxU_c6D.js +++ /dev/null @@ -1,79 +0,0 @@ -import{S as je,i as xe,s as Je,N as Ue,O as J,e as s,v as k,b as p,c as K,f as b,g as d,h as o,m as I,w as de,P as Ee,Q as Ke,k as Ie,R as We,n as Ge,t as N,a as V,o as u,d as W,C as Le,A as Xe,q as G,r as Ye}from"./index-Bp3jGQ0J.js";import{S as Ze}from"./SdkTabs-DxNNd6Sw.js";import{F as et}from"./FieldsQueryParam-zDO3HzQv.js";function Ne(r,l,a){const n=r.slice();return n[5]=l[a],n}function Ve(r,l,a){const n=r.slice();return n[5]=l[a],n}function ze(r,l){let a,n=l[5].code+"",m,_,i,h;function g(){return l[4](l[5])}return{key:r,first:null,c(){a=s("button"),m=k(n),_=p(),b(a,"class","tab-item"),G(a,"active",l[1]===l[5].code),this.first=a},m(v,w){d(v,a,w),o(a,m),o(a,_),i||(h=Ye(a,"click",g),i=!0)},p(v,w){l=v,w&4&&n!==(n=l[5].code+"")&&de(m,n),w&6&&G(a,"active",l[1]===l[5].code)},d(v){v&&u(a),i=!1,h()}}}function Qe(r,l){let a,n,m,_;return n=new Ue({props:{content:l[5].body}}),{key:r,first:null,c(){a=s("div"),K(n.$$.fragment),m=p(),b(a,"class","tab-item"),G(a,"active",l[1]===l[5].code),this.first=a},m(i,h){d(i,a,h),I(n,a,null),o(a,m),_=!0},p(i,h){l=i;const g={};h&4&&(g.content=l[5].body),n.$set(g),(!_||h&6)&&G(a,"active",l[1]===l[5].code)},i(i){_||(N(n.$$.fragment,i),_=!0)},o(i){V(n.$$.fragment,i),_=!1},d(i){i&&u(a),W(n)}}}function tt(r){var De,Fe;let l,a,n=r[0].name+"",m,_,i,h,g,v,w,B,X,S,z,ue,Q,M,pe,Y,U=r[0].name+"",Z,he,fe,j,ee,D,te,T,oe,be,F,C,le,me,ae,_e,f,ke,R,ge,ve,$e,se,ye,ne,Se,we,Te,re,Ce,Pe,A,ie,O,ce,P,H,y=[],Re=new Map,Ae,E,$=[],qe=new Map,q;v=new Ze({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${r[3]}'); - - ... - - const authData = await pb.collection('${(De=r[0])==null?void 0:De.name}').authRefresh(); - - // after the above you can also access the refreshed auth data from the authStore - console.log(pb.authStore.isValid); - console.log(pb.authStore.token); - console.log(pb.authStore.model.id); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${r[3]}'); - - ... - - final authData = await pb.collection('${(Fe=r[0])==null?void 0:Fe.name}').authRefresh(); - - // after the above you can also access the refreshed auth data from the authStore - print(pb.authStore.isValid); - print(pb.authStore.token); - print(pb.authStore.model.id); - `}}),R=new Ue({props:{content:"?expand=relField1,relField2.subRelField"}}),A=new et({props:{prefix:"record."}});let x=J(r[2]);const Be=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eReturns a new auth response (token and record data) for an - already authenticated record.

This method is usually called by users on page/screen reload to ensure that the previously stored - data in pb.authStore is still valid and up-to-date.

`,g=p(),K(v.$$.fragment),w=p(),B=s("h6"),B.textContent="API details",X=p(),S=s("div"),z=s("strong"),z.textContent="POST",ue=p(),Q=s("div"),M=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(U),he=k("/auth-refresh"),fe=p(),j=s("p"),j.innerHTML="Requires record Authorization:TOKEN header",ee=p(),D=s("div"),D.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='Param Type Description',be=p(),F=s("tbody"),C=s("tr"),le=s("td"),le.textContent="expand",me=p(),ae=s("td"),ae.innerHTML='String',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.: - `),K(R.$$.fragment),ge=k(` - Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(` - The expanded relations will be appended to the record under the - `),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`). - `),we=s("br"),Te=k(` - Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Pe=p(),K(A.$$.fragment),ie=p(),O=s("div"),O.textContent="Responses",ce=p(),P=s("div"),H=s("div");for(let e=0;ea(1,_=g.code);return r.$$set=g=>{"collection"in g&&a(0,m=g.collection)},r.$$.update=()=>{r.$$.dirty&1&&a(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Le.dummyCollectionRecord(m)},null,2)},{code:401,body:` - { - "code": 401, - "message": "The request requires valid record authorization token to be set.", - "data": {} - } - `},{code:403,body:` - { - "code": 403, - "message": "The authorized record model is not allowed to perform this action.", - "data": {} - } - `},{code:404,body:` - { - "code": 404, - "message": "Missing auth record context.", - "data": {} - } - `}])},a(3,n=Le.getApiExampleUrl(Xe.baseUrl)),[m,_,i,n,h]}class nt extends je{constructor(l){super(),xe(this,l,ot,tt,Je,{collection:0})}}export{nt as default}; diff --git a/ui/dist/assets/AuthRefreshDocs-DVyzazkj.js b/ui/dist/assets/AuthRefreshDocs-DVyzazkj.js new file mode 100644 index 00000000..7c3b4b3f --- /dev/null +++ b/ui/dist/assets/AuthRefreshDocs-DVyzazkj.js @@ -0,0 +1,79 @@ +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-B-F-pko3.js";import{F as et}from"./FieldsQueryParam-CW6KZfgu.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]}'); + + ... + + const authData = await pb.collection('${(De=r[0])==null?void 0:De.name}').authRefresh(); + + // after the above you can also access the refreshed auth data from the authStore + console.log(pb.authStore.isValid); + console.log(pb.authStore.token); + console.log(pb.authStore.record.id); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${r[3]}'); + + ... + + final authData = await pb.collection('${(Fe=r[0])==null?void 0:Fe.name}').authRefresh(); + + // after the above you can also access the refreshed auth data from the authStore + print(pb.authStore.isValid); + print(pb.authStore.token); + print(pb.authStore.record.id); + `}}),P=new Ne({props:{content:"?expand=relField1,relField2.subRelField"}}),A=new et({props:{prefix:"record."}});let j=J(r[2]);const Me=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eReturns a new auth response (token and record data) for an + already authenticated record.

This method is usually called by users on page/screen reload to ensure that the previously stored data + in pb.authStore is still valid and up-to-date.

`,g=p(),K(v.$$.fragment),w=p(),M=s("h6"),M.textContent="API details",X=p(),S=s("div"),x=s("strong"),x.textContent="POST",ue=p(),z=s("div"),q=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(N),he=k("/auth-refresh"),fe=p(),Q=s("p"),Q.innerHTML="Requires Authorization:TOKEN header",ee=p(),D=s("div"),D.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='Param Type Description',be=p(),F=s("tbody"),C=s("tr"),ae=s("td"),ae.textContent="expand",me=p(),le=s("td"),le.innerHTML='String',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.: + `),K(P.$$.fragment),ge=k(` + Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(` + The expanded relations will be appended to the record under the + `),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`). + `),we=s("br"),Te=k(` + Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Re=p(),K(A.$$.fragment),ie=p(),H=s("div"),H.textContent="Responses",ce=p(),R=s("div"),L=s("div");for(let e=0;el(1,_=g.code);return r.$$set=g=>{"collection"in g&&l(0,m=g.collection)},r.$$.update=()=>{r.$$.dirty&1&&l(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Ve.dummyCollectionRecord(m)},null,2)},{code:401,body:` + { + "code": 401, + "message": "The request requires valid record authorization token to be set.", + "data": {} + } + `},{code:403,body:` + { + "code": 403, + "message": "The authorized record model is not allowed to perform this action.", + "data": {} + } + `},{code:404,body:` + { + "code": 404, + "message": "Missing auth record context.", + "data": {} + } + `}])},l(3,n=Ve.getApiExampleUrl(Ye.baseURL)),[m,_,i,n,h]}class st extends Qe{constructor(a){super(),je(this,a,ot,tt,Je,{collection:0})}}export{st as default}; diff --git a/ui/dist/assets/AuthWithOAuth2Docs-CtVYpHU-.js b/ui/dist/assets/AuthWithOAuth2Docs-C0rwhR6l.js similarity index 59% rename from ui/dist/assets/AuthWithOAuth2Docs-CtVYpHU-.js rename to ui/dist/assets/AuthWithOAuth2Docs-C0rwhR6l.js index af2ea651..620504e2 100644 --- a/ui/dist/assets/AuthWithOAuth2Docs-CtVYpHU-.js +++ b/ui/dist/assets/AuthWithOAuth2Docs-C0rwhR6l.js @@ -1,4 +1,4 @@ -import{S as Ee,i as Je,s as Ne,N as Le,O as z,e as o,v as k,b as h,c as I,f as p,g as r,h as a,m as K,w as pe,P as Ue,Q as Qe,k as xe,R as ze,n as Ie,t as L,a as E,o as c,d as G,C as Be,A as Ke,q as X,r as Ge}from"./index-Bp3jGQ0J.js";import{S as Xe}from"./SdkTabs-DxNNd6Sw.js";import{F as Ye}from"./FieldsQueryParam-zDO3HzQv.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function He(s,l,n){const i=s.slice();return i[5]=l[n],i}function je(s,l){let n,i=l[5].code+"",f,g,d,m;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||(m=Ge(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,m()}}}function Ve(s,l){let n,i,f,g;return i=new Le({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,m){r(d,n,m),K(i,n,null),a(n,f),g=!0},p(d,m){l=d;const _={};m&4&&(_.content=l[5].body),i.$set(_),(!g||m&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(L(i.$$.fragment,d),g=!0)},o(d){E(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,m,_,v,O,P,Y,A,J,me,N,R,be,Z,Q=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,S,oe,ge,B,y,se,ke,ie,_e,b,ve,C,we,$e,Oe,re,Ae,ce,Se,ye,Te,de,Ce,qe,q,ue,F,he,T,H,$=[],De=new Map,Pe,j,w=[],Re=new Map,D;v=new Xe({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-B-F-pko3.js";import{F as Ye}from"./FieldsQueryParam-CW6KZfgu.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]}'); @@ -16,9 +16,9 @@ import{S as Ee,i as Je,s as Ne,N as Le,O as z,e as o,v as k,b as h,c as I,f as p // after the above you can also access the auth data from the authStore console.log(pb.authStore.isValid); console.log(pb.authStore.token); - console.log(pb.authStore.model.id); + console.log(pb.authStore.record.id); - // "logout" the last authenticated model + // "logout" pb.authStore.clear(); `,dart:` import 'package:pocketbase/pocketbase.dart'; @@ -41,22 +41,22 @@ import{S as Ee,i as Je,s as Ne,N as Le,O as z,e as o,v as k,b as h,c as I,f as p // after the above you can also access the auth data from the authStore print(pb.authStore.isValid); print(pb.authStore.token); - print(pb.authStore.model.id); + print(pb.authStore.record.id); - // "logout" the last authenticated model + // "logout" pb.authStore.clear(); - `}}),C=new Le({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ye({props:{prefix:"record."}});let x=z(s[2]);const Me=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eAuthenticate with an OAuth2 provider and returns a new auth token and record data.

For more details please check the + `}}),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;ee[5].code;for(let e=0;eAuthenticate with an OAuth2 provider and returns a new auth token and record data.

For more details please check the OAuth2 integration documentation - .

`,_=h(),I(v.$$.fragment),O=h(),P=o("h6"),P.textContent="API details",Y=h(),A=o("div"),J=o("strong"),J.textContent="POST",me=h(),N=o("div"),R=o("p"),be=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=`Param Type Description
Required provider
String The name of the OAuth2 client provider (eg. "google").
Required code
String The authorization code returned from the initial request.
Required codeVerifier
String The code verifier sent with the initial request as part of the code_challenge.
Required redirectUrl
String The redirect url sent with the initial request.
Optional createData
Object

Optional data that will be used when creating the auth record on OAuth2 sign-up.

The created auth record must comply with the same requirements and validations in the + .

`,_=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=`Param Type Description
Required provider
String The name of the OAuth2 client provider (eg. "google").
Required code
String The authorization code returned from the initial request.
Required codeVerifier
String The code verifier sent with the initial request as part of the code_challenge.
Required redirectURL
String The redirect url sent with the initial request.
Optional createData
Object

Optional data that will be used when creating the auth record on OAuth2 sign-up.

The created auth record must comply with the same requirements and validations in the regular create action.
The data can only be in json, aka. multipart/form-data and files - upload currently are not supported during OAuth2 sign-ups.

`,le=h(),U=o("div"),U.textContent="Query parameters",ne=h(),S=o("table"),oe=o("thead"),oe.innerHTML='Param Type Description',ge=h(),B=o("tbody"),y=o("tr"),se=o("td"),se.textContent="expand",ke=h(),ie=o("td"),ie.innerHTML='String',_e=h(),b=o("td"),ve=k(`Auto expand record relations. Ex.: + upload currently are not supported during OAuth2 sign-ups.

`,le=h(),U=o("div"),U.textContent="Query parameters",ne=h(),y=o("table"),oe=o("thead"),oe.innerHTML='Param Type Description',ge=h(),B=o("tbody"),S=o("tr"),se=o("td"),se.textContent="expand",ke=h(),ie=o("td"),ie.innerHTML='String',_e=h(),m=o("td"),ve=k(`Auto expand record relations. Ex.: `),I(C.$$.fragment),we=k(` Supports up to 6-levels depth nested relations expansion. `),$e=o("br"),Oe=k(` The expanded relations will be appended to the record under the - `),re=o("code"),re.textContent="expand",Ae=k(" property (eg. "),ce=o("code"),ce.textContent='"expand": {"relField1": {...}, ...}',Se=k(`). - `),ye=o("br"),Te=k(` - Only the relations to which the request user has permissions to `),de=o("strong"),de.textContent="view",Ce=k(" will be expanded."),qe=h(),I(q.$$.fragment),ue=h(),F=o("div"),F.textContent="Responses",he=h(),T=o("div"),H=o("div");for(let e=0;e<$.length;e+=1)$[e].c();Pe=h(),j=o("div");for(let e=0;en(1,g=_.code);return s.$$set=_=>{"collection"in _&&n(0,f=_.collection)},s.$$.update=()=>{s.$$.dirty&1&&n(2,d=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Be.dummyCollectionRecord(f),meta:{id:"abc123",name:"John Doe",username:"john.doe",email:"test@example.com",avatarUrl:"https://example.com/avatar.png",accessToken:"...",refreshToken:"...",rawUser:{}}},null,2)},{code:400,body:` + `),v.$set(u),(!R||t&1)&&Q!==(Q=e[0].name+"")&&pe(ee,Q),t&6&&(N=z(e[2]),$=Ue($,t,Me,1,e,N,Re,L,Ne,He,null,Le)),t&6&&(V=z(e[2]),ze(),w=Ue(w,t,We,1,e,V,Pe,H,Ie,Ve,null,Fe),Ke())},i(e){if(!R){j(v.$$.fragment,e),j(C.$$.fragment,e),j(q.$$.fragment,e);for(let t=0;tn(1,g=_.code);return s.$$set=_=>{"collection"in _&&n(0,f=_.collection)},s.$$.update=()=>{s.$$.dirty&1&&n(2,d=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Be.dummyCollectionRecord(f),meta:{id:"abc123",name:"John Doe",username:"john.doe",email:"test@example.com",avatarURL:"https://example.com/avatar.png",accessToken:"...",refreshToken:"...",rawUser:{}}},null,2)},{code:400,body:` { "code": 400, "message": "An error occurred while submitting the form.", @@ -114,4 +114,4 @@ import{S as Ee,i as Je,s as Ne,N as Le,O as z,e as o,v as k,b as h,c as I,f as p } } } - `}])},n(3,i=Be.getApiExampleUrl(Ke.baseUrl)),[f,g,d,i,m]}class nt extends Ee{constructor(l){super(),Je(this,l,et,Ze,Ne,{collection:0})}}export{nt as default}; + `}])},n(3,i=Be.getApiExampleUrl(Ge.baseURL)),[f,g,d,i,b]}class lt extends xe{constructor(l){super(),Ee(this,l,et,Ze,Je,{collection:0})}}export{lt as default}; diff --git a/ui/dist/assets/AuthWithOtpDocs-CB9fgmn9.js b/ui/dist/assets/AuthWithOtpDocs-CB9fgmn9.js new file mode 100644 index 00000000..7741479a --- /dev/null +++ b/ui/dist/assets/AuthWithOtpDocs-CB9fgmn9.js @@ -0,0 +1,129 @@ +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 Oe,Y as ne}from"./index-B-F-pko3.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 Te(s){let t,e,a,d,c,r,n,u=s[0].name+"",m,q,W,C,B,A,H,R,I,y,T,w=[],$=new Map,E,D,k=[],N=new Map,M,i=U(s[2]);const v=l=>l[4].code;for(let l=0;ll[4].code;for(let l=0;lParam Type Description
Required otpId
String The id of the OTP request.
Required password
String The one-time password.',H=S(),R=p("div"),R.textContent="Responses",I=S(),y=p("div"),T=p("div");for(let l=0;le(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.", + "data": { + "otpId": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}])},[a,d,c,r]}class Se extends ee{constructor(t){super(),te(this,t,Pe,Te,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,T,w=[],$=new Map,E,D,k=[],N=new Map,M,i=U(s[2]);const v=l=>l[4].code;for(let l=0;ll[4].code;for(let l=0;lParam Type Description
Required email
String The auth record email address to send the OTP request (if exists).',H=S(),R=p("div"),R.textContent="Responses",I=S(),y=p("div"),T=p("div");for(let l=0;le(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.", + "data": { + "email": { + "code": "validation_is_email", + "message": "Must be a valid email address." + } + } + } + `},{code:429,body:` + { + "code": 429, + "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:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${s[2]}'); + + ... + + // send OTP email to the provided auth record + const req = await pb.collection('${(D=s[0])==null?void 0:D.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 + const authData = await pb.collection('${(k=s[0])==null?void 0:k.name}').authWithOtp( + req.otpId, + "YOUR_OTP", + ); + + // after the above you can also access the auth data from the authStore + console.log(pb.authStore.isValid); + console.log(pb.authStore.token); + console.log(pb.authStore.record.id); + + // "logout" + pb.authStore.clear(); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${s[2]}'); + + ... + + // 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'); + + // ... 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( + req.otpId, + "YOUR_OTP", + ); + + // after the above you can also access the auth data from the authStore + print(pb.authStore.isValid); + print(pb.authStore.token); + print(pb.authStore.record.id); + + // "logout" + pb.authStore.clear(); + `}});let y=U(s[3]),T=[];for(let i=0;iJ($[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="

Authenticate with an one-time password (OTP).

",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;ie(1,r=u);return s.$$set=u=>{"collection"in u&&e(0,d=u.collection)},e(2,a=oe.getApiExampleUrl(Oe.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}; diff --git a/ui/dist/assets/AuthWithPasswordDocs-B1auplF0.js b/ui/dist/assets/AuthWithPasswordDocs-B1auplF0.js deleted file mode 100644 index 044685e5..00000000 --- a/ui/dist/assets/AuthWithPasswordDocs-B1auplF0.js +++ /dev/null @@ -1,98 +0,0 @@ -import{S as wt,i as yt,s as $t,N as vt,O as oe,e as n,v as p,b as d,c as ne,f as m,g as r,h as t,m as se,w as De,P as pt,Q as Pt,k as Rt,R as At,n as Ct,t as Z,a as x,o as c,d as ie,C as ft,A as Ot,q as re,r as Tt}from"./index-Bp3jGQ0J.js";import{S as Ut}from"./SdkTabs-DxNNd6Sw.js";import{F as Mt}from"./FieldsQueryParam-zDO3HzQv.js";function ht(s,l,a){const i=s.slice();return i[8]=l[a],i}function bt(s,l,a){const i=s.slice();return i[8]=l[a],i}function Dt(s){let l;return{c(){l=p("email")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function Et(s){let l;return{c(){l=p("username")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function Wt(s){let l;return{c(){l=p("username/email")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function mt(s){let l;return{c(){l=n("strong"),l.textContent="username"},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function _t(s){let l;return{c(){l=p("or")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function kt(s){let l;return{c(){l=n("strong"),l.textContent="email"},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function gt(s,l){let a,i=l[8].code+"",g,b,f,u;function _(){return l[7](l[8])}return{key:s,first:null,c(){a=n("button"),g=p(i),b=d(),m(a,"class","tab-item"),re(a,"active",l[3]===l[8].code),this.first=a},m(R,A){r(R,a,A),t(a,g),t(a,b),f||(u=Tt(a,"click",_),f=!0)},p(R,A){l=R,A&16&&i!==(i=l[8].code+"")&&De(g,i),A&24&&re(a,"active",l[3]===l[8].code)},d(R){R&&c(a),f=!1,u()}}}function St(s,l){let a,i,g,b;return i=new vt({props:{content:l[8].body}}),{key:s,first:null,c(){a=n("div"),ne(i.$$.fragment),g=d(),m(a,"class","tab-item"),re(a,"active",l[3]===l[8].code),this.first=a},m(f,u){r(f,a,u),se(i,a,null),t(a,g),b=!0},p(f,u){l=f;const _={};u&16&&(_.content=l[8].body),i.$set(_),(!b||u&24)&&re(a,"active",l[3]===l[8].code)},i(f){b||(Z(i.$$.fragment,f),b=!0)},o(f){x(i.$$.fragment,f),b=!1},d(f){f&&c(a),ie(i)}}}function Lt(s){var rt,ct;let l,a,i=s[0].name+"",g,b,f,u,_,R,A,C,q,Ee,ce,T,de,N,ue,U,ee,We,te,I,Le,pe,le=s[0].name+"",fe,qe,he,V,be,M,me,Be,Q,D,_e,Fe,ke,He,$,Ye,ge,Se,ve,Ne,we,ye,j,$e,E,Pe,Ie,J,W,Re,Ve,Ae,Qe,k,je,B,Je,Ke,ze,Ce,Ge,Oe,Xe,Ze,xe,Te,et,tt,F,Ue,K,Me,L,z,O=[],lt=new Map,at,G,S=[],ot=new Map,H;function nt(e,o){if(e[1]&&e[2])return Wt;if(e[1])return Et;if(e[2])return Dt}let Y=nt(s),P=Y&&Y(s);T=new Ut({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${s[6]}'); - - ... - - const authData = await pb.collection('${(rt=s[0])==null?void 0:rt.name}').authWithPassword( - '${s[5]}', - 'YOUR_PASSWORD', - ); - - // after the above you can also access the auth data from the authStore - console.log(pb.authStore.isValid); - console.log(pb.authStore.token); - console.log(pb.authStore.model.id); - - // "logout" the last authenticated account - pb.authStore.clear(); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${s[6]}'); - - ... - - final authData = await pb.collection('${(ct=s[0])==null?void 0:ct.name}').authWithPassword( - '${s[5]}', - 'YOUR_PASSWORD', - ); - - // after the above you can also access the auth data from the authStore - print(pb.authStore.isValid); - print(pb.authStore.token); - print(pb.authStore.model.id); - - // "logout" the last authenticated account - pb.authStore.clear(); - `}});let v=s[1]&&mt(),w=s[1]&&s[2]&&_t(),y=s[2]&&kt();B=new vt({props:{content:"?expand=relField1,relField2.subRelField"}}),F=new Mt({props:{prefix:"record."}});let ae=oe(s[4]);const st=e=>e[8].code;for(let e=0;ee[8].code;for(let e=0;eParam Type Description',Be=d(),Q=n("tbody"),D=n("tr"),_e=n("td"),_e.innerHTML='
Required identity
',Fe=d(),ke=n("td"),ke.innerHTML='String',He=d(),$=n("td"),Ye=p(`The - `),v&&v.c(),ge=d(),w&&w.c(),Se=d(),y&&y.c(),ve=p(` - of the record to authenticate.`),Ne=d(),we=n("tr"),we.innerHTML='
Required password
String The auth record password.',ye=d(),j=n("div"),j.textContent="Query parameters",$e=d(),E=n("table"),Pe=n("thead"),Pe.innerHTML='Param Type Description',Ie=d(),J=n("tbody"),W=n("tr"),Re=n("td"),Re.textContent="expand",Ve=d(),Ae=n("td"),Ae.innerHTML='String',Qe=d(),k=n("td"),je=p(`Auto expand record relations. Ex.: - `),ne(B.$$.fragment),Je=p(` - Supports up to 6-levels depth nested relations expansion. `),Ke=n("br"),ze=p(` - The expanded relations will be appended to the record under the - `),Ce=n("code"),Ce.textContent="expand",Ge=p(" property (eg. "),Oe=n("code"),Oe.textContent='"expand": {"relField1": {...}, ...}',Xe=p(`). - `),Ze=n("br"),xe=p(` - Only the relations to which the request user has permissions to `),Te=n("strong"),Te.textContent="view",et=p(" will be expanded."),tt=d(),ne(F.$$.fragment),Ue=d(),K=n("div"),K.textContent="Responses",Me=d(),L=n("div"),z=n("div");for(let e=0;ea(3,_=C.code);return s.$$set=C=>{"collection"in C&&a(0,u=C.collection)},s.$$.update=()=>{var C,q;s.$$.dirty&1&&a(2,g=(C=u==null?void 0:u.options)==null?void 0:C.allowEmailAuth),s.$$.dirty&1&&a(1,b=(q=u==null?void 0:u.options)==null?void 0:q.allowUsernameAuth),s.$$.dirty&6&&a(5,f=b&&g?"YOUR_USERNAME_OR_EMAIL":b?"YOUR_USERNAME":"YOUR_EMAIL"),s.$$.dirty&1&&a(4,R=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:ft.dummyCollectionRecord(u)},null,2)},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "identity": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}])},a(6,i=ft.getApiExampleUrl(Ot.baseUrl)),[u,b,g,_,R,f,i,A]}class Yt extends wt{constructor(l){super(),yt(this,l,qt,Lt,$t,{collection:0})}}export{Yt as default}; diff --git a/ui/dist/assets/AuthWithPasswordDocs-CTYk9AZ_.js b/ui/dist/assets/AuthWithPasswordDocs-CTYk9AZ_.js new file mode 100644 index 00000000..2bf72ad7 --- /dev/null +++ b/ui/dist/assets/AuthWithPasswordDocs-CTYk9AZ_.js @@ -0,0 +1,96 @@ +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-B-F-pko3.js";import{F as Ot}from"./FieldsQueryParam-CW6KZfgu.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]}'); + + ... + + const authData = await pb.collection('${(ot=i[0])==null?void 0:ot.name}').authWithPassword( + '${i[4]}', + 'YOUR_PASSWORD', + ); + + // after the above you can also access the auth data from the authStore + console.log(pb.authStore.isValid); + console.log(pb.authStore.token); + console.log(pb.authStore.record.id); + + // "logout" + pb.authStore.clear(); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${i[5]}'); + + ... + + final authData = await pb.collection('${(st=i[0])==null?void 0:st.name}').authWithPassword( + '${i[4]}', + 'YOUR_PASSWORD', + ); + + // after the above you can also access the auth data from the authStore + print(pb.authStore.isValid); + print(pb.authStore.token); + print(pb.authStore.record.id); + + // "logout" + pb.authStore.clear(); + `}});let B=L(i[1]),S=[];for(let e=0;ee[7].code;for(let e=0;ee[7].code;for(let e=0;eParam Type Description',Be=u(),V=s("tbody"),O=s("tr"),_e=s("td"),_e.innerHTML='
Required identity
',Le=u(),ke=s("td"),ke.innerHTML='String',qe=u(),Y=s("td");for(let e=0;e
Required password
String The auth record password.',Se=u(),E=s("div"),E.textContent="Query parameters",we=u(),A=s("table"),ye=s("thead"),ye.innerHTML='Param Type Description',je=u(),N=s("tbody"),D=s("tr"),$e=s("td"),$e.textContent="expand",Ve=u(),Pe=s("td"),Pe.innerHTML='String',Ye=u(),v=s("td"),Ee=f(`Auto expand record relations. Ex.: + `),ae(F.$$.fragment),Ne=f(` + Supports up to 6-levels depth nested relations expansion. `),Qe=s("br"),Ie=f(` + The expanded relations will be appended to the record under the + `),Re=s("code"),Re.textContent="expand",Je=f(" property (eg. "),Ce=s("code"),Ce.textContent='"expand": {"relField1": {...}, ...}',Ke=f(`). + `),Xe=s("br"),ze=f(` + Only the relations to which the request user has permissions to `),Te=s("strong"),Te.textContent="view",Ge=f(" will be expanded."),Ze=u(),ae(M.$$.fragment),Oe=u(),Q=s("div"),Q.textContent="Responses",Ae=u(),W=s("div"),I=s("div");for(let e=0;ea(2,h=_.code);return i.$$set=_=>{"collection"in _&&a(0,r=_.collection)},i.$$.update=()=>{var _;i.$$.dirty&1&&a(1,m=((_=r==null?void 0:r.passwordAuth)==null?void 0:_.identityFields)||[]),i.$$.dirty&2&&a(4,b=m.length==0?"NONE":"YOUR_"+m.join("_OR_").toUpperCase()),i.$$.dirty&1&&a(3,g=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:dt.dummyCollectionRecord(r)},null,2)},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "identity": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}])},a(5,n=dt.getApiExampleUrl(Ct.baseURL)),[r,m,h,g,b,n,$]}class Mt extends kt{constructor(o){super(),gt(this,o,Wt,Dt,vt,{collection:0})}}export{Mt as default}; diff --git a/ui/dist/assets/CodeEditor-CPgcqnd5.js b/ui/dist/assets/CodeEditor-CPgcqnd5.js new file mode 100644 index 00000000..fb034c1f --- /dev/null +++ b/ui/dist/assets/CodeEditor-CPgcqnd5.js @@ -0,0 +1,14 @@ +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-B-F-pko3.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=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.lastBigReductionSizen;)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]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;sn&1&&o==i)||r.push(a[s],i)}a=r}let t=[];for(let r=0;r>19,r=a&65535,s=this.stack.length-t*3;if(s<0||O.getGoto(this.stack[s],r,!1)<0){let i=this.findForcedReduction();if(i==null)return!1;a=i}this.storeNode(0,this.pos,this.pos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(a),!0}findForcedReduction(){let{parser:O}=this.p,a=[],t=(r,s)=>{if(!a.includes(r))return a.push(r),O.allActions(r,i=>{if(!(i&393216))if(i&65536){let o=(i>>19)-s;if(o>1){let n=i&65535,Q=this.stack.length-o*3;if(Q>=0&&O.getGoto(this.stack[Q],n,!1)>=0)return o<<19|65536|n}}else{let o=t(i,s+1);if(o!=null)return o}})};return t(this.state,0)}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(this.stack.length!=3)return!1;let{parser:O}=this.p;return O.data[O.stateSlot(this.state,1)]==65535&&!O.stateSlot(this.state,4)}restart(){this.storeNode(0,this.pos,this.pos,4,!0),this.state=this.stack[0],this.stack.length=0}sameState(O){if(this.state!=O.state||this.stack.length!=O.stack.length)return!1;for(let a=0;athis.lookAhead&&(this.emitLookAhead(),this.lookAhead=O)}close(){this.curContext&&this.curContext.tracker.strict&&this.emitContext(),this.lookAhead>0&&this.emitLookAhead()}}class KO{constructor(O,a){this.tracker=O,this.context=a,this.hash=O.strict?O.hash(a):0}}class pa{constructor(O){this.start=O,this.state=O.state,this.stack=O.stack,this.base=this.stack.length}reduce(O){let a=O&65535,t=O>>19;t==0?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=(t-1)*3;let r=this.start.p.parser.getGoto(this.stack[this.base-3],a,!0);this.state=r}}class oO{constructor(O,a,t){this.stack=O,this.pos=a,this.index=t,this.buffer=O.buffer,this.index==0&&this.maybeNext()}static create(O,a=O.bufferBase+O.buffer.length){return new oO(O,a,a-O.bufferBase)}maybeNext(){let O=this.stack.parent;O!=null&&(this.index=this.stack.bufferBase-O.bufferBase,this.stack=O,this.buffer=O.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,this.index==0&&this.maybeNext()}fork(){return new oO(this.stack,this.pos,this.index)}}function N(e,O=Uint16Array){if(typeof e!="string")return e;let a=null;for(let t=0,r=0;t=92&&i--,i>=34&&i--;let n=i-32;if(n>=46&&(n-=46,o=!0),s+=n,o)break;s*=46}a?a[r++]=s:a=new O(s)}return a}class aO{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const FO=new aO;class da{constructor(O,a){this.input=O,this.ranges=a,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=FO,this.rangeIndex=0,this.pos=this.chunkPos=a[0].from,this.range=a[0],this.end=a[a.length-1].to,this.readNext()}resolveOffset(O,a){let t=this.range,r=this.rangeIndex,s=this.pos+O;for(;st.to:s>=t.to;){if(r==this.ranges.length-1)return null;let i=this.ranges[++r];s+=i.from-t.to,t=i}return s}clipPos(O){if(O>=this.range.from&&OO)return Math.max(O,a.from);return this.end}peek(O){let a=this.chunkOff+O,t,r;if(a>=0&&a=this.chunk2Pos&&to.to&&(this.chunk2=this.chunk2.slice(0,o.to-t)),r=this.chunk2.charCodeAt(0)}}return t>=this.token.lookAhead&&(this.token.lookAhead=t+1),r}acceptToken(O,a=0){let t=a?this.resolveOffset(a,-1):this.pos;if(t==null||t=this.chunk2Pos&&this.posthis.range.to?O.slice(0,this.range.to-this.pos):O,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(O=1){for(this.chunkOff+=O;this.pos+O>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();O-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=O,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(O,a){if(a?(this.token=a,a.start=O,a.lookAhead=O+1,a.value=a.extended=-1):this.token=FO,this.pos!=O){if(this.pos=O,O==this.end)return this.setDone(),this;for(;O=this.range.to;)this.range=this.ranges[++this.rangeIndex];O>=this.chunkPos&&O=this.chunkPos&&a<=this.chunkPos+this.chunk.length)return this.chunk.slice(O-this.chunkPos,a-this.chunkPos);if(O>=this.chunk2Pos&&a<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(O-this.chunk2Pos,a-this.chunk2Pos);if(O>=this.range.from&&a<=this.range.to)return this.input.read(O,a);let t="";for(let r of this.ranges){if(r.from>=a)break;r.to>O&&(t+=this.input.read(Math.max(r.from,O),Math.min(r.to,a)))}return t}}class z{constructor(O,a){this.data=O,this.id=a}token(O,a){let{parser:t}=a.p;Ve(this.data,O,a,this.id,t.data,t.tokenPrecTable)}}z.prototype.contextual=z.prototype.fallback=z.prototype.extend=!1;class lO{constructor(O,a,t){this.precTable=a,this.elseToken=t,this.data=typeof O=="string"?N(O):O}token(O,a){let t=O.pos,r=0;for(;;){let s=O.next<0,i=O.resolveOffset(1,1);if(Ve(this.data,O,a,0,this.data,this.precTable),O.token.value>-1)break;if(this.elseToken==null)return;if(s||r++,i==null)break;O.reset(i,O.token)}r&&(O.reset(t,O.token),O.acceptToken(this.elseToken,r))}}lO.prototype.contextual=z.prototype.fallback=z.prototype.extend=!1;class k{constructor(O,a={}){this.token=O,this.contextual=!!a.contextual,this.fallback=!!a.fallback,this.extend=!!a.extend}}function Ve(e,O,a,t,r,s){let i=0,o=1<0){let d=e[h];if(n.allows(d)&&(O.token.value==-1||O.token.value==d||fa(d,O.token.value,r,s))){O.acceptToken(d);break}}let u=O.next,c=0,f=e[i+2];if(O.next<0&&f>c&&e[Q+f*3-3]==65535){i=e[Q+f*3-1];continue O}for(;c>1,d=Q+h+(h<<1),P=e[d],m=e[d+1]||65536;if(u=m)c=h+1;else{i=e[d+2],O.advance();continue O}}break}}function HO(e,O,a){for(let t=O,r;(r=e[t])!=65535;t++)if(r==a)return t-O;return-1}function fa(e,O,a,t){let r=HO(a,t,O);return r<0||HO(a,t,e)O)&&!t.type.isError)return a<0?Math.max(0,Math.min(t.to-1,O-25)):Math.min(e.length,Math.max(t.from+1,O+25));if(a<0?t.prevSibling():t.nextSibling())break;if(!t.parent())return a<0?0:e.length}}class $a{constructor(O,a){this.fragments=O,this.nodeSet=a,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let O=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(O){for(this.safeFrom=O.openStart?Oe(O.tree,O.from+O.offset,1)-O.offset:O.from,this.safeTo=O.openEnd?Oe(O.tree,O.to+O.offset,-1)-O.offset:O.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(O.tree),this.start.push(-O.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(O){if(OO)return this.nextStart=i,null;if(s instanceof tO){if(i==O){if(i=Math.max(this.safeFrom,O)&&(this.trees.push(s),this.start.push(i),this.index.push(0))}else this.index[a]++,this.nextStart=i+s.length}}}class Pa{constructor(O,a){this.stream=a,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=O.tokenizers.map(t=>new aO)}getActions(O){let a=0,t=null,{parser:r}=O.p,{tokenizers:s}=r,i=r.stateSlot(O.state,3),o=O.curContext?O.curContext.hash:0,n=0;for(let Q=0;Qc.end+25&&(n=Math.max(c.lookAhead,n)),c.value!=0)){let f=a;if(c.extended>-1&&(a=this.addActions(O,c.extended,c.end,a)),a=this.addActions(O,c.value,c.end,a),!u.extend&&(t=c,a>f))break}}for(;this.actions.length>a;)this.actions.pop();return n&&O.setLookAhead(n),!t&&O.pos==this.stream.end&&(t=new aO,t.value=O.p.parser.eofTerm,t.start=t.end=O.pos,a=this.addActions(O,t.value,t.end,a)),this.mainToken=t,this.actions}getMainToken(O){if(this.mainToken)return this.mainToken;let a=new aO,{pos:t,p:r}=O;return a.start=t,a.end=Math.min(t+1,r.stream.end),a.value=t==r.stream.end?r.parser.eofTerm:0,a}updateCachedToken(O,a,t){let r=this.stream.clipPos(t.pos);if(a.token(this.stream.reset(r,O),t),O.value>-1){let{parser:s}=t.p;for(let i=0;i=0&&t.p.parser.dialect.allows(o>>1)){o&1?O.extended=o>>1:O.value=o>>1;break}}}else O.value=0,O.end=this.stream.clipPos(r+1)}putAction(O,a,t,r){for(let s=0;sO.bufferLength*4?new $a(t,O.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let O=this.stacks,a=this.minStackPos,t=this.stacks=[],r,s;if(this.bigReductionCount>300&&O.length==1){let[i]=O;for(;i.forceReduce()&&i.stack.length&&i.stack[i.stack.length-2]>=this.lastBigReductionStart;);this.bigReductionCount=this.lastBigReductionSize=0}for(let i=0;ia)t.push(o);else{if(this.advanceStack(o,t,O))continue;{r||(r=[],s=[]),r.push(o);let n=this.tokens.getMainToken(o);s.push(n.value,n.end)}}break}}if(!t.length){let i=r&&Sa(r);if(i)return Z&&console.log("Finish with "+this.stackID(i)),this.stackToTree(i);if(this.parser.strict)throw Z&&r&&console.log("Stuck with token "+(this.tokens.mainToken?this.parser.getName(this.tokens.mainToken.value):"none")),new SyntaxError("No parse at "+a);this.recovering||(this.recovering=5)}if(this.recovering&&r){let i=this.stoppedAt!=null&&r[0].pos>this.stoppedAt?r[0]:this.runRecovery(r,s,t);if(i)return Z&&console.log("Force-finish "+this.stackID(i)),this.stackToTree(i.forceAll())}if(this.recovering){let i=this.recovering==1?1:this.recovering*3;if(t.length>i)for(t.sort((o,n)=>n.score-o.score);t.length>i;)t.pop();t.some(o=>o.reducePos>a)&&this.recovering--}else if(t.length>1){O:for(let i=0;i500&&Q.buffer.length>500)if((o.score-Q.score||o.buffer.length-Q.buffer.length)>0)t.splice(n--,1);else{t.splice(i--,1);continue O}}}t.length>12&&t.splice(12,t.length-12)}this.minStackPos=t[0].pos;for(let i=1;i ":"";if(this.stoppedAt!=null&&r>this.stoppedAt)return O.forceReduce()?O:null;if(this.fragments){let Q=O.curContext&&O.curContext.tracker.strict,u=Q?O.curContext.hash:0;for(let c=this.fragments.nodeAt(r);c;){let f=this.parser.nodeSet.types[c.type.id]==c.type?s.getGoto(O.state,c.type.id):-1;if(f>-1&&c.length&&(!Q||(c.prop(qO.contextHash)||0)==u))return O.useNode(c,f),Z&&console.log(i+this.stackID(O)+` (via reuse of ${s.getName(c.type.id)})`),!0;if(!(c instanceof tO)||c.children.length==0||c.positions[0]>0)break;let h=c.children[0];if(h instanceof tO&&c.positions[0]==0)c=h;else break}}let o=s.stateSlot(O.state,4);if(o>0)return O.reduce(o),Z&&console.log(i+this.stackID(O)+` (via always-reduce ${s.getName(o&65535)})`),!0;if(O.stack.length>=8400)for(;O.stack.length>6e3&&O.forceReduce(););let n=this.tokens.getActions(O);for(let Q=0;Qr?a.push(d):t.push(d)}return!1}advanceFully(O,a){let t=O.pos;for(;;){if(!this.advanceStack(O,null,null))return!1;if(O.pos>t)return ee(O,a),!0}}runRecovery(O,a,t){let r=null,s=!1;for(let i=0;i ":"";if(o.deadEnd&&(s||(s=!0,o.restart(),Z&&console.log(u+this.stackID(o)+" (restarted)"),this.advanceFully(o,t))))continue;let c=o.split(),f=u;for(let h=0;c.forceReduce()&&h<10&&(Z&&console.log(f+this.stackID(c)+" (via force-reduce)"),!this.advanceFully(c,t));h++)Z&&(f=this.stackID(c)+" -> ");for(let h of o.recoverByInsert(n))Z&&console.log(u+this.stackID(h)+" (via recover-insert)"),this.advanceFully(h,t);this.stream.end>o.pos?(Q==o.pos&&(Q++,n=0),o.recoverByDelete(n,Q),Z&&console.log(u+this.stackID(o)+` (via recover-delete ${this.parser.getName(n)})`),ee(o,t)):(!r||r.scoree;class je{constructor(O){this.start=O.start,this.shift=O.shift||dO,this.reduce=O.reduce||dO,this.reuse=O.reuse||dO,this.hash=O.hash||(()=>0),this.strict=O.strict!==!1}}class T extends Ct{constructor(O){if(super(),this.wrappers=[],O.version!=14)throw new RangeError(`Parser version (${O.version}) doesn't match runtime version (14)`);let a=O.nodeNames.split(" ");this.minRepeatTerm=a.length;for(let o=0;oO.topRules[o][1]),r=[];for(let o=0;o=0)s(u,n,o[Q++]);else{let c=o[Q+-u];for(let f=-u;f>0;f--)s(o[Q++],n,c);Q++}}}this.nodeSet=new At(a.map((o,n)=>Et.define({name:n>=this.minRepeatTerm?void 0:o,id:n,props:r[n],top:t.indexOf(n)>-1,error:n==0,skipped:O.skippedNodes&&O.skippedNodes.indexOf(n)>-1}))),O.propSources&&(this.nodeSet=this.nodeSet.extend(...O.propSources)),this.strict=!1,this.bufferLength=Nt;let i=N(O.tokenData);this.context=O.context,this.specializerSpecs=O.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let o=0;otypeof o=="number"?new z(i,o):o),this.topRules=O.topRules,this.dialects=O.dialects||{},this.dynamicPrecedences=O.dynamicPrecedences||null,this.tokenPrecTable=O.tokenPrec,this.termNames=O.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(O,a,t){let r=new ma(this,O,a,t);for(let s of this.wrappers)r=s(r,O,a,t);return r}getGoto(O,a,t=!1){let r=this.goto;if(a>=r[0])return-1;for(let s=r[a+1];;){let i=r[s++],o=i&1,n=r[s++];if(o&&t)return n;for(let Q=s+(i>>1);s0}validAction(O,a){return!!this.allActions(O,t=>t==a?!0:null)}allActions(O,a){let t=this.stateSlot(O,4),r=t?a(t):void 0;for(let s=this.stateSlot(O,1);r==null;s+=3){if(this.data[s]==65535)if(this.data[s+1]==1)s=R(this.data,s+2);else break;r=a(R(this.data,s+1))}return r}nextStates(O){let a=[];for(let t=this.stateSlot(O,1);;t+=3){if(this.data[t]==65535)if(this.data[t+1]==1)t=R(this.data,t+2);else break;if(!(this.data[t+2]&1)){let r=this.data[t+1];a.some((s,i)=>i&1&&s==r)||a.push(this.data[t],r)}}return a}configure(O){let a=Object.assign(Object.create(T.prototype),this);if(O.props&&(a.nodeSet=this.nodeSet.extend(...O.props)),O.top){let t=this.topRules[O.top];if(!t)throw new RangeError(`Invalid top rule name ${O.top}`);a.top=t}return O.tokenizers&&(a.tokenizers=this.tokenizers.map(t=>{let r=O.tokenizers.find(s=>s.from==t);return r?r.to:t})),O.specializers&&(a.specializers=this.specializers.slice(),a.specializerSpecs=this.specializerSpecs.map((t,r)=>{let s=O.specializers.find(o=>o.from==t.external);if(!s)return t;let i=Object.assign(Object.assign({},t),{external:s.to});return a.specializers[r]=te(i),i})),O.contextTracker&&(a.context=O.contextTracker),O.dialect&&(a.dialect=this.parseDialect(O.dialect)),O.strict!=null&&(a.strict=O.strict),O.wrap&&(a.wrappers=a.wrappers.concat(O.wrap)),O.bufferLength!=null&&(a.bufferLength=O.bufferLength),a}hasWrappers(){return this.wrappers.length>0}getName(O){return this.termNames?this.termNames[O]:String(O<=this.maxNode&&this.nodeSet.types[O].name||O)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(O){let a=this.dynamicPrecedences;return a==null?0:a[O]||0}parseDialect(O){let a=Object.keys(this.dialects),t=a.map(()=>!1);if(O)for(let s of O.split(" ")){let i=a.indexOf(s);i>=0&&(t[i]=!0)}let r=null;for(let s=0;st)&&a.p.parser.stateFlag(a.state,2)&&(!O||O.scoree.external(a,t)<<1|O}return e.get}const Za=54,ba=1,ka=55,xa=2,Xa=56,ya=3,ae=4,wa=5,nO=6,Ge=7,Ce=8,Ae=9,Ee=10,Ra=11,Ya=12,Ta=13,fO=57,Wa=14,re=58,Ne=20,_a=22,Me=23,qa=24,XO=26,Ie=27,va=28,Ua=31,za=34,Va=36,ja=37,Ga=0,Ca=1,Aa={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},Ea={dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},ie={dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}};function Na(e){return e==45||e==46||e==58||e>=65&&e<=90||e==95||e>=97&&e<=122||e>=161}function De(e){return e==9||e==10||e==13||e==32}let se=null,oe=null,le=0;function yO(e,O){let a=e.pos+O;if(le==a&&oe==e)return se;let t=e.peek(O);for(;De(t);)t=e.peek(++O);let r="";for(;Na(t);)r+=String.fromCharCode(t),t=e.peek(++O);return oe=e,le=a,se=r?r.toLowerCase():t==Ma||t==Ia?void 0:null}const Be=60,cO=62,zO=47,Ma=63,Ia=33,Da=45;function ne(e,O){this.name=e,this.parent=O}const Ba=[nO,Ee,Ge,Ce,Ae],Ja=new je({start:null,shift(e,O,a,t){return Ba.indexOf(O)>-1?new ne(yO(t,1)||"",e):e},reduce(e,O){return O==Ne&&e?e.parent:e},reuse(e,O,a,t){let r=O.type.id;return r==nO||r==Va?new ne(yO(t,1)||"",e):e},strict:!1}),La=new k((e,O)=>{if(e.next!=Be){e.next<0&&O.context&&e.acceptToken(fO);return}e.advance();let a=e.next==zO;a&&e.advance();let t=yO(e,0);if(t===void 0)return;if(!t)return e.acceptToken(a?Wa:nO);let r=O.context?O.context.name:null;if(a){if(t==r)return e.acceptToken(Ra);if(r&&Ea[r])return e.acceptToken(fO,-2);if(O.dialectEnabled(Ga))return e.acceptToken(Ya);for(let s=O.context;s;s=s.parent)if(s.name==t)return;e.acceptToken(Ta)}else{if(t=="script")return e.acceptToken(Ge);if(t=="style")return e.acceptToken(Ce);if(t=="textarea")return e.acceptToken(Ae);if(Aa.hasOwnProperty(t))return e.acceptToken(Ee);r&&ie[r]&&ie[r][t]?e.acceptToken(fO,-1):e.acceptToken(nO)}},{contextual:!0}),Ka=new k(e=>{for(let O=0,a=0;;a++){if(e.next<0){a&&e.acceptToken(re);break}if(e.next==Da)O++;else if(e.next==cO&&O>=2){a>=3&&e.acceptToken(re,-2);break}else O=0;e.advance()}});function Fa(e){for(;e;e=e.parent)if(e.name=="svg"||e.name=="math")return!0;return!1}const Ha=new k((e,O)=>{if(e.next==zO&&e.peek(1)==cO){let a=O.dialectEnabled(Ca)||Fa(O.context);e.acceptToken(a?wa:ae,2)}else e.next==cO&&e.acceptToken(ae,1)});function VO(e,O,a){let t=2+e.length;return new k(r=>{for(let s=0,i=0,o=0;;o++){if(r.next<0){o&&r.acceptToken(O);break}if(s==0&&r.next==Be||s==1&&r.next==zO||s>=2&&si?r.acceptToken(O,-i):r.acceptToken(a,-(i-2));break}else if((r.next==10||r.next==13)&&o){r.acceptToken(O,1);break}else s=i=0;r.advance()}})}const Or=VO("script",Za,ba),er=VO("style",ka,xa),tr=VO("textarea",Xa,ya),ar=B({"Text RawText":l.content,"StartTag StartCloseTag SelfClosingEndTag EndTag":l.angleBracket,TagName:l.tagName,"MismatchedCloseTag/TagName":[l.tagName,l.invalid],AttributeName:l.attributeName,"AttributeValue UnquotedAttributeValue":l.attributeValue,Is:l.definitionOperator,"EntityReference CharacterReference":l.character,Comment:l.blockComment,ProcessingInst:l.processingInstruction,DoctypeDecl:l.documentMeta}),rr=T.deserialize({version:14,states:",xOVO!rOOO!WQ#tO'#CqO!]Q#tO'#CzO!bQ#tO'#C}O!gQ#tO'#DQO!lQ#tO'#DSO!qOaO'#CpO!|ObO'#CpO#XOdO'#CpO$eO!rO'#CpOOO`'#Cp'#CpO$lO$fO'#DTO$tQ#tO'#DVO$yQ#tO'#DWOOO`'#Dk'#DkOOO`'#DY'#DYQVO!rOOO%OQ&rO,59]O%ZQ&rO,59fO%fQ&rO,59iO%qQ&rO,59lO%|Q&rO,59nOOOa'#D^'#D^O&XOaO'#CxO&dOaO,59[OOOb'#D_'#D_O&lObO'#C{O&wObO,59[OOOd'#D`'#D`O'POdO'#DOO'[OdO,59[OOO`'#Da'#DaO'dO!rO,59[O'kQ#tO'#DROOO`,59[,59[OOOp'#Db'#DbO'pO$fO,59oOOO`,59o,59oO'xQ#|O,59qO'}Q#|O,59rOOO`-E7W-E7WO(SQ&rO'#CsOOQW'#DZ'#DZO(bQ&rO1G.wOOOa1G.w1G.wOOO`1G/Y1G/YO(mQ&rO1G/QOOOb1G/Q1G/QO(xQ&rO1G/TOOOd1G/T1G/TO)TQ&rO1G/WOOO`1G/W1G/WO)`Q&rO1G/YOOOa-E7[-E7[O)kQ#tO'#CyOOO`1G.v1G.vOOOb-E7]-E7]O)pQ#tO'#C|OOOd-E7^-E7^O)uQ#tO'#DPOOO`-E7_-E7_O)zQ#|O,59mOOOp-E7`-E7`OOO`1G/Z1G/ZOOO`1G/]1G/]OOO`1G/^1G/^O*PQ,UO,59_OOQW-E7X-E7XOOOa7+$c7+$cOOO`7+$t7+$tOOOb7+$l7+$lOOOd7+$o7+$oOOO`7+$r7+$rO*[Q#|O,59eO*aQ#|O,59hO*fQ#|O,59kOOO`1G/X1G/XO*kO7[O'#CvO*|OMhO'#CvOOQW1G.y1G.yOOO`1G/P1G/POOO`1G/S1G/SOOO`1G/V1G/VOOOO'#D['#D[O+_O7[O,59bOOQW,59b,59bOOOO'#D]'#D]O+pOMhO,59bOOOO-E7Y-E7YOOQW1G.|1G.|OOOO-E7Z-E7Z",stateData:",]~O!^OS~OUSOVPOWQOXROYTO[]O][O^^O`^Oa^Ob^Oc^Ox^O{_O!dZO~OfaO~OfbO~OfcO~OfdO~OfeO~O!WfOPlP!ZlP~O!XiOQoP!ZoP~O!YlORrP!ZrP~OUSOVPOWQOXROYTOZqO[]O][O^^O`^Oa^Ob^Oc^Ox^O!dZO~O!ZrO~P#dO![sO!euO~OfvO~OfwO~OS|OT}OhyO~OS!POT}OhyO~OS!ROT}OhyO~OS!TOT}OhyO~OS}OT}OhyO~O!WfOPlX!ZlX~OP!WO!Z!XO~O!XiOQoX!ZoX~OQ!ZO!Z!XO~O!YlORrX!ZrX~OR!]O!Z!XO~O!Z!XO~P#dOf!_O~O![sO!e!aO~OS!bO~OS!cO~Oi!dOSgXTgXhgX~OS!fOT!gOhyO~OS!hOT!gOhyO~OS!iOT!gOhyO~OS!jOT!gOhyO~OS!gOT!gOhyO~Of!kO~Of!lO~Of!mO~OS!nO~Ok!qO!`!oO!b!pO~OS!rO~OS!sO~OS!tO~Oa!uOb!uOc!uO!`!wO!a!uO~Oa!xOb!xOc!xO!b!wO!c!xO~Oa!uOb!uOc!uO!`!{O!a!uO~Oa!xOb!xOc!xO!b!{O!c!xO~OT~bac!dx{!d~",goto:"%p!`PPPPPPPPPPPPPPPPPPPP!a!gP!mPP!yP!|#P#S#Y#]#`#f#i#l#r#x!aP!a!aP$O$U$l$r$x%O%U%[%bPPPPPPPP%hX^OX`pXUOX`pezabcde{!O!Q!S!UR!q!dRhUR!XhXVOX`pRkVR!XkXWOX`pRnWR!XnXXOX`pQrXR!XpXYOX`pQ`ORx`Q{aQ!ObQ!QcQ!SdQ!UeZ!e{!O!Q!S!UQ!v!oR!z!vQ!y!pR!|!yQgUR!VgQjVR!YjQmWR![mQpXR!^pQtZR!`tS_O`ToXp",nodeNames:"⚠ StartCloseTag StartCloseTag StartCloseTag EndTag SelfClosingEndTag StartTag StartTag StartTag StartTag StartTag StartCloseTag StartCloseTag StartCloseTag IncompleteCloseTag Document Text EntityReference CharacterReference InvalidEntity Element OpenTag TagName Attribute AttributeName Is AttributeValue UnquotedAttributeValue ScriptText CloseTag OpenTag StyleText CloseTag OpenTag TextareaText CloseTag OpenTag CloseTag SelfClosingTag Comment ProcessingInst MismatchedCloseTag CloseTag DoctypeDecl",maxTerm:67,context:Ja,nodeProps:[["closedBy",-10,1,2,3,7,8,9,10,11,12,13,"EndTag",6,"EndTag SelfClosingEndTag",-4,21,30,33,36,"CloseTag"],["openedBy",4,"StartTag StartCloseTag",5,"StartTag",-4,29,32,35,37,"OpenTag"],["group",-9,14,17,18,19,20,39,40,41,42,"Entity",16,"Entity TextContent",-3,28,31,34,"TextContent Entity"],["isolate",-11,21,29,30,32,33,35,36,37,38,41,42,"ltr",-3,26,27,39,""]],propSources:[ar],skippedNodes:[0],repeatNodeCount:9,tokenData:"!]tw8twx7Sx!P8t!P!Q5u!Q!]8t!]!^/^!^!a7S!a#S8t#S#T;{#T#s8t#s$f5u$f;'S8t;'S;=`>V<%l?Ah8t?Ah?BY5u?BY?Mn8t?MnO5u!Z5zbkWOX5uXZ7SZ[5u[^7S^p5uqr5urs7Sst+Ptw5uwx7Sx!]5u!]!^7w!^!a7S!a#S5u#S#T7S#T;'S5u;'S;=`8n<%lO5u!R7VVOp7Sqs7St!]7S!]!^7l!^;'S7S;'S;=`7q<%lO7S!R7qOa!R!R7tP;=`<%l7S!Z8OYkWa!ROX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+P!Z8qP;=`<%l5u!_8{ihSkWOX5uXZ7SZ[5u[^7S^p5uqr8trs7Sst/^tw8twx7Sx!P8t!P!Q5u!Q!]8t!]!^:j!^!a7S!a#S8t#S#T;{#T#s8t#s$f5u$f;'S8t;'S;=`>V<%l?Ah8t?Ah?BY5u?BY?Mn8t?MnO5u!_:sbhSkWa!ROX+PZ[+P^p+Pqr/^sw/^x!P/^!P!Q+P!Q!^/^!a#S/^#S#T0m#T#s/^#s$f+P$f;'S/^;'S;=`1e<%l?Ah/^?Ah?BY+P?BY?Mn/^?MnO+P!VP<%l?Ah;{?Ah?BY7S?BY?Mn;{?MnO7S!V=dXhSa!Rqr0msw0mx!P0m!Q!^0m!a#s0m$f;'S0m;'S;=`1_<%l?Ah0m?BY?Mn0m!V>SP;=`<%l;{!_>YP;=`<%l8t!_>dhhSkWOX@OXZAYZ[@O[^AY^p@OqrBwrsAYswBwwxAYx!PBw!P!Q@O!Q!]Bw!]!^/^!^!aAY!a#SBw#S#TE{#T#sBw#s$f@O$f;'SBw;'S;=`HS<%l?AhBw?Ah?BY@O?BY?MnBw?MnO@O!Z@TakWOX@OXZAYZ[@O[^AY^p@Oqr@OrsAYsw@OwxAYx!]@O!]!^Az!^!aAY!a#S@O#S#TAY#T;'S@O;'S;=`Bq<%lO@O!RA]UOpAYq!]AY!]!^Ao!^;'SAY;'S;=`At<%lOAY!RAtOb!R!RAwP;=`<%lAY!ZBRYkWb!ROX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+P!ZBtP;=`<%l@O!_COhhSkWOX@OXZAYZ[@O[^AY^p@OqrBwrsAYswBwwxAYx!PBw!P!Q@O!Q!]Bw!]!^Dj!^!aAY!a#SBw#S#TE{#T#sBw#s$f@O$f;'SBw;'S;=`HS<%l?AhBw?Ah?BY@O?BY?MnBw?MnO@O!_DsbhSkWb!ROX+PZ[+P^p+Pqr/^sw/^x!P/^!P!Q+P!Q!^/^!a#S/^#S#T0m#T#s/^#s$f+P$f;'S/^;'S;=`1e<%l?Ah/^?Ah?BY+P?BY?Mn/^?MnO+P!VFQbhSOpAYqrE{rsAYswE{wxAYx!PE{!P!QAY!Q!]E{!]!^GY!^!aAY!a#sE{#s$fAY$f;'SE{;'S;=`G|<%l?AhE{?Ah?BYAY?BY?MnE{?MnOAY!VGaXhSb!Rqr0msw0mx!P0m!Q!^0m!a#s0m$f;'S0m;'S;=`1_<%l?Ah0m?BY?Mn0m!VHPP;=`<%lE{!_HVP;=`<%lBw!ZHcW!bx`P!a`Or(trs'ksv(tw!^(t!^!_)e!_;'S(t;'S;=`*P<%lO(t!aIYlhS`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx}-_}!OKQ!O!P-_!P!Q$q!Q!^-_!^!_*V!_!a&X!a#S-_#S#T1k#T#s-_#s$f$q$f;'S-_;'S;=`3X<%l?Ah-_?Ah?BY$q?BY?Mn-_?MnO$q!aK_khS`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx!P-_!P!Q$q!Q!^-_!^!_*V!_!`&X!`!aMS!a#S-_#S#T1k#T#s-_#s$f$q$f;'S-_;'S;=`3X<%l?Ah-_?Ah?BY$q?BY?Mn-_?MnO$q!TM_X`P!a`!cp!eQOr&Xrs&}sv&Xwx(tx!^&X!^!_*V!_;'S&X;'S;=`*y<%lO&X!aNZ!ZhSfQ`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx}-_}!OMz!O!PMz!P!Q$q!Q![Mz![!]Mz!]!^-_!^!_*V!_!a&X!a!c-_!c!}Mz!}#R-_#R#SMz#S#T1k#T#oMz#o#s-_#s$f$q$f$}-_$}%OMz%O%W-_%W%oMz%o%p-_%p&aMz&a&b-_&b1pMz1p4UMz4U4dMz4d4e-_4e$ISMz$IS$I`-_$I`$IbMz$Ib$Je-_$Je$JgMz$Jg$Kh-_$Kh%#tMz%#t&/x-_&/x&EtMz&Et&FV-_&FV;'SMz;'S;:j!#|;:j;=`3X<%l?&r-_?&r?AhMz?Ah?BY$q?BY?MnMz?MnO$q!a!$PP;=`<%lMz!R!$ZY!a`!cpOq*Vqr!$yrs(Vsv*Vwx)ex!a*V!a!b!4t!b;'S*V;'S;=`*s<%lO*V!R!%Q]!a`!cpOr*Vrs(Vsv*Vwx)ex}*V}!O!%y!O!f*V!f!g!']!g#W*V#W#X!0`#X;'S*V;'S;=`*s<%lO*V!R!&QX!a`!cpOr*Vrs(Vsv*Vwx)ex}*V}!O!&m!O;'S*V;'S;=`*s<%lO*V!R!&vV!a`!cp!dPOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!'dX!a`!cpOr*Vrs(Vsv*Vwx)ex!q*V!q!r!(P!r;'S*V;'S;=`*s<%lO*V!R!(WX!a`!cpOr*Vrs(Vsv*Vwx)ex!e*V!e!f!(s!f;'S*V;'S;=`*s<%lO*V!R!(zX!a`!cpOr*Vrs(Vsv*Vwx)ex!v*V!v!w!)g!w;'S*V;'S;=`*s<%lO*V!R!)nX!a`!cpOr*Vrs(Vsv*Vwx)ex!{*V!{!|!*Z!|;'S*V;'S;=`*s<%lO*V!R!*bX!a`!cpOr*Vrs(Vsv*Vwx)ex!r*V!r!s!*}!s;'S*V;'S;=`*s<%lO*V!R!+UX!a`!cpOr*Vrs(Vsv*Vwx)ex!g*V!g!h!+q!h;'S*V;'S;=`*s<%lO*V!R!+xY!a`!cpOr!+qrs!,hsv!+qvw!-Swx!.[x!`!+q!`!a!/j!a;'S!+q;'S;=`!0Y<%lO!+qq!,mV!cpOv!,hvx!-Sx!`!,h!`!a!-q!a;'S!,h;'S;=`!.U<%lO!,hP!-VTO!`!-S!`!a!-f!a;'S!-S;'S;=`!-k<%lO!-SP!-kO{PP!-nP;=`<%l!-Sq!-xS!cp{POv(Vx;'S(V;'S;=`(h<%lO(Vq!.XP;=`<%l!,ha!.aX!a`Or!.[rs!-Ssv!.[vw!-Sw!`!.[!`!a!.|!a;'S!.[;'S;=`!/d<%lO!.[a!/TT!a`{POr)esv)ew;'S)e;'S;=`)y<%lO)ea!/gP;=`<%l!.[!R!/sV!a`!cp{POr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!0]P;=`<%l!+q!R!0gX!a`!cpOr*Vrs(Vsv*Vwx)ex#c*V#c#d!1S#d;'S*V;'S;=`*s<%lO*V!R!1ZX!a`!cpOr*Vrs(Vsv*Vwx)ex#V*V#V#W!1v#W;'S*V;'S;=`*s<%lO*V!R!1}X!a`!cpOr*Vrs(Vsv*Vwx)ex#h*V#h#i!2j#i;'S*V;'S;=`*s<%lO*V!R!2qX!a`!cpOr*Vrs(Vsv*Vwx)ex#m*V#m#n!3^#n;'S*V;'S;=`*s<%lO*V!R!3eX!a`!cpOr*Vrs(Vsv*Vwx)ex#d*V#d#e!4Q#e;'S*V;'S;=`*s<%lO*V!R!4XX!a`!cpOr*Vrs(Vsv*Vwx)ex#X*V#X#Y!+q#Y;'S*V;'S;=`*s<%lO*V!R!4{Y!a`!cpOr!4trs!5ksv!4tvw!6Vwx!8]x!a!4t!a!b!:]!b;'S!4t;'S;=`!;r<%lO!4tq!5pV!cpOv!5kvx!6Vx!a!5k!a!b!7W!b;'S!5k;'S;=`!8V<%lO!5kP!6YTO!a!6V!a!b!6i!b;'S!6V;'S;=`!7Q<%lO!6VP!6lTO!`!6V!`!a!6{!a;'S!6V;'S;=`!7Q<%lO!6VP!7QOxPP!7TP;=`<%l!6Vq!7]V!cpOv!5kvx!6Vx!`!5k!`!a!7r!a;'S!5k;'S;=`!8V<%lO!5kq!7yS!cpxPOv(Vx;'S(V;'S;=`(h<%lO(Vq!8YP;=`<%l!5ka!8bX!a`Or!8]rs!6Vsv!8]vw!6Vw!a!8]!a!b!8}!b;'S!8];'S;=`!:V<%lO!8]a!9SX!a`Or!8]rs!6Vsv!8]vw!6Vw!`!8]!`!a!9o!a;'S!8];'S;=`!:V<%lO!8]a!9vT!a`xPOr)esv)ew;'S)e;'S;=`)y<%lO)ea!:YP;=`<%l!8]!R!:dY!a`!cpOr!4trs!5ksv!4tvw!6Vwx!8]x!`!4t!`!a!;S!a;'S!4t;'S;=`!;r<%lO!4t!R!;]V!a`!cpxPOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!;uP;=`<%l!4t!V!{let Q=o.type.id;if(Q==va)return $O(o,n,a);if(Q==Ua)return $O(o,n,t);if(Q==za)return $O(o,n,r);if(Q==Ne&&s.length){let u=o.node,c=u.firstChild,f=c&&ce(c,n),h;if(f){for(let d of s)if(d.tag==f&&(!d.attrs||d.attrs(h||(h=Je(c,n))))){let P=u.lastChild,m=P.type.id==ja?P.from:u.to;if(m>c.to)return{parser:d.parser,overlay:[{from:c.to,to:m}]}}}}if(i&&Q==Me){let u=o.node,c;if(c=u.firstChild){let f=i[n.read(c.from,c.to)];if(f)for(let h of f){if(h.tagName&&h.tagName!=ce(u.parent,n))continue;let d=u.lastChild;if(d.type.id==XO){let P=d.from+1,m=d.lastChild,x=d.to-(m&&m.isError?0:1);if(x>P)return{parser:h.parser,overlay:[{from:P,to:x}]}}else if(d.type.id==Ie)return{parser:h.parser,overlay:[{from:d.from,to:d.to}]}}}}return null})}const ir=99,Qe=1,sr=100,or=101,he=2,Ke=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],lr=58,nr=40,Fe=95,cr=91,rO=45,Qr=46,hr=35,ur=37,pr=38,dr=92,fr=10;function M(e){return e>=65&&e<=90||e>=97&&e<=122||e>=161}function He(e){return e>=48&&e<=57}const $r=new k((e,O)=>{for(let a=!1,t=0,r=0;;r++){let{next:s}=e;if(M(s)||s==rO||s==Fe||a&&He(s))!a&&(s!=rO||r>0)&&(a=!0),t===r&&s==rO&&t++,e.advance();else if(s==dr&&e.peek(1)!=fr)e.advance(),e.next>-1&&e.advance(),a=!0;else{a&&e.acceptToken(s==nr?sr:t==2&&O.canShift(he)?he:or);break}}}),Pr=new k(e=>{if(Ke.includes(e.peek(-1))){let{next:O}=e;(M(O)||O==Fe||O==hr||O==Qr||O==cr||O==lr&&M(e.peek(1))||O==rO||O==pr)&&e.acceptToken(ir)}}),mr=new k(e=>{if(!Ke.includes(e.peek(-1))){let{next:O}=e;if(O==ur&&(e.advance(),e.acceptToken(Qe)),M(O)){do e.advance();while(M(e.next)||He(e.next));e.acceptToken(Qe)}}}),gr=B({"AtKeyword import charset namespace keyframes media supports":l.definitionKeyword,"from to selector":l.keyword,NamespaceName:l.namespace,KeyframeName:l.labelName,KeyframeRangeName:l.operatorKeyword,TagName:l.tagName,ClassName:l.className,PseudoClassName:l.constant(l.className),IdName:l.labelName,"FeatureName PropertyName":l.propertyName,AttributeName:l.attributeName,NumberLiteral:l.number,KeywordQuery:l.keyword,UnaryQueryOp:l.operatorKeyword,"CallTag ValueName":l.atom,VariableName:l.variableName,Callee:l.operatorKeyword,Unit:l.unit,"UniversalSelector NestingSelector":l.definitionOperator,MatchOp:l.compareOperator,"ChildOp SiblingOp, LogicOp":l.logicOperator,BinOp:l.arithmeticOperator,Important:l.modifier,Comment:l.blockComment,ColorLiteral:l.color,"ParenthesizedContent StringLiteral":l.string,":":l.punctuation,"PseudoOp #":l.derefOperator,"; ,":l.separator,"( )":l.paren,"[ ]":l.squareBracket,"{ }":l.brace}),Sr={__proto__:null,lang:32,"nth-child":32,"nth-last-child":32,"nth-of-type":32,"nth-last-of-type":32,dir:32,"host-context":32,url:60,"url-prefix":60,domain:60,regexp:60,selector:138},Zr={__proto__:null,"@import":118,"@media":142,"@charset":146,"@namespace":150,"@keyframes":156,"@supports":168},br={__proto__:null,not:132,only:132},kr=T.deserialize({version:14,states:":jQYQ[OOO#_Q[OOP#fOWOOOOQP'#Cd'#CdOOQP'#Cc'#CcO#kQ[O'#CfO$_QXO'#CaO$fQ[O'#ChO$qQ[O'#DTO$vQ[O'#DWOOQP'#Em'#EmO${QdO'#DgO%jQ[O'#DtO${QdO'#DvO%{Q[O'#DxO&WQ[O'#D{O&`Q[O'#ERO&nQ[O'#ETOOQS'#El'#ElOOQS'#EW'#EWQYQ[OOO&uQXO'#CdO'jQWO'#DcO'oQWO'#EsO'zQ[O'#EsQOQWOOP(UO#tO'#C_POOO)C@[)C@[OOQP'#Cg'#CgOOQP,59Q,59QO#kQ[O,59QO(aQ[O'#E[O({QWO,58{O)TQ[O,59SO$qQ[O,59oO$vQ[O,59rO(aQ[O,59uO(aQ[O,59wO(aQ[O,59xO)`Q[O'#DbOOQS,58{,58{OOQP'#Ck'#CkOOQO'#DR'#DROOQP,59S,59SO)gQWO,59SO)lQWO,59SOOQP'#DV'#DVOOQP,59o,59oOOQO'#DX'#DXO)qQ`O,59rOOQS'#Cp'#CpO${QdO'#CqO)yQvO'#CsO+ZQtO,5:ROOQO'#Cx'#CxO)lQWO'#CwO+oQWO'#CyO+tQ[O'#DOOOQS'#Ep'#EpOOQO'#Dj'#DjO+|Q[O'#DqO,[QWO'#EtO&`Q[O'#DoO,jQWO'#DrOOQO'#Eu'#EuO)OQWO,5:`O,oQpO,5:bOOQS'#Dz'#DzO,wQWO,5:dO,|Q[O,5:dOOQO'#D}'#D}O-UQWO,5:gO-ZQWO,5:mO-cQWO,5:oOOQS-E8U-E8UO-kQdO,59}O-{Q[O'#E^O.YQWO,5;_O.YQWO,5;_POOO'#EV'#EVP.eO#tO,58yPOOO,58y,58yOOQP1G.l1G.lO/[QXO,5:vOOQO-E8Y-E8YOOQS1G.g1G.gOOQP1G.n1G.nO)gQWO1G.nO)lQWO1G.nOOQP1G/Z1G/ZO/iQ`O1G/^O0SQXO1G/aO0jQXO1G/cO1QQXO1G/dO1hQWO,59|O1mQ[O'#DSO1tQdO'#CoOOQP1G/^1G/^O${QdO1G/^O1{QpO,59]OOQS,59_,59_O${QdO,59aO2TQWO1G/mOOQS,59c,59cO2YQ!bO,59eOOQS'#DP'#DPOOQS'#EY'#EYO2eQ[O,59jOOQS,59j,59jO2mQWO'#DjO2xQWO,5:VO2}QWO,5:]O&`Q[O,5:XO&`Q[O'#E_O3VQWO,5;`O3bQWO,5:ZO(aQ[O,5:^OOQS1G/z1G/zOOQS1G/|1G/|OOQS1G0O1G0OO3sQWO1G0OO3xQdO'#EOOOQS1G0R1G0ROOQS1G0X1G0XOOQS1G0Z1G0ZO4TQtO1G/iOOQO1G/i1G/iOOQO,5:x,5:xO4kQ[O,5:xOOQO-E8[-E8[O4xQWO1G0yPOOO-E8T-E8TPOOO1G.e1G.eOOQP7+$Y7+$YOOQP7+$x7+$xO${QdO7+$xOOQS1G/h1G/hO5TQXO'#ErO5[QWO,59nO5aQtO'#EXO6XQdO'#EoO6cQWO,59ZO6hQpO7+$xOOQS1G.w1G.wOOQS1G.{1G.{OOQS7+%X7+%XOOQS1G/P1G/PO6pQWO1G/POOQS-E8W-E8WOOQS1G/U1G/UO${QdO1G/qOOQO1G/w1G/wOOQO1G/s1G/sO6uQWO,5:yOOQO-E8]-E8]O7TQXO1G/xOOQS7+%j7+%jO7[QYO'#CsOOQO'#EQ'#EQO7gQ`O'#EPOOQO'#EP'#EPO7rQWO'#E`O7zQdO,5:jOOQS,5:j,5:jO8VQtO'#E]O${QdO'#E]O9WQdO7+%TOOQO7+%T7+%TOOQO1G0d1G0dO9kQpO<OAN>OO;]QdO,5:uOOQO-E8X-E8XOOQO<T![;'S%^;'S;=`%o<%lO%^l;TUo`Oy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^l;nYo`#e[Oy%^z!Q%^!Q![;g![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^l[[o`#e[Oy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^n?VSt^Oy%^z;'S%^;'S;=`%o<%lO%^l?hWjWOy%^z!O%^!O!P;O!P!Q%^!Q![>T![;'S%^;'S;=`%o<%lO%^n@VU#bQOy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^~@nTjWOy%^z{@}{;'S%^;'S;=`%o<%lO%^~AUSo`#[~Oy%^z;'S%^;'S;=`%o<%lO%^lAg[#e[Oy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^bBbU]QOy%^z![%^![!]Bt!];'S%^;'S;=`%o<%lO%^bB{S^Qo`Oy%^z;'S%^;'S;=`%o<%lO%^nC^S!Y^Oy%^z;'S%^;'S;=`%o<%lO%^dCoS|SOy%^z;'S%^;'S;=`%o<%lO%^bDQU!OQOy%^z!`%^!`!aDd!a;'S%^;'S;=`%o<%lO%^bDkS!OQo`Oy%^z;'S%^;'S;=`%o<%lO%^bDzWOy%^z!c%^!c!}Ed!}#T%^#T#oEd#o;'S%^;'S;=`%o<%lO%^bEk[![Qo`Oy%^z}%^}!OEd!O!Q%^!Q![Ed![!c%^!c!}Ed!}#T%^#T#oEd#o;'S%^;'S;=`%o<%lO%^nFfSq^Oy%^z;'S%^;'S;=`%o<%lO%^nFwSp^Oy%^z;'S%^;'S;=`%o<%lO%^bGWUOy%^z#b%^#b#cGj#c;'S%^;'S;=`%o<%lO%^bGoUo`Oy%^z#W%^#W#XHR#X;'S%^;'S;=`%o<%lO%^bHYS!bQo`Oy%^z;'S%^;'S;=`%o<%lO%^bHiUOy%^z#f%^#f#gHR#g;'S%^;'S;=`%o<%lO%^fIQS!TUOy%^z;'S%^;'S;=`%o<%lO%^nIcS!S^Oy%^z;'S%^;'S;=`%o<%lO%^fItU!RQOy%^z!_%^!_!`6y!`;'S%^;'S;=`%o<%lO%^`JZP;=`<%l$}",tokenizers:[Pr,mr,$r,1,2,3,4,new lO("m~RRYZ[z{a~~g~aO#^~~dP!P!Qg~lO#_~~",28,105)],topRules:{StyleSheet:[0,4],Styles:[1,86]},specialized:[{term:100,get:e=>Sr[e]||-1},{term:58,get:e=>Zr[e]||-1},{term:101,get:e=>br[e]||-1}],tokenPrec:1219});let PO=null;function mO(){if(!PO&&typeof document=="object"&&document.body){let{style:e}=document.body,O=[],a=new Set;for(let t in e)t!="cssText"&&t!="cssFloat"&&typeof e[t]=="string"&&(/[A-Z]/.test(t)&&(t=t.replace(/[A-Z]/g,r=>"-"+r.toLowerCase())),a.has(t)||(O.push(t),a.add(t)));PO=O.sort().map(t=>({type:"property",label:t}))}return PO||[]}const ue=["active","after","any-link","autofill","backdrop","before","checked","cue","default","defined","disabled","empty","enabled","file-selector-button","first","first-child","first-letter","first-line","first-of-type","focus","focus-visible","focus-within","fullscreen","has","host","host-context","hover","in-range","indeterminate","invalid","is","lang","last-child","last-of-type","left","link","marker","modal","not","nth-child","nth-last-child","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","part","placeholder","placeholder-shown","read-only","read-write","required","right","root","scope","selection","slotted","target","target-text","valid","visited","where"].map(e=>({type:"class",label:e})),pe=["above","absolute","activeborder","additive","activecaption","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","antialiased","appworkspace","asterisks","attr","auto","auto-flow","avoid","avoid-column","avoid-page","avoid-region","axis-pan","background","backwards","baseline","below","bidi-override","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","clear","clip","close-quote","col-resize","collapse","color","color-burn","color-dodge","column","column-reverse","compact","condensed","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","darken","dashed","decimal","decimal-leading-zero","default","default-button","dense","destination-atop","destination-in","destination-out","destination-over","difference","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic-abegede-gez","ethiopic-halehame-aa-er","ethiopic-halehame-gez","ew-resize","exclusion","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fill-box","fixed","flat","flex","flex-end","flex-start","footnotes","forwards","from","geometricPrecision","graytext","grid","groove","hand","hard-light","help","hidden","hide","higher","highlight","highlighttext","horizontal","hsl","hsla","hue","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-grid","inline-table","inset","inside","intrinsic","invert","italic","justify","keep-all","landscape","large","larger","left","level","lighter","lighten","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-hexadecimal","lower-latin","lower-norwegian","lowercase","ltr","luminosity","manipulation","match","matrix","matrix3d","medium","menu","menutext","message-box","middle","min-intrinsic","mix","monospace","move","multiple","multiple_mask_images","multiply","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","opacity","open-quote","optimizeLegibility","optimizeSpeed","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","perspective","pinch-zoom","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row","row-resize","row-reverse","rtl","run-in","running","s-resize","sans-serif","saturation","scale","scale3d","scaleX","scaleY","scaleZ","screen","scroll","scrollbar","scroll-position","se-resize","self-start","self-end","semi-condensed","semi-expanded","separate","serif","show","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","soft-light","solid","source-atop","source-in","source-out","source-over","space","space-around","space-between","space-evenly","spell-out","square","start","static","status-bar","stretch","stroke","stroke-box","sub","subpixel-antialiased","svg_masks","super","sw-resize","symbolic","symbols","system-ui","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","text","text-bottom","text-top","textarea","textfield","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","to","top","transform","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","unidirectional-pan","unset","up","upper-latin","uppercase","url","var","vertical","vertical-text","view-box","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","wrap","wrap-reverse","x-large","x-small","xor","xx-large","xx-small"].map(e=>({type:"keyword",label:e})).concat(["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"].map(e=>({type:"constant",label:e}))),xr=["a","abbr","address","article","aside","b","bdi","bdo","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","dd","del","details","dfn","dialog","div","dl","dt","em","figcaption","figure","footer","form","header","hgroup","h1","h2","h3","h4","h5","h6","hr","html","i","iframe","img","input","ins","kbd","label","legend","li","main","meter","nav","ol","output","p","pre","ruby","section","select","small","source","span","strong","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","tr","u","ul"].map(e=>({type:"type",label:e})),Xr=["@charset","@color-profile","@container","@counter-style","@font-face","@font-feature-values","@font-palette-values","@import","@keyframes","@layer","@media","@namespace","@page","@position-try","@property","@scope","@starting-style","@supports","@view-transition"].map(e=>({type:"keyword",label:e})),w=/^(\w[\w-]*|-\w[\w-]*|)$/,yr=/^-(-[\w-]*)?$/;function wr(e,O){var a;if((e.name=="("||e.type.isError)&&(e=e.parent||e),e.name!="ArgList")return!1;let t=(a=e.parent)===null||a===void 0?void 0:a.firstChild;return(t==null?void 0:t.name)!="Callee"?!1:O.sliceString(t.from,t.to)=="var"}const de=new _e,Rr=["Declaration"];function Yr(e){for(let O=e;;){if(O.type.isTop)return O;if(!(O=O.parent))return e}}function Ot(e,O,a){if(O.to-O.from>4096){let t=de.get(O);if(t)return t;let r=[],s=new Set,i=O.cursor(vO.IncludeAnonymous);if(i.firstChild())do for(let o of Ot(e,i.node,a))s.has(o.label)||(s.add(o.label),r.push(o));while(i.nextSibling());return de.set(O,r),r}else{let t=[],r=new Set;return O.cursor().iterate(s=>{var i;if(a(s)&&s.matchContext(Rr)&&((i=s.node.nextSibling)===null||i===void 0?void 0:i.name)==":"){let o=e.sliceString(s.from,s.to);r.has(o)||(r.add(o),t.push({label:o,type:"variable"}))}}),t}}const Tr=e=>O=>{let{state:a,pos:t}=O,r=G(a).resolveInner(t,-1),s=r.type.isError&&r.from==r.to-1&&a.doc.sliceString(r.from,r.to)=="-";if(r.name=="PropertyName"||(s||r.name=="TagName")&&/^(Block|Styles)$/.test(r.resolve(r.to).name))return{from:r.from,options:mO(),validFor:w};if(r.name=="ValueName")return{from:r.from,options:pe,validFor:w};if(r.name=="PseudoClassName")return{from:r.from,options:ue,validFor:w};if(e(r)||(O.explicit||s)&&wr(r,a.doc))return{from:e(r)||s?r.from:t,options:Ot(a.doc,Yr(r),e),validFor:yr};if(r.name=="TagName"){for(let{parent:n}=r;n;n=n.parent)if(n.name=="Block")return{from:r.from,options:mO(),validFor:w};return{from:r.from,options:xr,validFor:w}}if(r.name=="AtKeyword")return{from:r.from,options:Xr,validFor:w};if(!O.explicit)return null;let i=r.resolve(t),o=i.childBefore(t);return o&&o.name==":"&&i.name=="PseudoClassSelector"?{from:t,options:ue,validFor:w}:o&&o.name==":"&&i.name=="Declaration"||i.name=="ArgList"?{from:t,options:pe,validFor:w}:i.name=="Block"||i.name=="Styles"?{from:t,options:mO(),validFor:w}:null},Wr=Tr(e=>e.name=="VariableName"),QO=J.define({name:"css",parser:kr.configure({props:[L.add({Declaration:v()}),K.add({"Block KeyframeList":UO})]}),languageData:{commentTokens:{block:{open:"/*",close:"*/"}},indentOnInput:/^\s*\}$/,wordChars:"-"}});function _r(){return new F(QO,QO.data.of({autocomplete:Wr}))}const qr=312,fe=1,vr=2,Ur=3,zr=4,Vr=313,jr=315,Gr=316,Cr=5,Ar=6,Er=0,wO=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],et=125,Nr=59,RO=47,Mr=42,Ir=43,Dr=45,Br=60,Jr=44,Lr=63,Kr=46,Fr=new je({start:!1,shift(e,O){return O==Cr||O==Ar||O==jr?e:O==Gr},strict:!1}),Hr=new k((e,O)=>{let{next:a}=e;(a==et||a==-1||O.context)&&e.acceptToken(Vr)},{contextual:!0,fallback:!0}),Oi=new k((e,O)=>{let{next:a}=e,t;wO.indexOf(a)>-1||a==RO&&((t=e.peek(1))==RO||t==Mr)||a!=et&&a!=Nr&&a!=-1&&!O.context&&e.acceptToken(qr)},{contextual:!0}),ei=new k((e,O)=>{let{next:a}=e;if(a==Ir||a==Dr){if(e.advance(),a==e.next){e.advance();let t=!O.context&&O.canShift(fe);e.acceptToken(t?fe:vr)}}else a==Lr&&e.peek(1)==Kr&&(e.advance(),e.advance(),(e.next<48||e.next>57)&&e.acceptToken(Ur))},{contextual:!0});function gO(e,O){return e>=65&&e<=90||e>=97&&e<=122||e==95||e>=192||!O&&e>=48&&e<=57}const ti=new k((e,O)=>{if(e.next!=Br||!O.dialectEnabled(Er)||(e.advance(),e.next==RO))return;let a=0;for(;wO.indexOf(e.next)>-1;)e.advance(),a++;if(gO(e.next,!0)){for(e.advance(),a++;gO(e.next,!1);)e.advance(),a++;for(;wO.indexOf(e.next)>-1;)e.advance(),a++;if(e.next==Jr)return;for(let t=0;;t++){if(t==7){if(!gO(e.next,!0))return;break}if(e.next!="extends".charCodeAt(t))break;e.advance(),a++}}e.acceptToken(zr,-a)}),ai=B({"get set async static":l.modifier,"for while do if else switch try catch finally return throw break continue default case":l.controlKeyword,"in of await yield void typeof delete instanceof":l.operatorKeyword,"let var const using function class extends":l.definitionKeyword,"import export from":l.moduleKeyword,"with debugger as new":l.keyword,TemplateString:l.special(l.string),super:l.atom,BooleanLiteral:l.bool,this:l.self,null:l.null,Star:l.modifier,VariableName:l.variableName,"CallExpression/VariableName TaggedTemplateExpression/VariableName":l.function(l.variableName),VariableDefinition:l.definition(l.variableName),Label:l.labelName,PropertyName:l.propertyName,PrivatePropertyName:l.special(l.propertyName),"CallExpression/MemberExpression/PropertyName":l.function(l.propertyName),"FunctionDeclaration/VariableDefinition":l.function(l.definition(l.variableName)),"ClassDeclaration/VariableDefinition":l.definition(l.className),PropertyDefinition:l.definition(l.propertyName),PrivatePropertyDefinition:l.definition(l.special(l.propertyName)),UpdateOp:l.updateOperator,"LineComment Hashbang":l.lineComment,BlockComment:l.blockComment,Number:l.number,String:l.string,Escape:l.escape,ArithOp:l.arithmeticOperator,LogicOp:l.logicOperator,BitOp:l.bitwiseOperator,CompareOp:l.compareOperator,RegExp:l.regexp,Equals:l.definitionOperator,Arrow:l.function(l.punctuation),": Spread":l.punctuation,"( )":l.paren,"[ ]":l.squareBracket,"{ }":l.brace,"InterpolationStart InterpolationEnd":l.special(l.brace),".":l.derefOperator,", ;":l.separator,"@":l.meta,TypeName:l.typeName,TypeDefinition:l.definition(l.typeName),"type enum interface implements namespace module declare":l.definitionKeyword,"abstract global Privacy readonly override":l.modifier,"is keyof unique infer":l.operatorKeyword,JSXAttributeValue:l.attributeValue,JSXText:l.content,"JSXStartTag JSXStartCloseTag JSXSelfCloseEndTag JSXEndTag":l.angleBracket,"JSXIdentifier JSXNameSpacedName":l.tagName,"JSXAttribute/JSXIdentifier JSXAttribute/JSXNameSpacedName":l.attributeName,"JSXBuiltin/JSXIdentifier":l.standard(l.tagName)}),ri={__proto__:null,export:20,as:25,from:33,default:36,async:41,function:42,extends:54,this:58,true:66,false:66,null:78,void:82,typeof:86,super:102,new:136,delete:148,yield:157,await:161,class:166,public:229,private:229,protected:229,readonly:231,instanceof:250,satisfies:253,in:254,const:256,import:290,keyof:345,unique:349,infer:355,is:391,abstract:411,implements:413,type:415,let:418,var:420,using:423,interface:429,enum:433,namespace:439,module:441,declare:445,global:449,for:468,of:477,while:480,with:484,do:488,if:492,else:494,switch:498,case:504,try:510,catch:514,finally:518,return:522,throw:526,break:530,continue:534,debugger:538},ii={__proto__:null,async:123,get:125,set:127,declare:189,public:191,private:191,protected:191,static:193,abstract:195,override:197,readonly:203,accessor:205,new:395},si={__proto__:null,"<":187},oi=T.deserialize({version:14,states:"$@QO%TQ^OOO%[Q^OOO'_Q`OOP(lOWOOO*zQ?NdO'#CiO+RO!bO'#CjO+aO#tO'#CjO+oO!0LbO'#D^O.QQ^O'#DdO.bQ^O'#DoO%[Q^O'#DwO0fQ^O'#EPOOQ?Mr'#EX'#EXO1PQWO'#EUOOQO'#Em'#EmOOQO'#Ih'#IhO1XQWO'#GpO1dQWO'#ElO1iQWO'#ElO3hQ?NdO'#JmO6[Q?NdO'#JnO6uQWO'#F[O6zQ&jO'#FsOOQ?Mr'#Fe'#FeO7VO,YO'#FeO7eQ7[O'#FzO9RQWO'#FyOOQ?Mr'#Jn'#JnOOQ?Mp'#Jm'#JmO9WQWO'#GtOOQU'#KZ'#KZO9cQWO'#IUO9hQ?MxO'#IVOOQU'#JZ'#JZOOQU'#IZ'#IZQ`Q^OOO`Q^OOO9pQMnO'#DsO9wQ^O'#D{O:OQ^O'#D}O9^QWO'#GpO:VQ7[O'#CoO:eQWO'#EkO:pQWO'#EvO:uQ7[O'#FdO;dQWO'#GpOOQO'#K['#K[O;iQWO'#K[O;wQWO'#GxO;wQWO'#GyO;wQWO'#G{O9^QWO'#HOOVQWO'#CeO>gQWO'#H_O>oQWO'#HeO>oQWO'#HgO`Q^O'#HiO>oQWO'#HkO>oQWO'#HnO>tQWO'#HtO>yQ?MyO'#HzO%[Q^O'#H|O?UQ?MyO'#IOO?aQ?MyO'#IQO9hQ?MxO'#ISO?lQ?NdO'#CiO@nQ`O'#DiQOQWOOO%[Q^O'#D}OAUQWO'#EQO:VQ7[O'#EkOAaQWO'#EkOAlQpO'#FdOOQU'#Cg'#CgOOQ?Mp'#Dn'#DnOOQ?Mp'#Jq'#JqO%[Q^O'#JqOOQO'#Jt'#JtOOQO'#Id'#IdOBlQ`O'#EdOOQ?Mp'#Ec'#EcOOQ?Mp'#Jx'#JxOChQ?NQO'#EdOCrQ`O'#ETOOQO'#Js'#JsODWQ`O'#JtOEeQ`O'#ETOCrQ`O'#EdPErO#@ItO'#CbPOOO)CDx)CDxOOOO'#I['#I[OE}O!bO,59UOOQ?Mr,59U,59UOOOO'#I]'#I]OF]O#tO,59UO%[Q^O'#D`OOOO'#I_'#I_OFkO!0LbO,59xOOQ?Mr,59x,59xOFyQ^O'#I`OG^QWO'#JoOI]QrO'#JoO+}Q^O'#JoOIdQWO,5:OOIzQWO'#EmOJXQWO'#KOOJdQWO'#J}OJdQWO'#J}OJlQWO,5;ZOJqQWO'#J|OOQ?Mv,5:Z,5:ZOJxQ^O,5:ZOLvQ?NdO,5:cOMgQWO,5:kONQQ?MxO'#J{ONXQWO'#JzO9WQWO'#JzONmQWO'#JzONuQWO,5;YONzQWO'#JzO!#PQrO'#JnOOQ?Mr'#Ci'#CiO%[Q^O'#EPO!#oQrO,5:pOOQQ'#Ju'#JuOOQO-EpOOQU'#Jc'#JcOOQU,5>q,5>qOOQU-EtQWO'#HTO9^QWO'#HVO!DgQWO'#HVO:VQ7[O'#HXO!DlQWO'#HXOOQU,5=m,5=mO!DqQWO'#HYO!ESQWO'#CoO!EXQWO,59PO!EcQWO,59PO!GhQ^O,59POOQU,59P,59PO!GxQ?MxO,59PO%[Q^O,59PO!JTQ^O'#HaOOQU'#Hb'#HbOOQU'#Hc'#HcO`Q^O,5=yO!JkQWO,5=yO`Q^O,5>PO`Q^O,5>RO!JpQWO,5>TO`Q^O,5>VO!JuQWO,5>YO!JzQ^O,5>`OOQU,5>f,5>fO%[Q^O,5>fO9hQ?MxO,5>hOOQU,5>j,5>jO# UQWO,5>jOOQU,5>l,5>lO# UQWO,5>lOOQU,5>n,5>nO# rQ`O'#D[O%[Q^O'#JqO# |Q`O'#JqO#!kQ`O'#DjO#!|Q`O'#DjO#%_Q^O'#DjO#%fQWO'#JpO#%nQWO,5:TO#%sQWO'#EqO#&RQWO'#KPO#&ZQWO,5;[O#&`Q`O'#DjO#&mQ`O'#ESOOQ?Mr,5:l,5:lO%[Q^O,5:lO#&tQWO,5:lO>tQWO,5;VO!A}Q`O,5;VO!BVQ7[O,5;VO:VQ7[O,5;VO#&|QWO,5@]O#'RQ(CYO,5:pOOQO-EzO+}Q^O,5>zOOQO,5?Q,5?QO#*ZQ^O'#I`OOQO-E<^-E<^O#*hQWO,5@ZO#*pQrO,5@ZO#*wQWO,5@iOOQ?Mr1G/j1G/jO%[Q^O,5@jO#+PQWO'#IfOOQO-EuQ?NdO1G0|O#>|Q?NdO1G0|O#AZQ07bO'#CiO#CUQ07bO1G1_O#C]Q07bO'#JnO#CpQ?NdO,5?WOOQ?Mp-EoQWO1G3oO$3VQ^O1G3qO$7ZQ^O'#HpOOQU1G3t1G3tO$7hQWO'#HvO>tQWO'#HxOOQU1G3z1G3zO$7pQ^O1G3zO9hQ?MxO1G4QOOQU1G4S1G4SOOQ?Mp'#G]'#G]O9hQ?MxO1G4UO9hQ?MxO1G4WO$;wQWO,5@]O!(oQ^O,5;]O9WQWO,5;]O>tQWO,5:UO!(oQ^O,5:UO!A}Q`O,5:UO$;|Q07bO,5:UOOQO,5;],5;]O$tQWO1G0qO!A}Q`O1G0qO!BVQ7[O1G0qOOQ?Mp1G5w1G5wO!ArQ?MxO1G0ZOOQO1G0j1G0jO%[Q^O1G0jO$=aQ?MxO1G0jO$=lQ?MxO1G0jO!A}Q`O1G0ZOCrQ`O1G0ZO$=zQ?MxO1G0jOOQO1G0Z1G0ZO$>`Q?NdO1G0jPOOO-EjQpO,5rQrO1G4fOOQO1G4l1G4lO%[Q^O,5>zO$>|QWO1G5uO$?UQWO1G6TO$?^QrO1G6UO9WQWO,5?QO$?hQ?NdO1G6RO%[Q^O1G6RO$?xQ?MxO1G6RO$@ZQWO1G6QO$@ZQWO1G6QO9WQWO1G6QO$@cQWO,5?TO9WQWO,5?TOOQO,5?T,5?TO$@wQWO,5?TO$(PQWO,5?TOOQO-E[OOQU,5>[,5>[O%[Q^O'#HqO%8mQWO'#HsOOQU,5>b,5>bO9WQWO,5>bOOQU,5>d,5>dOOQU7+)f7+)fOOQU7+)l7+)lOOQU7+)p7+)pOOQU7+)r7+)rO%8rQ`O1G5wO%9WQ07bO1G0wO%9bQWO1G0wOOQO1G/p1G/pO%9mQ07bO1G/pO>tQWO1G/pO!(oQ^O'#DjOOQO,5>{,5>{OOQO-E<_-E<_OOQO,5?R,5?ROOQO-EtQWO7+&]O!A}Q`O7+&]OOQO7+%u7+%uO$>`Q?NdO7+&UOOQO7+&U7+&UO%[Q^O7+&UO%9wQ?MxO7+&UO!ArQ?MxO7+%uO!A}Q`O7+%uO%:SQ?MxO7+&UO%:bQ?NdO7++mO%[Q^O7++mO%:rQWO7++lO%:rQWO7++lOOQO1G4o1G4oO9WQWO1G4oO%:zQWO1G4oOOQQ7+%z7+%zO#&wQWO<|O%[Q^O,5>|OOQO-E<`-E<`O%FwQWO1G5xOOQ?Mr<]OOQU,5>_,5>_O&8uQWO1G3|O9WQWO7+&cO!(oQ^O7+&cOOQO7+%[7+%[O&8zQ07bO1G6UO>tQWO7+%[OOQ?Mr<tQWO<`Q?NdO<pQ?NdO,5?_O&@xQ?NdO7+'zO&CWQrO1G4hO&CbQ07bO7+&^O&EcQ07bO,5=UO&GgQ07bO,5=WO&GwQ07bO,5=UO&HXQ07bO,5=WO&HiQ07bO,59rO&JlQ07bO,5tQWO7+)hO'(OQWO<`Q?NdOAN?[OOQOAN>{AN>{O%[Q^OAN?[OOQO<`Q?NdOG24vO#&wQWOLD,nOOQULD,nLD,nO!&_Q7[OLD,nO'5TQrOLD,nO'5[Q07bO7+'xO'6}Q07bO,5?]O'8}Q07bO,5?_O':}Q07bO7+'zO'kOh%VOk+aO![']O%f+`O~O!d+cOa(WX![(WX'u(WX!Y(WX~Oa%lO![XO'u%lO~Oh%VO!i%cO~Oh%VO!i%cO(O%eO~O!d#vO#h(tO~Ob+nO%g+oO(O+kO(QTO(TUO!Z)TP~O!Y+pO`)SX~O[+tO~O`+uO~O![%}O(O%eO(P!lO`)SP~Oh%VO#]+zO~Oh%VOk+}O![$|O~O![,PO~O},RO![XO~O%k%tO~O!u,WO~Oe,]O~Ob,^O(O#nO(QTO(TUO!Z)RP~Oe%{O~O%g!QO(O&WO~P=RO[,cO`,bO~OPYOQYOSfOdzOeyOmkOoYOpkOqkOwkOyYO{YO!PWO!TkO!UkO!fuO!iZO!lYO!mYO!nYO!pvO!uxO!y]O%e}O(QTO(TUO([VO(j[O(yiO~O![!eO!r!gO$V!kO(O!dO~P!EkO`,bOa%lO'u%lO~OPYOQYOSfOd!jOe!iOmkOoYOpkOqkOwkOyYO{YO!PWO!TkO!UkO![!eO!fuO!iZO!lYO!mYO!nYO!pvO!u!hO$V!kO(O!dO(QTO(TUO([VO(j[O(yiO~Oa,hO!rwO#t!OO%i!OO%j!OO%k!OO~P!HTO!i&lO~O&Y,nO~O![,pO~O&k,rO&m,sOP&haQ&haS&haY&haa&had&hae&ham&hao&hap&haq&haw&hay&ha{&ha!P&ha!T&ha!U&ha![&ha!f&ha!i&ha!l&ha!m&ha!n&ha!p&ha!r&ha!u&ha!y&ha#t&ha$V&ha%e&ha%g&ha%i&ha%j&ha%k&ha%n&ha%p&ha%s&ha%t&ha%v&ha&S&ha&Y&ha&[&ha&^&ha&`&ha&c&ha&i&ha&o&ha&q&ha&s&ha&u&ha&w&ha's&ha(O&ha(Q&ha(T&ha([&ha(j&ha(y&ha!Z&ha&a&hab&ha&f&ha~O(O,xO~Oh!bX!Y!OX!Z!OX!d!OX!d!bX!i!bX#]!OX~O!Y!bX!Z!bX~P# ZO!d,}O#],|Oh(eX!Y#eX!Y(eX!Z#eX!Z(eX!d(eX!i(eX~Oh%VO!d-PO!i%cO!Y!^X!Z!^X~Op!nO!P!oO(QTO(TUO(`!mO~OP;POQ;POSfOdkOg'XX!Y'XX~P!+hO!Y.wOg(ka~OSfO![3uO$c3vO~O!Z3zO~Os3{O~P#.aOa$lq!Y$lq'u$lq's$lq!V$lq!h$lqs$lq![$lq%f$lq!d$lq~P!9mO!V3|O~P#.aO})zO!P){O(u%POk'ea(t'ea!Y'ea#]'ea~Og'ea#}'ea~P%)nO})zO!P){Ok'ga(t'ga(u'ga!Y'ga#]'ga~Og'ga#}'ga~P%*aO(m$YO~P#.aO!VfX!V$xX!YfX!Y$xX!d%PX#]fX~P!/gO(OQ#>g#@V#@e#@l#BR#Ba#C|#D[#Db#Dh#Dn#Dx#EO#EU#E`#Er#ExPPPPPPPPPP#FOPPPPPPP#Fs#Iz#KZ#Kb#KjPPP$!sP$!|$%t$,^$,a$,d$-P$-S$-Z$-cP$-i$-lP$.Y$.^$/U$0d$0i$1PPP$1U$1[$1`P$1c$1g$1k$2a$2x$3a$3e$3h$3k$3q$3t$3x$3|R!|RoqOXst!Z#d%k&o&q&r&t,k,p1|2PY!vQ']-]1a5eQ%rvQ%zyQ&R|Q&g!VS'T!e-TQ'c!iS'i!r!yU*e$|*V*jQ+i%{Q+v&TQ,[&aQ-Z'[Q-e'dQ-m'jQ0R*lQ1k,]R;v;T%QdOPWXYZstuvw!Z!`!g!o#S#W#Z#d#o#u#x#{$O$P$Q$R$S$T$U$V$W$X$_$a$e%k%r&P&h&k&o&q&r&t&x'Q'_'o(P(R(X(`(t(v(z)y+R+V,h,k,p-a-i-w-}.l.s/f0a0g0v1d1t1u1w1y1|2P2R2r2x3^5b5m5}6O6R6f8R8X8h8rS#q];Q!r)Z$Z$n'U)o,|-P.}2b3u5`6]9h9y;P;S;T;W;X;Y;Z;[;];^;_;`;a;b;c;d;f;i;v;x;y;{ < TypeParamList TypeDefinition extends ThisType this LiteralType ArithOp Number BooleanLiteral TemplateType InterpolationEnd Interpolation InterpolationStart NullType null VoidType void TypeofType typeof MemberExpression . PropertyName [ TemplateString Escape Interpolation super RegExp ] ArrayExpression Spread , } { ObjectExpression Property async get set PropertyDefinition Block : NewTarget new NewExpression ) ( ArgList UnaryExpression delete LogicOp BitOp YieldExpression yield AwaitExpression await ParenthesizedExpression ClassExpression class ClassBody MethodDeclaration Decorator @ MemberExpression PrivatePropertyName CallExpression TypeArgList CompareOp < declare Privacy static abstract override PrivatePropertyDefinition PropertyDeclaration readonly accessor Optional TypeAnnotation Equals StaticBlock FunctionExpression ArrowFunction ParamList ParamList ArrayPattern ObjectPattern PatternProperty Privacy readonly Arrow MemberExpression BinaryExpression ArithOp ArithOp ArithOp ArithOp BitOp CompareOp instanceof satisfies in const CompareOp BitOp BitOp BitOp LogicOp LogicOp ConditionalExpression LogicOp LogicOp AssignmentExpression UpdateOp PostfixExpression CallExpression InstantiationExpression TaggedTemplateExpression DynamicImport import ImportMeta JSXElement JSXSelfCloseEndTag JSXSelfClosingTag JSXIdentifier JSXBuiltin JSXIdentifier JSXNamespacedName JSXMemberExpression JSXSpreadAttribute JSXAttribute JSXAttributeValue JSXEscape JSXEndTag JSXOpenTag JSXFragmentTag JSXText JSXEscape JSXStartCloseTag JSXCloseTag PrefixCast ArrowFunction TypeParamList SequenceExpression InstantiationExpression KeyofType keyof UniqueType unique ImportType InferredType infer TypeName ParenthesizedType FunctionSignature ParamList NewSignature IndexedType TupleType Label ArrayType ReadonlyType ObjectType MethodType PropertyType IndexSignature PropertyDefinition CallSignature TypePredicate is NewSignature new UnionType LogicOp IntersectionType LogicOp ConditionalType ParameterizedType ClassDeclaration abstract implements type VariableDeclaration let var using TypeAliasDeclaration InterfaceDeclaration interface EnumDeclaration enum EnumBody NamespaceDeclaration namespace module AmbientDeclaration declare GlobalDeclaration global ClassDeclaration ClassBody AmbientFunctionDeclaration ExportGroup VariableName VariableName ImportDeclaration ImportGroup ForStatement for ForSpec ForInSpec ForOfSpec of WhileStatement while WithStatement with DoStatement do IfStatement if else SwitchStatement switch SwitchBody CaseLabel case DefaultLabel TryStatement try CatchClause catch FinallyClause finally ReturnStatement return ThrowStatement throw BreakStatement break ContinueStatement continue DebuggerStatement debugger LabeledStatement ExpressionStatement SingleExpression SingleClassItem",maxTerm:376,context:Fr,nodeProps:[["isolate",-8,5,6,14,34,36,48,50,52,""],["group",-26,9,17,19,65,204,208,212,213,215,218,221,231,233,239,241,243,245,248,254,260,262,264,266,268,270,271,"Statement",-34,13,14,29,32,33,39,48,51,52,54,59,67,69,73,77,79,81,82,107,108,117,118,135,138,140,141,142,143,144,146,147,166,167,169,"Expression",-23,28,30,34,38,40,42,171,173,175,176,178,179,180,182,183,184,186,187,188,198,200,202,203,"Type",-3,85,100,106,"ClassItem"],["openedBy",23,"<",35,"InterpolationStart",53,"[",57,"{",70,"(",159,"JSXStartCloseTag"],["closedBy",24,">",37,"InterpolationEnd",47,"]",58,"}",71,")",164,"JSXEndTag"]],propSources:[ai],skippedNodes:[0,5,6,274],repeatNodeCount:37,tokenData:"$Fq07[R!bOX%ZXY+gYZ-yZ[+g[]%Z]^.c^p%Zpq+gqr/mrs3cst:_tuEruvJSvwLkwx! Yxy!'iyz!(sz{!)}{|!,q|}!.O}!O!,q!O!P!/Y!P!Q!9j!Q!R#:O!R![#<_![!]#I_!]!^#Jk!^!_#Ku!_!`$![!`!a$$v!a!b$*T!b!c$,r!c!}Er!}#O$-|#O#P$/W#P#Q$4o#Q#R$5y#R#SEr#S#T$7W#T#o$8b#o#p$x#r#s$@U#s$f%Z$f$g+g$g#BYEr#BY#BZ$A`#BZ$ISEr$IS$I_$A`$I_$I|Er$I|$I}$Dk$I}$JO$Dk$JO$JTEr$JT$JU$A`$JU$KVEr$KV$KW$A`$KW&FUEr&FU&FV$A`&FV;'SEr;'S;=`I|<%l?HTEr?HT?HU$A`?HUOEr(n%d_$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z&j&hT$h&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c&j&zP;=`<%l&c'|'U]$h&j(U!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!b(SU(U!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!b(iP;=`<%l'}'|(oP;=`<%l&}'[(y]$h&j(RpOY(rYZ&cZr(rrs&cs!^(r!^!_)r!_#O(r#O#P&c#P#o(r#o#p)r#p;'S(r;'S;=`*a<%lO(rp)wU(RpOY)rZr)rs#O)r#P;'S)r;'S;=`*Z<%lO)rp*^P;=`<%l)r'[*dP;=`<%l(r#S*nX(Rp(U!bOY*gZr*grs'}sw*gwx)rx#O*g#P;'S*g;'S;=`+Z<%lO*g#S+^P;=`<%l*g(n+dP;=`<%l%Z07[+rq$h&j(Rp(U!b'w0/lOX%ZXY+gYZ&cZ[+g[p%Zpq+gqr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p$f%Z$f$g+g$g#BY%Z#BY#BZ+g#BZ$IS%Z$IS$I_+g$I_$JT%Z$JT$JU+g$JU$KV%Z$KV$KW+g$KW&FU%Z&FU&FV+g&FV;'S%Z;'S;=`+a<%l?HT%Z?HT?HU+g?HUO%Z07[.ST(S#S$h&j'x0/lO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c07[.n_$h&j(Rp(U!b'x0/lOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z)3p/x`$h&j!m),Q(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`0z!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z(KW1V`#u(Ch$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`2X!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z(KW2d_#u(Ch$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'At3l_(Q':f$h&j(U!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k(^4r_$h&j(U!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k&z5vX$h&jOr5qrs6cs!^5q!^!_6y!_#o5q#o#p6y#p;'S5q;'S;=`7h<%lO5q&z6jT$c`$h&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c`6|TOr6yrs7]s;'S6y;'S;=`7b<%lO6y`7bO$c``7eP;=`<%l6y&z7kP;=`<%l5q(^7w]$c`$h&j(U!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!r8uZ(U!bOY8pYZ6yZr8prs9hsw8pwx6yx#O8p#O#P6y#P;'S8p;'S;=`:R<%lO8p!r9oU$c`(U!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!r:UP;=`<%l8p(^:[P;=`<%l4k%9[:hh$h&j(Rp(U!bOY%ZYZ&cZq%Zqr`#P#o`x!^=^!^!_?q!_#O=^#O#P>`#P#o=^#o#p?q#p;'S=^;'S;=`@h<%lO=^&n>gXWS$h&jOY>`YZ&cZ!^>`!^!_?S!_#o>`#o#p?S#p;'S>`;'S;=`?k<%lO>`S?XSWSOY?SZ;'S?S;'S;=`?e<%lO?SS?hP;=`<%l?S&n?nP;=`<%l>`!f?xWWS(U!bOY?qZw?qwx?Sx#O?q#O#P?S#P;'S?q;'S;=`@b<%lO?q!f@eP;=`<%l?q(Q@kP;=`<%l=^'`@w]WS$h&j(RpOY@nYZ&cZr@nrs>`s!^@n!^!_Ap!_#O@n#O#P>`#P#o@n#o#pAp#p;'S@n;'S;=`Bg<%lO@ntAwWWS(RpOYApZrAprs?Ss#OAp#O#P?S#P;'SAp;'S;=`Ba<%lOAptBdP;=`<%lAp'`BjP;=`<%l@n#WBvYWS(Rp(U!bOYBmZrBmrs?qswBmwxApx#OBm#O#P?S#P;'SBm;'S;=`Cf<%lOBm#WCiP;=`<%lBm(rCoP;=`<%l^!Q^$h&j!U7`OY!=yYZ&cZ!P!=y!P!Q!>|!Q!^!=y!^!_!@c!_!}!=y!}#O!CW#O#P!Dy#P#o!=y#o#p!@c#p;'S!=y;'S;=`!Ek<%lO!=y|#X#Z&c#Z#[!>|#[#]&c#]#^!>|#^#a&c#a#b!>|#b#g&c#g#h!>|#h#i&c#i#j!>|#j#k!>|#k#m&c#m#n!>|#n#o&c#p;'S&c;'S;=`&w<%lO&c7`!@hX!U7`OY!@cZ!P!@c!P!Q!AT!Q!}!@c!}#O!Ar#O#P!Bq#P;'S!@c;'S;=`!CQ<%lO!@c7`!AYW!U7`#W#X!AT#Z#[!AT#]#^!AT#a#b!AT#g#h!AT#i#j!AT#j#k!AT#m#n!AT7`!AuVOY!ArZ#O!Ar#O#P!B[#P#Q!@c#Q;'S!Ar;'S;=`!Bk<%lO!Ar7`!B_SOY!ArZ;'S!Ar;'S;=`!Bk<%lO!Ar7`!BnP;=`<%l!Ar7`!BtSOY!@cZ;'S!@c;'S;=`!CQ<%lO!@c7`!CTP;=`<%l!@c^!Ezl$h&j(U!b!U7`OY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#W&}#W#X!Eq#X#Z&}#Z#[!Eq#[#]&}#]#^!Eq#^#a&}#a#b!Eq#b#g&}#g#h!Eq#h#i&}#i#j!Eq#j#k!Eq#k#m&}#m#n!Eq#n#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}8r!GyZ(U!b!U7`OY!GrZw!Grwx!@cx!P!Gr!P!Q!Hl!Q!}!Gr!}#O!JU#O#P!Bq#P;'S!Gr;'S;=`!J|<%lO!Gr8r!Hse(U!b!U7`OY'}Zw'}x#O'}#P#W'}#W#X!Hl#X#Z'}#Z#[!Hl#[#]'}#]#^!Hl#^#a'}#a#b!Hl#b#g'}#g#h!Hl#h#i'}#i#j!Hl#j#k!Hl#k#m'}#m#n!Hl#n;'S'};'S;=`(f<%lO'}8r!JZX(U!bOY!JUZw!JUwx!Arx#O!JU#O#P!B[#P#Q!Gr#Q;'S!JU;'S;=`!Jv<%lO!JU8r!JyP;=`<%l!JU8r!KPP;=`<%l!Gr>^!KZ^$h&j(U!bOY!KSYZ&cZw!KSwx!CWx!^!KS!^!_!JU!_#O!KS#O#P!DR#P#Q!^!LYP;=`<%l!KS>^!L`P;=`<%l!_#c#d#Bq#d#l%Z#l#m#Es#m#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#>j_$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#?rd$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#AQ!R!S#AQ!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#AQ#S#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#A]f$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#AQ!R!S#AQ!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#AQ#S#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Bzc$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!Y#DV!Y!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#DV#S#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Dbe$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!Y#DV!Y!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#DV#S#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#E|g$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q![#Ge![!^%Z!^!_*g!_!c%Z!c!i#Ge!i#O%Z#O#P&c#P#R%Z#R#S#Ge#S#T%Z#T#Z#Ge#Z#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Gpi$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q![#Ge![!^%Z!^!_*g!_!c%Z!c!i#Ge!i#O%Z#O#P&c#P#R%Z#R#S#Ge#S#T%Z#T#Z#Ge#Z#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z*)x#Il_!d$b$h&j#})Lv(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z)[#Jv_al$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z04f#LS^h#)`#O-ri[e]||-1},{term:338,get:e=>ii[e]||-1},{term:92,get:e=>si[e]||-1}],tokenPrec:14749}),tt=[S("function ${name}(${params}) {\n ${}\n}",{label:"function",detail:"definition",type:"keyword"}),S("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n ${}\n}",{label:"for",detail:"loop",type:"keyword"}),S("for (let ${name} of ${collection}) {\n ${}\n}",{label:"for",detail:"of loop",type:"keyword"}),S("do {\n ${}\n} while (${})",{label:"do",detail:"loop",type:"keyword"}),S("while (${}) {\n ${}\n}",{label:"while",detail:"loop",type:"keyword"}),S(`try { + \${} +} catch (\${error}) { + \${} +}`,{label:"try",detail:"/ catch block",type:"keyword"}),S("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),S(`if (\${}) { + \${} +} else { + \${} +}`,{label:"if",detail:"/ else block",type:"keyword"}),S(`class \${name} { + constructor(\${params}) { + \${} + } +}`,{label:"class",detail:"definition",type:"keyword"}),S('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),S('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],li=tt.concat([S("interface ${name} {\n ${}\n}",{label:"interface",detail:"definition",type:"keyword"}),S("type ${name} = ${type}",{label:"type",detail:"definition",type:"keyword"}),S("enum ${name} {\n ${}\n}",{label:"enum",detail:"definition",type:"keyword"})]),$e=new _e,at=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function A(e){return(O,a)=>{let t=O.node.getChild("VariableDefinition");return t&&a(t,e),!0}}const ni=["FunctionDeclaration"],ci={FunctionDeclaration:A("function"),ClassDeclaration:A("class"),ClassExpression:()=>!0,EnumDeclaration:A("constant"),TypeAliasDeclaration:A("type"),NamespaceDeclaration:A("namespace"),VariableDefinition(e,O){e.matchContext(ni)||O(e,"variable")},TypeDefinition(e,O){O(e,"type")},__proto__:null};function rt(e,O){let a=$e.get(O);if(a)return a;let t=[],r=!0;function s(i,o){let n=e.sliceString(i.from,i.to);t.push({label:n,type:o})}return O.cursor(vO.IncludeAnonymous).iterate(i=>{if(r)r=!1;else if(i.name){let o=ci[i.name];if(o&&o(i,s)||at.has(i.name))return!1}else if(i.to-i.from>8192){for(let o of rt(e,i.node))t.push(o);return!1}}),$e.set(O,t),t}const Pe=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,it=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName",".","?."];function Qi(e){let O=G(e.state).resolveInner(e.pos,-1);if(it.indexOf(O.name)>-1)return null;let a=O.name=="VariableName"||O.to-O.from<20&&Pe.test(e.state.sliceDoc(O.from,O.to));if(!a&&!e.explicit)return null;let t=[];for(let r=O;r;r=r.parent)at.has(r.name)&&(t=t.concat(rt(e.state.doc,r)));return{options:t,from:a?O.from:e.pos,validFor:Pe}}const y=J.define({name:"javascript",parser:oi.configure({props:[L.add({IfStatement:v({except:/^\s*({|else\b)/}),TryStatement:v({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:It,SwitchBody:e=>{let O=e.textAfter,a=/^\s*\}/.test(O),t=/^\s*(case|default)\b/.test(O);return e.baseIndent+(a?0:t?1:2)*e.unit},Block:Dt({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":v({except:/^{/}),JSXElement(e){let O=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},JSXEscape(e){let O=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),K.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":UO,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),st={test:e=>/^JSX/.test(e.name),facet:Bt({commentTokens:{block:{open:"{/*",close:"*/}"}}})},ot=y.configure({dialect:"ts"},"typescript"),lt=y.configure({dialect:"jsx",props:[qe.add(e=>e.isTop?[st]:void 0)]}),nt=y.configure({dialect:"jsx ts",props:[qe.add(e=>e.isTop?[st]:void 0)]},"typescript");let ct=e=>({label:e,type:"keyword"});const Qt="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(ct),hi=Qt.concat(["declare","implements","private","protected","public"].map(ct));function ht(e={}){let O=e.jsx?e.typescript?nt:lt:e.typescript?ot:y,a=e.typescript?li.concat(hi):tt.concat(Qt);return new F(O,[y.data.of({autocomplete:ve(it,Ue(a))}),y.data.of({autocomplete:Qi}),e.jsx?di:[]])}function ui(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(e.name=="JSXEscape"||!e.parent)return null;e=e.parent}}function me(e,O,a=e.length){for(let t=O==null?void 0:O.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return e.sliceString(t.from,Math.min(t.to,a));return""}const pi=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),di=q.inputHandler.of((e,O,a,t,r)=>{if((pi?e.composing:e.compositionStarted)||e.state.readOnly||O!=a||t!=">"&&t!="/"||!y.isActiveAt(e.state,O,-1))return!1;let s=r(),{state:i}=s,o=i.changeByRange(n=>{var Q;let{head:u}=n,c=G(i).resolveInner(u-1,-1),f;if(c.name=="JSXStartTag"&&(c=c.parent),!(i.doc.sliceString(u-1,u)!=t||c.name=="JSXAttributeValue"&&c.to>u)){if(t==">"&&c.name=="JSXFragmentTag")return{range:n,changes:{from:u,insert:""}};if(t=="/"&&c.name=="JSXStartCloseTag"){let h=c.parent,d=h.parent;if(d&&h.from==u-2&&((f=me(i.doc,d.firstChild,u))||((Q=d.firstChild)===null||Q===void 0?void 0:Q.name)=="JSXFragmentTag")){let P=`${f}>`;return{range:ze.cursor(u+P.length,-1),changes:{from:u,insert:P}}}}else if(t==">"){let h=ui(c);if(h&&h.name=="JSXOpenTag"&&!/^\/?>|^<\//.test(i.doc.sliceString(u,u+2))&&(f=me(i.doc,h,u)))return{range:n,changes:{from:u,insert:``}}}}return{range:n}});return o.changes.empty?!1:(e.dispatch([s,i.update(o,{userEvent:"input.complete",scrollIntoView:!0})]),!0)}),E=["_blank","_self","_top","_parent"],SO=["ascii","utf-8","utf-16","latin1","latin1"],ZO=["get","post","put","delete"],bO=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],b=["true","false"],p={},fi={a:{attrs:{href:null,ping:null,type:null,media:null,target:E,hreflang:null}},abbr:p,address:p,area:{attrs:{alt:null,coords:null,href:null,target:null,ping:null,media:null,hreflang:null,type:null,shape:["default","rect","circle","poly"]}},article:p,aside:p,audio:{attrs:{src:null,mediagroup:null,crossorigin:["anonymous","use-credentials"],preload:["none","metadata","auto"],autoplay:["autoplay"],loop:["loop"],controls:["controls"]}},b:p,base:{attrs:{href:null,target:E}},bdi:p,bdo:p,blockquote:{attrs:{cite:null}},body:p,br:p,button:{attrs:{form:null,formaction:null,name:null,value:null,autofocus:["autofocus"],disabled:["autofocus"],formenctype:bO,formmethod:ZO,formnovalidate:["novalidate"],formtarget:E,type:["submit","reset","button"]}},canvas:{attrs:{width:null,height:null}},caption:p,center:p,cite:p,code:p,col:{attrs:{span:null}},colgroup:{attrs:{span:null}},command:{attrs:{type:["command","checkbox","radio"],label:null,icon:null,radiogroup:null,command:null,title:null,disabled:["disabled"],checked:["checked"]}},data:{attrs:{value:null}},datagrid:{attrs:{disabled:["disabled"],multiple:["multiple"]}},datalist:{attrs:{data:null}},dd:p,del:{attrs:{cite:null,datetime:null}},details:{attrs:{open:["open"]}},dfn:p,div:p,dl:p,dt:p,em:p,embed:{attrs:{src:null,type:null,width:null,height:null}},eventsource:{attrs:{src:null}},fieldset:{attrs:{disabled:["disabled"],form:null,name:null}},figcaption:p,figure:p,footer:p,form:{attrs:{action:null,name:null,"accept-charset":SO,autocomplete:["on","off"],enctype:bO,method:ZO,novalidate:["novalidate"],target:E}},h1:p,h2:p,h3:p,h4:p,h5:p,h6:p,head:{children:["title","base","link","style","meta","script","noscript","command"]},header:p,hgroup:p,hr:p,html:{attrs:{manifest:null}},i:p,iframe:{attrs:{src:null,srcdoc:null,name:null,width:null,height:null,sandbox:["allow-top-navigation","allow-same-origin","allow-forms","allow-scripts"],seamless:["seamless"]}},img:{attrs:{alt:null,src:null,ismap:null,usemap:null,width:null,height:null,crossorigin:["anonymous","use-credentials"]}},input:{attrs:{alt:null,dirname:null,form:null,formaction:null,height:null,list:null,max:null,maxlength:null,min:null,name:null,pattern:null,placeholder:null,size:null,src:null,step:null,value:null,width:null,accept:["audio/*","video/*","image/*"],autocomplete:["on","off"],autofocus:["autofocus"],checked:["checked"],disabled:["disabled"],formenctype:bO,formmethod:ZO,formnovalidate:["novalidate"],formtarget:E,multiple:["multiple"],readonly:["readonly"],required:["required"],type:["hidden","text","search","tel","url","email","password","datetime","date","month","week","time","datetime-local","number","range","color","checkbox","radio","file","submit","image","reset","button"]}},ins:{attrs:{cite:null,datetime:null}},kbd:p,keygen:{attrs:{challenge:null,form:null,name:null,autofocus:["autofocus"],disabled:["disabled"],keytype:["RSA"]}},label:{attrs:{for:null,form:null}},legend:p,li:{attrs:{value:null}},link:{attrs:{href:null,type:null,hreflang:null,media:null,sizes:["all","16x16","16x16 32x32","16x16 32x32 64x64"]}},map:{attrs:{name:null}},mark:p,menu:{attrs:{label:null,type:["list","context","toolbar"]}},meta:{attrs:{content:null,charset:SO,name:["viewport","application-name","author","description","generator","keywords"],"http-equiv":["content-language","content-type","default-style","refresh"]}},meter:{attrs:{value:null,min:null,low:null,high:null,max:null,optimum:null}},nav:p,noscript:p,object:{attrs:{data:null,type:null,name:null,usemap:null,form:null,width:null,height:null,typemustmatch:["typemustmatch"]}},ol:{attrs:{reversed:["reversed"],start:null,type:["1","a","A","i","I"]},children:["li","script","template","ul","ol"]},optgroup:{attrs:{disabled:["disabled"],label:null}},option:{attrs:{disabled:["disabled"],label:null,selected:["selected"],value:null}},output:{attrs:{for:null,form:null,name:null}},p,param:{attrs:{name:null,value:null}},pre:p,progress:{attrs:{value:null,max:null}},q:{attrs:{cite:null}},rp:p,rt:p,ruby:p,samp:p,script:{attrs:{type:["text/javascript"],src:null,async:["async"],defer:["defer"],charset:SO}},section:p,select:{attrs:{form:null,name:null,size:null,autofocus:["autofocus"],disabled:["disabled"],multiple:["multiple"]}},slot:{attrs:{name:null}},small:p,source:{attrs:{src:null,type:null,media:null}},span:p,strong:p,style:{attrs:{type:["text/css"],media:null,scoped:null}},sub:p,summary:p,sup:p,table:p,tbody:p,td:{attrs:{colspan:null,rowspan:null,headers:null}},template:p,textarea:{attrs:{dirname:null,form:null,maxlength:null,name:null,placeholder:null,rows:null,cols:null,autofocus:["autofocus"],disabled:["disabled"],readonly:["readonly"],required:["required"],wrap:["soft","hard"]}},tfoot:p,th:{attrs:{colspan:null,rowspan:null,headers:null,scope:["row","col","rowgroup","colgroup"]}},thead:p,time:{attrs:{datetime:null}},title:p,tr:p,track:{attrs:{src:null,label:null,default:null,kind:["subtitles","captions","descriptions","chapters","metadata"],srclang:null}},ul:{children:["li","script","template","ul","ol"]},var:p,video:{attrs:{src:null,poster:null,width:null,height:null,crossorigin:["anonymous","use-credentials"],preload:["auto","metadata","none"],autoplay:["autoplay"],mediagroup:["movie"],muted:["muted"],controls:["controls"]}},wbr:p},ut={accesskey:null,class:null,contenteditable:b,contextmenu:null,dir:["ltr","rtl","auto"],draggable:["true","false","auto"],dropzone:["copy","move","link","string:","file:"],hidden:["hidden"],id:null,inert:["inert"],itemid:null,itemprop:null,itemref:null,itemscope:["itemscope"],itemtype:null,lang:["ar","bn","de","en-GB","en-US","es","fr","hi","id","ja","pa","pt","ru","tr","zh"],spellcheck:b,autocorrect:b,autocapitalize:b,style:null,tabindex:null,title:null,translate:["yes","no"],rel:["stylesheet","alternate","author","bookmark","help","license","next","nofollow","noreferrer","prefetch","prev","search","tag"],role:"alert application article banner button cell checkbox complementary contentinfo dialog document feed figure form grid gridcell heading img list listbox listitem main navigation region row rowgroup search switch tab table tabpanel textbox timer".split(" "),"aria-activedescendant":null,"aria-atomic":b,"aria-autocomplete":["inline","list","both","none"],"aria-busy":b,"aria-checked":["true","false","mixed","undefined"],"aria-controls":null,"aria-describedby":null,"aria-disabled":b,"aria-dropeffect":null,"aria-expanded":["true","false","undefined"],"aria-flowto":null,"aria-grabbed":["true","false","undefined"],"aria-haspopup":b,"aria-hidden":b,"aria-invalid":["true","false","grammar","spelling"],"aria-label":null,"aria-labelledby":null,"aria-level":null,"aria-live":["off","polite","assertive"],"aria-multiline":b,"aria-multiselectable":b,"aria-owns":null,"aria-posinset":null,"aria-pressed":["true","false","mixed","undefined"],"aria-readonly":b,"aria-relevant":null,"aria-required":b,"aria-selected":["true","false","undefined"],"aria-setsize":null,"aria-sort":["ascending","descending","none","other"],"aria-valuemax":null,"aria-valuemin":null,"aria-valuenow":null,"aria-valuetext":null},pt="beforeunload copy cut dragstart dragover dragleave dragenter dragend drag paste focus blur change click load mousedown mouseenter mouseleave mouseup keydown keyup resize scroll unload".split(" ").map(e=>"on"+e);for(let e of pt)ut[e]=null;class hO{constructor(O,a){this.tags=Object.assign(Object.assign({},fi),O),this.globalAttrs=Object.assign(Object.assign({},ut),a),this.allTags=Object.keys(this.tags),this.globalAttrNames=Object.keys(this.globalAttrs)}}hO.default=new hO;function V(e,O,a=e.length){if(!O)return"";let t=O.firstChild,r=t&&t.getChild("TagName");return r?e.sliceString(r.from,Math.min(r.to,a)):""}function j(e,O=!1){for(;e;e=e.parent)if(e.name=="Element")if(O)O=!1;else return e;return null}function dt(e,O,a){let t=a.tags[V(e,j(O))];return(t==null?void 0:t.children)||a.allTags}function jO(e,O){let a=[];for(let t=j(O);t&&!t.type.isTop;t=j(t.parent)){let r=V(e,t);if(r&&t.lastChild.name=="CloseTag")break;r&&a.indexOf(r)<0&&(O.name=="EndTag"||O.from>=t.firstChild.to)&&a.push(r)}return a}const ft=/^[:\-\.\w\u00b7-\uffff]*$/;function ge(e,O,a,t,r){let s=/\s*>/.test(e.sliceDoc(r,r+5))?"":">",i=j(a,!0);return{from:t,to:r,options:dt(e.doc,i,O).map(o=>({label:o,type:"type"})).concat(jO(e.doc,a).map((o,n)=>({label:"/"+o,apply:"/"+o+s,type:"type",boost:99-n}))),validFor:/^\/?[:\-\.\w\u00b7-\uffff]*$/}}function Se(e,O,a,t){let r=/\s*>/.test(e.sliceDoc(t,t+5))?"":">";return{from:a,to:t,options:jO(e.doc,O).map((s,i)=>({label:s,apply:s+r,type:"type",boost:99-i})),validFor:ft}}function $i(e,O,a,t){let r=[],s=0;for(let i of dt(e.doc,a,O))r.push({label:"<"+i,type:"type"});for(let i of jO(e.doc,a))r.push({label:"",type:"type",boost:99-s++});return{from:t,to:t,options:r,validFor:/^<\/?[:\-\.\w\u00b7-\uffff]*$/}}function Pi(e,O,a,t,r){let s=j(a),i=s?O.tags[V(e.doc,s)]:null,o=i&&i.attrs?Object.keys(i.attrs):[],n=i&&i.globalAttrs===!1?o:o.length?o.concat(O.globalAttrNames):O.globalAttrNames;return{from:t,to:r,options:n.map(Q=>({label:Q,type:"property"})),validFor:ft}}function mi(e,O,a,t,r){var s;let i=(s=a.parent)===null||s===void 0?void 0:s.getChild("AttributeName"),o=[],n;if(i){let Q=e.sliceDoc(i.from,i.to),u=O.globalAttrs[Q];if(!u){let c=j(a),f=c?O.tags[V(e.doc,c)]:null;u=(f==null?void 0:f.attrs)&&f.attrs[Q]}if(u){let c=e.sliceDoc(t,r).toLowerCase(),f='"',h='"';/^['"]/.test(c)?(n=c[0]=='"'?/^[^"]*$/:/^[^']*$/,f="",h=e.sliceDoc(r,r+1)==c[0]?"":c[0],c=c.slice(1),t++):n=/^[^\s<>='"]*$/;for(let d of u)o.push({label:d,apply:f+d+h,type:"constant"})}}return{from:t,to:r,options:o,validFor:n}}function gi(e,O){let{state:a,pos:t}=O,r=G(a).resolveInner(t,-1),s=r.resolve(t);for(let i=t,o;s==r&&(o=r.childBefore(i));){let n=o.lastChild;if(!n||!n.type.isError||n.fromgi(t,r)}const Zi=y.parser.configure({top:"SingleExpression"}),$t=[{tag:"script",attrs:e=>e.type=="text/typescript"||e.lang=="ts",parser:ot.parser},{tag:"script",attrs:e=>e.type=="text/babel"||e.type=="text/jsx",parser:lt.parser},{tag:"script",attrs:e=>e.type=="text/typescript-jsx",parser:nt.parser},{tag:"script",attrs(e){return/^(importmap|speculationrules|application\/(.+\+)?json)$/i.test(e.type)},parser:Zi},{tag:"script",attrs(e){return!e.type||/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i.test(e.type)},parser:y.parser},{tag:"style",attrs(e){return(!e.lang||e.lang=="css")&&(!e.type||/^(text\/)?(x-)?(stylesheet|css)$/i.test(e.type))},parser:QO.parser}],Pt=[{name:"style",parser:QO.parser.configure({top:"Styles"})}].concat(pt.map(e=>({name:e,parser:y.parser}))),mt=J.define({name:"html",parser:rr.configure({props:[L.add({Element(e){let O=/^(\s*)(<\/)?/.exec(e.textAfter);return e.node.to<=e.pos+O[0].length?e.continue():e.lineIndent(e.node.from)+(O[2]?0:e.unit)},"OpenTag CloseTag SelfClosingTag"(e){return e.column(e.node.from)+e.unit},Document(e){if(e.pos+/\s*/.exec(e.textAfter)[0].lengthe.getChild("TagName")})]}),languageData:{commentTokens:{block:{open:""}},indentOnInput:/^\s*<\/\w+\W$/,wordChars:"-._"}}),iO=mt.configure({wrap:Le($t,Pt)});function bi(e={}){let O="",a;e.matchClosingTags===!1&&(O="noMatch"),e.selfClosingTags===!0&&(O=(O?O+" ":"")+"selfClosing"),(e.nestedLanguages&&e.nestedLanguages.length||e.nestedAttributes&&e.nestedAttributes.length)&&(a=Le((e.nestedLanguages||[]).concat($t),(e.nestedAttributes||[]).concat(Pt)));let t=a?mt.configure({wrap:a,dialect:O}):O?iO.configure({dialect:O}):iO;return new F(t,[iO.data.of({autocomplete:Si(e)}),e.autoCloseTags!==!1?ki:[],ht().support,_r().support])}const Ze=new Set("area base br col command embed frame hr img input keygen link meta param source track wbr menuitem".split(" ")),ki=q.inputHandler.of((e,O,a,t,r)=>{if(e.composing||e.state.readOnly||O!=a||t!=">"&&t!="/"||!iO.isActiveAt(e.state,O,-1))return!1;let s=r(),{state:i}=s,o=i.changeByRange(n=>{var Q,u,c;let f=i.doc.sliceString(n.from-1,n.to)==t,{head:h}=n,d=G(i).resolveInner(h,-1),P;if(f&&t==">"&&d.name=="EndTag"){let m=d.parent;if(((u=(Q=m.parent)===null||Q===void 0?void 0:Q.lastChild)===null||u===void 0?void 0:u.name)!="CloseTag"&&(P=V(i.doc,m.parent,h))&&!Ze.has(P)){let x=h+(i.doc.sliceString(h,h+1)===">"?1:0),X=``;return{range:n,changes:{from:h,to:x,insert:X}}}}else if(f&&t=="/"&&d.name=="IncompleteCloseTag"){let m=d.parent;if(d.from==h-2&&((c=m.lastChild)===null||c===void 0?void 0:c.name)!="CloseTag"&&(P=V(i.doc,m,h))&&!Ze.has(P)){let x=h+(i.doc.sliceString(h,h+1)===">"?1:0),X=`${P}>`;return{range:ze.cursor(h+X.length,-1),changes:{from:h,to:x,insert:X}}}}return{range:n}});return o.changes.empty?!1:(e.dispatch([s,i.update(o,{userEvent:"input.complete",scrollIntoView:!0})]),!0)}),xi=B({String:l.string,Number:l.number,"True False":l.bool,PropertyName:l.propertyName,Null:l.null,",":l.separator,"[ ]":l.squareBracket,"{ }":l.brace}),Xi=T.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#CjOOQO'#Cp'#CpQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CrOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59U,59UO!iQPO,59UOVQPO,59QOqQPO'#CkO!nQPO,59^OOQO1G.k1G.kOVQPO'#ClO!vQPO,59aOOQO1G.p1G.pOOQO1G.l1G.lOOQO,59V,59VOOQO-E6i-E6iOOQO,59W,59WOOQO-E6j-E6j",stateData:"#O~OcOS~OQSORSOSSOTSOWQO]ROePO~OVXOeUO~O[[O~PVOg^O~Oh_OVfX~OVaO~OhbO[iX~O[dO~Oh_OVfa~OhbO[ia~O",goto:"!kjPPPPPPkPPkqwPPk{!RPPP!XP!ePP!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"⚠ JsonText True False Null Number String } { Object Property PropertyName ] [ Array",maxTerm:25,nodeProps:[["isolate",-2,6,11,""],["openedBy",7,"{",12,"["],["closedBy",8,"}",13,"]"]],propSources:[xi],skippedNodes:[0],repeatNodeCount:2,tokenData:"(|~RaXY!WYZ!W]^!Wpq!Wrs!]|}$u}!O$z!Q!R%T!R![&c![!]&t!}#O&y#P#Q'O#Y#Z'T#b#c'r#h#i(Z#o#p(r#q#r(w~!]Oc~~!`Wpq!]qr!]rs!xs#O!]#O#P!}#P;'S!];'S;=`$o<%lO!]~!}Oe~~#QXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#m~#pR!Q![#y!c!i#y#T#Z#y~#|R!Q![$V!c!i$V#T#Z$V~$YR!Q![$c!c!i$c#T#Z$c~$fR!Q![!]!c!i!]#T#Z!]~$rP;=`<%l!]~$zOh~~$}Q!Q!R%T!R![&c~%YRT~!O!P%c!g!h%w#X#Y%w~%fP!Q![%i~%nRT~!Q![%i!g!h%w#X#Y%w~%zR{|&T}!O&T!Q![&Z~&WP!Q![&Z~&`PT~!Q![&Z~&hST~!O!P%c!Q![&c!g!h%w#X#Y%w~&yOg~~'OO]~~'TO[~~'WP#T#U'Z~'^P#`#a'a~'dP#g#h'g~'jP#X#Y'm~'rOR~~'uP#i#j'x~'{P#`#a(O~(RP#`#a(U~(ZOS~~(^P#f#g(a~(dP#i#j(g~(jP#X#Y(m~(rOQ~~(wOW~~(|OV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0}),yi=J.define({name:"json",parser:Xi.configure({props:[L.add({Object:v({except:/^\s*\}/}),Array:v({except:/^\s*\]/})}),K.add({"Object Array":UO})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function wi(){return new F(yi)}const Ri=36,be=1,Yi=2,U=3,kO=4,Ti=5,Wi=6,_i=7,qi=8,vi=9,Ui=10,zi=11,Vi=12,ji=13,Gi=14,Ci=15,Ai=16,Ei=17,ke=18,Ni=19,gt=20,St=21,xe=22,Mi=23,Ii=24;function YO(e){return e>=65&&e<=90||e>=97&&e<=122||e>=48&&e<=57}function Di(e){return e>=48&&e<=57||e>=97&&e<=102||e>=65&&e<=70}function _(e,O,a){for(let t=!1;;){if(e.next<0)return;if(e.next==O&&!t){e.advance();return}t=a&&!t&&e.next==92,e.advance()}}function Bi(e,O){O:for(;;){if(e.next<0)return;if(e.next==36){e.advance();for(let a=0;a)".charCodeAt(a);for(;;){if(e.next<0)return;if(e.next==t&&e.peek(1)==39){e.advance(2);return}e.advance()}}function TO(e,O){for(;!(e.next!=95&&!YO(e.next));)O!=null&&(O+=String.fromCharCode(e.next)),e.advance();return O}function Li(e){if(e.next==39||e.next==34||e.next==96){let O=e.next;e.advance(),_(e,O,!1)}else TO(e)}function Xe(e,O){for(;e.next==48||e.next==49;)e.advance();O&&e.next==O&&e.advance()}function ye(e,O){for(;;){if(e.next==46){if(O)break;O=!0}else if(e.next<48||e.next>57)break;e.advance()}if(e.next==69||e.next==101)for(e.advance(),(e.next==43||e.next==45)&&e.advance();e.next>=48&&e.next<=57;)e.advance()}function we(e){for(;!(e.next<0||e.next==10);)e.advance()}function W(e,O){for(let a=0;a!=&|~^/",specialVar:"?",identifierQuotes:'"',caseInsensitiveIdentifiers:!1,words:Zt(Fi,Ki)};function Hi(e,O,a,t){let r={};for(let s in WO)r[s]=(e.hasOwnProperty(s)?e:WO)[s];return O&&(r.words=Zt(O,a||"",t)),r}function bt(e){return new k(O=>{var a;let{next:t}=O;if(O.advance(),W(t,xO)){for(;W(O.next,xO);)O.advance();O.acceptToken(Ri)}else if(t==36&&e.doubleDollarQuotedStrings){let r=TO(O,"");O.next==36&&(O.advance(),Bi(O,r),O.acceptToken(U))}else if(t==39||t==34&&e.doubleQuotedStrings)_(O,t,e.backslashEscapes),O.acceptToken(U);else if(t==35&&e.hashComments||t==47&&O.next==47&&e.slashComments)we(O),O.acceptToken(be);else if(t==45&&O.next==45&&(!e.spaceAfterDashes||O.peek(1)==32))we(O),O.acceptToken(be);else if(t==47&&O.next==42){O.advance();for(let r=1;;){let s=O.next;if(O.next<0)break;if(O.advance(),s==42&&O.next==47){if(r--,O.advance(),!r)break}else s==47&&O.next==42&&(r++,O.advance())}O.acceptToken(Yi)}else if((t==101||t==69)&&O.next==39)O.advance(),_(O,39,!0),O.acceptToken(U);else if((t==110||t==78)&&O.next==39&&e.charSetCasts)O.advance(),_(O,39,e.backslashEscapes),O.acceptToken(U);else if(t==95&&e.charSetCasts)for(let r=0;;r++){if(O.next==39&&r>1){O.advance(),_(O,39,e.backslashEscapes),O.acceptToken(U);break}if(!YO(O.next))break;O.advance()}else if(e.plsqlQuotingMechanism&&(t==113||t==81)&&O.next==39&&O.peek(1)>0&&!W(O.peek(1),xO)){let r=O.peek(1);O.advance(2),Ji(O,r),O.acceptToken(U)}else if(t==40)O.acceptToken(_i);else if(t==41)O.acceptToken(qi);else if(t==123)O.acceptToken(vi);else if(t==125)O.acceptToken(Ui);else if(t==91)O.acceptToken(zi);else if(t==93)O.acceptToken(Vi);else if(t==59)O.acceptToken(ji);else if(e.unquotedBitLiterals&&t==48&&O.next==98)O.advance(),Xe(O),O.acceptToken(xe);else if((t==98||t==66)&&(O.next==39||O.next==34)){const r=O.next;O.advance(),e.treatBitsAsBytes?(_(O,r,e.backslashEscapes),O.acceptToken(Mi)):(Xe(O,r),O.acceptToken(xe))}else if(t==48&&(O.next==120||O.next==88)||(t==120||t==88)&&O.next==39){let r=O.next==39;for(O.advance();Di(O.next);)O.advance();r&&O.next==39&&O.advance(),O.acceptToken(kO)}else if(t==46&&O.next>=48&&O.next<=57)ye(O,!0),O.acceptToken(kO);else if(t==46)O.acceptToken(Gi);else if(t>=48&&t<=57)ye(O,!1),O.acceptToken(kO);else if(W(t,e.operatorChars)){for(;W(O.next,e.operatorChars);)O.advance();O.acceptToken(Ci)}else if(W(t,e.specialVar))O.next==t&&O.advance(),Li(O),O.acceptToken(Ei);else if(W(t,e.identifierQuotes))_(O,t,!1),O.acceptToken(Ni);else if(t==58||t==44)O.acceptToken(Ai);else if(YO(t)){let r=TO(O,String.fromCharCode(t));O.acceptToken(O.next==46||O.peek(-r.length-1)==46?ke:(a=e.words[r.toLowerCase()])!==null&&a!==void 0?a:ke)}})}const kt=bt(WO),Os=T.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"⚠ LineComment BlockComment String Number Bool Null ( ) { } [ ] ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,nodeProps:[["isolate",-4,1,2,3,19,""]],skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,kt],topRules:{Script:[0,25]},tokenPrec:0});function _O(e){let O=e.cursor().moveTo(e.from,-1);for(;/Comment/.test(O.name);)O.moveTo(O.from,-1);return O.node}function I(e,O){let a=e.sliceString(O.from,O.to),t=/^([`'"])(.*)\1$/.exec(a);return t?t[2]:a}function uO(e){return e&&(e.name=="Identifier"||e.name=="QuotedIdentifier")}function es(e,O){if(O.name=="CompositeIdentifier"){let a=[];for(let t=O.firstChild;t;t=t.nextSibling)uO(t)&&a.push(I(e,t));return a}return[I(e,O)]}function Re(e,O){for(let a=[];;){if(!O||O.name!=".")return a;let t=_O(O);if(!uO(t))return a;a.unshift(I(e,t)),O=_O(t)}}function ts(e,O){let a=G(e).resolveInner(O,-1),t=rs(e.doc,a);return a.name=="Identifier"||a.name=="QuotedIdentifier"||a.name=="Keyword"?{from:a.from,quoted:a.name=="QuotedIdentifier"?e.doc.sliceString(a.from,a.from+1):null,parents:Re(e.doc,_O(a)),aliases:t}:a.name=="."?{from:O,quoted:null,parents:Re(e.doc,a),aliases:t}:{from:O,quoted:null,parents:[],empty:!0,aliases:t}}const as=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));function rs(e,O){let a;for(let r=O;!a;r=r.parent){if(!r)return null;r.name=="Statement"&&(a=r)}let t=null;for(let r=a.firstChild,s=!1,i=null;r;r=r.nextSibling){let o=r.name=="Keyword"?e.sliceString(r.from,r.to).toLowerCase():null,n=null;if(!s)s=o=="from";else if(o=="as"&&i&&uO(r.nextSibling))n=I(e,r.nextSibling);else{if(o&&as.has(o))break;i&&uO(r)&&(n=I(e,r))}n&&(t||(t=Object.create(null)),t[n]=es(e,i)),i=/Identifier$/.test(r.name)?r:null}return t}function is(e,O){return e?O.map(a=>Object.assign(Object.assign({},a),{label:a.label[0]==e?a.label:e+a.label+e,apply:void 0})):O}const ss=/^\w*$/,os=/^[`'"]?\w*[`'"]?$/;function Ye(e){return e.self&&typeof e.self.label=="string"}class GO{constructor(O,a){this.idQuote=O,this.idCaseInsensitive=a,this.list=[],this.children=void 0}child(O){let a=this.children||(this.children=Object.create(null)),t=a[O];return t||(O&&!this.list.some(r=>r.label==O)&&this.list.push(Te(O,"type",this.idQuote,this.idCaseInsensitive)),a[O]=new GO(this.idQuote,this.idCaseInsensitive))}maybeChild(O){return this.children?this.children[O]:null}addCompletion(O){let a=this.list.findIndex(t=>t.label==O.label);a>-1?this.list[a]=O:this.list.push(O)}addCompletions(O){for(let a of O)this.addCompletion(typeof a=="string"?Te(a,"property",this.idQuote,this.idCaseInsensitive):a)}addNamespace(O){Array.isArray(O)?this.addCompletions(O):Ye(O)?this.addNamespace(O.children):this.addNamespaceObject(O)}addNamespaceObject(O){for(let a of Object.keys(O)){let t=O[a],r=null,s=a.replace(/\\?\./g,o=>o=="."?"\0":o).split("\0"),i=this;Ye(t)&&(r=t.self,t=t.children);for(let o=0;o{let{parents:c,from:f,quoted:h,empty:d,aliases:P}=ts(u.state,u.pos);if(d&&!u.explicit)return null;P&&c.length==1&&(c=P[c[0]]||c);let m=n;for(let Y of c){for(;!m.children||!m.children[Y];)if(m==n&&Q)m=Q;else if(m==Q&&t)m=m.child(t);else return null;let H=m.maybeChild(Y);if(!H)return null;m=H}let x=h&&u.state.sliceDoc(u.pos,u.pos+1)==h,X=m.list;return m==n&&P&&(X=X.concat(Object.keys(P).map(Y=>({label:Y,type:"constant"})))),{from:f,to:x?u.pos+1:void 0,options:is(h,X),validFor:h?os:ss}}}function ns(e,O){let a=Object.keys(e).map(t=>({label:O?t.toUpperCase():t,type:e[t]==St?"type":e[t]==gt?"keyword":"variable",boost:-1}));return ve(["QuotedIdentifier","SpecialVar","String","LineComment","BlockComment","."],Ue(a))}let cs=Os.configure({props:[L.add({Statement:v()}),K.add({Statement(e,O){return{from:Math.min(e.from+100,O.doc.lineAt(e.from).to),to:e.to}},BlockComment(e){return{from:e.from+2,to:e.to-2}}}),B({Keyword:l.keyword,Type:l.typeName,Builtin:l.standard(l.name),Bits:l.number,Bytes:l.string,Bool:l.bool,Null:l.null,Number:l.number,String:l.string,Identifier:l.name,QuotedIdentifier:l.special(l.string),SpecialVar:l.special(l.name),LineComment:l.lineComment,BlockComment:l.blockComment,Operator:l.operator,"Semi Punctuation":l.punctuation,"( )":l.paren,"{ }":l.brace,"[ ]":l.squareBracket})]});class D{constructor(O,a,t){this.dialect=O,this.language=a,this.spec=t}get extension(){return this.language.extension}static define(O){let a=Hi(O,O.keywords,O.types,O.builtin),t=J.define({name:"sql",parser:cs.configure({tokenizers:[{from:kt,to:bt(a)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new D(a,t,O)}}function Qs(e,O=!1){return ns(e.dialect.words,O)}function hs(e,O=!1){return e.language.data.of({autocomplete:Qs(e,O)})}function us(e){return e.schema?ls(e.schema,e.tables,e.schemas,e.defaultTable,e.defaultSchema,e.dialect||CO):()=>null}function ps(e){return e.schema?(e.dialect||CO).language.data.of({autocomplete:us(e)}):[]}function We(e={}){let O=e.dialect||CO;return new F(O.language,[ps(e),hs(O,!!e.upperCaseKeywords)])}const CO=D.define({});function ds(e){let O;return{c(){O=Tt("div"),Wt(O,"class","code-editor"),OO(O,"min-height",e[0]?e[0]+"px":null),OO(O,"max-height",e[1]?e[1]+"px":"auto")},m(a,t){_t(a,O,t),e[11](O)},p(a,[t]){t&1&&OO(O,"min-height",a[0]?a[0]+"px":null),t&2&&OO(O,"max-height",a[1]?a[1]+"px":"auto")},i:BO,o:BO,d(a){a&&qt(O),e[11](null)}}}function fs(e,O,a){let t;vt(e,Ut,$=>a(12,t=$));const r=zt();let{id:s=""}=O,{value:i=""}=O,{minHeight:o=null}=O,{maxHeight:n=null}=O,{disabled:Q=!1}=O,{placeholder:u=""}=O,{language:c="javascript"}=O,{singleLine:f=!1}=O,h,d,P=new eO,m=new eO,x=new eO,X=new eO;function Y(){h==null||h.focus()}function H(){d==null||d.dispatchEvent(new CustomEvent("change",{detail:{value:i},bubbles:!0})),r("change",i)}function AO(){if(!s)return;const $=document.querySelectorAll('[for="'+s+'"]');for(let g of $)g.removeEventListener("click",Y)}function EO(){if(!s)return;AO();const $=document.querySelectorAll('[for="'+s+'"]');for(let g of $)g.addEventListener("click",Y)}function NO(){switch(c){case"html":return bi();case"json":return wi();case"sql-create-index":return We({dialect:D.define({keywords:"create unique index if not exists on collate asc desc where like isnull notnull date time datetime unixepoch strftime lower upper substr case when then iif if else json_extract json_each json_tree json_array_length json_valid ",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"}),upperCaseKeywords:!0});case"sql-select":let $={};for(let g of t)$[g.name]=jt.getAllCollectionIdentifiers(g);return We({dialect:D.define({keywords:"select distinct from where having group by order limit offset join left right inner with like not in match asc desc regexp isnull notnull glob count avg sum min max current random cast as int real text bool date time datetime unixepoch strftime coalesce lower upper substr case when then iif if else json_extract json_each json_tree json_array_length json_valid ",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"}),schema:$,upperCaseKeywords:!0});default:return ht()}}Vt(()=>{const $={key:"Enter",run:g=>{f&&r("submit",i)}};return EO(),a(10,h=new q({parent:d,state:C.create({doc:i,extensions:[Lt(),Kt(),Ft(),Ht(),Oa(),C.allowMultipleSelections.of(!0),ea(ta,{fallback:!0}),aa(),ra(),ia(),sa(),oa.of([$,...la,...na,ca.find(g=>g.key==="Mod-d"),...Qa,...ha]),q.lineWrapping,ua({icons:!1}),P.of(NO()),X.of(JO(u)),m.of(q.editable.of(!0)),x.of(C.readOnly.of(!1)),C.transactionFilter.of(g=>{var MO,IO,DO;if(f&&g.newDoc.lines>1){if(!((DO=(IO=(MO=g.changes)==null?void 0:MO.inserted)==null?void 0:IO.filter(Xt=>!!Xt.text.find(yt=>yt)))!=null&&DO.length))return[];g.newDoc.text=[g.newDoc.text.join(" ")]}return g}),q.updateListener.of(g=>{!g.docChanged||Q||(a(3,i=g.state.doc.toString()),H())})]})})),()=>{AO(),h==null||h.destroy()}});function xt($){Gt[$?"unshift":"push"](()=>{d=$,a(2,d)})}return e.$$set=$=>{"id"in $&&a(4,s=$.id),"value"in $&&a(3,i=$.value),"minHeight"in $&&a(0,o=$.minHeight),"maxHeight"in $&&a(1,n=$.maxHeight),"disabled"in $&&a(5,Q=$.disabled),"placeholder"in $&&a(6,u=$.placeholder),"language"in $&&a(7,c=$.language),"singleLine"in $&&a(8,f=$.singleLine)},e.$$.update=()=>{e.$$.dirty&16&&s&&EO(),e.$$.dirty&1152&&h&&c&&h.dispatch({effects:[P.reconfigure(NO())]}),e.$$.dirty&1056&&h&&typeof Q<"u"&&h.dispatch({effects:[m.reconfigure(q.editable.of(!Q)),x.reconfigure(C.readOnly.of(Q))]}),e.$$.dirty&1032&&h&&i!=h.state.doc.toString()&&h.dispatch({changes:{from:0,to:h.state.doc.length,insert:i}}),e.$$.dirty&1088&&h&&typeof u<"u"&&h.dispatch({effects:[X.reconfigure(JO(u))]})},[o,n,d,i,s,Q,u,c,f,Y,h,xt]}class ms extends wt{constructor(O){super(),Rt(this,O,fs,ds,Yt,{id:4,value:3,minHeight:0,maxHeight:1,disabled:5,placeholder:6,language:7,singleLine:8,focus:9})}get focus(){return this.$$.ctx[9]}}export{ms as default}; diff --git a/ui/dist/assets/CodeEditor-CZ0EgQcM.js b/ui/dist/assets/CodeEditor-CZ0EgQcM.js deleted file mode 100644 index 85c92a40..00000000 --- a/ui/dist/assets/CodeEditor-CZ0EgQcM.js +++ /dev/null @@ -1,14 +0,0 @@ -import{S as wt,i as Rt,s as Yt,e as Tt,f as Wt,U as OO,g as _t,x as BO,o as qt,J as vt,K as Ut,L as zt,I as Vt,C as jt,M as Gt}from"./index-Bp3jGQ0J.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 qe,O as ve,E as q,Q as Ue,R as S,U as It,V as Dt,W as ze,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-BztyTJOx.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;this.reducePos=2e3&&!(!((a=this.p.parser.nodeSet.types[r])===null||a===void 0)&&a.isAnonymous)&&(n==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=Q):this.p.lastBigReductionSizeo;)this.stack.pop();this.reduceContext(r,n)}storeNode(O,a,t,r=4,s=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]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)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;sn&1&&o==i)||r.push(a[s],i)}a=r}let t=[];for(let r=0;r>19,r=a&65535,s=this.stack.length-t*3;if(s<0||O.getGoto(this.stack[s],r,!1)<0){let i=this.findForcedReduction();if(i==null)return!1;a=i}this.storeNode(0,this.pos,this.pos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(a),!0}findForcedReduction(){let{parser:O}=this.p,a=[],t=(r,s)=>{if(!a.includes(r))return a.push(r),O.allActions(r,i=>{if(!(i&393216))if(i&65536){let o=(i>>19)-s;if(o>1){let n=i&65535,Q=this.stack.length-o*3;if(Q>=0&&O.getGoto(this.stack[Q],n,!1)>=0)return o<<19|65536|n}}else{let o=t(i,s+1);if(o!=null)return o}})};return t(this.state,0)}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(this.stack.length!=3)return!1;let{parser:O}=this.p;return O.data[O.stateSlot(this.state,1)]==65535&&!O.stateSlot(this.state,4)}restart(){this.storeNode(0,this.pos,this.pos,4,!0),this.state=this.stack[0],this.stack.length=0}sameState(O){if(this.state!=O.state||this.stack.length!=O.stack.length)return!1;for(let a=0;athis.lookAhead&&(this.emitLookAhead(),this.lookAhead=O)}close(){this.curContext&&this.curContext.tracker.strict&&this.emitContext(),this.lookAhead>0&&this.emitLookAhead()}}class KO{constructor(O,a){this.tracker=O,this.context=a,this.hash=O.strict?O.hash(a):0}}class pa{constructor(O){this.start=O,this.state=O.state,this.stack=O.stack,this.base=this.stack.length}reduce(O){let a=O&65535,t=O>>19;t==0?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=(t-1)*3;let r=this.start.p.parser.getGoto(this.stack[this.base-3],a,!0);this.state=r}}class oO{constructor(O,a,t){this.stack=O,this.pos=a,this.index=t,this.buffer=O.buffer,this.index==0&&this.maybeNext()}static create(O,a=O.bufferBase+O.buffer.length){return new oO(O,a,a-O.bufferBase)}maybeNext(){let O=this.stack.parent;O!=null&&(this.index=this.stack.bufferBase-O.bufferBase,this.stack=O,this.buffer=O.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,this.index==0&&this.maybeNext()}fork(){return new oO(this.stack,this.pos,this.index)}}function N(e,O=Uint16Array){if(typeof e!="string")return e;let a=null;for(let t=0,r=0;t=92&&i--,i>=34&&i--;let n=i-32;if(n>=46&&(n-=46,o=!0),s+=n,o)break;s*=46}a?a[r++]=s:a=new O(s)}return a}class aO{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const FO=new aO;class da{constructor(O,a){this.input=O,this.ranges=a,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=FO,this.rangeIndex=0,this.pos=this.chunkPos=a[0].from,this.range=a[0],this.end=a[a.length-1].to,this.readNext()}resolveOffset(O,a){let t=this.range,r=this.rangeIndex,s=this.pos+O;for(;st.to:s>=t.to;){if(r==this.ranges.length-1)return null;let i=this.ranges[++r];s+=i.from-t.to,t=i}return s}clipPos(O){if(O>=this.range.from&&OO)return Math.max(O,a.from);return this.end}peek(O){let a=this.chunkOff+O,t,r;if(a>=0&&a=this.chunk2Pos&&to.to&&(this.chunk2=this.chunk2.slice(0,o.to-t)),r=this.chunk2.charCodeAt(0)}}return t>=this.token.lookAhead&&(this.token.lookAhead=t+1),r}acceptToken(O,a=0){let t=a?this.resolveOffset(a,-1):this.pos;if(t==null||t=this.chunk2Pos&&this.posthis.range.to?O.slice(0,this.range.to-this.pos):O,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(O=1){for(this.chunkOff+=O;this.pos+O>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();O-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=O,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(O,a){if(a?(this.token=a,a.start=O,a.lookAhead=O+1,a.value=a.extended=-1):this.token=FO,this.pos!=O){if(this.pos=O,O==this.end)return this.setDone(),this;for(;O=this.range.to;)this.range=this.ranges[++this.rangeIndex];O>=this.chunkPos&&O=this.chunkPos&&a<=this.chunkPos+this.chunk.length)return this.chunk.slice(O-this.chunkPos,a-this.chunkPos);if(O>=this.chunk2Pos&&a<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(O-this.chunk2Pos,a-this.chunk2Pos);if(O>=this.range.from&&a<=this.range.to)return this.input.read(O,a);let t="";for(let r of this.ranges){if(r.from>=a)break;r.to>O&&(t+=this.input.read(Math.max(r.from,O),Math.min(r.to,a)))}return t}}class z{constructor(O,a){this.data=O,this.id=a}token(O,a){let{parser:t}=a.p;Ve(this.data,O,a,this.id,t.data,t.tokenPrecTable)}}z.prototype.contextual=z.prototype.fallback=z.prototype.extend=!1;class lO{constructor(O,a,t){this.precTable=a,this.elseToken=t,this.data=typeof O=="string"?N(O):O}token(O,a){let t=O.pos,r=0;for(;;){let s=O.next<0,i=O.resolveOffset(1,1);if(Ve(this.data,O,a,0,this.data,this.precTable),O.token.value>-1)break;if(this.elseToken==null)return;if(s||r++,i==null)break;O.reset(i,O.token)}r&&(O.reset(t,O.token),O.acceptToken(this.elseToken,r))}}lO.prototype.contextual=z.prototype.fallback=z.prototype.extend=!1;class k{constructor(O,a={}){this.token=O,this.contextual=!!a.contextual,this.fallback=!!a.fallback,this.extend=!!a.extend}}function Ve(e,O,a,t,r,s){let i=0,o=1<0){let d=e[h];if(n.allows(d)&&(O.token.value==-1||O.token.value==d||fa(d,O.token.value,r,s))){O.acceptToken(d);break}}let u=O.next,c=0,f=e[i+2];if(O.next<0&&f>c&&e[Q+f*3-3]==65535){i=e[Q+f*3-1];continue O}for(;c>1,d=Q+h+(h<<1),P=e[d],m=e[d+1]||65536;if(u=m)c=h+1;else{i=e[d+2],O.advance();continue O}}break}}function HO(e,O,a){for(let t=O,r;(r=e[t])!=65535;t++)if(r==a)return t-O;return-1}function fa(e,O,a,t){let r=HO(a,t,O);return r<0||HO(a,t,e)O)&&!t.type.isError)return a<0?Math.max(0,Math.min(t.to-1,O-25)):Math.min(e.length,Math.max(t.from+1,O+25));if(a<0?t.prevSibling():t.nextSibling())break;if(!t.parent())return a<0?0:e.length}}class $a{constructor(O,a){this.fragments=O,this.nodeSet=a,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let O=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(O){for(this.safeFrom=O.openStart?Oe(O.tree,O.from+O.offset,1)-O.offset:O.from,this.safeTo=O.openEnd?Oe(O.tree,O.to+O.offset,-1)-O.offset:O.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(O.tree),this.start.push(-O.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(O){if(OO)return this.nextStart=i,null;if(s instanceof tO){if(i==O){if(i=Math.max(this.safeFrom,O)&&(this.trees.push(s),this.start.push(i),this.index.push(0))}else this.index[a]++,this.nextStart=i+s.length}}}class Pa{constructor(O,a){this.stream=a,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=O.tokenizers.map(t=>new aO)}getActions(O){let a=0,t=null,{parser:r}=O.p,{tokenizers:s}=r,i=r.stateSlot(O.state,3),o=O.curContext?O.curContext.hash:0,n=0;for(let Q=0;Qc.end+25&&(n=Math.max(c.lookAhead,n)),c.value!=0)){let f=a;if(c.extended>-1&&(a=this.addActions(O,c.extended,c.end,a)),a=this.addActions(O,c.value,c.end,a),!u.extend&&(t=c,a>f))break}}for(;this.actions.length>a;)this.actions.pop();return n&&O.setLookAhead(n),!t&&O.pos==this.stream.end&&(t=new aO,t.value=O.p.parser.eofTerm,t.start=t.end=O.pos,a=this.addActions(O,t.value,t.end,a)),this.mainToken=t,this.actions}getMainToken(O){if(this.mainToken)return this.mainToken;let a=new aO,{pos:t,p:r}=O;return a.start=t,a.end=Math.min(t+1,r.stream.end),a.value=t==r.stream.end?r.parser.eofTerm:0,a}updateCachedToken(O,a,t){let r=this.stream.clipPos(t.pos);if(a.token(this.stream.reset(r,O),t),O.value>-1){let{parser:s}=t.p;for(let i=0;i=0&&t.p.parser.dialect.allows(o>>1)){o&1?O.extended=o>>1:O.value=o>>1;break}}}else O.value=0,O.end=this.stream.clipPos(r+1)}putAction(O,a,t,r){for(let s=0;sO.bufferLength*4?new $a(t,O.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let O=this.stacks,a=this.minStackPos,t=this.stacks=[],r,s;if(this.bigReductionCount>300&&O.length==1){let[i]=O;for(;i.forceReduce()&&i.stack.length&&i.stack[i.stack.length-2]>=this.lastBigReductionStart;);this.bigReductionCount=this.lastBigReductionSize=0}for(let i=0;ia)t.push(o);else{if(this.advanceStack(o,t,O))continue;{r||(r=[],s=[]),r.push(o);let n=this.tokens.getMainToken(o);s.push(n.value,n.end)}}break}}if(!t.length){let i=r&&Sa(r);if(i)return Z&&console.log("Finish with "+this.stackID(i)),this.stackToTree(i);if(this.parser.strict)throw Z&&r&&console.log("Stuck with token "+(this.tokens.mainToken?this.parser.getName(this.tokens.mainToken.value):"none")),new SyntaxError("No parse at "+a);this.recovering||(this.recovering=5)}if(this.recovering&&r){let i=this.stoppedAt!=null&&r[0].pos>this.stoppedAt?r[0]:this.runRecovery(r,s,t);if(i)return Z&&console.log("Force-finish "+this.stackID(i)),this.stackToTree(i.forceAll())}if(this.recovering){let i=this.recovering==1?1:this.recovering*3;if(t.length>i)for(t.sort((o,n)=>n.score-o.score);t.length>i;)t.pop();t.some(o=>o.reducePos>a)&&this.recovering--}else if(t.length>1){O:for(let i=0;i500&&Q.buffer.length>500)if((o.score-Q.score||o.buffer.length-Q.buffer.length)>0)t.splice(n--,1);else{t.splice(i--,1);continue O}}}t.length>12&&t.splice(12,t.length-12)}this.minStackPos=t[0].pos;for(let i=1;i ":"";if(this.stoppedAt!=null&&r>this.stoppedAt)return O.forceReduce()?O:null;if(this.fragments){let Q=O.curContext&&O.curContext.tracker.strict,u=Q?O.curContext.hash:0;for(let c=this.fragments.nodeAt(r);c;){let f=this.parser.nodeSet.types[c.type.id]==c.type?s.getGoto(O.state,c.type.id):-1;if(f>-1&&c.length&&(!Q||(c.prop(qO.contextHash)||0)==u))return O.useNode(c,f),Z&&console.log(i+this.stackID(O)+` (via reuse of ${s.getName(c.type.id)})`),!0;if(!(c instanceof tO)||c.children.length==0||c.positions[0]>0)break;let h=c.children[0];if(h instanceof tO&&c.positions[0]==0)c=h;else break}}let o=s.stateSlot(O.state,4);if(o>0)return O.reduce(o),Z&&console.log(i+this.stackID(O)+` (via always-reduce ${s.getName(o&65535)})`),!0;if(O.stack.length>=8400)for(;O.stack.length>6e3&&O.forceReduce(););let n=this.tokens.getActions(O);for(let Q=0;Qr?a.push(d):t.push(d)}return!1}advanceFully(O,a){let t=O.pos;for(;;){if(!this.advanceStack(O,null,null))return!1;if(O.pos>t)return ee(O,a),!0}}runRecovery(O,a,t){let r=null,s=!1;for(let i=0;i ":"";if(o.deadEnd&&(s||(s=!0,o.restart(),Z&&console.log(u+this.stackID(o)+" (restarted)"),this.advanceFully(o,t))))continue;let c=o.split(),f=u;for(let h=0;c.forceReduce()&&h<10&&(Z&&console.log(f+this.stackID(c)+" (via force-reduce)"),!this.advanceFully(c,t));h++)Z&&(f=this.stackID(c)+" -> ");for(let h of o.recoverByInsert(n))Z&&console.log(u+this.stackID(h)+" (via recover-insert)"),this.advanceFully(h,t);this.stream.end>o.pos?(Q==o.pos&&(Q++,n=0),o.recoverByDelete(n,Q),Z&&console.log(u+this.stackID(o)+` (via recover-delete ${this.parser.getName(n)})`),ee(o,t)):(!r||r.scoree;class je{constructor(O){this.start=O.start,this.shift=O.shift||dO,this.reduce=O.reduce||dO,this.reuse=O.reuse||dO,this.hash=O.hash||(()=>0),this.strict=O.strict!==!1}}class T extends Ct{constructor(O){if(super(),this.wrappers=[],O.version!=14)throw new RangeError(`Parser version (${O.version}) doesn't match runtime version (14)`);let a=O.nodeNames.split(" ");this.minRepeatTerm=a.length;for(let o=0;oO.topRules[o][1]),r=[];for(let o=0;o=0)s(u,n,o[Q++]);else{let c=o[Q+-u];for(let f=-u;f>0;f--)s(o[Q++],n,c);Q++}}}this.nodeSet=new At(a.map((o,n)=>Et.define({name:n>=this.minRepeatTerm?void 0:o,id:n,props:r[n],top:t.indexOf(n)>-1,error:n==0,skipped:O.skippedNodes&&O.skippedNodes.indexOf(n)>-1}))),O.propSources&&(this.nodeSet=this.nodeSet.extend(...O.propSources)),this.strict=!1,this.bufferLength=Nt;let i=N(O.tokenData);this.context=O.context,this.specializerSpecs=O.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let o=0;otypeof o=="number"?new z(i,o):o),this.topRules=O.topRules,this.dialects=O.dialects||{},this.dynamicPrecedences=O.dynamicPrecedences||null,this.tokenPrecTable=O.tokenPrec,this.termNames=O.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(O,a,t){let r=new ma(this,O,a,t);for(let s of this.wrappers)r=s(r,O,a,t);return r}getGoto(O,a,t=!1){let r=this.goto;if(a>=r[0])return-1;for(let s=r[a+1];;){let i=r[s++],o=i&1,n=r[s++];if(o&&t)return n;for(let Q=s+(i>>1);s0}validAction(O,a){return!!this.allActions(O,t=>t==a?!0:null)}allActions(O,a){let t=this.stateSlot(O,4),r=t?a(t):void 0;for(let s=this.stateSlot(O,1);r==null;s+=3){if(this.data[s]==65535)if(this.data[s+1]==1)s=w(this.data,s+2);else break;r=a(w(this.data,s+1))}return r}nextStates(O){let a=[];for(let t=this.stateSlot(O,1);;t+=3){if(this.data[t]==65535)if(this.data[t+1]==1)t=w(this.data,t+2);else break;if(!(this.data[t+2]&1)){let r=this.data[t+1];a.some((s,i)=>i&1&&s==r)||a.push(this.data[t],r)}}return a}configure(O){let a=Object.assign(Object.create(T.prototype),this);if(O.props&&(a.nodeSet=this.nodeSet.extend(...O.props)),O.top){let t=this.topRules[O.top];if(!t)throw new RangeError(`Invalid top rule name ${O.top}`);a.top=t}return O.tokenizers&&(a.tokenizers=this.tokenizers.map(t=>{let r=O.tokenizers.find(s=>s.from==t);return r?r.to:t})),O.specializers&&(a.specializers=this.specializers.slice(),a.specializerSpecs=this.specializerSpecs.map((t,r)=>{let s=O.specializers.find(o=>o.from==t.external);if(!s)return t;let i=Object.assign(Object.assign({},t),{external:s.to});return a.specializers[r]=te(i),i})),O.contextTracker&&(a.context=O.contextTracker),O.dialect&&(a.dialect=this.parseDialect(O.dialect)),O.strict!=null&&(a.strict=O.strict),O.wrap&&(a.wrappers=a.wrappers.concat(O.wrap)),O.bufferLength!=null&&(a.bufferLength=O.bufferLength),a}hasWrappers(){return this.wrappers.length>0}getName(O){return this.termNames?this.termNames[O]:String(O<=this.maxNode&&this.nodeSet.types[O].name||O)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(O){let a=this.dynamicPrecedences;return a==null?0:a[O]||0}parseDialect(O){let a=Object.keys(this.dialects),t=a.map(()=>!1);if(O)for(let s of O.split(" ")){let i=a.indexOf(s);i>=0&&(t[i]=!0)}let r=null;for(let s=0;st)&&a.p.parser.stateFlag(a.state,2)&&(!O||O.scoree.external(a,t)<<1|O}return e.get}const Za=54,ba=1,ka=55,xa=2,Xa=56,ya=3,ae=4,wa=5,nO=6,Ge=7,Ce=8,Ae=9,Ee=10,Ra=11,Ya=12,Ta=13,fO=57,Wa=14,re=58,Ne=20,_a=22,Me=23,qa=24,XO=26,Ie=27,va=28,Ua=31,za=34,Va=36,ja=37,Ga=0,Ca=1,Aa={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},Ea={dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},ie={dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}};function Na(e){return e==45||e==46||e==58||e>=65&&e<=90||e==95||e>=97&&e<=122||e>=161}function De(e){return e==9||e==10||e==13||e==32}let se=null,oe=null,le=0;function yO(e,O){let a=e.pos+O;if(le==a&&oe==e)return se;let t=e.peek(O);for(;De(t);)t=e.peek(++O);let r="";for(;Na(t);)r+=String.fromCharCode(t),t=e.peek(++O);return oe=e,le=a,se=r?r.toLowerCase():t==Ma||t==Ia?void 0:null}const Be=60,cO=62,zO=47,Ma=63,Ia=33,Da=45;function ne(e,O){this.name=e,this.parent=O}const Ba=[nO,Ee,Ge,Ce,Ae],Ja=new je({start:null,shift(e,O,a,t){return Ba.indexOf(O)>-1?new ne(yO(t,1)||"",e):e},reduce(e,O){return O==Ne&&e?e.parent:e},reuse(e,O,a,t){let r=O.type.id;return r==nO||r==Va?new ne(yO(t,1)||"",e):e},strict:!1}),La=new k((e,O)=>{if(e.next!=Be){e.next<0&&O.context&&e.acceptToken(fO);return}e.advance();let a=e.next==zO;a&&e.advance();let t=yO(e,0);if(t===void 0)return;if(!t)return e.acceptToken(a?Wa:nO);let r=O.context?O.context.name:null;if(a){if(t==r)return e.acceptToken(Ra);if(r&&Ea[r])return e.acceptToken(fO,-2);if(O.dialectEnabled(Ga))return e.acceptToken(Ya);for(let s=O.context;s;s=s.parent)if(s.name==t)return;e.acceptToken(Ta)}else{if(t=="script")return e.acceptToken(Ge);if(t=="style")return e.acceptToken(Ce);if(t=="textarea")return e.acceptToken(Ae);if(Aa.hasOwnProperty(t))return e.acceptToken(Ee);r&&ie[r]&&ie[r][t]?e.acceptToken(fO,-1):e.acceptToken(nO)}},{contextual:!0}),Ka=new k(e=>{for(let O=0,a=0;;a++){if(e.next<0){a&&e.acceptToken(re);break}if(e.next==Da)O++;else if(e.next==cO&&O>=2){a>=3&&e.acceptToken(re,-2);break}else O=0;e.advance()}});function Fa(e){for(;e;e=e.parent)if(e.name=="svg"||e.name=="math")return!0;return!1}const Ha=new k((e,O)=>{if(e.next==zO&&e.peek(1)==cO){let a=O.dialectEnabled(Ca)||Fa(O.context);e.acceptToken(a?wa:ae,2)}else e.next==cO&&e.acceptToken(ae,1)});function VO(e,O,a){let t=2+e.length;return new k(r=>{for(let s=0,i=0,o=0;;o++){if(r.next<0){o&&r.acceptToken(O);break}if(s==0&&r.next==Be||s==1&&r.next==zO||s>=2&&si?r.acceptToken(O,-i):r.acceptToken(a,-(i-2));break}else if((r.next==10||r.next==13)&&o){r.acceptToken(O,1);break}else s=i=0;r.advance()}})}const Or=VO("script",Za,ba),er=VO("style",ka,xa),tr=VO("textarea",Xa,ya),ar=B({"Text RawText":l.content,"StartTag StartCloseTag SelfClosingEndTag EndTag":l.angleBracket,TagName:l.tagName,"MismatchedCloseTag/TagName":[l.tagName,l.invalid],AttributeName:l.attributeName,"AttributeValue UnquotedAttributeValue":l.attributeValue,Is:l.definitionOperator,"EntityReference CharacterReference":l.character,Comment:l.blockComment,ProcessingInst:l.processingInstruction,DoctypeDecl:l.documentMeta}),rr=T.deserialize({version:14,states:",xOVO!rOOO!WQ#tO'#CqO!]Q#tO'#CzO!bQ#tO'#C}O!gQ#tO'#DQO!lQ#tO'#DSO!qOaO'#CpO!|ObO'#CpO#XOdO'#CpO$eO!rO'#CpOOO`'#Cp'#CpO$lO$fO'#DTO$tQ#tO'#DVO$yQ#tO'#DWOOO`'#Dk'#DkOOO`'#DY'#DYQVO!rOOO%OQ&rO,59]O%ZQ&rO,59fO%fQ&rO,59iO%qQ&rO,59lO%|Q&rO,59nOOOa'#D^'#D^O&XOaO'#CxO&dOaO,59[OOOb'#D_'#D_O&lObO'#C{O&wObO,59[OOOd'#D`'#D`O'POdO'#DOO'[OdO,59[OOO`'#Da'#DaO'dO!rO,59[O'kQ#tO'#DROOO`,59[,59[OOOp'#Db'#DbO'pO$fO,59oOOO`,59o,59oO'xQ#|O,59qO'}Q#|O,59rOOO`-E7W-E7WO(SQ&rO'#CsOOQW'#DZ'#DZO(bQ&rO1G.wOOOa1G.w1G.wOOO`1G/Y1G/YO(mQ&rO1G/QOOOb1G/Q1G/QO(xQ&rO1G/TOOOd1G/T1G/TO)TQ&rO1G/WOOO`1G/W1G/WO)`Q&rO1G/YOOOa-E7[-E7[O)kQ#tO'#CyOOO`1G.v1G.vOOOb-E7]-E7]O)pQ#tO'#C|OOOd-E7^-E7^O)uQ#tO'#DPOOO`-E7_-E7_O)zQ#|O,59mOOOp-E7`-E7`OOO`1G/Z1G/ZOOO`1G/]1G/]OOO`1G/^1G/^O*PQ,UO,59_OOQW-E7X-E7XOOOa7+$c7+$cOOO`7+$t7+$tOOOb7+$l7+$lOOOd7+$o7+$oOOO`7+$r7+$rO*[Q#|O,59eO*aQ#|O,59hO*fQ#|O,59kOOO`1G/X1G/XO*kO7[O'#CvO*|OMhO'#CvOOQW1G.y1G.yOOO`1G/P1G/POOO`1G/S1G/SOOO`1G/V1G/VOOOO'#D['#D[O+_O7[O,59bOOQW,59b,59bOOOO'#D]'#D]O+pOMhO,59bOOOO-E7Y-E7YOOQW1G.|1G.|OOOO-E7Z-E7Z",stateData:",]~O!^OS~OUSOVPOWQOXROYTO[]O][O^^O`^Oa^Ob^Oc^Ox^O{_O!dZO~OfaO~OfbO~OfcO~OfdO~OfeO~O!WfOPlP!ZlP~O!XiOQoP!ZoP~O!YlORrP!ZrP~OUSOVPOWQOXROYTOZqO[]O][O^^O`^Oa^Ob^Oc^Ox^O!dZO~O!ZrO~P#dO![sO!euO~OfvO~OfwO~OS|OT}OhyO~OS!POT}OhyO~OS!ROT}OhyO~OS!TOT}OhyO~OS}OT}OhyO~O!WfOPlX!ZlX~OP!WO!Z!XO~O!XiOQoX!ZoX~OQ!ZO!Z!XO~O!YlORrX!ZrX~OR!]O!Z!XO~O!Z!XO~P#dOf!_O~O![sO!e!aO~OS!bO~OS!cO~Oi!dOSgXTgXhgX~OS!fOT!gOhyO~OS!hOT!gOhyO~OS!iOT!gOhyO~OS!jOT!gOhyO~OS!gOT!gOhyO~Of!kO~Of!lO~Of!mO~OS!nO~Ok!qO!`!oO!b!pO~OS!rO~OS!sO~OS!tO~Oa!uOb!uOc!uO!`!wO!a!uO~Oa!xOb!xOc!xO!b!wO!c!xO~Oa!uOb!uOc!uO!`!{O!a!uO~Oa!xOb!xOc!xO!b!{O!c!xO~OT~bac!dx{!d~",goto:"%p!`PPPPPPPPPPPPPPPPPPPP!a!gP!mPP!yP!|#P#S#Y#]#`#f#i#l#r#x!aP!a!aP$O$U$l$r$x%O%U%[%bPPPPPPPP%hX^OX`pXUOX`pezabcde{!O!Q!S!UR!q!dRhUR!XhXVOX`pRkVR!XkXWOX`pRnWR!XnXXOX`pQrXR!XpXYOX`pQ`ORx`Q{aQ!ObQ!QcQ!SdQ!UeZ!e{!O!Q!S!UQ!v!oR!z!vQ!y!pR!|!yQgUR!VgQjVR!YjQmWR![mQpXR!^pQtZR!`tS_O`ToXp",nodeNames:"⚠ StartCloseTag StartCloseTag StartCloseTag EndTag SelfClosingEndTag StartTag StartTag StartTag StartTag StartTag StartCloseTag StartCloseTag StartCloseTag IncompleteCloseTag Document Text EntityReference CharacterReference InvalidEntity Element OpenTag TagName Attribute AttributeName Is AttributeValue UnquotedAttributeValue ScriptText CloseTag OpenTag StyleText CloseTag OpenTag TextareaText CloseTag OpenTag CloseTag SelfClosingTag Comment ProcessingInst MismatchedCloseTag CloseTag DoctypeDecl",maxTerm:67,context:Ja,nodeProps:[["closedBy",-10,1,2,3,7,8,9,10,11,12,13,"EndTag",6,"EndTag SelfClosingEndTag",-4,21,30,33,36,"CloseTag"],["openedBy",4,"StartTag StartCloseTag",5,"StartTag",-4,29,32,35,37,"OpenTag"],["group",-9,14,17,18,19,20,39,40,41,42,"Entity",16,"Entity TextContent",-3,28,31,34,"TextContent Entity"],["isolate",-11,21,29,30,32,33,35,36,37,38,41,42,"ltr",-3,26,27,39,""]],propSources:[ar],skippedNodes:[0],repeatNodeCount:9,tokenData:"!]tw8twx7Sx!P8t!P!Q5u!Q!]8t!]!^/^!^!a7S!a#S8t#S#T;{#T#s8t#s$f5u$f;'S8t;'S;=`>V<%l?Ah8t?Ah?BY5u?BY?Mn8t?MnO5u!Z5zbkWOX5uXZ7SZ[5u[^7S^p5uqr5urs7Sst+Ptw5uwx7Sx!]5u!]!^7w!^!a7S!a#S5u#S#T7S#T;'S5u;'S;=`8n<%lO5u!R7VVOp7Sqs7St!]7S!]!^7l!^;'S7S;'S;=`7q<%lO7S!R7qOa!R!R7tP;=`<%l7S!Z8OYkWa!ROX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+P!Z8qP;=`<%l5u!_8{ihSkWOX5uXZ7SZ[5u[^7S^p5uqr8trs7Sst/^tw8twx7Sx!P8t!P!Q5u!Q!]8t!]!^:j!^!a7S!a#S8t#S#T;{#T#s8t#s$f5u$f;'S8t;'S;=`>V<%l?Ah8t?Ah?BY5u?BY?Mn8t?MnO5u!_:sbhSkWa!ROX+PZ[+P^p+Pqr/^sw/^x!P/^!P!Q+P!Q!^/^!a#S/^#S#T0m#T#s/^#s$f+P$f;'S/^;'S;=`1e<%l?Ah/^?Ah?BY+P?BY?Mn/^?MnO+P!VP<%l?Ah;{?Ah?BY7S?BY?Mn;{?MnO7S!V=dXhSa!Rqr0msw0mx!P0m!Q!^0m!a#s0m$f;'S0m;'S;=`1_<%l?Ah0m?BY?Mn0m!V>SP;=`<%l;{!_>YP;=`<%l8t!_>dhhSkWOX@OXZAYZ[@O[^AY^p@OqrBwrsAYswBwwxAYx!PBw!P!Q@O!Q!]Bw!]!^/^!^!aAY!a#SBw#S#TE{#T#sBw#s$f@O$f;'SBw;'S;=`HS<%l?AhBw?Ah?BY@O?BY?MnBw?MnO@O!Z@TakWOX@OXZAYZ[@O[^AY^p@Oqr@OrsAYsw@OwxAYx!]@O!]!^Az!^!aAY!a#S@O#S#TAY#T;'S@O;'S;=`Bq<%lO@O!RA]UOpAYq!]AY!]!^Ao!^;'SAY;'S;=`At<%lOAY!RAtOb!R!RAwP;=`<%lAY!ZBRYkWb!ROX+PZ[+P^p+Pqr+Psw+Px!^+P!a#S+P#T;'S+P;'S;=`+t<%lO+P!ZBtP;=`<%l@O!_COhhSkWOX@OXZAYZ[@O[^AY^p@OqrBwrsAYswBwwxAYx!PBw!P!Q@O!Q!]Bw!]!^Dj!^!aAY!a#SBw#S#TE{#T#sBw#s$f@O$f;'SBw;'S;=`HS<%l?AhBw?Ah?BY@O?BY?MnBw?MnO@O!_DsbhSkWb!ROX+PZ[+P^p+Pqr/^sw/^x!P/^!P!Q+P!Q!^/^!a#S/^#S#T0m#T#s/^#s$f+P$f;'S/^;'S;=`1e<%l?Ah/^?Ah?BY+P?BY?Mn/^?MnO+P!VFQbhSOpAYqrE{rsAYswE{wxAYx!PE{!P!QAY!Q!]E{!]!^GY!^!aAY!a#sE{#s$fAY$f;'SE{;'S;=`G|<%l?AhE{?Ah?BYAY?BY?MnE{?MnOAY!VGaXhSb!Rqr0msw0mx!P0m!Q!^0m!a#s0m$f;'S0m;'S;=`1_<%l?Ah0m?BY?Mn0m!VHPP;=`<%lE{!_HVP;=`<%lBw!ZHcW!bx`P!a`Or(trs'ksv(tw!^(t!^!_)e!_;'S(t;'S;=`*P<%lO(t!aIYlhS`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx}-_}!OKQ!O!P-_!P!Q$q!Q!^-_!^!_*V!_!a&X!a#S-_#S#T1k#T#s-_#s$f$q$f;'S-_;'S;=`3X<%l?Ah-_?Ah?BY$q?BY?Mn-_?MnO$q!aK_khS`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx!P-_!P!Q$q!Q!^-_!^!_*V!_!`&X!`!aMS!a#S-_#S#T1k#T#s-_#s$f$q$f;'S-_;'S;=`3X<%l?Ah-_?Ah?BY$q?BY?Mn-_?MnO$q!TM_X`P!a`!cp!eQOr&Xrs&}sv&Xwx(tx!^&X!^!_*V!_;'S&X;'S;=`*y<%lO&X!aNZ!ZhSfQ`PkW!a`!cpOX$qXZ&XZ[$q[^&X^p$qpq&Xqr-_rs&}sv-_vw/^wx(tx}-_}!OMz!O!PMz!P!Q$q!Q![Mz![!]Mz!]!^-_!^!_*V!_!a&X!a!c-_!c!}Mz!}#R-_#R#SMz#S#T1k#T#oMz#o#s-_#s$f$q$f$}-_$}%OMz%O%W-_%W%oMz%o%p-_%p&aMz&a&b-_&b1pMz1p4UMz4U4dMz4d4e-_4e$ISMz$IS$I`-_$I`$IbMz$Ib$Je-_$Je$JgMz$Jg$Kh-_$Kh%#tMz%#t&/x-_&/x&EtMz&Et&FV-_&FV;'SMz;'S;:j!#|;:j;=`3X<%l?&r-_?&r?AhMz?Ah?BY$q?BY?MnMz?MnO$q!a!$PP;=`<%lMz!R!$ZY!a`!cpOq*Vqr!$yrs(Vsv*Vwx)ex!a*V!a!b!4t!b;'S*V;'S;=`*s<%lO*V!R!%Q]!a`!cpOr*Vrs(Vsv*Vwx)ex}*V}!O!%y!O!f*V!f!g!']!g#W*V#W#X!0`#X;'S*V;'S;=`*s<%lO*V!R!&QX!a`!cpOr*Vrs(Vsv*Vwx)ex}*V}!O!&m!O;'S*V;'S;=`*s<%lO*V!R!&vV!a`!cp!dPOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!'dX!a`!cpOr*Vrs(Vsv*Vwx)ex!q*V!q!r!(P!r;'S*V;'S;=`*s<%lO*V!R!(WX!a`!cpOr*Vrs(Vsv*Vwx)ex!e*V!e!f!(s!f;'S*V;'S;=`*s<%lO*V!R!(zX!a`!cpOr*Vrs(Vsv*Vwx)ex!v*V!v!w!)g!w;'S*V;'S;=`*s<%lO*V!R!)nX!a`!cpOr*Vrs(Vsv*Vwx)ex!{*V!{!|!*Z!|;'S*V;'S;=`*s<%lO*V!R!*bX!a`!cpOr*Vrs(Vsv*Vwx)ex!r*V!r!s!*}!s;'S*V;'S;=`*s<%lO*V!R!+UX!a`!cpOr*Vrs(Vsv*Vwx)ex!g*V!g!h!+q!h;'S*V;'S;=`*s<%lO*V!R!+xY!a`!cpOr!+qrs!,hsv!+qvw!-Swx!.[x!`!+q!`!a!/j!a;'S!+q;'S;=`!0Y<%lO!+qq!,mV!cpOv!,hvx!-Sx!`!,h!`!a!-q!a;'S!,h;'S;=`!.U<%lO!,hP!-VTO!`!-S!`!a!-f!a;'S!-S;'S;=`!-k<%lO!-SP!-kO{PP!-nP;=`<%l!-Sq!-xS!cp{POv(Vx;'S(V;'S;=`(h<%lO(Vq!.XP;=`<%l!,ha!.aX!a`Or!.[rs!-Ssv!.[vw!-Sw!`!.[!`!a!.|!a;'S!.[;'S;=`!/d<%lO!.[a!/TT!a`{POr)esv)ew;'S)e;'S;=`)y<%lO)ea!/gP;=`<%l!.[!R!/sV!a`!cp{POr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!0]P;=`<%l!+q!R!0gX!a`!cpOr*Vrs(Vsv*Vwx)ex#c*V#c#d!1S#d;'S*V;'S;=`*s<%lO*V!R!1ZX!a`!cpOr*Vrs(Vsv*Vwx)ex#V*V#V#W!1v#W;'S*V;'S;=`*s<%lO*V!R!1}X!a`!cpOr*Vrs(Vsv*Vwx)ex#h*V#h#i!2j#i;'S*V;'S;=`*s<%lO*V!R!2qX!a`!cpOr*Vrs(Vsv*Vwx)ex#m*V#m#n!3^#n;'S*V;'S;=`*s<%lO*V!R!3eX!a`!cpOr*Vrs(Vsv*Vwx)ex#d*V#d#e!4Q#e;'S*V;'S;=`*s<%lO*V!R!4XX!a`!cpOr*Vrs(Vsv*Vwx)ex#X*V#X#Y!+q#Y;'S*V;'S;=`*s<%lO*V!R!4{Y!a`!cpOr!4trs!5ksv!4tvw!6Vwx!8]x!a!4t!a!b!:]!b;'S!4t;'S;=`!;r<%lO!4tq!5pV!cpOv!5kvx!6Vx!a!5k!a!b!7W!b;'S!5k;'S;=`!8V<%lO!5kP!6YTO!a!6V!a!b!6i!b;'S!6V;'S;=`!7Q<%lO!6VP!6lTO!`!6V!`!a!6{!a;'S!6V;'S;=`!7Q<%lO!6VP!7QOxPP!7TP;=`<%l!6Vq!7]V!cpOv!5kvx!6Vx!`!5k!`!a!7r!a;'S!5k;'S;=`!8V<%lO!5kq!7yS!cpxPOv(Vx;'S(V;'S;=`(h<%lO(Vq!8YP;=`<%l!5ka!8bX!a`Or!8]rs!6Vsv!8]vw!6Vw!a!8]!a!b!8}!b;'S!8];'S;=`!:V<%lO!8]a!9SX!a`Or!8]rs!6Vsv!8]vw!6Vw!`!8]!`!a!9o!a;'S!8];'S;=`!:V<%lO!8]a!9vT!a`xPOr)esv)ew;'S)e;'S;=`)y<%lO)ea!:YP;=`<%l!8]!R!:dY!a`!cpOr!4trs!5ksv!4tvw!6Vwx!8]x!`!4t!`!a!;S!a;'S!4t;'S;=`!;r<%lO!4t!R!;]V!a`!cpxPOr*Vrs(Vsv*Vwx)ex;'S*V;'S;=`*s<%lO*V!R!;uP;=`<%l!4t!V!{let Q=o.type.id;if(Q==va)return $O(o,n,a);if(Q==Ua)return $O(o,n,t);if(Q==za)return $O(o,n,r);if(Q==Ne&&s.length){let u=o.node,c=u.firstChild,f=c&&ce(c,n),h;if(f){for(let d of s)if(d.tag==f&&(!d.attrs||d.attrs(h||(h=Je(c,n))))){let P=u.lastChild,m=P.type.id==ja?P.from:u.to;if(m>c.to)return{parser:d.parser,overlay:[{from:c.to,to:m}]}}}}if(i&&Q==Me){let u=o.node,c;if(c=u.firstChild){let f=i[n.read(c.from,c.to)];if(f)for(let h of f){if(h.tagName&&h.tagName!=ce(u.parent,n))continue;let d=u.lastChild;if(d.type.id==XO){let P=d.from+1,m=d.lastChild,x=d.to-(m&&m.isError?0:1);if(x>P)return{parser:h.parser,overlay:[{from:P,to:x}]}}else if(d.type.id==Ie)return{parser:h.parser,overlay:[{from:d.from,to:d.to}]}}}}return null})}const ir=99,Qe=1,sr=100,or=101,he=2,Ke=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],lr=58,nr=40,Fe=95,cr=91,rO=45,Qr=46,hr=35,ur=37,pr=38,dr=92,fr=10;function M(e){return e>=65&&e<=90||e>=97&&e<=122||e>=161}function He(e){return e>=48&&e<=57}const $r=new k((e,O)=>{for(let a=!1,t=0,r=0;;r++){let{next:s}=e;if(M(s)||s==rO||s==Fe||a&&He(s))!a&&(s!=rO||r>0)&&(a=!0),t===r&&s==rO&&t++,e.advance();else if(s==dr&&e.peek(1)!=fr)e.advance(),e.next>-1&&e.advance(),a=!0;else{a&&e.acceptToken(s==nr?sr:t==2&&O.canShift(he)?he:or);break}}}),Pr=new k(e=>{if(Ke.includes(e.peek(-1))){let{next:O}=e;(M(O)||O==Fe||O==hr||O==Qr||O==cr||O==lr&&M(e.peek(1))||O==rO||O==pr)&&e.acceptToken(ir)}}),mr=new k(e=>{if(!Ke.includes(e.peek(-1))){let{next:O}=e;if(O==ur&&(e.advance(),e.acceptToken(Qe)),M(O)){do e.advance();while(M(e.next)||He(e.next));e.acceptToken(Qe)}}}),gr=B({"AtKeyword import charset namespace keyframes media supports":l.definitionKeyword,"from to selector":l.keyword,NamespaceName:l.namespace,KeyframeName:l.labelName,KeyframeRangeName:l.operatorKeyword,TagName:l.tagName,ClassName:l.className,PseudoClassName:l.constant(l.className),IdName:l.labelName,"FeatureName PropertyName":l.propertyName,AttributeName:l.attributeName,NumberLiteral:l.number,KeywordQuery:l.keyword,UnaryQueryOp:l.operatorKeyword,"CallTag ValueName":l.atom,VariableName:l.variableName,Callee:l.operatorKeyword,Unit:l.unit,"UniversalSelector NestingSelector":l.definitionOperator,MatchOp:l.compareOperator,"ChildOp SiblingOp, LogicOp":l.logicOperator,BinOp:l.arithmeticOperator,Important:l.modifier,Comment:l.blockComment,ColorLiteral:l.color,"ParenthesizedContent StringLiteral":l.string,":":l.punctuation,"PseudoOp #":l.derefOperator,"; ,":l.separator,"( )":l.paren,"[ ]":l.squareBracket,"{ }":l.brace}),Sr={__proto__:null,lang:32,"nth-child":32,"nth-last-child":32,"nth-of-type":32,"nth-last-of-type":32,dir:32,"host-context":32,url:60,"url-prefix":60,domain:60,regexp:60,selector:138},Zr={__proto__:null,"@import":118,"@media":142,"@charset":146,"@namespace":150,"@keyframes":156,"@supports":168},br={__proto__:null,not:132,only:132},kr=T.deserialize({version:14,states:":^QYQ[OOO#_Q[OOP#fOWOOOOQP'#Cd'#CdOOQP'#Cc'#CcO#kQ[O'#CfO$_QXO'#CaO$fQ[O'#ChO$qQ[O'#DTO$vQ[O'#DWOOQP'#Em'#EmO${QdO'#DgO%jQ[O'#DtO${QdO'#DvO%{Q[O'#DxO&WQ[O'#D{O&`Q[O'#ERO&nQ[O'#ETOOQS'#El'#ElOOQS'#EW'#EWQYQ[OOO&uQXO'#CdO'jQWO'#DcO'oQWO'#EsO'zQ[O'#EsQOQWOOP(UO#tO'#C_POOO)C@[)C@[OOQP'#Cg'#CgOOQP,59Q,59QO#kQ[O,59QO(aQ[O'#E[O({QWO,58{O)TQ[O,59SO$qQ[O,59oO$vQ[O,59rO(aQ[O,59uO(aQ[O,59wO(aQ[O,59xO)`Q[O'#DbOOQS,58{,58{OOQP'#Ck'#CkOOQO'#DR'#DROOQP,59S,59SO)gQWO,59SO)lQWO,59SOOQP'#DV'#DVOOQP,59o,59oOOQO'#DX'#DXO)qQ`O,59rOOQS'#Cp'#CpO${QdO'#CqO)yQvO'#CsO+ZQtO,5:ROOQO'#Cx'#CxO)lQWO'#CwO+oQWO'#CyO+tQ[O'#DOOOQS'#Ep'#EpOOQO'#Dj'#DjO+|Q[O'#DqO,[QWO'#EtO&`Q[O'#DoO,jQWO'#DrOOQO'#Eu'#EuO)OQWO,5:`O,oQpO,5:bOOQS'#Dz'#DzO,wQWO,5:dO,|Q[O,5:dOOQO'#D}'#D}O-UQWO,5:gO-ZQWO,5:mO-cQWO,5:oOOQS-E8U-E8UO${QdO,59}O-kQ[O'#E^O-xQWO,5;_O-xQWO,5;_POOO'#EV'#EVP.TO#tO,58yPOOO,58y,58yOOQP1G.l1G.lO.zQXO,5:vOOQO-E8Y-E8YOOQS1G.g1G.gOOQP1G.n1G.nO)gQWO1G.nO)lQWO1G.nOOQP1G/Z1G/ZO/XQ`O1G/^O/rQXO1G/aO0YQXO1G/cO0pQXO1G/dO1WQWO,59|O1]Q[O'#DSO1dQdO'#CoOOQP1G/^1G/^O${QdO1G/^O1kQpO,59]OOQS,59_,59_O${QdO,59aO1sQWO1G/mOOQS,59c,59cO1xQ!bO,59eOOQS'#DP'#DPOOQS'#EY'#EYO2QQ[O,59jOOQS,59j,59jO2YQWO'#DjO2eQWO,5:VO2jQWO,5:]O&`Q[O,5:XO&`Q[O'#E_O2rQWO,5;`O2}QWO,5:ZO(aQ[O,5:^OOQS1G/z1G/zOOQS1G/|1G/|OOQS1G0O1G0OO3`QWO1G0OO3eQdO'#EOOOQS1G0R1G0ROOQS1G0X1G0XOOQS1G0Z1G0ZO3pQtO1G/iOOQO,5:x,5:xO4WQ[O,5:xOOQO-E8[-E8[O4eQWO1G0yPOOO-E8T-E8TPOOO1G.e1G.eOOQP7+$Y7+$YOOQP7+$x7+$xO${QdO7+$xOOQS1G/h1G/hO4pQXO'#ErO4wQWO,59nO4|QtO'#EXO5tQdO'#EoO6OQWO,59ZO6TQpO7+$xOOQS1G.w1G.wOOQS1G.{1G.{OOQS7+%X7+%XO6]QWO1G/POOQS-E8W-E8WOOQS1G/U1G/UO${QdO1G/qOOQO1G/w1G/wOOQO1G/s1G/sO6bQWO,5:yOOQO-E8]-E8]O6pQXO1G/xOOQS7+%j7+%jO6wQYO'#CsOOQO'#EQ'#EQO7SQ`O'#EPOOQO'#EP'#EPO7_QWO'#E`O7gQdO,5:jOOQS,5:j,5:jO7rQtO'#E]O${QdO'#E]O8sQdO7+%TOOQO7+%T7+%TOOQO1G0d1G0dO9WQpO<OAN>OO:xQdO,5:uOOQO-E8X-E8XOOQO<T![;'S%^;'S;=`%o<%lO%^l;TUo`Oy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^l;nYo`#e[Oy%^z!Q%^!Q![;g![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^l[[o`#e[Oy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^n?VSt^Oy%^z;'S%^;'S;=`%o<%lO%^l?hWjWOy%^z!O%^!O!P;O!P!Q%^!Q![>T![;'S%^;'S;=`%o<%lO%^n@VU#bQOy%^z!Q%^!Q![;g![;'S%^;'S;=`%o<%lO%^~@nTjWOy%^z{@}{;'S%^;'S;=`%o<%lO%^~AUSo`#[~Oy%^z;'S%^;'S;=`%o<%lO%^lAg[#e[Oy%^z!O%^!O!P;g!P!Q%^!Q![>T![!g%^!g!h<^!h#X%^#X#Y<^#Y;'S%^;'S;=`%o<%lO%^bBbU]QOy%^z![%^![!]Bt!];'S%^;'S;=`%o<%lO%^bB{S^Qo`Oy%^z;'S%^;'S;=`%o<%lO%^nC^S!Y^Oy%^z;'S%^;'S;=`%o<%lO%^dCoS|SOy%^z;'S%^;'S;=`%o<%lO%^bDQU!OQOy%^z!`%^!`!aDd!a;'S%^;'S;=`%o<%lO%^bDkS!OQo`Oy%^z;'S%^;'S;=`%o<%lO%^bDzWOy%^z!c%^!c!}Ed!}#T%^#T#oEd#o;'S%^;'S;=`%o<%lO%^bEk[![Qo`Oy%^z}%^}!OEd!O!Q%^!Q![Ed![!c%^!c!}Ed!}#T%^#T#oEd#o;'S%^;'S;=`%o<%lO%^nFfSq^Oy%^z;'S%^;'S;=`%o<%lO%^nFwSp^Oy%^z;'S%^;'S;=`%o<%lO%^bGWUOy%^z#b%^#b#cGj#c;'S%^;'S;=`%o<%lO%^bGoUo`Oy%^z#W%^#W#XHR#X;'S%^;'S;=`%o<%lO%^bHYS!bQo`Oy%^z;'S%^;'S;=`%o<%lO%^bHiUOy%^z#f%^#f#gHR#g;'S%^;'S;=`%o<%lO%^fIQS!TUOy%^z;'S%^;'S;=`%o<%lO%^nIcS!S^Oy%^z;'S%^;'S;=`%o<%lO%^fItU!RQOy%^z!_%^!_!`6y!`;'S%^;'S;=`%o<%lO%^`JZP;=`<%l$}",tokenizers:[Pr,mr,$r,1,2,3,4,new lO("m~RRYZ[z{a~~g~aO#^~~dP!P!Qg~lO#_~~",28,105)],topRules:{StyleSheet:[0,4],Styles:[1,86]},specialized:[{term:100,get:e=>Sr[e]||-1},{term:58,get:e=>Zr[e]||-1},{term:101,get:e=>br[e]||-1}],tokenPrec:1200});let PO=null;function mO(){if(!PO&&typeof document=="object"&&document.body){let{style:e}=document.body,O=[],a=new Set;for(let t in e)t!="cssText"&&t!="cssFloat"&&typeof e[t]=="string"&&(/[A-Z]/.test(t)&&(t=t.replace(/[A-Z]/g,r=>"-"+r.toLowerCase())),a.has(t)||(O.push(t),a.add(t)));PO=O.sort().map(t=>({type:"property",label:t}))}return PO||[]}const ue=["active","after","any-link","autofill","backdrop","before","checked","cue","default","defined","disabled","empty","enabled","file-selector-button","first","first-child","first-letter","first-line","first-of-type","focus","focus-visible","focus-within","fullscreen","has","host","host-context","hover","in-range","indeterminate","invalid","is","lang","last-child","last-of-type","left","link","marker","modal","not","nth-child","nth-last-child","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","part","placeholder","placeholder-shown","read-only","read-write","required","right","root","scope","selection","slotted","target","target-text","valid","visited","where"].map(e=>({type:"class",label:e})),pe=["above","absolute","activeborder","additive","activecaption","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","antialiased","appworkspace","asterisks","attr","auto","auto-flow","avoid","avoid-column","avoid-page","avoid-region","axis-pan","background","backwards","baseline","below","bidi-override","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","clear","clip","close-quote","col-resize","collapse","color","color-burn","color-dodge","column","column-reverse","compact","condensed","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","darken","dashed","decimal","decimal-leading-zero","default","default-button","dense","destination-atop","destination-in","destination-out","destination-over","difference","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic-abegede-gez","ethiopic-halehame-aa-er","ethiopic-halehame-gez","ew-resize","exclusion","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fill-box","fixed","flat","flex","flex-end","flex-start","footnotes","forwards","from","geometricPrecision","graytext","grid","groove","hand","hard-light","help","hidden","hide","higher","highlight","highlighttext","horizontal","hsl","hsla","hue","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-grid","inline-table","inset","inside","intrinsic","invert","italic","justify","keep-all","landscape","large","larger","left","level","lighter","lighten","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-hexadecimal","lower-latin","lower-norwegian","lowercase","ltr","luminosity","manipulation","match","matrix","matrix3d","medium","menu","menutext","message-box","middle","min-intrinsic","mix","monospace","move","multiple","multiple_mask_images","multiply","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","opacity","open-quote","optimizeLegibility","optimizeSpeed","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","perspective","pinch-zoom","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row","row-resize","row-reverse","rtl","run-in","running","s-resize","sans-serif","saturation","scale","scale3d","scaleX","scaleY","scaleZ","screen","scroll","scrollbar","scroll-position","se-resize","self-start","self-end","semi-condensed","semi-expanded","separate","serif","show","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","soft-light","solid","source-atop","source-in","source-out","source-over","space","space-around","space-between","space-evenly","spell-out","square","start","static","status-bar","stretch","stroke","stroke-box","sub","subpixel-antialiased","svg_masks","super","sw-resize","symbolic","symbols","system-ui","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","text","text-bottom","text-top","textarea","textfield","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","to","top","transform","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","unidirectional-pan","unset","up","upper-latin","uppercase","url","var","vertical","vertical-text","view-box","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","wrap","wrap-reverse","x-large","x-small","xor","xx-large","xx-small"].map(e=>({type:"keyword",label:e})).concat(["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"].map(e=>({type:"constant",label:e}))),xr=["a","abbr","address","article","aside","b","bdi","bdo","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","dd","del","details","dfn","dialog","div","dl","dt","em","figcaption","figure","footer","form","header","hgroup","h1","h2","h3","h4","h5","h6","hr","html","i","iframe","img","input","ins","kbd","label","legend","li","main","meter","nav","ol","output","p","pre","ruby","section","select","small","source","span","strong","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","tr","u","ul"].map(e=>({type:"type",label:e})),Y=/^(\w[\w-]*|-\w[\w-]*|)$/,Xr=/^-(-[\w-]*)?$/;function yr(e,O){var a;if((e.name=="("||e.type.isError)&&(e=e.parent||e),e.name!="ArgList")return!1;let t=(a=e.parent)===null||a===void 0?void 0:a.firstChild;return(t==null?void 0:t.name)!="Callee"?!1:O.sliceString(t.from,t.to)=="var"}const de=new _e,wr=["Declaration"];function Rr(e){for(let O=e;;){if(O.type.isTop)return O;if(!(O=O.parent))return e}}function Ot(e,O,a){if(O.to-O.from>4096){let t=de.get(O);if(t)return t;let r=[],s=new Set,i=O.cursor(vO.IncludeAnonymous);if(i.firstChild())do for(let o of Ot(e,i.node,a))s.has(o.label)||(s.add(o.label),r.push(o));while(i.nextSibling());return de.set(O,r),r}else{let t=[],r=new Set;return O.cursor().iterate(s=>{var i;if(a(s)&&s.matchContext(wr)&&((i=s.node.nextSibling)===null||i===void 0?void 0:i.name)==":"){let o=e.sliceString(s.from,s.to);r.has(o)||(r.add(o),t.push({label:o,type:"variable"}))}}),t}}const Yr=e=>O=>{let{state:a,pos:t}=O,r=G(a).resolveInner(t,-1),s=r.type.isError&&r.from==r.to-1&&a.doc.sliceString(r.from,r.to)=="-";if(r.name=="PropertyName"||(s||r.name=="TagName")&&/^(Block|Styles)$/.test(r.resolve(r.to).name))return{from:r.from,options:mO(),validFor:Y};if(r.name=="ValueName")return{from:r.from,options:pe,validFor:Y};if(r.name=="PseudoClassName")return{from:r.from,options:ue,validFor:Y};if(e(r)||(O.explicit||s)&&yr(r,a.doc))return{from:e(r)||s?r.from:t,options:Ot(a.doc,Rr(r),e),validFor:Xr};if(r.name=="TagName"){for(let{parent:n}=r;n;n=n.parent)if(n.name=="Block")return{from:r.from,options:mO(),validFor:Y};return{from:r.from,options:xr,validFor:Y}}if(!O.explicit)return null;let i=r.resolve(t),o=i.childBefore(t);return o&&o.name==":"&&i.name=="PseudoClassSelector"?{from:t,options:ue,validFor:Y}:o&&o.name==":"&&i.name=="Declaration"||i.name=="ArgList"?{from:t,options:pe,validFor:Y}:i.name=="Block"||i.name=="Styles"?{from:t,options:mO(),validFor:Y}:null},Tr=Yr(e=>e.name=="VariableName"),QO=J.define({name:"css",parser:kr.configure({props:[L.add({Declaration:v()}),K.add({"Block KeyframeList":UO})]}),languageData:{commentTokens:{block:{open:"/*",close:"*/"}},indentOnInput:/^\s*\}$/,wordChars:"-"}});function Wr(){return new F(QO,QO.data.of({autocomplete:Tr}))}const _r=312,fe=1,qr=2,vr=3,Ur=4,zr=313,Vr=315,jr=316,Gr=5,Cr=6,Ar=0,wO=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],et=125,Er=59,RO=47,Nr=42,Mr=43,Ir=45,Dr=60,Br=44,Jr=63,Lr=46,Kr=new je({start:!1,shift(e,O){return O==Gr||O==Cr||O==Vr?e:O==jr},strict:!1}),Fr=new k((e,O)=>{let{next:a}=e;(a==et||a==-1||O.context)&&e.acceptToken(zr)},{contextual:!0,fallback:!0}),Hr=new k((e,O)=>{let{next:a}=e,t;wO.indexOf(a)>-1||a==RO&&((t=e.peek(1))==RO||t==Nr)||a!=et&&a!=Er&&a!=-1&&!O.context&&e.acceptToken(_r)},{contextual:!0}),Oi=new k((e,O)=>{let{next:a}=e;if(a==Mr||a==Ir){if(e.advance(),a==e.next){e.advance();let t=!O.context&&O.canShift(fe);e.acceptToken(t?fe:qr)}}else a==Jr&&e.peek(1)==Lr&&(e.advance(),e.advance(),(e.next<48||e.next>57)&&e.acceptToken(vr))},{contextual:!0});function gO(e,O){return e>=65&&e<=90||e>=97&&e<=122||e==95||e>=192||!O&&e>=48&&e<=57}const ei=new k((e,O)=>{if(e.next!=Dr||!O.dialectEnabled(Ar)||(e.advance(),e.next==RO))return;let a=0;for(;wO.indexOf(e.next)>-1;)e.advance(),a++;if(gO(e.next,!0)){for(e.advance(),a++;gO(e.next,!1);)e.advance(),a++;for(;wO.indexOf(e.next)>-1;)e.advance(),a++;if(e.next==Br)return;for(let t=0;;t++){if(t==7){if(!gO(e.next,!0))return;break}if(e.next!="extends".charCodeAt(t))break;e.advance(),a++}}e.acceptToken(Ur,-a)}),ti=B({"get set async static":l.modifier,"for while do if else switch try catch finally return throw break continue default case":l.controlKeyword,"in of await yield void typeof delete instanceof":l.operatorKeyword,"let var const using function class extends":l.definitionKeyword,"import export from":l.moduleKeyword,"with debugger as new":l.keyword,TemplateString:l.special(l.string),super:l.atom,BooleanLiteral:l.bool,this:l.self,null:l.null,Star:l.modifier,VariableName:l.variableName,"CallExpression/VariableName TaggedTemplateExpression/VariableName":l.function(l.variableName),VariableDefinition:l.definition(l.variableName),Label:l.labelName,PropertyName:l.propertyName,PrivatePropertyName:l.special(l.propertyName),"CallExpression/MemberExpression/PropertyName":l.function(l.propertyName),"FunctionDeclaration/VariableDefinition":l.function(l.definition(l.variableName)),"ClassDeclaration/VariableDefinition":l.definition(l.className),PropertyDefinition:l.definition(l.propertyName),PrivatePropertyDefinition:l.definition(l.special(l.propertyName)),UpdateOp:l.updateOperator,"LineComment Hashbang":l.lineComment,BlockComment:l.blockComment,Number:l.number,String:l.string,Escape:l.escape,ArithOp:l.arithmeticOperator,LogicOp:l.logicOperator,BitOp:l.bitwiseOperator,CompareOp:l.compareOperator,RegExp:l.regexp,Equals:l.definitionOperator,Arrow:l.function(l.punctuation),": Spread":l.punctuation,"( )":l.paren,"[ ]":l.squareBracket,"{ }":l.brace,"InterpolationStart InterpolationEnd":l.special(l.brace),".":l.derefOperator,", ;":l.separator,"@":l.meta,TypeName:l.typeName,TypeDefinition:l.definition(l.typeName),"type enum interface implements namespace module declare":l.definitionKeyword,"abstract global Privacy readonly override":l.modifier,"is keyof unique infer":l.operatorKeyword,JSXAttributeValue:l.attributeValue,JSXText:l.content,"JSXStartTag JSXStartCloseTag JSXSelfCloseEndTag JSXEndTag":l.angleBracket,"JSXIdentifier JSXNameSpacedName":l.tagName,"JSXAttribute/JSXIdentifier JSXAttribute/JSXNameSpacedName":l.attributeName,"JSXBuiltin/JSXIdentifier":l.standard(l.tagName)}),ai={__proto__:null,export:20,as:25,from:33,default:36,async:41,function:42,extends:54,this:58,true:66,false:66,null:78,void:82,typeof:86,super:102,new:136,delete:148,yield:157,await:161,class:166,public:229,private:229,protected:229,readonly:231,instanceof:250,satisfies:253,in:254,const:256,import:290,keyof:345,unique:349,infer:355,is:391,abstract:411,implements:413,type:415,let:418,var:420,using:423,interface:429,enum:433,namespace:439,module:441,declare:445,global:449,for:468,of:477,while:480,with:484,do:488,if:492,else:494,switch:498,case:504,try:510,catch:514,finally:518,return:522,throw:526,break:530,continue:534,debugger:538},ri={__proto__:null,async:123,get:125,set:127,declare:189,public:191,private:191,protected:191,static:193,abstract:195,override:197,readonly:203,accessor:205,new:395},ii={__proto__:null,"<":187},si=T.deserialize({version:14,states:"$@QO%TQ^OOO%[Q^OOO'_Q`OOP(lOWOOO*zQ?NdO'#CiO+RO!bO'#CjO+aO#tO'#CjO+oO!0LbO'#D^O.QQ^O'#DdO.bQ^O'#DoO%[Q^O'#DwO0fQ^O'#EPOOQ?Mr'#EX'#EXO1PQWO'#EUOOQO'#Em'#EmOOQO'#Ih'#IhO1XQWO'#GpO1dQWO'#ElO1iQWO'#ElO3hQ?NdO'#JmO6[Q?NdO'#JnO6uQWO'#F[O6zQ&jO'#FsOOQ?Mr'#Fe'#FeO7VO,YO'#FeO7eQ7[O'#FzO9RQWO'#FyOOQ?Mr'#Jn'#JnOOQ?Mp'#Jm'#JmO9WQWO'#GtOOQU'#KZ'#KZO9cQWO'#IUO9hQ?MxO'#IVOOQU'#JZ'#JZOOQU'#IZ'#IZQ`Q^OOO`Q^OOO9pQMnO'#DsO9wQ^O'#D{O:OQ^O'#D}O9^QWO'#GpO:VQ7[O'#CoO:eQWO'#EkO:pQWO'#EvO:uQ7[O'#FdO;dQWO'#GpOOQO'#K['#K[O;iQWO'#K[O;wQWO'#GxO;wQWO'#GyO;wQWO'#G{O9^QWO'#HOOVQWO'#CeO>gQWO'#H_O>oQWO'#HeO>oQWO'#HgO`Q^O'#HiO>oQWO'#HkO>oQWO'#HnO>tQWO'#HtO>yQ?MyO'#HzO%[Q^O'#H|O?UQ?MyO'#IOO?aQ?MyO'#IQO9hQ?MxO'#ISO?lQ?NdO'#CiO@nQ`O'#DiQOQWOOO%[Q^O'#D}OAUQWO'#EQO:VQ7[O'#EkOAaQWO'#EkOAlQpO'#FdOOQU'#Cg'#CgOOQ?Mp'#Dn'#DnOOQ?Mp'#Jq'#JqO%[Q^O'#JqOOQO'#Jt'#JtOOQO'#Id'#IdOBlQ`O'#EdOOQ?Mp'#Ec'#EcOOQ?Mp'#Jx'#JxOChQ?NQO'#EdOCrQ`O'#ETOOQO'#Js'#JsODWQ`O'#JtOEeQ`O'#ETOCrQ`O'#EdPErO#@ItO'#CbPOOO)CDx)CDxOOOO'#I['#I[OE}O!bO,59UOOQ?Mr,59U,59UOOOO'#I]'#I]OF]O#tO,59UO%[Q^O'#D`OOOO'#I_'#I_OFkO!0LbO,59xOOQ?Mr,59x,59xOFyQ^O'#I`OG^QWO'#JoOI]QrO'#JoO+}Q^O'#JoOIdQWO,5:OOIzQWO'#EmOJXQWO'#KOOJdQWO'#J}OJdQWO'#J}OJlQWO,5;ZOJqQWO'#J|OOQ?Mv,5:Z,5:ZOJxQ^O,5:ZOLvQ?NdO,5:cOMgQWO,5:kONQQ?MxO'#J{ONXQWO'#JzO9WQWO'#JzONmQWO'#JzONuQWO,5;YONzQWO'#JzO!#PQrO'#JnOOQ?Mr'#Ci'#CiO%[Q^O'#EPO!#oQrO,5:pOOQQ'#Ju'#JuOOQO-EpOOQU'#Jc'#JcOOQU,5>q,5>qOOQU-EtQWO'#HTO9^QWO'#HVO!DgQWO'#HVO:VQ7[O'#HXO!DlQWO'#HXOOQU,5=m,5=mO!DqQWO'#HYO!ESQWO'#CoO!EXQWO,59PO!EcQWO,59PO!GhQ^O,59POOQU,59P,59PO!GxQ?MxO,59PO%[Q^O,59PO!JTQ^O'#HaOOQU'#Hb'#HbOOQU'#Hc'#HcO`Q^O,5=yO!JkQWO,5=yO`Q^O,5>PO`Q^O,5>RO!JpQWO,5>TO`Q^O,5>VO!JuQWO,5>YO!JzQ^O,5>`OOQU,5>f,5>fO%[Q^O,5>fO9hQ?MxO,5>hOOQU,5>j,5>jO# UQWO,5>jOOQU,5>l,5>lO# UQWO,5>lOOQU,5>n,5>nO# rQ`O'#D[O%[Q^O'#JqO# |Q`O'#JqO#!kQ`O'#DjO#!|Q`O'#DjO#%_Q^O'#DjO#%fQWO'#JpO#%nQWO,5:TO#%sQWO'#EqO#&RQWO'#KPO#&ZQWO,5;[O#&`Q`O'#DjO#&mQ`O'#ESOOQ?Mr,5:l,5:lO%[Q^O,5:lO#&tQWO,5:lO>tQWO,5;VO!A}Q`O,5;VO!BVQ7[O,5;VO:VQ7[O,5;VO#&|QWO,5@]O#'RQ(CYO,5:pOOQO-EzO+}Q^O,5>zOOQO,5?Q,5?QO#*ZQ^O'#I`OOQO-E<^-E<^O#*hQWO,5@ZO#*pQrO,5@ZO#*wQWO,5@iOOQ?Mr1G/j1G/jO%[Q^O,5@jO#+PQWO'#IfOOQO-EuQ?NdO1G0|O#>|Q?NdO1G0|O#AZQ07bO'#CiO#CUQ07bO1G1_O#C]Q07bO'#JnO#CpQ?NdO,5?WOOQ?Mp-EoQWO1G3oO$3VQ^O1G3qO$7ZQ^O'#HpOOQU1G3t1G3tO$7hQWO'#HvO>tQWO'#HxOOQU1G3z1G3zO$7pQ^O1G3zO9hQ?MxO1G4QOOQU1G4S1G4SOOQ?Mp'#G]'#G]O9hQ?MxO1G4UO9hQ?MxO1G4WO$;wQWO,5@]O!(oQ^O,5;]O9WQWO,5;]O>tQWO,5:UO!(oQ^O,5:UO!A}Q`O,5:UO$;|Q07bO,5:UOOQO,5;],5;]O$tQWO1G0qO!A}Q`O1G0qO!BVQ7[O1G0qOOQ?Mp1G5w1G5wO!ArQ?MxO1G0ZOOQO1G0j1G0jO%[Q^O1G0jO$=aQ?MxO1G0jO$=lQ?MxO1G0jO!A}Q`O1G0ZOCrQ`O1G0ZO$=zQ?MxO1G0jOOQO1G0Z1G0ZO$>`Q?NdO1G0jPOOO-EjQpO,5rQrO1G4fOOQO1G4l1G4lO%[Q^O,5>zO$>|QWO1G5uO$?UQWO1G6TO$?^QrO1G6UO9WQWO,5?QO$?hQ?NdO1G6RO%[Q^O1G6RO$?xQ?MxO1G6RO$@ZQWO1G6QO$@ZQWO1G6QO9WQWO1G6QO$@cQWO,5?TO9WQWO,5?TOOQO,5?T,5?TO$@wQWO,5?TO$(PQWO,5?TOOQO-E[OOQU,5>[,5>[O%[Q^O'#HqO%8mQWO'#HsOOQU,5>b,5>bO9WQWO,5>bOOQU,5>d,5>dOOQU7+)f7+)fOOQU7+)l7+)lOOQU7+)p7+)pOOQU7+)r7+)rO%8rQ`O1G5wO%9WQ07bO1G0wO%9bQWO1G0wOOQO1G/p1G/pO%9mQ07bO1G/pO>tQWO1G/pO!(oQ^O'#DjOOQO,5>{,5>{OOQO-E<_-E<_OOQO,5?R,5?ROOQO-EtQWO7+&]O!A}Q`O7+&]OOQO7+%u7+%uO$>`Q?NdO7+&UOOQO7+&U7+&UO%[Q^O7+&UO%9wQ?MxO7+&UO!ArQ?MxO7+%uO!A}Q`O7+%uO%:SQ?MxO7+&UO%:bQ?NdO7++mO%[Q^O7++mO%:rQWO7++lO%:rQWO7++lOOQO1G4o1G4oO9WQWO1G4oO%:zQWO1G4oOOQQ7+%z7+%zO#&wQWO<|O%[Q^O,5>|OOQO-E<`-E<`O%FwQWO1G5xOOQ?Mr<]OOQU,5>_,5>_O&8uQWO1G3|O9WQWO7+&cO!(oQ^O7+&cOOQO7+%[7+%[O&8zQ07bO1G6UO>tQWO7+%[OOQ?Mr<tQWO<`Q?NdO<pQ?NdO,5?_O&@xQ?NdO7+'zO&CWQrO1G4hO&CbQ07bO7+&^O&EcQ07bO,5=UO&GgQ07bO,5=WO&GwQ07bO,5=UO&HXQ07bO,5=WO&HiQ07bO,59rO&JlQ07bO,5tQWO7+)hO'(OQWO<`Q?NdOAN?[OOQOAN>{AN>{O%[Q^OAN?[OOQO<`Q?NdOG24vO#&wQWOLD,nOOQULD,nLD,nO!&_Q7[OLD,nO'5TQrOLD,nO'5[Q07bO7+'xO'6}Q07bO,5?]O'8}Q07bO,5?_O':}Q07bO7+'zO'kOh%VOk+aO![']O%f+`O~O!d+cOa(WX![(WX'u(WX!Y(WX~Oa%lO![XO'u%lO~Oh%VO!i%cO~Oh%VO!i%cO(O%eO~O!d#vO#h(tO~Ob+nO%g+oO(O+kO(QTO(TUO!Z)TP~O!Y+pO`)SX~O[+tO~O`+uO~O![%}O(O%eO(P!lO`)SP~Oh%VO#]+zO~Oh%VOk+}O![$|O~O![,PO~O},RO![XO~O%k%tO~O!u,WO~Oe,]O~Ob,^O(O#nO(QTO(TUO!Z)RP~Oe%{O~O%g!QO(O&WO~P=RO[,cO`,bO~OPYOQYOSfOdzOeyOmkOoYOpkOqkOwkOyYO{YO!PWO!TkO!UkO!fuO!iZO!lYO!mYO!nYO!pvO!uxO!y]O%e}O(QTO(TUO([VO(j[O(yiO~O![!eO!r!gO$V!kO(O!dO~P!EkO`,bOa%lO'u%lO~OPYOQYOSfOd!jOe!iOmkOoYOpkOqkOwkOyYO{YO!PWO!TkO!UkO![!eO!fuO!iZO!lYO!mYO!nYO!pvO!u!hO$V!kO(O!dO(QTO(TUO([VO(j[O(yiO~Oa,hO!rwO#t!OO%i!OO%j!OO%k!OO~P!HTO!i&lO~O&Y,nO~O![,pO~O&k,rO&m,sOP&haQ&haS&haY&haa&had&hae&ham&hao&hap&haq&haw&hay&ha{&ha!P&ha!T&ha!U&ha![&ha!f&ha!i&ha!l&ha!m&ha!n&ha!p&ha!r&ha!u&ha!y&ha#t&ha$V&ha%e&ha%g&ha%i&ha%j&ha%k&ha%n&ha%p&ha%s&ha%t&ha%v&ha&S&ha&Y&ha&[&ha&^&ha&`&ha&c&ha&i&ha&o&ha&q&ha&s&ha&u&ha&w&ha's&ha(O&ha(Q&ha(T&ha([&ha(j&ha(y&ha!Z&ha&a&hab&ha&f&ha~O(O,xO~Oh!bX!Y!OX!Z!OX!d!OX!d!bX!i!bX#]!OX~O!Y!bX!Z!bX~P# ZO!d,}O#],|Oh(eX!Y#eX!Y(eX!Z#eX!Z(eX!d(eX!i(eX~Oh%VO!d-PO!i%cO!Y!^X!Z!^X~Op!nO!P!oO(QTO(TUO(`!mO~OP;POQ;POSfOdkOg'XX!Y'XX~P!+hO!Y.wOg(ka~OSfO![3uO$c3vO~O!Z3zO~Os3{O~P#.aOa$lq!Y$lq'u$lq's$lq!V$lq!h$lqs$lq![$lq%f$lq!d$lq~P!9mO!V3|O~P#.aO})zO!P){O(u%POk'ea(t'ea!Y'ea#]'ea~Og'ea#}'ea~P%)nO})zO!P){Ok'ga(t'ga(u'ga!Y'ga#]'ga~Og'ga#}'ga~P%*aO(m$YO~P#.aO!VfX!V$xX!YfX!Y$xX!d%PX#]fX~P!/gO(OQ#>g#@V#@e#@l#BR#Ba#C|#D[#Db#Dh#Dn#Dx#EO#EU#E`#Er#ExPPPPPPPPPP#FOPPPPPPP#Fs#Iz#KZ#Kb#KjPPP$!sP$!|$%t$,^$,a$,d$-P$-S$-Z$-cP$-i$-lP$.Y$.^$/U$0d$0i$1PPP$1U$1[$1`P$1c$1g$1k$2a$2x$3a$3e$3h$3k$3q$3t$3x$3|R!|RoqOXst!Z#d%k&o&q&r&t,k,p1|2PY!vQ']-]1a5eQ%rvQ%zyQ&R|Q&g!VS'T!e-TQ'c!iS'i!r!yU*e$|*V*jQ+i%{Q+v&TQ,[&aQ-Z'[Q-e'dQ-m'jQ0R*lQ1k,]R;v;T%QdOPWXYZstuvw!Z!`!g!o#S#W#Z#d#o#u#x#{$O$P$Q$R$S$T$U$V$W$X$_$a$e%k%r&P&h&k&o&q&r&t&x'Q'_'o(P(R(X(`(t(v(z)y+R+V,h,k,p-a-i-w-}.l.s/f0a0g0v1d1t1u1w1y1|2P2R2r2x3^5b5m5}6O6R6f8R8X8h8rS#q];Q!r)Z$Z$n'U)o,|-P.}2b3u5`6]9h9y;P;S;T;W;X;Y;Z;[;];^;_;`;a;b;c;d;f;i;v;x;y;{ < TypeParamList TypeDefinition extends ThisType this LiteralType ArithOp Number BooleanLiteral TemplateType InterpolationEnd Interpolation InterpolationStart NullType null VoidType void TypeofType typeof MemberExpression . PropertyName [ TemplateString Escape Interpolation super RegExp ] ArrayExpression Spread , } { ObjectExpression Property async get set PropertyDefinition Block : NewTarget new NewExpression ) ( ArgList UnaryExpression delete LogicOp BitOp YieldExpression yield AwaitExpression await ParenthesizedExpression ClassExpression class ClassBody MethodDeclaration Decorator @ MemberExpression PrivatePropertyName CallExpression TypeArgList CompareOp < declare Privacy static abstract override PrivatePropertyDefinition PropertyDeclaration readonly accessor Optional TypeAnnotation Equals StaticBlock FunctionExpression ArrowFunction ParamList ParamList ArrayPattern ObjectPattern PatternProperty Privacy readonly Arrow MemberExpression BinaryExpression ArithOp ArithOp ArithOp ArithOp BitOp CompareOp instanceof satisfies in const CompareOp BitOp BitOp BitOp LogicOp LogicOp ConditionalExpression LogicOp LogicOp AssignmentExpression UpdateOp PostfixExpression CallExpression InstantiationExpression TaggedTemplateExpression DynamicImport import ImportMeta JSXElement JSXSelfCloseEndTag JSXSelfClosingTag JSXIdentifier JSXBuiltin JSXIdentifier JSXNamespacedName JSXMemberExpression JSXSpreadAttribute JSXAttribute JSXAttributeValue JSXEscape JSXEndTag JSXOpenTag JSXFragmentTag JSXText JSXEscape JSXStartCloseTag JSXCloseTag PrefixCast ArrowFunction TypeParamList SequenceExpression InstantiationExpression KeyofType keyof UniqueType unique ImportType InferredType infer TypeName ParenthesizedType FunctionSignature ParamList NewSignature IndexedType TupleType Label ArrayType ReadonlyType ObjectType MethodType PropertyType IndexSignature PropertyDefinition CallSignature TypePredicate is NewSignature new UnionType LogicOp IntersectionType LogicOp ConditionalType ParameterizedType ClassDeclaration abstract implements type VariableDeclaration let var using TypeAliasDeclaration InterfaceDeclaration interface EnumDeclaration enum EnumBody NamespaceDeclaration namespace module AmbientDeclaration declare GlobalDeclaration global ClassDeclaration ClassBody AmbientFunctionDeclaration ExportGroup VariableName VariableName ImportDeclaration ImportGroup ForStatement for ForSpec ForInSpec ForOfSpec of WhileStatement while WithStatement with DoStatement do IfStatement if else SwitchStatement switch SwitchBody CaseLabel case DefaultLabel TryStatement try CatchClause catch FinallyClause finally ReturnStatement return ThrowStatement throw BreakStatement break ContinueStatement continue DebuggerStatement debugger LabeledStatement ExpressionStatement SingleExpression SingleClassItem",maxTerm:376,context:Kr,nodeProps:[["isolate",-8,5,6,14,34,36,48,50,52,""],["group",-26,9,17,19,65,204,208,212,213,215,218,221,231,233,239,241,243,245,248,254,260,262,264,266,268,270,271,"Statement",-34,13,14,29,32,33,39,48,51,52,54,59,67,69,73,77,79,81,82,107,108,117,118,135,138,140,141,142,143,144,146,147,166,167,169,"Expression",-23,28,30,34,38,40,42,171,173,175,176,178,179,180,182,183,184,186,187,188,198,200,202,203,"Type",-3,85,100,106,"ClassItem"],["openedBy",23,"<",35,"InterpolationStart",53,"[",57,"{",70,"(",159,"JSXStartCloseTag"],["closedBy",24,">",37,"InterpolationEnd",47,"]",58,"}",71,")",164,"JSXEndTag"]],propSources:[ti],skippedNodes:[0,5,6,274],repeatNodeCount:37,tokenData:"$Fq07[R!bOX%ZXY+gYZ-yZ[+g[]%Z]^.c^p%Zpq+gqr/mrs3cst:_tuEruvJSvwLkwx! Yxy!'iyz!(sz{!)}{|!,q|}!.O}!O!,q!O!P!/Y!P!Q!9j!Q!R#:O!R![#<_![!]#I_!]!^#Jk!^!_#Ku!_!`$![!`!a$$v!a!b$*T!b!c$,r!c!}Er!}#O$-|#O#P$/W#P#Q$4o#Q#R$5y#R#SEr#S#T$7W#T#o$8b#o#p$x#r#s$@U#s$f%Z$f$g+g$g#BYEr#BY#BZ$A`#BZ$ISEr$IS$I_$A`$I_$I|Er$I|$I}$Dk$I}$JO$Dk$JO$JTEr$JT$JU$A`$JU$KVEr$KV$KW$A`$KW&FUEr&FU&FV$A`&FV;'SEr;'S;=`I|<%l?HTEr?HT?HU$A`?HUOEr(n%d_$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z&j&hT$h&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c&j&zP;=`<%l&c'|'U]$h&j(U!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!b(SU(U!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!b(iP;=`<%l'}'|(oP;=`<%l&}'[(y]$h&j(RpOY(rYZ&cZr(rrs&cs!^(r!^!_)r!_#O(r#O#P&c#P#o(r#o#p)r#p;'S(r;'S;=`*a<%lO(rp)wU(RpOY)rZr)rs#O)r#P;'S)r;'S;=`*Z<%lO)rp*^P;=`<%l)r'[*dP;=`<%l(r#S*nX(Rp(U!bOY*gZr*grs'}sw*gwx)rx#O*g#P;'S*g;'S;=`+Z<%lO*g#S+^P;=`<%l*g(n+dP;=`<%l%Z07[+rq$h&j(Rp(U!b'w0/lOX%ZXY+gYZ&cZ[+g[p%Zpq+gqr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p$f%Z$f$g+g$g#BY%Z#BY#BZ+g#BZ$IS%Z$IS$I_+g$I_$JT%Z$JT$JU+g$JU$KV%Z$KV$KW+g$KW&FU%Z&FU&FV+g&FV;'S%Z;'S;=`+a<%l?HT%Z?HT?HU+g?HUO%Z07[.ST(S#S$h&j'x0/lO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c07[.n_$h&j(Rp(U!b'x0/lOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z)3p/x`$h&j!m),Q(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`0z!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z(KW1V`#u(Ch$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_!`2X!`#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z(KW2d_#u(Ch$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'At3l_(Q':f$h&j(U!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k(^4r_$h&j(U!bOY4kYZ5qZr4krs7nsw4kwx5qx!^4k!^!_8p!_#O4k#O#P5q#P#o4k#o#p8p#p;'S4k;'S;=`:X<%lO4k&z5vX$h&jOr5qrs6cs!^5q!^!_6y!_#o5q#o#p6y#p;'S5q;'S;=`7h<%lO5q&z6jT$c`$h&jO!^&c!_#o&c#p;'S&c;'S;=`&w<%lO&c`6|TOr6yrs7]s;'S6y;'S;=`7b<%lO6y`7bO$c``7eP;=`<%l6y&z7kP;=`<%l5q(^7w]$c`$h&j(U!bOY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}!r8uZ(U!bOY8pYZ6yZr8prs9hsw8pwx6yx#O8p#O#P6y#P;'S8p;'S;=`:R<%lO8p!r9oU$c`(U!bOY'}Zw'}x#O'}#P;'S'};'S;=`(f<%lO'}!r:UP;=`<%l8p(^:[P;=`<%l4k%9[:hh$h&j(Rp(U!bOY%ZYZ&cZq%Zqr`#P#o`x!^=^!^!_?q!_#O=^#O#P>`#P#o=^#o#p?q#p;'S=^;'S;=`@h<%lO=^&n>gXWS$h&jOY>`YZ&cZ!^>`!^!_?S!_#o>`#o#p?S#p;'S>`;'S;=`?k<%lO>`S?XSWSOY?SZ;'S?S;'S;=`?e<%lO?SS?hP;=`<%l?S&n?nP;=`<%l>`!f?xWWS(U!bOY?qZw?qwx?Sx#O?q#O#P?S#P;'S?q;'S;=`@b<%lO?q!f@eP;=`<%l?q(Q@kP;=`<%l=^'`@w]WS$h&j(RpOY@nYZ&cZr@nrs>`s!^@n!^!_Ap!_#O@n#O#P>`#P#o@n#o#pAp#p;'S@n;'S;=`Bg<%lO@ntAwWWS(RpOYApZrAprs?Ss#OAp#O#P?S#P;'SAp;'S;=`Ba<%lOAptBdP;=`<%lAp'`BjP;=`<%l@n#WBvYWS(Rp(U!bOYBmZrBmrs?qswBmwxApx#OBm#O#P?S#P;'SBm;'S;=`Cf<%lOBm#WCiP;=`<%lBm(rCoP;=`<%l^!Q^$h&j!U7`OY!=yYZ&cZ!P!=y!P!Q!>|!Q!^!=y!^!_!@c!_!}!=y!}#O!CW#O#P!Dy#P#o!=y#o#p!@c#p;'S!=y;'S;=`!Ek<%lO!=y|#X#Z&c#Z#[!>|#[#]&c#]#^!>|#^#a&c#a#b!>|#b#g&c#g#h!>|#h#i&c#i#j!>|#j#k!>|#k#m&c#m#n!>|#n#o&c#p;'S&c;'S;=`&w<%lO&c7`!@hX!U7`OY!@cZ!P!@c!P!Q!AT!Q!}!@c!}#O!Ar#O#P!Bq#P;'S!@c;'S;=`!CQ<%lO!@c7`!AYW!U7`#W#X!AT#Z#[!AT#]#^!AT#a#b!AT#g#h!AT#i#j!AT#j#k!AT#m#n!AT7`!AuVOY!ArZ#O!Ar#O#P!B[#P#Q!@c#Q;'S!Ar;'S;=`!Bk<%lO!Ar7`!B_SOY!ArZ;'S!Ar;'S;=`!Bk<%lO!Ar7`!BnP;=`<%l!Ar7`!BtSOY!@cZ;'S!@c;'S;=`!CQ<%lO!@c7`!CTP;=`<%l!@c^!Ezl$h&j(U!b!U7`OY&}YZ&cZw&}wx&cx!^&}!^!_'}!_#O&}#O#P&c#P#W&}#W#X!Eq#X#Z&}#Z#[!Eq#[#]&}#]#^!Eq#^#a&}#a#b!Eq#b#g&}#g#h!Eq#h#i&}#i#j!Eq#j#k!Eq#k#m&}#m#n!Eq#n#o&}#o#p'}#p;'S&};'S;=`(l<%lO&}8r!GyZ(U!b!U7`OY!GrZw!Grwx!@cx!P!Gr!P!Q!Hl!Q!}!Gr!}#O!JU#O#P!Bq#P;'S!Gr;'S;=`!J|<%lO!Gr8r!Hse(U!b!U7`OY'}Zw'}x#O'}#P#W'}#W#X!Hl#X#Z'}#Z#[!Hl#[#]'}#]#^!Hl#^#a'}#a#b!Hl#b#g'}#g#h!Hl#h#i'}#i#j!Hl#j#k!Hl#k#m'}#m#n!Hl#n;'S'};'S;=`(f<%lO'}8r!JZX(U!bOY!JUZw!JUwx!Arx#O!JU#O#P!B[#P#Q!Gr#Q;'S!JU;'S;=`!Jv<%lO!JU8r!JyP;=`<%l!JU8r!KPP;=`<%l!Gr>^!KZ^$h&j(U!bOY!KSYZ&cZw!KSwx!CWx!^!KS!^!_!JU!_#O!KS#O#P!DR#P#Q!^!LYP;=`<%l!KS>^!L`P;=`<%l!_#c#d#Bq#d#l%Z#l#m#Es#m#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#>j_$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#?rd$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#AQ!R!S#AQ!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#AQ#S#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#A]f$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!R#AQ!R!S#AQ!S!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#AQ#S#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Bzc$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!Y#DV!Y!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#DV#S#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Dbe$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q!Y#DV!Y!^%Z!^!_*g!_#O%Z#O#P&c#P#R%Z#R#S#DV#S#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#E|g$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q![#Ge![!^%Z!^!_*g!_!c%Z!c!i#Ge!i#O%Z#O#P&c#P#R%Z#R#S#Ge#S#T%Z#T#Z#Ge#Z#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z'Ad#Gpi$h&j(Rp(U!bp'9tOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!Q%Z!Q![#Ge![!^%Z!^!_*g!_!c%Z!c!i#Ge!i#O%Z#O#P&c#P#R%Z#R#S#Ge#S#T%Z#T#Z#Ge#Z#b%Z#b#c#>_#c#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z*)x#Il_!d$b$h&j#})Lv(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z)[#Jv_al$h&j(Rp(U!bOY%ZYZ&cZr%Zrs&}sw%Zwx(rx!^%Z!^!_*g!_#O%Z#O#P&c#P#o%Z#o#p*g#p;'S%Z;'S;=`+a<%lO%Z04f#LS^h#)`#O-ai[e]||-1},{term:338,get:e=>ri[e]||-1},{term:92,get:e=>ii[e]||-1}],tokenPrec:14749}),tt=[S("function ${name}(${params}) {\n ${}\n}",{label:"function",detail:"definition",type:"keyword"}),S("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n ${}\n}",{label:"for",detail:"loop",type:"keyword"}),S("for (let ${name} of ${collection}) {\n ${}\n}",{label:"for",detail:"of loop",type:"keyword"}),S("do {\n ${}\n} while (${})",{label:"do",detail:"loop",type:"keyword"}),S("while (${}) {\n ${}\n}",{label:"while",detail:"loop",type:"keyword"}),S(`try { - \${} -} catch (\${error}) { - \${} -}`,{label:"try",detail:"/ catch block",type:"keyword"}),S("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),S(`if (\${}) { - \${} -} else { - \${} -}`,{label:"if",detail:"/ else block",type:"keyword"}),S(`class \${name} { - constructor(\${params}) { - \${} - } -}`,{label:"class",detail:"definition",type:"keyword"}),S('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),S('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],oi=tt.concat([S("interface ${name} {\n ${}\n}",{label:"interface",detail:"definition",type:"keyword"}),S("type ${name} = ${type}",{label:"type",detail:"definition",type:"keyword"}),S("enum ${name} {\n ${}\n}",{label:"enum",detail:"definition",type:"keyword"})]),$e=new _e,at=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function A(e){return(O,a)=>{let t=O.node.getChild("VariableDefinition");return t&&a(t,e),!0}}const li=["FunctionDeclaration"],ni={FunctionDeclaration:A("function"),ClassDeclaration:A("class"),ClassExpression:()=>!0,EnumDeclaration:A("constant"),TypeAliasDeclaration:A("type"),NamespaceDeclaration:A("namespace"),VariableDefinition(e,O){e.matchContext(li)||O(e,"variable")},TypeDefinition(e,O){O(e,"type")},__proto__:null};function rt(e,O){let a=$e.get(O);if(a)return a;let t=[],r=!0;function s(i,o){let n=e.sliceString(i.from,i.to);t.push({label:n,type:o})}return O.cursor(vO.IncludeAnonymous).iterate(i=>{if(r)r=!1;else if(i.name){let o=ni[i.name];if(o&&o(i,s)||at.has(i.name))return!1}else if(i.to-i.from>8192){for(let o of rt(e,i.node))t.push(o);return!1}}),$e.set(O,t),t}const Pe=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,it=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName",".","?."];function ci(e){let O=G(e.state).resolveInner(e.pos,-1);if(it.indexOf(O.name)>-1)return null;let a=O.name=="VariableName"||O.to-O.from<20&&Pe.test(e.state.sliceDoc(O.from,O.to));if(!a&&!e.explicit)return null;let t=[];for(let r=O;r;r=r.parent)at.has(r.name)&&(t=t.concat(rt(e.state.doc,r)));return{options:t,from:a?O.from:e.pos,validFor:Pe}}const y=J.define({name:"javascript",parser:si.configure({props:[L.add({IfStatement:v({except:/^\s*({|else\b)/}),TryStatement:v({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:It,SwitchBody:e=>{let O=e.textAfter,a=/^\s*\}/.test(O),t=/^\s*(case|default)\b/.test(O);return e.baseIndent+(a?0:t?1:2)*e.unit},Block:Dt({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":v({except:/^{/}),JSXElement(e){let O=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},JSXEscape(e){let O=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),K.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":UO,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),st={test:e=>/^JSX/.test(e.name),facet:Bt({commentTokens:{block:{open:"{/*",close:"*/}"}}})},ot=y.configure({dialect:"ts"},"typescript"),lt=y.configure({dialect:"jsx",props:[ze.add(e=>e.isTop?[st]:void 0)]}),nt=y.configure({dialect:"jsx ts",props:[ze.add(e=>e.isTop?[st]:void 0)]},"typescript");let ct=e=>({label:e,type:"keyword"});const Qt="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(ct),Qi=Qt.concat(["declare","implements","private","protected","public"].map(ct));function ht(e={}){let O=e.jsx?e.typescript?nt:lt:e.typescript?ot:y,a=e.typescript?oi.concat(Qi):tt.concat(Qt);return new F(O,[y.data.of({autocomplete:qe(it,ve(a))}),y.data.of({autocomplete:ci}),e.jsx?pi:[]])}function hi(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(e.name=="JSXEscape"||!e.parent)return null;e=e.parent}}function me(e,O,a=e.length){for(let t=O==null?void 0:O.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return e.sliceString(t.from,Math.min(t.to,a));return""}const ui=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),pi=q.inputHandler.of((e,O,a,t,r)=>{if((ui?e.composing:e.compositionStarted)||e.state.readOnly||O!=a||t!=">"&&t!="/"||!y.isActiveAt(e.state,O,-1))return!1;let s=r(),{state:i}=s,o=i.changeByRange(n=>{var Q;let{head:u}=n,c=G(i).resolveInner(u-1,-1),f;if(c.name=="JSXStartTag"&&(c=c.parent),!(i.doc.sliceString(u-1,u)!=t||c.name=="JSXAttributeValue"&&c.to>u)){if(t==">"&&c.name=="JSXFragmentTag")return{range:n,changes:{from:u,insert:""}};if(t=="/"&&c.name=="JSXStartCloseTag"){let h=c.parent,d=h.parent;if(d&&h.from==u-2&&((f=me(i.doc,d.firstChild,u))||((Q=d.firstChild)===null||Q===void 0?void 0:Q.name)=="JSXFragmentTag")){let P=`${f}>`;return{range:Ue.cursor(u+P.length,-1),changes:{from:u,insert:P}}}}else if(t==">"){let h=hi(c);if(h&&h.name=="JSXOpenTag"&&!/^\/?>|^<\//.test(i.doc.sliceString(u,u+2))&&(f=me(i.doc,h,u)))return{range:n,changes:{from:u,insert:``}}}}return{range:n}});return o.changes.empty?!1:(e.dispatch([s,i.update(o,{userEvent:"input.complete",scrollIntoView:!0})]),!0)}),E=["_blank","_self","_top","_parent"],SO=["ascii","utf-8","utf-16","latin1","latin1"],ZO=["get","post","put","delete"],bO=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],b=["true","false"],p={},di={a:{attrs:{href:null,ping:null,type:null,media:null,target:E,hreflang:null}},abbr:p,address:p,area:{attrs:{alt:null,coords:null,href:null,target:null,ping:null,media:null,hreflang:null,type:null,shape:["default","rect","circle","poly"]}},article:p,aside:p,audio:{attrs:{src:null,mediagroup:null,crossorigin:["anonymous","use-credentials"],preload:["none","metadata","auto"],autoplay:["autoplay"],loop:["loop"],controls:["controls"]}},b:p,base:{attrs:{href:null,target:E}},bdi:p,bdo:p,blockquote:{attrs:{cite:null}},body:p,br:p,button:{attrs:{form:null,formaction:null,name:null,value:null,autofocus:["autofocus"],disabled:["autofocus"],formenctype:bO,formmethod:ZO,formnovalidate:["novalidate"],formtarget:E,type:["submit","reset","button"]}},canvas:{attrs:{width:null,height:null}},caption:p,center:p,cite:p,code:p,col:{attrs:{span:null}},colgroup:{attrs:{span:null}},command:{attrs:{type:["command","checkbox","radio"],label:null,icon:null,radiogroup:null,command:null,title:null,disabled:["disabled"],checked:["checked"]}},data:{attrs:{value:null}},datagrid:{attrs:{disabled:["disabled"],multiple:["multiple"]}},datalist:{attrs:{data:null}},dd:p,del:{attrs:{cite:null,datetime:null}},details:{attrs:{open:["open"]}},dfn:p,div:p,dl:p,dt:p,em:p,embed:{attrs:{src:null,type:null,width:null,height:null}},eventsource:{attrs:{src:null}},fieldset:{attrs:{disabled:["disabled"],form:null,name:null}},figcaption:p,figure:p,footer:p,form:{attrs:{action:null,name:null,"accept-charset":SO,autocomplete:["on","off"],enctype:bO,method:ZO,novalidate:["novalidate"],target:E}},h1:p,h2:p,h3:p,h4:p,h5:p,h6:p,head:{children:["title","base","link","style","meta","script","noscript","command"]},header:p,hgroup:p,hr:p,html:{attrs:{manifest:null}},i:p,iframe:{attrs:{src:null,srcdoc:null,name:null,width:null,height:null,sandbox:["allow-top-navigation","allow-same-origin","allow-forms","allow-scripts"],seamless:["seamless"]}},img:{attrs:{alt:null,src:null,ismap:null,usemap:null,width:null,height:null,crossorigin:["anonymous","use-credentials"]}},input:{attrs:{alt:null,dirname:null,form:null,formaction:null,height:null,list:null,max:null,maxlength:null,min:null,name:null,pattern:null,placeholder:null,size:null,src:null,step:null,value:null,width:null,accept:["audio/*","video/*","image/*"],autocomplete:["on","off"],autofocus:["autofocus"],checked:["checked"],disabled:["disabled"],formenctype:bO,formmethod:ZO,formnovalidate:["novalidate"],formtarget:E,multiple:["multiple"],readonly:["readonly"],required:["required"],type:["hidden","text","search","tel","url","email","password","datetime","date","month","week","time","datetime-local","number","range","color","checkbox","radio","file","submit","image","reset","button"]}},ins:{attrs:{cite:null,datetime:null}},kbd:p,keygen:{attrs:{challenge:null,form:null,name:null,autofocus:["autofocus"],disabled:["disabled"],keytype:["RSA"]}},label:{attrs:{for:null,form:null}},legend:p,li:{attrs:{value:null}},link:{attrs:{href:null,type:null,hreflang:null,media:null,sizes:["all","16x16","16x16 32x32","16x16 32x32 64x64"]}},map:{attrs:{name:null}},mark:p,menu:{attrs:{label:null,type:["list","context","toolbar"]}},meta:{attrs:{content:null,charset:SO,name:["viewport","application-name","author","description","generator","keywords"],"http-equiv":["content-language","content-type","default-style","refresh"]}},meter:{attrs:{value:null,min:null,low:null,high:null,max:null,optimum:null}},nav:p,noscript:p,object:{attrs:{data:null,type:null,name:null,usemap:null,form:null,width:null,height:null,typemustmatch:["typemustmatch"]}},ol:{attrs:{reversed:["reversed"],start:null,type:["1","a","A","i","I"]},children:["li","script","template","ul","ol"]},optgroup:{attrs:{disabled:["disabled"],label:null}},option:{attrs:{disabled:["disabled"],label:null,selected:["selected"],value:null}},output:{attrs:{for:null,form:null,name:null}},p,param:{attrs:{name:null,value:null}},pre:p,progress:{attrs:{value:null,max:null}},q:{attrs:{cite:null}},rp:p,rt:p,ruby:p,samp:p,script:{attrs:{type:["text/javascript"],src:null,async:["async"],defer:["defer"],charset:SO}},section:p,select:{attrs:{form:null,name:null,size:null,autofocus:["autofocus"],disabled:["disabled"],multiple:["multiple"]}},slot:{attrs:{name:null}},small:p,source:{attrs:{src:null,type:null,media:null}},span:p,strong:p,style:{attrs:{type:["text/css"],media:null,scoped:null}},sub:p,summary:p,sup:p,table:p,tbody:p,td:{attrs:{colspan:null,rowspan:null,headers:null}},template:p,textarea:{attrs:{dirname:null,form:null,maxlength:null,name:null,placeholder:null,rows:null,cols:null,autofocus:["autofocus"],disabled:["disabled"],readonly:["readonly"],required:["required"],wrap:["soft","hard"]}},tfoot:p,th:{attrs:{colspan:null,rowspan:null,headers:null,scope:["row","col","rowgroup","colgroup"]}},thead:p,time:{attrs:{datetime:null}},title:p,tr:p,track:{attrs:{src:null,label:null,default:null,kind:["subtitles","captions","descriptions","chapters","metadata"],srclang:null}},ul:{children:["li","script","template","ul","ol"]},var:p,video:{attrs:{src:null,poster:null,width:null,height:null,crossorigin:["anonymous","use-credentials"],preload:["auto","metadata","none"],autoplay:["autoplay"],mediagroup:["movie"],muted:["muted"],controls:["controls"]}},wbr:p},ut={accesskey:null,class:null,contenteditable:b,contextmenu:null,dir:["ltr","rtl","auto"],draggable:["true","false","auto"],dropzone:["copy","move","link","string:","file:"],hidden:["hidden"],id:null,inert:["inert"],itemid:null,itemprop:null,itemref:null,itemscope:["itemscope"],itemtype:null,lang:["ar","bn","de","en-GB","en-US","es","fr","hi","id","ja","pa","pt","ru","tr","zh"],spellcheck:b,autocorrect:b,autocapitalize:b,style:null,tabindex:null,title:null,translate:["yes","no"],rel:["stylesheet","alternate","author","bookmark","help","license","next","nofollow","noreferrer","prefetch","prev","search","tag"],role:"alert application article banner button cell checkbox complementary contentinfo dialog document feed figure form grid gridcell heading img list listbox listitem main navigation region row rowgroup search switch tab table tabpanel textbox timer".split(" "),"aria-activedescendant":null,"aria-atomic":b,"aria-autocomplete":["inline","list","both","none"],"aria-busy":b,"aria-checked":["true","false","mixed","undefined"],"aria-controls":null,"aria-describedby":null,"aria-disabled":b,"aria-dropeffect":null,"aria-expanded":["true","false","undefined"],"aria-flowto":null,"aria-grabbed":["true","false","undefined"],"aria-haspopup":b,"aria-hidden":b,"aria-invalid":["true","false","grammar","spelling"],"aria-label":null,"aria-labelledby":null,"aria-level":null,"aria-live":["off","polite","assertive"],"aria-multiline":b,"aria-multiselectable":b,"aria-owns":null,"aria-posinset":null,"aria-pressed":["true","false","mixed","undefined"],"aria-readonly":b,"aria-relevant":null,"aria-required":b,"aria-selected":["true","false","undefined"],"aria-setsize":null,"aria-sort":["ascending","descending","none","other"],"aria-valuemax":null,"aria-valuemin":null,"aria-valuenow":null,"aria-valuetext":null},pt="beforeunload copy cut dragstart dragover dragleave dragenter dragend drag paste focus blur change click load mousedown mouseenter mouseleave mouseup keydown keyup resize scroll unload".split(" ").map(e=>"on"+e);for(let e of pt)ut[e]=null;class hO{constructor(O,a){this.tags=Object.assign(Object.assign({},di),O),this.globalAttrs=Object.assign(Object.assign({},ut),a),this.allTags=Object.keys(this.tags),this.globalAttrNames=Object.keys(this.globalAttrs)}}hO.default=new hO;function V(e,O,a=e.length){if(!O)return"";let t=O.firstChild,r=t&&t.getChild("TagName");return r?e.sliceString(r.from,Math.min(r.to,a)):""}function j(e,O=!1){for(;e;e=e.parent)if(e.name=="Element")if(O)O=!1;else return e;return null}function dt(e,O,a){let t=a.tags[V(e,j(O))];return(t==null?void 0:t.children)||a.allTags}function jO(e,O){let a=[];for(let t=j(O);t&&!t.type.isTop;t=j(t.parent)){let r=V(e,t);if(r&&t.lastChild.name=="CloseTag")break;r&&a.indexOf(r)<0&&(O.name=="EndTag"||O.from>=t.firstChild.to)&&a.push(r)}return a}const ft=/^[:\-\.\w\u00b7-\uffff]*$/;function ge(e,O,a,t,r){let s=/\s*>/.test(e.sliceDoc(r,r+5))?"":">",i=j(a,!0);return{from:t,to:r,options:dt(e.doc,i,O).map(o=>({label:o,type:"type"})).concat(jO(e.doc,a).map((o,n)=>({label:"/"+o,apply:"/"+o+s,type:"type",boost:99-n}))),validFor:/^\/?[:\-\.\w\u00b7-\uffff]*$/}}function Se(e,O,a,t){let r=/\s*>/.test(e.sliceDoc(t,t+5))?"":">";return{from:a,to:t,options:jO(e.doc,O).map((s,i)=>({label:s,apply:s+r,type:"type",boost:99-i})),validFor:ft}}function fi(e,O,a,t){let r=[],s=0;for(let i of dt(e.doc,a,O))r.push({label:"<"+i,type:"type"});for(let i of jO(e.doc,a))r.push({label:"",type:"type",boost:99-s++});return{from:t,to:t,options:r,validFor:/^<\/?[:\-\.\w\u00b7-\uffff]*$/}}function $i(e,O,a,t,r){let s=j(a),i=s?O.tags[V(e.doc,s)]:null,o=i&&i.attrs?Object.keys(i.attrs):[],n=i&&i.globalAttrs===!1?o:o.length?o.concat(O.globalAttrNames):O.globalAttrNames;return{from:t,to:r,options:n.map(Q=>({label:Q,type:"property"})),validFor:ft}}function Pi(e,O,a,t,r){var s;let i=(s=a.parent)===null||s===void 0?void 0:s.getChild("AttributeName"),o=[],n;if(i){let Q=e.sliceDoc(i.from,i.to),u=O.globalAttrs[Q];if(!u){let c=j(a),f=c?O.tags[V(e.doc,c)]:null;u=(f==null?void 0:f.attrs)&&f.attrs[Q]}if(u){let c=e.sliceDoc(t,r).toLowerCase(),f='"',h='"';/^['"]/.test(c)?(n=c[0]=='"'?/^[^"]*$/:/^[^']*$/,f="",h=e.sliceDoc(r,r+1)==c[0]?"":c[0],c=c.slice(1),t++):n=/^[^\s<>='"]*$/;for(let d of u)o.push({label:d,apply:f+d+h,type:"constant"})}}return{from:t,to:r,options:o,validFor:n}}function mi(e,O){let{state:a,pos:t}=O,r=G(a).resolveInner(t,-1),s=r.resolve(t);for(let i=t,o;s==r&&(o=r.childBefore(i));){let n=o.lastChild;if(!n||!n.type.isError||n.frommi(t,r)}const Si=y.parser.configure({top:"SingleExpression"}),$t=[{tag:"script",attrs:e=>e.type=="text/typescript"||e.lang=="ts",parser:ot.parser},{tag:"script",attrs:e=>e.type=="text/babel"||e.type=="text/jsx",parser:lt.parser},{tag:"script",attrs:e=>e.type=="text/typescript-jsx",parser:nt.parser},{tag:"script",attrs(e){return/^(importmap|speculationrules|application\/(.+\+)?json)$/i.test(e.type)},parser:Si},{tag:"script",attrs(e){return!e.type||/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i.test(e.type)},parser:y.parser},{tag:"style",attrs(e){return(!e.lang||e.lang=="css")&&(!e.type||/^(text\/)?(x-)?(stylesheet|css)$/i.test(e.type))},parser:QO.parser}],Pt=[{name:"style",parser:QO.parser.configure({top:"Styles"})}].concat(pt.map(e=>({name:e,parser:y.parser}))),mt=J.define({name:"html",parser:rr.configure({props:[L.add({Element(e){let O=/^(\s*)(<\/)?/.exec(e.textAfter);return e.node.to<=e.pos+O[0].length?e.continue():e.lineIndent(e.node.from)+(O[2]?0:e.unit)},"OpenTag CloseTag SelfClosingTag"(e){return e.column(e.node.from)+e.unit},Document(e){if(e.pos+/\s*/.exec(e.textAfter)[0].lengthe.getChild("TagName")})]}),languageData:{commentTokens:{block:{open:""}},indentOnInput:/^\s*<\/\w+\W$/,wordChars:"-._"}}),iO=mt.configure({wrap:Le($t,Pt)});function Zi(e={}){let O="",a;e.matchClosingTags===!1&&(O="noMatch"),e.selfClosingTags===!0&&(O=(O?O+" ":"")+"selfClosing"),(e.nestedLanguages&&e.nestedLanguages.length||e.nestedAttributes&&e.nestedAttributes.length)&&(a=Le((e.nestedLanguages||[]).concat($t),(e.nestedAttributes||[]).concat(Pt)));let t=a?mt.configure({wrap:a,dialect:O}):O?iO.configure({dialect:O}):iO;return new F(t,[iO.data.of({autocomplete:gi(e)}),e.autoCloseTags!==!1?bi:[],ht().support,Wr().support])}const Ze=new Set("area base br col command embed frame hr img input keygen link meta param source track wbr menuitem".split(" ")),bi=q.inputHandler.of((e,O,a,t,r)=>{if(e.composing||e.state.readOnly||O!=a||t!=">"&&t!="/"||!iO.isActiveAt(e.state,O,-1))return!1;let s=r(),{state:i}=s,o=i.changeByRange(n=>{var Q,u,c;let f=i.doc.sliceString(n.from-1,n.to)==t,{head:h}=n,d=G(i).resolveInner(h,-1),P;if(f&&t==">"&&d.name=="EndTag"){let m=d.parent;if(((u=(Q=m.parent)===null||Q===void 0?void 0:Q.lastChild)===null||u===void 0?void 0:u.name)!="CloseTag"&&(P=V(i.doc,m.parent,h))&&!Ze.has(P)){let x=h+(i.doc.sliceString(h,h+1)===">"?1:0),X=``;return{range:n,changes:{from:h,to:x,insert:X}}}}else if(f&&t=="/"&&d.name=="IncompleteCloseTag"){let m=d.parent;if(d.from==h-2&&((c=m.lastChild)===null||c===void 0?void 0:c.name)!="CloseTag"&&(P=V(i.doc,m,h))&&!Ze.has(P)){let x=h+(i.doc.sliceString(h,h+1)===">"?1:0),X=`${P}>`;return{range:Ue.cursor(h+X.length,-1),changes:{from:h,to:x,insert:X}}}}return{range:n}});return o.changes.empty?!1:(e.dispatch([s,i.update(o,{userEvent:"input.complete",scrollIntoView:!0})]),!0)}),ki=B({String:l.string,Number:l.number,"True False":l.bool,PropertyName:l.propertyName,Null:l.null,",":l.separator,"[ ]":l.squareBracket,"{ }":l.brace}),xi=T.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#CjOOQO'#Cp'#CpQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CrOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59U,59UO!iQPO,59UOVQPO,59QOqQPO'#CkO!nQPO,59^OOQO1G.k1G.kOVQPO'#ClO!vQPO,59aOOQO1G.p1G.pOOQO1G.l1G.lOOQO,59V,59VOOQO-E6i-E6iOOQO,59W,59WOOQO-E6j-E6j",stateData:"#O~OcOS~OQSORSOSSOTSOWQO]ROePO~OVXOeUO~O[[O~PVOg^O~Oh_OVfX~OVaO~OhbO[iX~O[dO~Oh_OVfa~OhbO[ia~O",goto:"!kjPPPPPPkPPkqwPPk{!RPPP!XP!ePP!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"⚠ JsonText True False Null Number String } { Object Property PropertyName ] [ Array",maxTerm:25,nodeProps:[["isolate",-2,6,11,""],["openedBy",7,"{",12,"["],["closedBy",8,"}",13,"]"]],propSources:[ki],skippedNodes:[0],repeatNodeCount:2,tokenData:"(|~RaXY!WYZ!W]^!Wpq!Wrs!]|}$u}!O$z!Q!R%T!R![&c![!]&t!}#O&y#P#Q'O#Y#Z'T#b#c'r#h#i(Z#o#p(r#q#r(w~!]Oc~~!`Wpq!]qr!]rs!xs#O!]#O#P!}#P;'S!];'S;=`$o<%lO!]~!}Oe~~#QXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#m~#pR!Q![#y!c!i#y#T#Z#y~#|R!Q![$V!c!i$V#T#Z$V~$YR!Q![$c!c!i$c#T#Z$c~$fR!Q![!]!c!i!]#T#Z!]~$rP;=`<%l!]~$zOh~~$}Q!Q!R%T!R![&c~%YRT~!O!P%c!g!h%w#X#Y%w~%fP!Q![%i~%nRT~!Q![%i!g!h%w#X#Y%w~%zR{|&T}!O&T!Q![&Z~&WP!Q![&Z~&`PT~!Q![&Z~&hST~!O!P%c!Q![&c!g!h%w#X#Y%w~&yOg~~'OO]~~'TO[~~'WP#T#U'Z~'^P#`#a'a~'dP#g#h'g~'jP#X#Y'm~'rOR~~'uP#i#j'x~'{P#`#a(O~(RP#`#a(U~(ZOS~~(^P#f#g(a~(dP#i#j(g~(jP#X#Y(m~(rOQ~~(wOW~~(|OV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0}),Xi=J.define({name:"json",parser:xi.configure({props:[L.add({Object:v({except:/^\s*\}/}),Array:v({except:/^\s*\]/})}),K.add({"Object Array":UO})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function yi(){return new F(Xi)}const wi=36,be=1,Ri=2,U=3,kO=4,Yi=5,Ti=6,Wi=7,_i=8,qi=9,vi=10,Ui=11,zi=12,Vi=13,ji=14,Gi=15,Ci=16,Ai=17,ke=18,Ei=19,gt=20,St=21,xe=22,Ni=23,Mi=24;function YO(e){return e>=65&&e<=90||e>=97&&e<=122||e>=48&&e<=57}function Ii(e){return e>=48&&e<=57||e>=97&&e<=102||e>=65&&e<=70}function _(e,O,a){for(let t=!1;;){if(e.next<0)return;if(e.next==O&&!t){e.advance();return}t=a&&!t&&e.next==92,e.advance()}}function Di(e,O){O:for(;;){if(e.next<0)return console.log("exit at end",e.pos);if(e.next==36){e.advance();for(let a=0;a)".charCodeAt(a);for(;;){if(e.next<0)return;if(e.next==t&&e.peek(1)==39){e.advance(2);return}e.advance()}}function TO(e,O){for(;!(e.next!=95&&!YO(e.next));)O!=null&&(O+=String.fromCharCode(e.next)),e.advance();return O}function Ji(e){if(e.next==39||e.next==34||e.next==96){let O=e.next;e.advance(),_(e,O,!1)}else TO(e)}function Xe(e,O){for(;e.next==48||e.next==49;)e.advance();O&&e.next==O&&e.advance()}function ye(e,O){for(;;){if(e.next==46){if(O)break;O=!0}else if(e.next<48||e.next>57)break;e.advance()}if(e.next==69||e.next==101)for(e.advance(),(e.next==43||e.next==45)&&e.advance();e.next>=48&&e.next<=57;)e.advance()}function we(e){for(;!(e.next<0||e.next==10);)e.advance()}function W(e,O){for(let a=0;a!=&|~^/",specialVar:"?",identifierQuotes:'"',caseInsensitiveIdentifiers:!1,words:Zt(Ki,Li)};function Fi(e,O,a,t){let r={};for(let s in WO)r[s]=(e.hasOwnProperty(s)?e:WO)[s];return O&&(r.words=Zt(O,a||"",t)),r}function bt(e){return new k(O=>{var a;let{next:t}=O;if(O.advance(),W(t,xO)){for(;W(O.next,xO);)O.advance();O.acceptToken(wi)}else if(t==36&&e.doubleDollarQuotedStrings){let r=TO(O,"");O.next==36&&(O.advance(),Di(O,r),O.acceptToken(U))}else if(t==39||t==34&&e.doubleQuotedStrings)_(O,t,e.backslashEscapes),O.acceptToken(U);else if(t==35&&e.hashComments||t==47&&O.next==47&&e.slashComments)we(O),O.acceptToken(be);else if(t==45&&O.next==45&&(!e.spaceAfterDashes||O.peek(1)==32))we(O),O.acceptToken(be);else if(t==47&&O.next==42){O.advance();for(let r=1;;){let s=O.next;if(O.next<0)break;if(O.advance(),s==42&&O.next==47){if(r--,O.advance(),!r)break}else s==47&&O.next==42&&(r++,O.advance())}O.acceptToken(Ri)}else if((t==101||t==69)&&O.next==39)O.advance(),_(O,39,!0),O.acceptToken(U);else if((t==110||t==78)&&O.next==39&&e.charSetCasts)O.advance(),_(O,39,e.backslashEscapes),O.acceptToken(U);else if(t==95&&e.charSetCasts)for(let r=0;;r++){if(O.next==39&&r>1){O.advance(),_(O,39,e.backslashEscapes),O.acceptToken(U);break}if(!YO(O.next))break;O.advance()}else if(e.plsqlQuotingMechanism&&(t==113||t==81)&&O.next==39&&O.peek(1)>0&&!W(O.peek(1),xO)){let r=O.peek(1);O.advance(2),Bi(O,r),O.acceptToken(U)}else if(t==40)O.acceptToken(Wi);else if(t==41)O.acceptToken(_i);else if(t==123)O.acceptToken(qi);else if(t==125)O.acceptToken(vi);else if(t==91)O.acceptToken(Ui);else if(t==93)O.acceptToken(zi);else if(t==59)O.acceptToken(Vi);else if(e.unquotedBitLiterals&&t==48&&O.next==98)O.advance(),Xe(O),O.acceptToken(xe);else if((t==98||t==66)&&(O.next==39||O.next==34)){const r=O.next;O.advance(),e.treatBitsAsBytes?(_(O,r,e.backslashEscapes),O.acceptToken(Ni)):(Xe(O,r),O.acceptToken(xe))}else if(t==48&&(O.next==120||O.next==88)||(t==120||t==88)&&O.next==39){let r=O.next==39;for(O.advance();Ii(O.next);)O.advance();r&&O.next==39&&O.advance(),O.acceptToken(kO)}else if(t==46&&O.next>=48&&O.next<=57)ye(O,!0),O.acceptToken(kO);else if(t==46)O.acceptToken(ji);else if(t>=48&&t<=57)ye(O,!1),O.acceptToken(kO);else if(W(t,e.operatorChars)){for(;W(O.next,e.operatorChars);)O.advance();O.acceptToken(Gi)}else if(W(t,e.specialVar))O.next==t&&O.advance(),Ji(O),O.acceptToken(Ai);else if(W(t,e.identifierQuotes))_(O,t,!1),O.acceptToken(Ei);else if(t==58||t==44)O.acceptToken(Ci);else if(YO(t)){let r=TO(O,String.fromCharCode(t));O.acceptToken(O.next==46||O.peek(-r.length-1)==46?ke:(a=e.words[r.toLowerCase()])!==null&&a!==void 0?a:ke)}})}const kt=bt(WO),Hi=T.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"⚠ LineComment BlockComment String Number Bool Null ( ) { } [ ] ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,nodeProps:[["isolate",-4,1,2,3,19,""]],skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,kt],topRules:{Script:[0,25]},tokenPrec:0});function _O(e){let O=e.cursor().moveTo(e.from,-1);for(;/Comment/.test(O.name);)O.moveTo(O.from,-1);return O.node}function I(e,O){let a=e.sliceString(O.from,O.to),t=/^([`'"])(.*)\1$/.exec(a);return t?t[2]:a}function uO(e){return e&&(e.name=="Identifier"||e.name=="QuotedIdentifier")}function Os(e,O){if(O.name=="CompositeIdentifier"){let a=[];for(let t=O.firstChild;t;t=t.nextSibling)uO(t)&&a.push(I(e,t));return a}return[I(e,O)]}function Re(e,O){for(let a=[];;){if(!O||O.name!=".")return a;let t=_O(O);if(!uO(t))return a;a.unshift(I(e,t)),O=_O(t)}}function es(e,O){let a=G(e).resolveInner(O,-1),t=as(e.doc,a);return a.name=="Identifier"||a.name=="QuotedIdentifier"||a.name=="Keyword"?{from:a.from,quoted:a.name=="QuotedIdentifier"?e.doc.sliceString(a.from,a.from+1):null,parents:Re(e.doc,_O(a)),aliases:t}:a.name=="."?{from:O,quoted:null,parents:Re(e.doc,a),aliases:t}:{from:O,quoted:null,parents:[],empty:!0,aliases:t}}const ts=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));function as(e,O){let a;for(let r=O;!a;r=r.parent){if(!r)return null;r.name=="Statement"&&(a=r)}let t=null;for(let r=a.firstChild,s=!1,i=null;r;r=r.nextSibling){let o=r.name=="Keyword"?e.sliceString(r.from,r.to).toLowerCase():null,n=null;if(!s)s=o=="from";else if(o=="as"&&i&&uO(r.nextSibling))n=I(e,r.nextSibling);else{if(o&&ts.has(o))break;i&&uO(r)&&(n=I(e,r))}n&&(t||(t=Object.create(null)),t[n]=Os(e,i)),i=/Identifier$/.test(r.name)?r:null}return t}function rs(e,O){return e?O.map(a=>Object.assign(Object.assign({},a),{label:a.label[0]==e?a.label:e+a.label+e,apply:void 0})):O}const is=/^\w*$/,ss=/^[`'"]?\w*[`'"]?$/;function Ye(e){return e.self&&typeof e.self.label=="string"}class GO{constructor(O,a){this.idQuote=O,this.idCaseInsensitive=a,this.list=[],this.children=void 0}child(O){let a=this.children||(this.children=Object.create(null)),t=a[O];return t||(O&&!this.list.some(r=>r.label==O)&&this.list.push(Te(O,"type",this.idQuote,this.idCaseInsensitive)),a[O]=new GO(this.idQuote,this.idCaseInsensitive))}maybeChild(O){return this.children?this.children[O]:null}addCompletion(O){let a=this.list.findIndex(t=>t.label==O.label);a>-1?this.list[a]=O:this.list.push(O)}addCompletions(O){for(let a of O)this.addCompletion(typeof a=="string"?Te(a,"property",this.idQuote,this.idCaseInsensitive):a)}addNamespace(O){Array.isArray(O)?this.addCompletions(O):Ye(O)?this.addNamespace(O.children):this.addNamespaceObject(O)}addNamespaceObject(O){for(let a of Object.keys(O)){let t=O[a],r=null,s=a.replace(/\\?\./g,o=>o=="."?"\0":o).split("\0"),i=this;Ye(t)&&(r=t.self,t=t.children);for(let o=0;o{let{parents:c,from:f,quoted:h,empty:d,aliases:P}=es(u.state,u.pos);if(d&&!u.explicit)return null;P&&c.length==1&&(c=P[c[0]]||c);let m=n;for(let R of c){for(;!m.children||!m.children[R];)if(m==n&&Q)m=Q;else if(m==Q&&t)m=m.child(t);else return null;let H=m.maybeChild(R);if(!H)return null;m=H}let x=h&&u.state.sliceDoc(u.pos,u.pos+1)==h,X=m.list;return m==n&&P&&(X=X.concat(Object.keys(P).map(R=>({label:R,type:"constant"})))),{from:f,to:x?u.pos+1:void 0,options:rs(h,X),validFor:h?ss:is}}}function ls(e,O){let a=Object.keys(e).map(t=>({label:O?t.toUpperCase():t,type:e[t]==St?"type":e[t]==gt?"keyword":"variable",boost:-1}));return qe(["QuotedIdentifier","SpecialVar","String","LineComment","BlockComment","."],ve(a))}let ns=Hi.configure({props:[L.add({Statement:v()}),K.add({Statement(e,O){return{from:Math.min(e.from+100,O.doc.lineAt(e.from).to),to:e.to}},BlockComment(e){return{from:e.from+2,to:e.to-2}}}),B({Keyword:l.keyword,Type:l.typeName,Builtin:l.standard(l.name),Bits:l.number,Bytes:l.string,Bool:l.bool,Null:l.null,Number:l.number,String:l.string,Identifier:l.name,QuotedIdentifier:l.special(l.string),SpecialVar:l.special(l.name),LineComment:l.lineComment,BlockComment:l.blockComment,Operator:l.operator,"Semi Punctuation":l.punctuation,"( )":l.paren,"{ }":l.brace,"[ ]":l.squareBracket})]});class D{constructor(O,a,t){this.dialect=O,this.language=a,this.spec=t}get extension(){return this.language.extension}static define(O){let a=Fi(O,O.keywords,O.types,O.builtin),t=J.define({name:"sql",parser:ns.configure({tokenizers:[{from:kt,to:bt(a)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new D(a,t,O)}}function cs(e,O=!1){return ls(e.dialect.words,O)}function Qs(e,O=!1){return e.language.data.of({autocomplete:cs(e,O)})}function hs(e){return e.schema?os(e.schema,e.tables,e.schemas,e.defaultTable,e.defaultSchema,e.dialect||CO):()=>null}function us(e){return e.schema?(e.dialect||CO).language.data.of({autocomplete:hs(e)}):[]}function We(e={}){let O=e.dialect||CO;return new F(O.language,[us(e),Qs(O,!!e.upperCaseKeywords)])}const CO=D.define({});function ps(e){let O;return{c(){O=Tt("div"),Wt(O,"class","code-editor"),OO(O,"min-height",e[0]?e[0]+"px":null),OO(O,"max-height",e[1]?e[1]+"px":"auto")},m(a,t){_t(a,O,t),e[11](O)},p(a,[t]){t&1&&OO(O,"min-height",a[0]?a[0]+"px":null),t&2&&OO(O,"max-height",a[1]?a[1]+"px":"auto")},i:BO,o:BO,d(a){a&&qt(O),e[11](null)}}}function ds(e,O,a){let t;vt(e,Ut,$=>a(12,t=$));const r=zt();let{id:s=""}=O,{value:i=""}=O,{minHeight:o=null}=O,{maxHeight:n=null}=O,{disabled:Q=!1}=O,{placeholder:u=""}=O,{language:c="javascript"}=O,{singleLine:f=!1}=O,h,d,P=new eO,m=new eO,x=new eO,X=new eO;function R(){h==null||h.focus()}function H(){d==null||d.dispatchEvent(new CustomEvent("change",{detail:{value:i},bubbles:!0})),r("change",i)}function AO(){if(!s)return;const $=document.querySelectorAll('[for="'+s+'"]');for(let g of $)g.removeEventListener("click",R)}function EO(){if(!s)return;AO();const $=document.querySelectorAll('[for="'+s+'"]');for(let g of $)g.addEventListener("click",R)}function NO(){switch(c){case"html":return Zi();case"json":return yi();case"sql-create-index":return We({dialect:D.define({keywords:"create unique index if not exists on collate asc desc where like isnull notnull date time datetime unixepoch strftime lower upper substr case when then iif if else json_extract json_each json_tree json_array_length json_valid ",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"}),upperCaseKeywords:!0});case"sql-select":let $={};for(let g of t)$[g.name]=jt.getAllCollectionIdentifiers(g);return We({dialect:D.define({keywords:"select distinct from where having group by order limit offset join left right inner with like not in match asc desc regexp isnull notnull glob count avg sum min max current random cast as int real text bool date time datetime unixepoch strftime coalesce lower upper substr case when then iif if else json_extract json_each json_tree json_array_length json_valid ",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"}),schema:$,upperCaseKeywords:!0});default:return ht()}}Vt(()=>{const $={key:"Enter",run:g=>{f&&r("submit",i)}};return EO(),a(10,h=new q({parent:d,state:C.create({doc:i,extensions:[Lt(),Kt(),Ft(),Ht(),Oa(),C.allowMultipleSelections.of(!0),ea(ta,{fallback:!0}),aa(),ra(),ia(),sa(),oa.of([$,...la,...na,ca.find(g=>g.key==="Mod-d"),...Qa,...ha]),q.lineWrapping,ua({icons:!1}),P.of(NO()),X.of(JO(u)),m.of(q.editable.of(!0)),x.of(C.readOnly.of(!1)),C.transactionFilter.of(g=>{var MO,IO,DO;if(f&&g.newDoc.lines>1){if(!((DO=(IO=(MO=g.changes)==null?void 0:MO.inserted)==null?void 0:IO.filter(Xt=>!!Xt.text.find(yt=>yt)))!=null&&DO.length))return[];g.newDoc.text=[g.newDoc.text.join(" ")]}return g}),q.updateListener.of(g=>{!g.docChanged||Q||(a(3,i=g.state.doc.toString()),H())})]})})),()=>{AO(),h==null||h.destroy()}});function xt($){Gt[$?"unshift":"push"](()=>{d=$,a(2,d)})}return e.$$set=$=>{"id"in $&&a(4,s=$.id),"value"in $&&a(3,i=$.value),"minHeight"in $&&a(0,o=$.minHeight),"maxHeight"in $&&a(1,n=$.maxHeight),"disabled"in $&&a(5,Q=$.disabled),"placeholder"in $&&a(6,u=$.placeholder),"language"in $&&a(7,c=$.language),"singleLine"in $&&a(8,f=$.singleLine)},e.$$.update=()=>{e.$$.dirty&16&&s&&EO(),e.$$.dirty&1152&&h&&c&&h.dispatch({effects:[P.reconfigure(NO())]}),e.$$.dirty&1056&&h&&typeof Q<"u"&&h.dispatch({effects:[m.reconfigure(q.editable.of(!Q)),x.reconfigure(C.readOnly.of(Q))]}),e.$$.dirty&1032&&h&&i!=h.state.doc.toString()&&h.dispatch({changes:{from:0,to:h.state.doc.length,insert:i}}),e.$$.dirty&1088&&h&&typeof u<"u"&&h.dispatch({effects:[X.reconfigure(JO(u))]})},[o,n,d,i,s,Q,u,c,f,R,h,xt]}class Ps extends wt{constructor(O){super(),Rt(this,O,ds,ps,Yt,{id:4,value:3,minHeight:0,maxHeight:1,disabled:5,placeholder:6,language:7,singleLine:8,focus:9})}get focus(){return this.$$.ctx[9]}}export{Ps as default}; diff --git a/ui/dist/assets/ConfirmEmailChangeDocs-DBFq8TK_.js b/ui/dist/assets/ConfirmEmailChangeDocs-DBFq8TK_.js deleted file mode 100644 index 05f86ad6..00000000 --- a/ui/dist/assets/ConfirmEmailChangeDocs-DBFq8TK_.js +++ /dev/null @@ -1,57 +0,0 @@ -import{S as Pe,i as Se,s as Oe,O as Y,e as r,v,b as k,c as Ce,f as b,g as d,h as n,m as $e,w as j,P as _e,Q as ye,k as Re,R as Te,n as Ae,t as ee,a as te,o as m,d as we,C as Ee,A as qe,q as H,r as Be,N as Ue}from"./index-Bp3jGQ0J.js";import{S as De}from"./SdkTabs-DxNNd6Sw.js";function he(o,l,s){const a=o.slice();return a[5]=l[s],a}function ke(o,l,s){const a=o.slice();return a[5]=l[s],a}function ge(o,l){let s,a=l[5].code+"",_,u,i,p;function f(){return l[4](l[5])}return{key:o,first:null,c(){s=r("button"),_=v(a),u=k(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(C,$){d(C,s,$),n(s,_),n(s,u),i||(p=Be(s,"click",f),i=!0)},p(C,$){l=C,$&4&&a!==(a=l[5].code+"")&&j(_,a),$&6&&H(s,"active",l[1]===l[5].code)},d(C){C&&m(s),i=!1,p()}}}function ve(o,l){let s,a,_,u;return a=new Ue({props:{content:l[5].body}}),{key:o,first:null,c(){s=r("div"),Ce(a.$$.fragment),_=k(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(i,p){d(i,s,p),$e(a,s,null),n(s,_),u=!0},p(i,p){l=i;const f={};p&4&&(f.content=l[5].body),a.$set(f),(!u||p&6)&&H(s,"active",l[1]===l[5].code)},i(i){u||(ee(a.$$.fragment,i),u=!0)},o(i){te(a.$$.fragment,i),u=!1},d(i){i&&m(s),we(a)}}}function Ne(o){var pe,fe;let l,s,a=o[0].name+"",_,u,i,p,f,C,$,D=o[0].name+"",F,le,se,I,L,w,Q,y,z,P,N,ae,K,R,ne,G,M=o[0].name+"",J,oe,V,T,X,A,Z,E,x,S,q,g=[],ie=new Map,ce,B,h=[],re=new Map,O;w=new De({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(pe=o[0])==null?void 0:pe.name}').confirmEmailChange( - 'TOKEN', - 'YOUR_PASSWORD', - ); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(fe=o[0])==null?void 0:fe.name}').confirmEmailChange( - 'TOKEN', - 'YOUR_PASSWORD', - ); - `}});let W=Y(o[2]);const de=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description
Required token
String The token from the change email request email.
Required password
String The account password to confirm the email change.',Z=k(),E=r("div"),E.textContent="Responses",x=k(),S=r("div"),q=r("div");for(let e=0;es(1,u=f.code);return o.$$set=f=>{"collection"in f&&s(0,_=f.collection)},s(3,a=Ee.getApiExampleUrl(qe.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "token": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}]),[_,u,i,a,p]}class Ye extends Pe{constructor(l){super(),Se(this,l,Ke,Ne,Oe,{collection:0})}}export{Ye as default}; diff --git a/ui/dist/assets/ConfirmPasswordResetDocs-DZJDH7s9.js b/ui/dist/assets/ConfirmPasswordResetDocs-DZJDH7s9.js deleted file mode 100644 index 553af60e..00000000 --- a/ui/dist/assets/ConfirmPasswordResetDocs-DZJDH7s9.js +++ /dev/null @@ -1,85 +0,0 @@ -import{S as Ne,i as $e,s as Ce,O as K,e as c,v as w,b as k,c as Ae,f as b,g as r,h as n,m as Re,w as U,P as we,Q as Ee,k as ye,R as De,n as Te,t as ee,a as te,o as p,d as Oe,C as qe,A as Be,q as j,r as Me,N as Fe}from"./index-Bp3jGQ0J.js";import{S as Ie}from"./SdkTabs-DxNNd6Sw.js";function Se(o,l,s){const a=o.slice();return a[5]=l[s],a}function Pe(o,l,s){const a=o.slice();return a[5]=l[s],a}function We(o,l){let s,a=l[5].code+"",_,m,i,u;function f(){return l[4](l[5])}return{key:o,first:null,c(){s=c("button"),_=w(a),m=k(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(S,P){r(S,s,P),n(s,_),n(s,m),i||(u=Me(s,"click",f),i=!0)},p(S,P){l=S,P&4&&a!==(a=l[5].code+"")&&U(_,a),P&6&&j(s,"active",l[1]===l[5].code)},d(S){S&&p(s),i=!1,u()}}}function ge(o,l){let s,a,_,m;return a=new Fe({props:{content:l[5].body}}),{key:o,first:null,c(){s=c("div"),Ae(a.$$.fragment),_=k(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(i,u){r(i,s,u),Re(a,s,null),n(s,_),m=!0},p(i,u){l=i;const f={};u&4&&(f.content=l[5].body),a.$set(f),(!m||u&6)&&j(s,"active",l[1]===l[5].code)},i(i){m||(ee(a.$$.fragment,i),m=!0)},o(i){te(a.$$.fragment,i),m=!1},d(i){i&&p(s),Oe(a)}}}function Ke(o){var ue,fe,me,be;let l,s,a=o[0].name+"",_,m,i,u,f,S,P,q=o[0].name+"",H,le,se,L,Q,W,z,O,G,g,B,ae,M,N,oe,J,F=o[0].name+"",V,ne,X,$,Y,C,Z,E,x,A,y,v=[],ie=new Map,de,D,h=[],ce=new Map,R;W=new Ie({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - let oldAuth = pb.authStore.model; - - await pb.collection('${(ue=o[0])==null?void 0:ue.name}').confirmPasswordReset( - 'TOKEN', - 'NEW_PASSWORD', - 'NEW_PASSWORD_CONFIRM', - ); - - // reauthenticate if needed - // (after the above call all previously issued tokens are invalidated) - await pb.collection('${(fe=o[0])==null?void 0:fe.name}').authWithPassword(oldAuth.email, 'NEW_PASSWORD'); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - final oldAuth = pb.authStore.model; - - await pb.collection('${(me=o[0])==null?void 0:me.name}').confirmPasswordReset( - 'TOKEN', - 'NEW_PASSWORD', - 'NEW_PASSWORD_CONFIRM', - ); - - // reauthenticate if needed - // (after the above call all previously issued tokens are invalidated) - await pb.collection('${(be=o[0])==null?void 0:be.name}').authWithPassword(oldAuth.email, 'NEW_PASSWORD'); - `}});let I=K(o[2]);const re=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description
Required token
String The token from the password reset request email.
Required password
String The new password to set.
Required passwordConfirm
String The new password confirmation.',Z=k(),E=c("div"),E.textContent="Responses",x=k(),A=c("div"),y=c("div");for(let e=0;es(1,m=f.code);return o.$$set=f=>{"collection"in f&&s(0,_=f.collection)},s(3,a=qe.getApiExampleUrl(Be.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "token": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}]),[_,m,i,a,u]}class Le extends Ne{constructor(l){super(),$e(this,l,Ue,Ke,Ce,{collection:0})}}export{Le as default}; diff --git a/ui/dist/assets/ConfirmVerificationDocs-CzG7odGM.js b/ui/dist/assets/ConfirmVerificationDocs-CzG7odGM.js deleted file mode 100644 index c39aa942..00000000 --- a/ui/dist/assets/ConfirmVerificationDocs-CzG7odGM.js +++ /dev/null @@ -1,56 +0,0 @@ -import{S as Se,i as Te,s as Be,O as D,e as r,v as g,b as k,c as ye,f as h,g as f,h as n,m as Ce,w as H,P as ke,Q as qe,k as Re,R as Oe,n as Ae,t as x,a as ee,o as d,d as Pe,C as Ee,A as Ne,q as F,r as Ve,N as Ke}from"./index-Bp3jGQ0J.js";import{S as Me}from"./SdkTabs-DxNNd6Sw.js";function ve(o,l,s){const a=o.slice();return a[5]=l[s],a}function ge(o,l,s){const a=o.slice();return a[5]=l[s],a}function we(o,l){let s,a=l[5].code+"",b,m,i,p;function u(){return l[4](l[5])}return{key:o,first:null,c(){s=r("button"),b=g(a),m=k(),h(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(w,$){f(w,s,$),n(s,b),n(s,m),i||(p=Ve(s,"click",u),i=!0)},p(w,$){l=w,$&4&&a!==(a=l[5].code+"")&&H(b,a),$&6&&F(s,"active",l[1]===l[5].code)},d(w){w&&d(s),i=!1,p()}}}function $e(o,l){let s,a,b,m;return a=new Ke({props:{content:l[5].body}}),{key:o,first:null,c(){s=r("div"),ye(a.$$.fragment),b=k(),h(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(i,p){f(i,s,p),Ce(a,s,null),n(s,b),m=!0},p(i,p){l=i;const u={};p&4&&(u.content=l[5].body),a.$set(u),(!m||p&6)&&F(s,"active",l[1]===l[5].code)},i(i){m||(x(a.$$.fragment,i),m=!0)},o(i){ee(a.$$.fragment,i),m=!1},d(i){i&&d(s),Pe(a)}}}function Ue(o){var fe,de,pe,ue;let l,s,a=o[0].name+"",b,m,i,p,u,w,$,V=o[0].name+"",I,te,L,y,Q,T,z,C,K,le,M,B,se,G,U=o[0].name+"",J,ae,W,q,X,R,Y,O,Z,P,A,v=[],oe=new Map,ne,E,_=[],ie=new Map,S;y=new Me({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(fe=o[0])==null?void 0:fe.name}').confirmVerification('TOKEN'); - - // optionally refresh the previous authStore state with the latest record changes - await pb.collection('${(de=o[0])==null?void 0:de.name}').authRefresh(); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(pe=o[0])==null?void 0:pe.name}').confirmVerification('TOKEN'); - - // optionally refresh the previous authStore state with the latest record changes - await pb.collection('${(ue=o[0])==null?void 0:ue.name}').authRefresh(); - `}});let j=D(o[2]);const ce=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description
Required token
String The token from the verification request email.',Y=k(),O=r("div"),O.textContent="Responses",Z=k(),P=r("div"),A=r("div");for(let e=0;es(1,m=u.code);return o.$$set=u=>{"collection"in u&&s(0,b=u.collection)},s(3,a=Ee.getApiExampleUrl(Ne.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "token": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}]),[b,m,i,a,p]}class Fe extends Se{constructor(l){super(),Te(this,l,je,Ue,Be,{collection:0})}}export{Fe as default}; diff --git a/ui/dist/assets/CreateApiDocs-Cvocn8eg.js b/ui/dist/assets/CreateApiDocs-Cvocn8eg.js new file mode 100644 index 00000000..cef90ada --- /dev/null +++ b/ui/dist/assets/CreateApiDocs-Cvocn8eg.js @@ -0,0 +1,90 @@ +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-B-F-pko3.js";import{F as Rt}from"./FieldsQueryParam-CW6KZfgu.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 Authorization:TOKEN 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='Auth specific fields',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='String',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='Boolean',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='
Required password
String Auth record password.',K=p(),I=s("tr"),I.innerHTML='
Required passwordConfirm
String Auth record password confirmation.',re=p(),Y=s("tr"),Y.innerHTML=`
Optional verified
Boolean Indicates whether the auth record is verified or not. +
+ This field can be set only by superusers or auth records with "Manage" access.`,x=p(),G=s("tr"),G.innerHTML='Other fields',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 + already uploaded file(s).`)},m(h,g){r(h,e,g),r(h,t,g),r(h,l,g),r(h,u,g),r(h,c,g),r(h,f,g),r(h,b,g),r(h,m,g),r(h,$,g)},p:we,d(h){h&&(d(e),d(t),d(l),d(u),d(c),d(f),d(b),d(m),d($))}}}function Ut(a){let e;return{c(){e=_("URL address.")},m(t,l){r(t,e,l)},p:we,d(t){t&&d(e)}}}function It(a){let e;return{c(){e=_("Email address.")},m(t,l){r(t,e,l)},p:we,d(t){t&&d(e)}}}function Qt(a){let e;return{c(){e=_("JSON array or object.")},m(t,l){r(t,e,l)},p:we,d(t){t&&d(e)}}}function Wt(a){let e;return{c(){e=_("Number value.")},m(t,l){r(t,e,l)},p:we,d(t){t&&d(e)}}}function zt(a){let e,t,l=a[15].autogeneratePattern&&yt();return{c(){e=_(`Plain text value. + `),l&&l.c(),t=Ht()},m(u,c){r(u,e,c),l&&l.m(u,c),r(u,t,c)},p(u,c){u[15].autogeneratePattern?l||(l=yt(),l.c(),l.m(t.parentNode,t)):l&&(l.d(1),l=null)},d(u){u&&(d(e),d(t)),l&&l.d(u)}}}function yt(a){let e;return{c(){e=_("It is autogenerated if not set.")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function vt(a,e){let t,l,u,c,f,b=e[15].name+"",m,$,h,g,B=ee.getFieldValueType(e[15])+"",S,O,R,M;function U(k,F){return!k[15].required||k[15].type=="text"&&k[15].autogeneratePattern?Dt:Nt}let J=U(e),T=J(e);function W(k,F){if(k[15].type==="text")return zt;if(k[15].type==="number")return Wt;if(k[15].type==="json")return Qt;if(k[15].type==="email")return It;if(k[15].type==="url")return Ut;if(k[15].type==="file")return Et;if(k[15].type==="relation")return Jt}let P=W(e),q=P&&P(e);return{key:a,first:null,c(){t=s("tr"),l=s("td"),u=s("div"),T.c(),c=p(),f=s("span"),m=_(b),$=p(),h=s("td"),g=s("span"),S=_(B),O=p(),R=s("td"),q&&q.c(),M=p(),w(u,"class","inline-flex"),w(g,"class","label"),this.first=t},m(k,F){r(k,t,F),i(t,l),i(l,u),T.m(u,null),i(u,c),i(u,f),i(f,m),i(t,$),i(t,h),i(h,g),i(g,S),i(t,O),i(t,R),q&&q.m(R,null),i(t,M)},p(k,F){e=k,J!==(J=U(e))&&(T.d(1),T=J(e),T&&(T.c(),T.m(u,c))),F&64&&b!==(b=e[15].name+"")&&oe(m,b),F&64&&B!==(B=ee.getFieldValueType(e[15])+"")&&oe(S,B),P===(P=W(e))&&q?q.p(e,F):(q&&q.d(1),q=P&&P(e),q&&(q.c(),q.m(R,null)))},d(k){k&&d(t),T.d(),q&&q.d()}}}function wt(a,e){let t,l=e[10].code+"",u,c,f,b;function m(){return e[9](e[10])}return{key:a,first:null,c(){t=s("button"),u=_(l),c=p(),w(t,"class","tab-item"),Se(t,"active",e[2]===e[10].code),this.first=t},m($,h){r($,t,h),i(t,u),i(t,c),f||(b=Lt(t,"click",m),f=!0)},p($,h){e=$,h&8&&l!==(l=e[10].code+"")&&oe(u,l),h&12&&Se(t,"active",e[2]===e[10].code)},d($){$&&d(t),f=!1,b()}}}function gt(a,e){let t,l,u,c;return l=new Ct({props:{content:e[10].body}}),{key:a,first:null,c(){t=s("div"),$e(l.$$.fragment),u=p(),w(t,"class","tab-item"),Se(t,"active",e[2]===e[10].code),this.first=t},m(f,b){r(f,t,b),qe(l,t,null),i(t,u),c=!0},p(f,b){e=f;const m={};b&8&&(m.content=e[10].body),l.$set(m),(!c||b&12)&&Se(t,"active",e[2]===e[10].code)},i(f){c||(ye(l.$$.fragment,f),c=!0)},o(f){ve(l.$$.fragment,f),c=!1},d(f){f&&d(t),Te(l)}}}function Kt(a){var at,st,ot,rt;let e,t,l=a[0].name+"",u,c,f,b,m,$,h,g=a[0].name+"",B,S,O,R,M,U,J,T,W,P,q,k,F,te,K,I,re,Y,x=a[0].name+"",G,fe,le,E,X,Z,L,y,C,V,z,H=[],Ne=new Map,Oe,pe,Me,ne,Pe,De,me,ie,Fe,Je,Le,Ee,A,Ue,de,Ie,Qe,We,He,ze,Re,Ke,Ye,xe,Ae,Ge,Xe,ce,Be,be,je,ae,_e,Q=[],Ze=new Map,et,ke,N=[],tt=new Map,se;T=new St({props:{js:` +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('${a[5]}'); + +... + +// example create data +const data = ${JSON.stringify(Object.assign({},a[4],ee.dummyCollectionSchemaData(a[0],!0)),null,4)}; + +const record = await pb.collection('${(at=a[0])==null?void 0:at.name}').create(data); +`+(a[1]?` +// (optional) send an email verification request +await pb.collection('${(st=a[0])==null?void 0:st.name}').requestVerification('test@example.com'); +`:""),dart:` +import 'package:pocketbase/pocketbase.dart'; + +final pb = PocketBase('${a[5]}'); + +... + +// example create body +final body = ${JSON.stringify(Object.assign({},a[4],ee.dummyCollectionSchemaData(a[0],!0)),null,2)}; + +final record = await pb.collection('${(ot=a[0])==null?void 0:ot.name}').create(body: body); +`+(a[1]?` +// (optional) send an email verification request +await pb.collection('${(rt=a[0])==null?void 0:rt.name}').requestVerification('test@example.com'); +`:"")}});let D=a[7]&&kt(),j=a[1]&&ht(a),ge=ue(a[6]);const lt=n=>n[15].name;for(let n=0;nn[10].code;for(let n=0;nn[10].code;for(let n=0;napplication/json or + multipart/form-data.`,M=p(),U=s("p"),U.innerHTML=`File upload is supported only via multipart/form-data. +
+ For more info and examples you could check the detailed + Files upload and handling docs + .`,J=p(),$e(T.$$.fragment),W=p(),P=s("h6"),P.textContent="API details",q=p(),k=s("div"),F=s("strong"),F.textContent="POST",te=p(),K=s("div"),I=s("p"),re=_("/api/collections/"),Y=s("strong"),G=_(x),fe=_("/records"),le=p(),D&&D.c(),E=p(),X=s("div"),X.textContent="Body Parameters",Z=p(),L=s("table"),y=s("thead"),y.innerHTML='Param Type Description',C=p(),V=s("tbody"),j&&j.c(),z=p();for(let n=0;nParam Type Description',De=p(),me=s("tbody"),ie=s("tr"),Fe=s("td"),Fe.textContent="expand",Je=p(),Le=s("td"),Le.innerHTML='String',Ee=p(),A=s("td"),Ue=_(`Auto expand relations when returning the created record. Ex.: + `),$e(de.$$.fragment),Ie=_(` + Supports up to 6-levels depth nested relations expansion. `),Qe=s("br"),We=_(` + The expanded relations will be appended to the record under the + `),He=s("code"),He.textContent="expand",ze=_(" property (eg. "),Re=s("code"),Re.textContent='"expand": {"relField1": {...}, ...}',Ke=_(`). + `),Ye=s("br"),xe=_(` + Only the relations to which the request user has permissions to `),Ae=s("strong"),Ae.textContent="view",Ge=_(" will be expanded."),Xe=p(),$e(ce.$$.fragment),Be=p(),be=s("div"),be.textContent="Responses",je=p(),ae=s("div"),_e=s("div");for(let n=0;n${JSON.stringify(Object.assign({},n[4],ee.dummyCollectionSchemaData(n[0],!0)),null,2)}; + +final record = await pb.collection('${(ut=n[0])==null?void 0:ut.name}').create(body: body); +`+(n[1]?` +// (optional) send an email verification request +await pb.collection('${(ft=n[0])==null?void 0:ft.name}').requestVerification('test@example.com'); +`:"")),T.$set(v),(!se||o&1)&&x!==(x=n[0].name+"")&&oe(G,x),n[7]?D||(D=kt(),D.c(),D.m(k,null)):D&&(D.d(1),D=null),n[1]?j?j.p(n,o):(j=ht(n),j.c(),j.m(V,z)):j&&(j.d(1),j=null),o&64&&(ge=ue(n[6]),H=Ve(H,o,lt,1,n,ge,Ne,V,pt,vt,null,_t)),o&12&&(Ce=ue(n[3]),Q=Ve(Q,o,nt,1,n,Ce,Ze,_e,pt,wt,null,bt)),o&12&&(he=ue(n[3]),Ot(),N=Ve(N,o,it,1,n,he,tt,ke,Mt,gt,null,mt),Pt())},i(n){if(!se){ye(T.$$.fragment,n),ye(de.$$.fragment,n),ye(ce.$$.fragment,n);for(let o=0;oa.name=="emailVisibility",xt=a=>a.name=="email";function Gt(a,e,t){let l,u,c,f,b,{collection:m}=e,$=200,h=[],g={};const B=S=>t(2,$=S.code);return a.$$set=S=>{"collection"in S&&t(0,m=S.collection)},a.$$.update=()=>{var S,O,R;a.$$.dirty&1&&t(1,l=m.type==="auth"),a.$$.dirty&1&&t(7,u=(m==null?void 0:m.createRule)===null),a.$$.dirty&2&&t(8,c=l?["password","verified","email","emailVisibility"]:[]),a.$$.dirty&257&&t(6,f=((S=m==null?void 0:m.fields)==null?void 0:S.filter(M=>!M.hidden&&M.type!="autodate"&&!c.includes(M.name)))||[]),a.$$.dirty&1&&t(3,h=[{code:200,body:JSON.stringify(ee.dummyCollectionRecord(m),null,2)},{code:400,body:` + { + "code": 400, + "message": "Failed to create record.", + "data": { + "${(R=(O=m==null?void 0:m.fields)==null?void 0:O[0])==null?void 0:R.name}": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `},{code:403,body:` + { + "code": 403, + "message": "You are not allowed to perform this request.", + "data": {} + } + `}]),a.$$.dirty&2&&(l?t(4,g={password:"12345678",passwordConfirm:"12345678"}):t(4,g={}))},t(5,b=ee.getApiExampleUrl(Ft.baseURL)),[m,l,$,h,g,b,f,u,c,B]}class el extends $t{constructor(e){super(),qt(this,e,Gt,Kt,Tt,{collection:0})}}export{el as default}; diff --git a/ui/dist/assets/CreateApiDocs-n2O_YbPr.js b/ui/dist/assets/CreateApiDocs-n2O_YbPr.js deleted file mode 100644 index 9d75e5d2..00000000 --- a/ui/dist/assets/CreateApiDocs-n2O_YbPr.js +++ /dev/null @@ -1,92 +0,0 @@ -import{S as qt,i as Ot,s as Mt,C as Q,O as ne,N as Tt,e as s,v as _,b as f,c as _e,f as v,g as r,h as n,m as he,w as x,P as Be,Q as ht,k as Ht,R as Lt,n as Pt,t as fe,a as ue,o as d,d as ke,A as At,q as ye,r as Ft,x as ae}from"./index-Bp3jGQ0J.js";import{S as Rt}from"./SdkTabs-DxNNd6Sw.js";import{F as Bt}from"./FieldsQueryParam-zDO3HzQv.js";function kt(o,e,t){const a=o.slice();return a[8]=e[t],a}function yt(o,e,t){const a=o.slice();return a[8]=e[t],a}function vt(o,e,t){const a=o.slice();return a[13]=e[t],a}function gt(o){let e;return{c(){e=s("p"),e.innerHTML="Requires admin Authorization:TOKEN header",v(e,"class","txt-hint txt-sm txt-right")},m(t,a){r(t,e,a)},d(t){t&&d(e)}}}function wt(o){let e,t,a,u,m,c,p,y,S,T,w,H,D,E,P,I,j,B,C,N,q,g,b;function O(h,$){var ee,K;return(K=(ee=h[0])==null?void 0:ee.options)!=null&&K.requireEmail?Dt:jt}let z=O(o),A=z(o);return{c(){e=s("tr"),e.innerHTML='Auth fields',t=f(),a=s("tr"),a.innerHTML=`
Optional username
String The username of the auth record. -
- If not set, it will be auto generated.`,u=f(),m=s("tr"),c=s("td"),p=s("div"),A.c(),y=f(),S=s("span"),S.textContent="email",T=f(),w=s("td"),w.innerHTML='String',H=f(),D=s("td"),D.textContent="Auth record email address.",E=f(),P=s("tr"),P.innerHTML='
Optional emailVisibility
Boolean Whether to show/hide the auth record email when fetching the record data.',I=f(),j=s("tr"),j.innerHTML='
Required password
String Auth record password.',B=f(),C=s("tr"),C.innerHTML='
Required passwordConfirm
String Auth record password confirmation.',N=f(),q=s("tr"),q.innerHTML=`
Optional verified
Boolean Indicates whether the auth record is verified or not. -
- This field can be set only by admins or auth records with "Manage" access.`,g=f(),b=s("tr"),b.innerHTML='Schema fields',v(p,"class","inline-flex")},m(h,$){r(h,e,$),r(h,t,$),r(h,a,$),r(h,u,$),r(h,m,$),n(m,c),n(c,p),A.m(p,null),n(p,y),n(p,S),n(m,T),n(m,w),n(m,H),n(m,D),r(h,E,$),r(h,P,$),r(h,I,$),r(h,j,$),r(h,B,$),r(h,C,$),r(h,N,$),r(h,q,$),r(h,g,$),r(h,b,$)},p(h,$){z!==(z=O(h))&&(A.d(1),A=z(h),A&&(A.c(),A.m(p,y)))},d(h){h&&(d(e),d(t),d(a),d(u),d(m),d(E),d(P),d(I),d(j),d(B),d(C),d(N),d(q),d(g),d(b)),A.d()}}}function jt(o){let e;return{c(){e=s("span"),e.textContent="Optional",v(e,"class","label label-warning")},m(t,a){r(t,e,a)},d(t){t&&d(e)}}}function Dt(o){let e;return{c(){e=s("span"),e.textContent="Required",v(e,"class","label label-success")},m(t,a){r(t,e,a)},d(t){t&&d(e)}}}function Nt(o){let e;return{c(){e=s("span"),e.textContent="Optional",v(e,"class","label label-warning")},m(t,a){r(t,e,a)},d(t){t&&d(e)}}}function Vt(o){let e;return{c(){e=s("span"),e.textContent="Required",v(e,"class","label label-success")},m(t,a){r(t,e,a)},d(t){t&&d(e)}}}function Jt(o){var m;let e,t=((m=o[13].options)==null?void 0:m.maxSelect)===1?"id":"ids",a,u;return{c(){e=_("Relation record "),a=_(t),u=_(".")},m(c,p){r(c,e,p),r(c,a,p),r(c,u,p)},p(c,p){var y;p&1&&t!==(t=((y=c[13].options)==null?void 0:y.maxSelect)===1?"id":"ids")&&x(a,t)},d(c){c&&(d(e),d(a),d(u))}}}function Et(o){let e,t,a,u,m;return{c(){e=_("File object."),t=s("br"),a=_(` - Set to `),u=s("code"),u.textContent="null",m=_(" to delete already uploaded file(s).")},m(c,p){r(c,e,p),r(c,t,p),r(c,a,p),r(c,u,p),r(c,m,p)},p:ae,d(c){c&&(d(e),d(t),d(a),d(u),d(m))}}}function It(o){let e;return{c(){e=_("URL address.")},m(t,a){r(t,e,a)},p:ae,d(t){t&&d(e)}}}function Ut(o){let e;return{c(){e=_("Email address.")},m(t,a){r(t,e,a)},p:ae,d(t){t&&d(e)}}}function Qt(o){let e;return{c(){e=_("JSON array or object.")},m(t,a){r(t,e,a)},p:ae,d(t){t&&d(e)}}}function zt(o){let e;return{c(){e=_("Number value.")},m(t,a){r(t,e,a)},p:ae,d(t){t&&d(e)}}}function Kt(o){let e;return{c(){e=_("Plain text value.")},m(t,a){r(t,e,a)},p:ae,d(t){t&&d(e)}}}function Ct(o,e){let t,a,u,m,c,p=e[13].name+"",y,S,T,w,H=Q.getFieldValueType(e[13])+"",D,E,P,I;function j(b,O){return b[13].required?Vt:Nt}let B=j(e),C=B(e);function N(b,O){if(b[13].type==="text")return Kt;if(b[13].type==="number")return zt;if(b[13].type==="json")return Qt;if(b[13].type==="email")return Ut;if(b[13].type==="url")return It;if(b[13].type==="file")return Et;if(b[13].type==="relation")return Jt}let q=N(e),g=q&&q(e);return{key:o,first:null,c(){t=s("tr"),a=s("td"),u=s("div"),C.c(),m=f(),c=s("span"),y=_(p),S=f(),T=s("td"),w=s("span"),D=_(H),E=f(),P=s("td"),g&&g.c(),I=f(),v(u,"class","inline-flex"),v(w,"class","label"),this.first=t},m(b,O){r(b,t,O),n(t,a),n(a,u),C.m(u,null),n(u,m),n(u,c),n(c,y),n(t,S),n(t,T),n(T,w),n(w,D),n(t,E),n(t,P),g&&g.m(P,null),n(t,I)},p(b,O){e=b,B!==(B=j(e))&&(C.d(1),C=B(e),C&&(C.c(),C.m(u,m))),O&1&&p!==(p=e[13].name+"")&&x(y,p),O&1&&H!==(H=Q.getFieldValueType(e[13])+"")&&x(D,H),q===(q=N(e))&&g?g.p(e,O):(g&&g.d(1),g=q&&q(e),g&&(g.c(),g.m(P,null)))},d(b){b&&d(t),C.d(),g&&g.d()}}}function $t(o,e){let t,a=e[8].code+"",u,m,c,p;function y(){return e[7](e[8])}return{key:o,first:null,c(){t=s("button"),u=_(a),m=f(),v(t,"class","tab-item"),ye(t,"active",e[2]===e[8].code),this.first=t},m(S,T){r(S,t,T),n(t,u),n(t,m),c||(p=Ft(t,"click",y),c=!0)},p(S,T){e=S,T&8&&a!==(a=e[8].code+"")&&x(u,a),T&12&&ye(t,"active",e[2]===e[8].code)},d(S){S&&d(t),c=!1,p()}}}function St(o,e){let t,a,u,m;return a=new Tt({props:{content:e[8].body}}),{key:o,first:null,c(){t=s("div"),_e(a.$$.fragment),u=f(),v(t,"class","tab-item"),ye(t,"active",e[2]===e[8].code),this.first=t},m(c,p){r(c,t,p),he(a,t,null),n(t,u),m=!0},p(c,p){e=c;const y={};p&8&&(y.content=e[8].body),a.$set(y),(!m||p&12)&&ye(t,"active",e[2]===e[8].code)},i(c){m||(fe(a.$$.fragment,c),m=!0)},o(c){ue(a.$$.fragment,c),m=!1},d(c){c&&d(t),ke(a)}}}function Wt(o){var ot,rt,dt,ct,pt;let e,t,a=o[0].name+"",u,m,c,p,y,S,T,w=o[0].name+"",H,D,E,P,I,j,B,C,N,q,g,b,O,z,A,h,$,ee,K=o[0].name+"",ve,je,De,ge,ie,we,W,Ce,Ne,U,$e,Ve,Se,V=[],Je=new Map,Te,se,qe,Y,Oe,Ee,oe,G,Me,Ie,He,Ue,M,Qe,te,ze,Ke,We,Le,Ye,Pe,Ge,Xe,Ze,Ae,xe,et,le,Fe,re,Re,X,de,J=[],tt=new Map,lt,ce,F=[],nt=new Map,Z;C=new Rt({props:{js:` -import PocketBase from 'pocketbase'; - -const pb = new PocketBase('${o[5]}'); - -... - -// example create data -const data = ${JSON.stringify(Object.assign({},o[4],Q.dummyCollectionSchemaData(o[0])),null,4)}; - -const record = await pb.collection('${(ot=o[0])==null?void 0:ot.name}').create(data); -`+(o[1]?` -// (optional) send an email verification request -await pb.collection('${(rt=o[0])==null?void 0:rt.name}').requestVerification('test@example.com'); -`:""),dart:` -import 'package:pocketbase/pocketbase.dart'; - -final pb = PocketBase('${o[5]}'); - -... - -// example create body -final body = ${JSON.stringify(Object.assign({},o[4],Q.dummyCollectionSchemaData(o[0])),null,2)}; - -final record = await pb.collection('${(dt=o[0])==null?void 0:dt.name}').create(body: body); -`+(o[1]?` -// (optional) send an email verification request -await pb.collection('${(ct=o[0])==null?void 0:ct.name}').requestVerification('test@example.com'); -`:"")}});let R=o[6]&>(),L=o[1]&&wt(o),me=ne((pt=o[0])==null?void 0:pt.schema);const at=l=>l[13].name;for(let l=0;ll[8].code;for(let l=0;ll[8].code;for(let l=0;lapplication/json or - multipart/form-data.`,I=f(),j=s("p"),j.innerHTML=`File upload is supported only via multipart/form-data. -
- For more info and examples you could check the detailed - Files upload and handling docs - .`,B=f(),_e(C.$$.fragment),N=f(),q=s("h6"),q.textContent="API details",g=f(),b=s("div"),O=s("strong"),O.textContent="POST",z=f(),A=s("div"),h=s("p"),$=_("/api/collections/"),ee=s("strong"),ve=_(K),je=_("/records"),De=f(),R&&R.c(),ge=f(),ie=s("div"),ie.textContent="Body Parameters",we=f(),W=s("table"),Ce=s("thead"),Ce.innerHTML='Param Type Description',Ne=f(),U=s("tbody"),$e=s("tr"),$e.innerHTML=`
Optional id
String 15 characters string to store as record ID. -
- If not set, it will be auto generated.`,Ve=f(),L&&L.c(),Se=f();for(let l=0;lParam Type Description',Ee=f(),oe=s("tbody"),G=s("tr"),Me=s("td"),Me.textContent="expand",Ie=f(),He=s("td"),He.innerHTML='String',Ue=f(),M=s("td"),Qe=_(`Auto expand relations when returning the created record. Ex.: - `),_e(te.$$.fragment),ze=_(` - Supports up to 6-levels depth nested relations expansion. `),Ke=s("br"),We=_(` - The expanded relations will be appended to the record under the - `),Le=s("code"),Le.textContent="expand",Ye=_(" property (eg. "),Pe=s("code"),Pe.textContent='"expand": {"relField1": {...}, ...}',Ge=_(`). - `),Xe=s("br"),Ze=_(` - Only the relations to which the request user has permissions to `),Ae=s("strong"),Ae.textContent="view",xe=_(" will be expanded."),et=f(),_e(le.$$.fragment),Fe=f(),re=s("div"),re.textContent="Responses",Re=f(),X=s("div"),de=s("div");for(let l=0;l${JSON.stringify(Object.assign({},l[4],Q.dummyCollectionSchemaData(l[0])),null,2)}; - -final record = await pb.collection('${(mt=l[0])==null?void 0:mt.name}').create(body: body); -`+(l[1]?` -// (optional) send an email verification request -await pb.collection('${(bt=l[0])==null?void 0:bt.name}').requestVerification('test@example.com'); -`:"")),C.$set(k),(!Z||i&1)&&K!==(K=l[0].name+"")&&x(ve,K),l[6]?R||(R=gt(),R.c(),R.m(b,null)):R&&(R.d(1),R=null),l[1]?L?L.p(l,i):(L=wt(l),L.c(),L.m(U,Se)):L&&(L.d(1),L=null),i&1&&(me=ne((_t=l[0])==null?void 0:_t.schema),V=Be(V,i,at,1,l,me,Je,U,ht,Ct,null,vt)),i&12&&(be=ne(l[3]),J=Be(J,i,it,1,l,be,tt,de,ht,$t,null,yt)),i&12&&(pe=ne(l[3]),Ht(),F=Be(F,i,st,1,l,pe,nt,ce,Lt,St,null,kt),Pt())},i(l){if(!Z){fe(C.$$.fragment,l),fe(te.$$.fragment,l),fe(le.$$.fragment,l);for(let i=0;it(2,p=w.code);return o.$$set=w=>{"collection"in w&&t(0,c=w.collection)},o.$$.update=()=>{var w,H;o.$$.dirty&1&&t(1,a=c.type==="auth"),o.$$.dirty&1&&t(6,u=(c==null?void 0:c.createRule)===null),o.$$.dirty&1&&t(3,y=[{code:200,body:JSON.stringify(Q.dummyCollectionRecord(c),null,2)},{code:400,body:` - { - "code": 400, - "message": "Failed to create record.", - "data": { - "${(H=(w=c==null?void 0:c.schema)==null?void 0:w[0])==null?void 0:H.name}": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `},{code:403,body:` - { - "code": 403, - "message": "You are not allowed to perform this request.", - "data": {} - } - `}]),o.$$.dirty&2&&(a?t(4,S={username:"test_username",email:"test@example.com",emailVisibility:!0,password:"12345678",passwordConfirm:"12345678"}):t(4,S={}))},t(5,m=Q.getApiExampleUrl(At.baseUrl)),[c,a,p,y,S,m,u,T]}class xt extends qt{constructor(e){super(),Ot(this,e,Yt,Wt,Mt,{collection:0})}}export{xt as default}; diff --git a/ui/dist/assets/DeleteApiDocs-CPOP5CUw.js b/ui/dist/assets/DeleteApiDocs-CPOP5CUw.js new file mode 100644 index 00000000..68e5f322 --- /dev/null +++ b/ui/dist/assets/DeleteApiDocs-CPOP5CUw.js @@ -0,0 +1,53 @@ +import{S as Re,i as Ee,s as Pe,Q as Te,T as j,e as c,w as y,b as k,c as De,f as m,g as p,h as i,m as Ce,x as ee,U as he,V as Be,k as Oe,W as Ie,n as Ae,t as te,a as le,o as f,d as we,C as Me,p as qe,r as z,u as Le,R as Se}from"./index-B-F-pko3.js";function ke(a,l,s){const o=a.slice();return o[6]=l[s],o}function ge(a,l,s){const o=a.slice();return o[6]=l[s],o}function ve(a){let l;return{c(){l=c("p"),l.innerHTML="Requires superuser Authorization:TOKEN header",m(l,"class","txt-hint txt-sm txt-right")},m(s,o){p(s,l,o)},d(s){s&&f(l)}}}function ye(a,l){let s,o,h;function r(){return l[5](l[6])}return{key:a,first:null,c(){s=c("button"),s.textContent=`${l[6].code} `,m(s,"class","tab-item"),z(s,"active",l[2]===l[6].code),this.first=s},m(n,d){p(n,s,d),o||(h=Le(s,"click",r),o=!0)},p(n,d){l=n,d&20&&z(s,"active",l[2]===l[6].code)},d(n){n&&f(s),o=!1,h()}}}function $e(a,l){let s,o,h,r;return o=new Se({props:{content:l[6].body}}),{key:a,first:null,c(){s=c("div"),De(o.$$.fragment),h=k(),m(s,"class","tab-item"),z(s,"active",l[2]===l[6].code),this.first=s},m(n,d){p(n,s,d),Ce(o,s,null),i(s,h),r=!0},p(n,d){l=n,(!r||d&20)&&z(s,"active",l[2]===l[6].code)},i(n){r||(te(o.$$.fragment,n),r=!0)},o(n){le(o.$$.fragment,n),r=!1},d(n){n&&f(s),we(o)}}}function Ue(a){var fe,me;let l,s,o=a[0].name+"",h,r,n,d,$,D,F,q=a[0].name+"",K,se,N,C,Q,P,V,g,L,ae,S,E,oe,W,U=a[0].name+"",G,ne,J,ie,X,T,Y,B,Z,O,x,w,I,v=[],ce=new Map,re,A,b=[],de=new Map,R;C=new Te({props:{js:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${a[3]}'); + + ... + + await pb.collection('${(fe=a[0])==null?void 0:fe.name}').delete('RECORD_ID'); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${a[3]}'); + + ... + + await pb.collection('${(me=a[0])==null?void 0:me.name}').delete('RECORD_ID'); + `}});let _=a[1]&&ve(),H=j(a[4]);const ue=e=>e[6].code;for(let e=0;ee[6].code;for(let e=0;eParam Type Description id String ID of the record to delete.',Z=k(),O=c("div"),O.textContent="Responses",x=k(),w=c("div"),I=c("div");for(let e=0;es(2,n=D.code);return a.$$set=D=>{"collection"in D&&s(0,r=D.collection)},a.$$.update=()=>{a.$$.dirty&1&&s(1,o=(r==null?void 0:r.deleteRule)===null),a.$$.dirty&3&&r!=null&&r.id&&(d.push({code:204,body:` + null + `}),d.push({code:400,body:` + { + "code": 400, + "message": "Failed to delete record. Make sure that the record is not part of a required relation reference.", + "data": {} + } + `}),o&&d.push({code:403,body:` + { + "code": 403, + "message": "Only superusers can access this action.", + "data": {} + } + `}),d.push({code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `}))},s(3,h=Me.getApiExampleUrl(qe.baseURL)),[r,o,n,h,d,$]}class ze extends Re{constructor(l){super(),Ee(this,l,He,Ue,Pe,{collection:0})}}export{ze as default}; diff --git a/ui/dist/assets/DeleteApiDocs-DninUosh.js b/ui/dist/assets/DeleteApiDocs-DninUosh.js deleted file mode 100644 index bb030664..00000000 --- a/ui/dist/assets/DeleteApiDocs-DninUosh.js +++ /dev/null @@ -1,53 +0,0 @@ -import{S as Re,i as Pe,s as Ee,O as j,e as c,v as y,b as k,c as Ce,f as m,g as p,h as i,m as De,w as ee,P as he,Q as Oe,k as Te,R as Ae,n as Be,t as te,a as le,o as u,d as we,C as Ie,A as qe,q as N,r as Me,N as Se}from"./index-Bp3jGQ0J.js";import{S as He}from"./SdkTabs-DxNNd6Sw.js";function ke(a,l,s){const o=a.slice();return o[6]=l[s],o}function ge(a,l,s){const o=a.slice();return o[6]=l[s],o}function ve(a){let l;return{c(){l=c("p"),l.innerHTML="Requires admin Authorization:TOKEN header",m(l,"class","txt-hint txt-sm txt-right")},m(s,o){p(s,l,o)},d(s){s&&u(l)}}}function ye(a,l){let s,o,h;function d(){return l[5](l[6])}return{key:a,first:null,c(){s=c("button"),s.textContent=`${l[6].code} `,m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(n,r){p(n,s,r),o||(h=Me(s,"click",d),o=!0)},p(n,r){l=n,r&20&&N(s,"active",l[2]===l[6].code)},d(n){n&&u(s),o=!1,h()}}}function $e(a,l){let s,o,h,d;return o=new Se({props:{content:l[6].body}}),{key:a,first:null,c(){s=c("div"),Ce(o.$$.fragment),h=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(n,r){p(n,s,r),De(o,s,null),i(s,h),d=!0},p(n,r){l=n,(!d||r&20)&&N(s,"active",l[2]===l[6].code)},i(n){d||(te(o.$$.fragment,n),d=!0)},o(n){le(o.$$.fragment,n),d=!1},d(n){n&&u(s),we(o)}}}function Le(a){var ue,me;let l,s,o=a[0].name+"",h,d,n,r,$,C,z,M=a[0].name+"",F,se,K,D,Q,E,G,g,S,ae,H,P,oe,J,L=a[0].name+"",V,ne,W,ie,X,O,Y,T,Z,A,x,w,B,v=[],ce=new Map,de,I,b=[],re=new Map,R;D=new He({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${a[3]}'); - - ... - - await pb.collection('${(ue=a[0])==null?void 0:ue.name}').delete('RECORD_ID'); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${a[3]}'); - - ... - - await pb.collection('${(me=a[0])==null?void 0:me.name}').delete('RECORD_ID'); - `}});let _=a[1]&&ve(),U=j(a[4]);const fe=e=>e[6].code;for(let e=0;ee[6].code;for(let e=0;eParam Type Description id String ID of the record to delete.',Z=k(),A=c("div"),A.textContent="Responses",x=k(),w=c("div"),B=c("div");for(let e=0;es(2,n=C.code);return a.$$set=C=>{"collection"in C&&s(0,d=C.collection)},a.$$.update=()=>{a.$$.dirty&1&&s(1,o=(d==null?void 0:d.deleteRule)===null),a.$$.dirty&3&&d!=null&&d.id&&(r.push({code:204,body:` - null - `}),r.push({code:400,body:` - { - "code": 400, - "message": "Failed to delete record. Make sure that the record is not part of a required relation reference.", - "data": {} - } - `}),o&&r.push({code:403,body:` - { - "code": 403, - "message": "Only admins can access this action.", - "data": {} - } - `}),r.push({code:404,body:` - { - "code": 404, - "message": "The requested resource wasn't found.", - "data": {} - } - `}))},s(3,h=Ie.getApiExampleUrl(qe.baseUrl)),[d,o,n,h,r,$]}class ze extends Re{constructor(l){super(),Pe(this,l,Ue,Le,Ee,{collection:0})}}export{ze as default}; diff --git a/ui/dist/assets/EmailChangeDocs-VdDPaZK5.js b/ui/dist/assets/EmailChangeDocs-VdDPaZK5.js new file mode 100644 index 00000000..e4840744 --- /dev/null +++ b/ui/dist/assets/EmailChangeDocs-VdDPaZK5.js @@ -0,0 +1,120 @@ +import{S as se,i as oe,s as ie,T as K,e as p,b as y,w as U,f as b,g,h as u,x as J,U as le,V as Re,k as ne,W as Se,n as ae,t as Q,a as V,o as v,r as Y,u as ce,R as Oe,c as x,m as ee,d as te,Q as Me,X as _e,C as Be,p as De,Y as be}from"./index-B-F-pko3.js";function ge(n,e,t){const l=n.slice();return l[4]=e[t],l}function ve(n,e,t){const l=n.slice();return l[4]=e[t],l}function ke(n,e){let t,l=e[4].code+"",d,i,r,a;function m(){return e[3](e[4])}return{key:n,first:null,c(){t=p("button"),d=U(l),i=y(),b(t,"class","tab-item"),Y(t,"active",e[1]===e[4].code),this.first=t},m(k,P){g(k,t,P),u(t,d),u(t,i),r||(a=ce(t,"click",m),r=!0)},p(k,P){e=k,P&4&&l!==(l=e[4].code+"")&&J(d,l),P&6&&Y(t,"active",e[1]===e[4].code)},d(k){k&&v(t),r=!1,a()}}}function $e(n,e){let t,l,d,i;return l=new Oe({props:{content:e[4].body}}),{key:n,first:null,c(){t=p("div"),x(l.$$.fragment),d=y(),b(t,"class","tab-item"),Y(t,"active",e[1]===e[4].code),this.first=t},m(r,a){g(r,t,a),ee(l,t,null),u(t,d),i=!0},p(r,a){e=r;const m={};a&4&&(m.content=e[4].body),l.$set(m),(!i||a&6)&&Y(t,"active",e[1]===e[4].code)},i(r){i||(Q(l.$$.fragment,r),i=!0)},o(r){V(l.$$.fragment,r),i=!1},d(r){r&&v(t),te(l)}}}function Ne(n){let e,t,l,d,i,r,a,m=n[0].name+"",k,P,G,H,F,L,z,B,D,S,N,T=[],O=new Map,A,j,q=[],W=new Map,w,E=K(n[2]);const M=c=>c[4].code;for(let c=0;cc[4].code;for(let c=0;c<_.length;c+=1){let f=ge(n,_,c),s=X(f);W.set(s,q[c]=$e(s,f))}return{c(){e=p("div"),t=p("strong"),t.textContent="POST",l=y(),d=p("div"),i=p("p"),r=U("/api/collections/"),a=p("strong"),k=U(m),P=U("/confirm-email-change"),G=y(),H=p("div"),H.textContent="Body Parameters",F=y(),L=p("table"),L.innerHTML='Param Type Description
Required token
String The token from the change email request email.
Required password
String The account password to confirm the email change.',z=y(),B=p("div"),B.textContent="Responses",D=y(),S=p("div"),N=p("div");for(let c=0;ct(1,d=a.code);return n.$$set=a=>{"collection"in a&&t(0,l=a.collection)},t(2,i=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "token": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}]),[l,d,i,r]}class He extends se{constructor(e){super(),oe(this,e,We,Ne,ie,{collection:0})}}function we(n,e,t){const l=n.slice();return l[4]=e[t],l}function Ce(n,e,t){const l=n.slice();return l[4]=e[t],l}function ye(n,e){let t,l=e[4].code+"",d,i,r,a;function m(){return e[3](e[4])}return{key:n,first:null,c(){t=p("button"),d=U(l),i=y(),b(t,"class","tab-item"),Y(t,"active",e[1]===e[4].code),this.first=t},m(k,P){g(k,t,P),u(t,d),u(t,i),r||(a=ce(t,"click",m),r=!0)},p(k,P){e=k,P&4&&l!==(l=e[4].code+"")&&J(d,l),P&6&&Y(t,"active",e[1]===e[4].code)},d(k){k&&v(t),r=!1,a()}}}function Ee(n,e){let t,l,d,i;return l=new Oe({props:{content:e[4].body}}),{key:n,first:null,c(){t=p("div"),x(l.$$.fragment),d=y(),b(t,"class","tab-item"),Y(t,"active",e[1]===e[4].code),this.first=t},m(r,a){g(r,t,a),ee(l,t,null),u(t,d),i=!0},p(r,a){e=r;const m={};a&4&&(m.content=e[4].body),l.$set(m),(!i||a&6)&&Y(t,"active",e[1]===e[4].code)},i(r){i||(Q(l.$$.fragment,r),i=!0)},o(r){V(l.$$.fragment,r),i=!1},d(r){r&&v(t),te(l)}}}function Le(n){let e,t,l,d,i,r,a,m=n[0].name+"",k,P,G,H,F,L,z,B,D,S,N,T,O,A=[],j=new Map,q,W,w=[],E=new Map,M,_=K(n[2]);const X=s=>s[4].code;for(let s=0;s<_.length;s+=1){let h=Ce(n,_,s),R=X(h);j.set(R,A[s]=ye(R,h))}let c=K(n[2]);const f=s=>s[4].code;for(let s=0;sAuthorization:TOKEN header",F=y(),L=p("div"),L.textContent="Body Parameters",z=y(),B=p("table"),B.innerHTML='Param Type Description
Required newEmail
String The new email address to send the change email request.',D=y(),S=p("div"),S.textContent="Responses",N=y(),T=p("div"),O=p("div");for(let s=0;st(1,d=a.code);return n.$$set=a=>{"collection"in a&&t(0,l=a.collection)},t(2,i=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "newEmail": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `},{code:401,body:` + { + "code": 401, + "message": "The request requires valid record authorization token to be set.", + "data": {} + } + `},{code:403,body:` + { + "code": 403, + "message": "The authorized record model is not allowed to perform this action.", + "data": {} + } + `}]),[l,d,i,r]}class Ie extends se{constructor(e){super(),oe(this,e,Ue,Le,ie,{collection:0})}}function Te(n,e,t){const l=n.slice();return l[5]=e[t],l[7]=t,l}function qe(n,e,t){const l=n.slice();return l[5]=e[t],l[7]=t,l}function Pe(n){let e,t,l,d,i;function r(){return n[4](n[7])}return{c(){e=p("button"),t=p("div"),t.textContent=`${n[5].title}`,l=y(),b(t,"class","txt"),b(e,"class","tab-item"),Y(e,"active",n[1]==n[7])},m(a,m){g(a,e,m),u(e,t),u(e,l),d||(i=ce(e,"click",r),d=!0)},p(a,m){n=a,m&2&&Y(e,"active",n[1]==n[7])},d(a){a&&v(e),d=!1,i()}}}function Ae(n){let e,t,l,d;var i=n[5].component;function r(a,m){return{props:{collection:a[0]}}}return i&&(t=be(i,r(n))),{c(){e=p("div"),t&&x(t.$$.fragment),l=y(),b(e,"class","tab-item"),Y(e,"active",n[1]==n[7])},m(a,m){g(a,e,m),t&&ee(t,e,null),u(e,l),d=!0},p(a,m){if(i!==(i=a[5].component)){if(t){ne();const k=t;V(k.$$.fragment,1,0,()=>{te(k,1)}),ae()}i?(t=be(i,r(a)),x(t.$$.fragment),Q(t.$$.fragment,1),ee(t,e,l)):t=null}else if(i){const k={};m&1&&(k.collection=a[0]),t.$set(k)}(!d||m&2)&&Y(e,"active",a[1]==a[7])},i(a){d||(t&&Q(t.$$.fragment,a),d=!0)},o(a){t&&V(t.$$.fragment,a),d=!1},d(a){a&&v(e),t&&te(t)}}}function Ke(n){var c,f,s,h,R,re;let e,t,l=n[0].name+"",d,i,r,a,m,k,P,G=n[0].name+"",H,F,L,z,B,D,S,N,T,O,A,j,q,W;D=new Me({props:{js:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${n[2]}'); + + ... + + await pb.collection('${(c=n[0])==null?void 0:c.name}').authWithPassword('test@example.com', '1234567890'); + + await pb.collection('${(f=n[0])==null?void 0:f.name}').requestEmailChange('new@example.com'); + + // --- + // (optional) in your custom confirmation page: + // --- + + // note: after this call all previously issued auth tokens are invalidated + await pb.collection('${(s=n[0])==null?void 0:s.name}').confirmEmailChange( + 'EMAIL_CHANGE_TOKEN', + 'YOUR_PASSWORD', + ); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${n[2]}'); + + ... + + await pb.collection('${(h=n[0])==null?void 0:h.name}').authWithPassword('test@example.com', '1234567890'); + + await pb.collection('${(R=n[0])==null?void 0:R.name}').requestEmailChange('new@example.com'); + + ... + + // --- + // (optional) in your custom confirmation page: + // --- + + // note: after this call all previously issued auth tokens are invalidated + await pb.collection('${(re=n[0])==null?void 0:re.name}').confirmEmailChange( + 'EMAIL_CHANGE_TOKEN', + 'YOUR_PASSWORD', + ); + `}});let w=K(n[3]),E=[];for(let o=0;oV(_[o],1,1,()=>{_[o]=null});return{c(){e=p("h3"),t=U("Email change ("),d=U(l),i=U(")"),r=y(),a=p("div"),m=p("p"),k=U("Sends "),P=p("strong"),H=U(G),F=U(" email change request."),L=y(),z=p("p"),z.textContent=`On successful email change all previously issued auth tokens for the specific record will be + automatically invalidated.`,B=y(),x(D.$$.fragment),S=y(),N=p("h6"),N.textContent="API details",T=y(),O=p("div"),A=p("div");for(let o=0;ot(1,r=m);return n.$$set=m=>{"collection"in m&&t(0,d=m.collection)},t(2,l=Be.getApiExampleUrl(De.baseURL)),[d,r,l,i,a]}class ze extends se{constructor(e){super(),oe(this,e,Ye,Ke,ie,{collection:0})}}export{ze as default}; diff --git a/ui/dist/assets/FieldsQueryParam-zDO3HzQv.js b/ui/dist/assets/FieldsQueryParam-CW6KZfgu.js similarity index 54% rename from ui/dist/assets/FieldsQueryParam-zDO3HzQv.js rename to ui/dist/assets/FieldsQueryParam-CW6KZfgu.js index f5642514..4d68e1fc 100644 --- a/ui/dist/assets/FieldsQueryParam-zDO3HzQv.js +++ b/ui/dist/assets/FieldsQueryParam-CW6KZfgu.js @@ -1,7 +1,7 @@ -import{S as J,i as O,s as P,N as Q,e as t,b as c,v as i,c as R,f as j,g as z,h as e,m as A,w as D,t as G,a as K,o as U,d as V}from"./index-Bp3jGQ0J.js";function W(f){let n,o,u,d,k,s,p,w,h,y,r,F,_,S,b,E,C,a,$,L,q,H,M,N,m,T,v,B,x;return r=new Q({props:{content:"?fields=*,"+f[0]+"expand.relField.name"}}),{c(){n=t("tr"),o=t("td"),o.textContent="fields",u=c(),d=t("td"),d.innerHTML='String',k=c(),s=t("td"),p=t("p"),w=i(`Comma separated string of the fields to return in the JSON response +import{S as J,i as N,s as O,R as P,e as t,b as c,w as i,c as Q,f as j,g as z,h as e,m as A,x as D,t as G,a as K,o as U,d as V}from"./index-B-F-pko3.js";function W(f){let n,o,u,d,v,s,p,w,h,y,r,F,_,S,b,E,C,a,$,L,q,H,M,R,m,T,k,B,x;return r=new P({props:{content:"?fields=*,"+f[0]+"expand.relField.name"}}),{c(){n=t("tr"),o=t("td"),o.textContent="fields",u=c(),d=t("td"),d.innerHTML='String',v=c(),s=t("td"),p=t("p"),w=i(`Comma separated string of the fields to return in the JSON response `),h=t("em"),h.textContent="(by default returns all fields)",y=i(`. Ex.: - `),R(r.$$.fragment),F=c(),_=t("p"),_.innerHTML="* targets all keys from the specific depth level.",S=c(),b=t("p"),b.textContent="In addition, the following field modifiers are also supported:",E=c(),C=t("ul"),a=t("li"),$=t("code"),$.textContent=":excerpt(maxLength, withEllipsis?)",L=c(),q=t("br"),H=i(` + `),Q(r.$$.fragment),F=c(),_=t("p"),_.innerHTML="* targets all keys from the specific depth level.",S=c(),b=t("p"),b.textContent="In addition, the following field modifiers are also supported:",E=c(),C=t("ul"),a=t("li"),$=t("code"),$.textContent=":excerpt(maxLength, withEllipsis?)",L=c(),q=t("br"),H=i(` Returns a short plain text version of the field string value. - `),M=t("br"),N=i(` + `),M=t("br"),R=i(` Ex.: - `),m=t("code"),T=i("?fields=*,"),v=i(f[0]),B=i("description:excerpt(200,true)"),j(o,"id","query-page")},m(l,g){z(l,n,g),e(n,o),e(n,u),e(n,d),e(n,k),e(n,s),e(s,p),e(p,w),e(p,h),e(p,y),A(r,p,null),e(s,F),e(s,_),e(s,S),e(s,b),e(s,E),e(s,C),e(C,a),e(a,$),e(a,L),e(a,q),e(a,H),e(a,M),e(a,N),e(a,m),e(m,T),e(m,v),e(m,B),x=!0},p(l,[g]){const I={};g&1&&(I.content="?fields=*,"+l[0]+"expand.relField.name"),r.$set(I),(!x||g&1)&&D(v,l[0])},i(l){x||(G(r.$$.fragment,l),x=!0)},o(l){K(r.$$.fragment,l),x=!1},d(l){l&&U(n),V(r)}}}function X(f,n,o){let{prefix:u=""}=n;return f.$$set=d=>{"prefix"in d&&o(0,u=d.prefix)},[u]}class Z extends J{constructor(n){super(),O(this,n,X,W,P,{prefix:0})}}export{Z as F}; + `),m=t("code"),T=i("?fields=*,"),k=i(f[0]),B=i("description:excerpt(200,true)"),j(o,"id","query-page")},m(l,g){z(l,n,g),e(n,o),e(n,u),e(n,d),e(n,v),e(n,s),e(s,p),e(p,w),e(p,h),e(p,y),A(r,p,null),e(s,F),e(s,_),e(s,S),e(s,b),e(s,E),e(s,C),e(C,a),e(a,$),e(a,L),e(a,q),e(a,H),e(a,M),e(a,R),e(a,m),e(m,T),e(m,k),e(m,B),x=!0},p(l,[g]){const I={};g&1&&(I.content="?fields=*,"+l[0]+"expand.relField.name"),r.$set(I),(!x||g&1)&&D(k,l[0])},i(l){x||(G(r.$$.fragment,l),x=!0)},o(l){K(r.$$.fragment,l),x=!1},d(l){l&&U(n),V(r)}}}function X(f,n,o){let{prefix:u=""}=n;return f.$$set=d=>{"prefix"in d&&o(0,u=d.prefix)},[u]}class Z extends J{constructor(n){super(),N(this,n,X,W,O,{prefix:0})}}export{Z as F}; diff --git a/ui/dist/assets/FilterAutocompleteInput-l9cXyHQU.js b/ui/dist/assets/FilterAutocompleteInput-DvxlPb20.js similarity index 50% rename from ui/dist/assets/FilterAutocompleteInput-l9cXyHQU.js rename to ui/dist/assets/FilterAutocompleteInput-DvxlPb20.js index e23dd416..0cdf9294 100644 --- a/ui/dist/assets/FilterAutocompleteInput-l9cXyHQU.js +++ b/ui/dist/assets/FilterAutocompleteInput-DvxlPb20.js @@ -1 +1 @@ -import{S as $,i as ee,s as te,e as ne,f as re,g as ae,x as O,o as ie,J as oe,K as le,L as se,I as de,C as u,M as ce}from"./index-Bp3jGQ0J.js";import{c as fe,d as ue,s as ge,h as he,a as ye,E,b as S,e as pe,f as ke,g as me,i as xe,j as be,k as we,l as Ee,m as Se,r as Ke,n as Ce,o as Re,p as Le,q as j,C as R,S as qe,t as We,u as ve,v as _e}from"./index-BztyTJOx.js";function De(e){return new Worker(""+new URL("autocomplete.worker-Dy9W6Fpj.js",import.meta.url).href,{name:e==null?void 0:e.name})}function Oe(e){G(e,"start");var r={},t=e.languageData||{},g=!1;for(var h in e)if(h!=t&&e.hasOwnProperty(h))for(var f=r[h]=[],i=e[h],a=0;a2&&i.token&&typeof i.token!="string"){t.pending=[];for(var s=2;s-1)return null;var h=t.indent.length-1,f=e[t.state];e:for(;;){for(var i=0;it(21,g=n));const h=se();let{id:f=""}=r,{value:i=""}=r,{disabled:a=!1}=r,{placeholder:o=""}=r,{baseCollection:s=null}=r,{singleLine:y=!1}=r,{extraAutocompleteKeys:L=[]}=r,{disableRequestKeys:b=!1}=r,{disableCollectionJoinKeys:m=!1}=r,d,p,q=a,I=new R,J=new R,M=new R,A=new R,W=new De,B=[],H=[],T=[],K="",v="";function _(){d==null||d.focus()}let D=null;W.onmessage=n=>{T=n.data.baseKeys||[],B=n.data.requestKeys||[],H=n.data.collectionJoinKeys||[]};function V(){clearTimeout(D),D=setTimeout(()=>{W.postMessage({baseCollection:s,collections:z(g),disableRequestKeys:b,disableCollectionJoinKeys:m})},250)}function z(n){let c=n.slice();return s&&u.pushOrReplaceByKey(c,s,"id"),c}function F(){p==null||p.dispatchEvent(new CustomEvent("change",{detail:{value:i},bubbles:!0}))}function U(){if(!f)return;const n=document.querySelectorAll('[for="'+f+'"]');for(let c of n)c.removeEventListener("click",_)}function N(){if(!f)return;U();const n=document.querySelectorAll('[for="'+f+'"]');for(let c of n)c.addEventListener("click",_)}function Q(n=!0,c=!0){let l=[].concat(L);return l=l.concat(T||[]),n&&(l=l.concat(B||[])),c&&(l=l.concat(H||[])),l}function X(n){var w;let c=n.matchBefore(/[\'\"\@\w\.]*/);if(c&&c.from==c.to&&!n.explicit)return null;let l=_e(n.state).resolveInner(n.pos,-1);if(((w=l==null?void 0:l.type)==null?void 0:w.name)=="comment")return null;let x=[{label:"false"},{label:"true"},{label:"@now"},{label:"@second"},{label:"@minute"},{label:"@hour"},{label:"@year"},{label:"@day"},{label:"@month"},{label:"@weekday"},{label:"@todayStart"},{label:"@todayEnd"},{label:"@monthStart"},{label:"@monthEnd"},{label:"@yearStart"},{label:"@yearEnd"}];m||x.push({label:"@collection.*",apply:"@collection."});let C=Q(!b&&c.text.startsWith("@r"),!m&&c.text.startsWith("@c"));for(const k of C)x.push({label:k.endsWith(".")?k+"*":k,apply:k,boost:k.indexOf("_via_")>0?-1:0});return{from:c.from,options:x}}function P(){return qe.define(Oe({start:[{regex:/true|false|null/,token:"atom"},{regex:/\/\/.*/,token:"comment"},{regex:/"(?:[^\\]|\\.)*?(?:"|$)/,token:"string"},{regex:/'(?:[^\\]|\\.)*?(?:'|$)/,token:"string"},{regex:/0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,token:"number"},{regex:/\&\&|\|\||\=|\!\=|\~|\!\~|\>|\<|\>\=|\<\=/,token:"operator"},{regex:/[\{\[\(]/,indent:!0},{regex:/[\}\]\)]/,dedent:!0},{regex:/\w+[\w\.]*\w+/,token:"keyword"},{regex:u.escapeRegExp("@now"),token:"keyword"},{regex:u.escapeRegExp("@second"),token:"keyword"},{regex:u.escapeRegExp("@minute"),token:"keyword"},{regex:u.escapeRegExp("@hour"),token:"keyword"},{regex:u.escapeRegExp("@year"),token:"keyword"},{regex:u.escapeRegExp("@day"),token:"keyword"},{regex:u.escapeRegExp("@month"),token:"keyword"},{regex:u.escapeRegExp("@weekday"),token:"keyword"},{regex:u.escapeRegExp("@todayStart"),token:"keyword"},{regex:u.escapeRegExp("@todayEnd"),token:"keyword"},{regex:u.escapeRegExp("@monthStart"),token:"keyword"},{regex:u.escapeRegExp("@monthEnd"),token:"keyword"},{regex:u.escapeRegExp("@yearStart"),token:"keyword"},{regex:u.escapeRegExp("@yearEnd"),token:"keyword"},{regex:u.escapeRegExp("@request.method"),token:"keyword"}],meta:{lineComment:"//"}}))}de(()=>{const n={key:"Enter",run:l=>{y&&h("submit",i)}};N();let c=[n,...fe,...ue,ge.find(l=>l.key==="Mod-d"),...he,...ye];return y||c.push(We),t(11,d=new E({parent:p,state:S.create({doc:i,extensions:[pe(),ke(),me(),xe(),be(),S.allowMultipleSelections.of(!0),we(ve,{fallback:!0}),Ee(),Se(),Ke(),Ce(),Re.of(c),E.lineWrapping,Le({override:[X],icons:!1}),A.of(j(o)),J.of(E.editable.of(!a)),M.of(S.readOnly.of(a)),I.of(P()),S.transactionFilter.of(l=>{var x,C,w;if(y&&l.newDoc.lines>1){if(!((w=(C=(x=l.changes)==null?void 0:x.inserted)==null?void 0:C.filter(k=>!!k.text.find(Z=>Z)))!=null&&w.length))return[];l.newDoc.text=[l.newDoc.text.join(" ")]}return l}),E.updateListener.of(l=>{!l.docChanged||a||(t(1,i=l.state.doc.toString()),F())})]})})),()=>{clearTimeout(D),U(),d==null||d.destroy(),W.terminate()}});function Y(n){ce[n?"unshift":"push"](()=>{p=n,t(0,p)})}return e.$$set=n=>{"id"in n&&t(2,f=n.id),"value"in n&&t(1,i=n.value),"disabled"in n&&t(3,a=n.disabled),"placeholder"in n&&t(4,o=n.placeholder),"baseCollection"in n&&t(5,s=n.baseCollection),"singleLine"in n&&t(6,y=n.singleLine),"extraAutocompleteKeys"in n&&t(7,L=n.extraAutocompleteKeys),"disableRequestKeys"in n&&t(8,b=n.disableRequestKeys),"disableCollectionJoinKeys"in n&&t(9,m=n.disableCollectionJoinKeys)},e.$$.update=()=>{e.$$.dirty[0]&32&&t(13,K=Te(s)),e.$$.dirty[0]&25352&&!a&&(v!=K||b!==-1||m!==-1)&&(t(14,v=K),V()),e.$$.dirty[0]&4&&f&&N(),e.$$.dirty[0]&2080&&d&&s!=null&&s.schema&&d.dispatch({effects:[I.reconfigure(P())]}),e.$$.dirty[0]&6152&&d&&q!=a&&(d.dispatch({effects:[J.reconfigure(E.editable.of(!a)),M.reconfigure(S.readOnly.of(a))]}),t(12,q=a),F()),e.$$.dirty[0]&2050&&d&&i!=d.state.doc.toString()&&d.dispatch({changes:{from:0,to:d.state.doc.length,insert:i}}),e.$$.dirty[0]&2064&&d&&typeof o<"u"&&d.dispatch({effects:[A.reconfigure(j(o))]})},[p,i,f,a,o,s,y,L,b,m,_,d,q,K,v,Y]}class Pe extends ${constructor(r){super(),ee(this,r,Fe,He,te,{id:2,value:1,disabled:3,placeholder:4,baseCollection:5,singleLine:6,extraAutocompleteKeys:7,disableRequestKeys:8,disableCollectionJoinKeys:9,focus:10},null,[-1,-1])}get focus(){return this.$$.ctx[10]}}export{Pe as default}; +import{S as $,i as ee,s as te,e as ne,f as re,g as ie,y as D,o as ae,J as oe,N as le,O as se,L as de,C as u,P as ce}from"./index-B-F-pko3.js";import{c as fe,d as ue,s as ge,h as he,a as ye,E,b as S,e as pe,f as ke,g as me,i as xe,j as be,k as we,l as Ee,m as Se,r as Ce,n as Ke,o as Re,p as Le,q as G,C as R,S as qe,t as ve,u as Oe,v as We}from"./index-B5ReTu-C.js";function _e(e){return new Worker(""+new URL("autocomplete.worker-Br7MPIGR.js",import.meta.url).href,{name:e==null?void 0:e.name})}function De(e){V(e,"start");var r={},t=e.languageData||{},g=!1;for(var h in e)if(h!=t&&e.hasOwnProperty(h))for(var f=r[h]=[],a=e[h],i=0;i2&&a.token&&typeof a.token!="string"){t.pending=[];for(var s=2;s-1)return null;var h=t.indent.length-1,f=e[t.state];e:for(;;){for(var a=0;at(21,g=n));const h=se();let{id:f=""}=r,{value:a=""}=r,{disabled:i=!1}=r,{placeholder:o=""}=r,{baseCollection:s=null}=r,{singleLine:y=!1}=r,{extraAutocompleteKeys:L=[]}=r,{disableRequestKeys:b=!1}=r,{disableCollectionJoinKeys:m=!1}=r,d,p,q=i,I=new R,J=new R,M=new R,A=new R,v=new _e,B=[],H=[],T=[],C="",O="";function W(){d==null||d.focus()}let _=null;v.onmessage=n=>{T=n.data.baseKeys||[],B=n.data.requestKeys||[],H=n.data.collectionJoinKeys||[]};function j(){clearTimeout(_),_=setTimeout(()=>{v.postMessage({baseCollection:s,collections:z(g),disableRequestKeys:b,disableCollectionJoinKeys:m})},250)}function z(n){let c=n.slice();return s&&u.pushOrReplaceByKey(c,s,"id"),c}function F(){p==null||p.dispatchEvent(new CustomEvent("change",{detail:{value:a},bubbles:!0}))}function P(){if(!f)return;const n=document.querySelectorAll('[for="'+f+'"]');for(let c of n)c.removeEventListener("click",W)}function U(){if(!f)return;P();const n=document.querySelectorAll('[for="'+f+'"]');for(let c of n)c.addEventListener("click",W)}function Q(n=!0,c=!0){let l=[].concat(L);return l=l.concat(T||[]),n&&(l=l.concat(B||[])),c&&(l=l.concat(H||[])),l}function X(n){var w;let c=n.matchBefore(/[\'\"\@\w\.]*/);if(c&&c.from==c.to&&!n.explicit)return null;let l=We(n.state).resolveInner(n.pos,-1);if(((w=l==null?void 0:l.type)==null?void 0:w.name)=="comment")return null;let x=[{label:"false"},{label:"true"},{label:"@now"},{label:"@second"},{label:"@minute"},{label:"@hour"},{label:"@year"},{label:"@day"},{label:"@month"},{label:"@weekday"},{label:"@todayStart"},{label:"@todayEnd"},{label:"@monthStart"},{label:"@monthEnd"},{label:"@yearStart"},{label:"@yearEnd"}];m||x.push({label:"@collection.*",apply:"@collection."});let K=Q(!b&&c.text.startsWith("@r"),!m&&c.text.startsWith("@c"));for(const k of K)x.push({label:k.endsWith(".")?k+"*":k,apply:k,boost:k.indexOf("_via_")>0?-1:0});return{from:c.from,options:x}}function N(){return qe.define(De({start:[{regex:/true|false|null/,token:"atom"},{regex:/\/\/.*/,token:"comment"},{regex:/"(?:[^\\]|\\.)*?(?:"|$)/,token:"string"},{regex:/'(?:[^\\]|\\.)*?(?:'|$)/,token:"string"},{regex:/0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,token:"number"},{regex:/\&\&|\|\||\=|\!\=|\~|\!\~|\>|\<|\>\=|\<\=/,token:"operator"},{regex:/[\{\[\(]/,indent:!0},{regex:/[\}\]\)]/,dedent:!0},{regex:/\w+[\w\.]*\w+/,token:"keyword"},{regex:u.escapeRegExp("@now"),token:"keyword"},{regex:u.escapeRegExp("@second"),token:"keyword"},{regex:u.escapeRegExp("@minute"),token:"keyword"},{regex:u.escapeRegExp("@hour"),token:"keyword"},{regex:u.escapeRegExp("@year"),token:"keyword"},{regex:u.escapeRegExp("@day"),token:"keyword"},{regex:u.escapeRegExp("@month"),token:"keyword"},{regex:u.escapeRegExp("@weekday"),token:"keyword"},{regex:u.escapeRegExp("@todayStart"),token:"keyword"},{regex:u.escapeRegExp("@todayEnd"),token:"keyword"},{regex:u.escapeRegExp("@monthStart"),token:"keyword"},{regex:u.escapeRegExp("@monthEnd"),token:"keyword"},{regex:u.escapeRegExp("@yearStart"),token:"keyword"},{regex:u.escapeRegExp("@yearEnd"),token:"keyword"},{regex:u.escapeRegExp("@request.method"),token:"keyword"}],meta:{lineComment:"//"}}))}de(()=>{const n={key:"Enter",run:l=>{y&&h("submit",a)}};U();let c=[n,...fe,...ue,ge.find(l=>l.key==="Mod-d"),...he,...ye];return y||c.push(ve),t(11,d=new E({parent:p,state:S.create({doc:a,extensions:[pe(),ke(),me(),xe(),be(),S.allowMultipleSelections.of(!0),we(Oe,{fallback:!0}),Ee(),Se(),Ce(),Ke(),Re.of(c),E.lineWrapping,Le({override:[X],icons:!1}),A.of(G(o)),J.of(E.editable.of(!i)),M.of(S.readOnly.of(i)),I.of(N()),S.transactionFilter.of(l=>{var x,K,w;if(y&&l.newDoc.lines>1){if(!((w=(K=(x=l.changes)==null?void 0:x.inserted)==null?void 0:K.filter(k=>!!k.text.find(Z=>Z)))!=null&&w.length))return[];l.newDoc.text=[l.newDoc.text.join(" ")]}return l}),E.updateListener.of(l=>{!l.docChanged||i||(t(1,a=l.state.doc.toString()),F())})]})})),()=>{clearTimeout(_),P(),d==null||d.destroy(),v.terminate()}});function Y(n){ce[n?"unshift":"push"](()=>{p=n,t(0,p)})}return e.$$set=n=>{"id"in n&&t(2,f=n.id),"value"in n&&t(1,a=n.value),"disabled"in n&&t(3,i=n.disabled),"placeholder"in n&&t(4,o=n.placeholder),"baseCollection"in n&&t(5,s=n.baseCollection),"singleLine"in n&&t(6,y=n.singleLine),"extraAutocompleteKeys"in n&&t(7,L=n.extraAutocompleteKeys),"disableRequestKeys"in n&&t(8,b=n.disableRequestKeys),"disableCollectionJoinKeys"in n&&t(9,m=n.disableCollectionJoinKeys)},e.$$.update=()=>{e.$$.dirty[0]&32&&t(13,C=Te(s)),e.$$.dirty[0]&25352&&!i&&(O!=C||b!==-1||m!==-1)&&(t(14,O=C),j()),e.$$.dirty[0]&4&&f&&U(),e.$$.dirty[0]&2080&&d&&s!=null&&s.fields&&d.dispatch({effects:[I.reconfigure(N())]}),e.$$.dirty[0]&6152&&d&&q!=i&&(d.dispatch({effects:[J.reconfigure(E.editable.of(!i)),M.reconfigure(S.readOnly.of(i))]}),t(12,q=i),F()),e.$$.dirty[0]&2050&&d&&a!=d.state.doc.toString()&&d.dispatch({changes:{from:0,to:d.state.doc.length,insert:a}}),e.$$.dirty[0]&2064&&d&&typeof o<"u"&&d.dispatch({effects:[A.reconfigure(G(o))]})},[p,a,f,i,o,s,y,L,b,m,W,d,q,C,O,Y]}class Ne extends ${constructor(r){super(),ee(this,r,Fe,He,te,{id:2,value:1,disabled:3,placeholder:4,baseCollection:5,singleLine:6,extraAutocompleteKeys:7,disableRequestKeys:8,disableCollectionJoinKeys:9,focus:10},null,[-1,-1])}get focus(){return this.$$.ctx[10]}}export{Ne as default}; diff --git a/ui/dist/assets/ListApiDocs-DX-LwRkY.js b/ui/dist/assets/ListApiDocs-C0epAOOQ.js similarity index 71% rename from ui/dist/assets/ListApiDocs-DX-LwRkY.js rename to ui/dist/assets/ListApiDocs-C0epAOOQ.js index bbca7a75..c64c5089 100644 --- a/ui/dist/assets/ListApiDocs-DX-LwRkY.js +++ b/ui/dist/assets/ListApiDocs-C0epAOOQ.js @@ -1,12 +1,12 @@ -import{S as Ze,i as tl,s as el,e,b as s,E as sl,f as a,g as u,r as ll,x as Qe,o as m,v as _,h as t,N as Fe,O as se,c as Qt,m as Ut,w as ke,P as Ue,Q as nl,k as ol,R as al,n as il,t as $t,a as Ct,d as jt,T as rl,C as ve,A as cl,q as Le}from"./index-Bp3jGQ0J.js";import{S as dl}from"./SdkTabs-DxNNd6Sw.js";import{F as pl}from"./FieldsQueryParam-zDO3HzQv.js";function fl(d){let n,o,i;return{c(){n=e("span"),n.textContent="Show details",o=s(),i=e("i"),a(n,"class","txt"),a(i,"class","ri-arrow-down-s-line")},m(f,h){u(f,n,h),u(f,o,h),u(f,i,h)},d(f){f&&(m(n),m(o),m(i))}}}function ul(d){let n,o,i;return{c(){n=e("span"),n.textContent="Hide details",o=s(),i=e("i"),a(n,"class","txt"),a(i,"class","ri-arrow-up-s-line")},m(f,h){u(f,n,h),u(f,o,h),u(f,i,h)},d(f){f&&(m(n),m(o),m(i))}}}function je(d){let n,o,i,f,h,r,b,$,C,g,p,tt,kt,zt,E,Kt,H,rt,R,et,ne,Q,U,oe,ct,yt,lt,vt,ae,dt,pt,st,N,Jt,Ft,y,nt,Lt,Vt,At,j,ot,Tt,Wt,Pt,F,ft,Rt,ie,ut,re,M,Ot,at,St,O,mt,ce,z,Et,Xt,Nt,de,q,Yt,K,ht,pe,I,fe,B,ue,P,qt,J,bt,me,gt,he,x,Dt,it,Ht,be,Mt,Zt,V,_t,ge,It,_e,wt,we,W,G,xe,xt,te,X,ee,L,Y,S,Bt,$e,Z,v,Gt;return{c(){n=e("p"),n.innerHTML=`The syntax basically follows the format +import{S as Ze,i as tl,s as el,e,b as s,E as sl,f as a,g as u,u as ll,y as Ue,o as m,w as _,h as t,Q as nl,R as Fe,T as se,c as Ut,m as Qt,x as ke,U as Qe,V as ol,k as al,W as il,n as rl,t as $t,a as Ct,d as jt,X as cl,C as ve,p as dl,r as Le}from"./index-B-F-pko3.js";import{F as pl}from"./FieldsQueryParam-CW6KZfgu.js";function fl(d){let n,o,i;return{c(){n=e("span"),n.textContent="Show details",o=s(),i=e("i"),a(n,"class","txt"),a(i,"class","ri-arrow-down-s-line")},m(f,h){u(f,n,h),u(f,o,h),u(f,i,h)},d(f){f&&(m(n),m(o),m(i))}}}function ul(d){let n,o,i;return{c(){n=e("span"),n.textContent="Hide details",o=s(),i=e("i"),a(n,"class","txt"),a(i,"class","ri-arrow-up-s-line")},m(f,h){u(f,n,h),u(f,o,h),u(f,i,h)},d(f){f&&(m(n),m(o),m(i))}}}function je(d){let n,o,i,f,h,r,b,$,C,g,p,tt,kt,zt,S,Kt,H,rt,R,et,ne,U,Q,oe,ct,yt,lt,vt,ae,dt,pt,st,N,Jt,Ft,y,nt,Lt,Vt,At,j,ot,Tt,Wt,Pt,F,ft,Rt,ie,ut,re,M,Ot,at,Et,O,mt,ce,z,St,Xt,Nt,de,q,Yt,K,ht,pe,I,fe,B,ue,P,qt,J,bt,me,gt,he,x,Dt,it,Ht,be,Mt,Zt,V,_t,ge,It,_e,wt,we,W,G,xe,xt,te,X,ee,L,Y,E,Bt,$e,Z,v,Gt;return{c(){n=e("p"),n.innerHTML=`The syntax basically follows the format OPERAND OPERATOR OPERAND, where:`,o=s(),i=e("ul"),f=e("li"),f.innerHTML=`OPERAND - could be any of the above field literal, string (single or double quoted), number, null, true, false`,h=s(),r=e("li"),b=e("code"),b.textContent="OPERATOR",$=_(` - is one of: - `),C=e("br"),g=s(),p=e("ul"),tt=e("li"),kt=e("code"),kt.textContent="=",zt=s(),E=e("span"),E.textContent="Equal",Kt=s(),H=e("li"),rt=e("code"),rt.textContent="!=",R=s(),et=e("span"),et.textContent="NOT equal",ne=s(),Q=e("li"),U=e("code"),U.textContent=">",oe=s(),ct=e("span"),ct.textContent="Greater than",yt=s(),lt=e("li"),vt=e("code"),vt.textContent=">=",ae=s(),dt=e("span"),dt.textContent="Greater than or equal",pt=s(),st=e("li"),N=e("code"),N.textContent="<",Jt=s(),Ft=e("span"),Ft.textContent="Less than",y=s(),nt=e("li"),Lt=e("code"),Lt.textContent="<=",Vt=s(),At=e("span"),At.textContent="Less than or equal",j=s(),ot=e("li"),Tt=e("code"),Tt.textContent="~",Wt=s(),Pt=e("span"),Pt.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for + `),C=e("br"),g=s(),p=e("ul"),tt=e("li"),kt=e("code"),kt.textContent="=",zt=s(),S=e("span"),S.textContent="Equal",Kt=s(),H=e("li"),rt=e("code"),rt.textContent="!=",R=s(),et=e("span"),et.textContent="NOT equal",ne=s(),U=e("li"),Q=e("code"),Q.textContent=">",oe=s(),ct=e("span"),ct.textContent="Greater than",yt=s(),lt=e("li"),vt=e("code"),vt.textContent=">=",ae=s(),dt=e("span"),dt.textContent="Greater than or equal",pt=s(),st=e("li"),N=e("code"),N.textContent="<",Jt=s(),Ft=e("span"),Ft.textContent="Less than",y=s(),nt=e("li"),Lt=e("code"),Lt.textContent="<=",Vt=s(),At=e("span"),At.textContent="Less than or equal",j=s(),ot=e("li"),Tt=e("code"),Tt.textContent="~",Wt=s(),Pt=e("span"),Pt.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for wildcard match)`,F=s(),ft=e("li"),Rt=e("code"),Rt.textContent="!~",ie=s(),ut=e("span"),ut.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for - wildcard match)`,re=s(),M=e("li"),Ot=e("code"),Ot.textContent="?=",at=s(),St=e("em"),St.textContent="Any/At least one of",O=s(),mt=e("span"),mt.textContent="Equal",ce=s(),z=e("li"),Et=e("code"),Et.textContent="?!=",Xt=s(),Nt=e("em"),Nt.textContent="Any/At least one of",de=s(),q=e("span"),q.textContent="NOT equal",Yt=s(),K=e("li"),ht=e("code"),ht.textContent="?>",pe=s(),I=e("em"),I.textContent="Any/At least one of",fe=s(),B=e("span"),B.textContent="Greater than",ue=s(),P=e("li"),qt=e("code"),qt.textContent="?>=",J=s(),bt=e("em"),bt.textContent="Any/At least one of",me=s(),gt=e("span"),gt.textContent="Greater than or equal",he=s(),x=e("li"),Dt=e("code"),Dt.textContent="?<",it=s(),Ht=e("em"),Ht.textContent="Any/At least one of",be=s(),Mt=e("span"),Mt.textContent="Less than",Zt=s(),V=e("li"),_t=e("code"),_t.textContent="?<=",ge=s(),It=e("em"),It.textContent="Any/At least one of",_e=s(),wt=e("span"),wt.textContent="Less than or equal",we=s(),W=e("li"),G=e("code"),G.textContent="?~",xe=s(),xt=e("em"),xt.textContent="Any/At least one of",te=s(),X=e("span"),X.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for - wildcard match)`,ee=s(),L=e("li"),Y=e("code"),Y.textContent="?!~",S=s(),Bt=e("em"),Bt.textContent="Any/At least one of",$e=s(),Z=e("span"),Z.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for + wildcard match)`,re=s(),M=e("li"),Ot=e("code"),Ot.textContent="?=",at=s(),Et=e("em"),Et.textContent="Any/At least one of",O=s(),mt=e("span"),mt.textContent="Equal",ce=s(),z=e("li"),St=e("code"),St.textContent="?!=",Xt=s(),Nt=e("em"),Nt.textContent="Any/At least one of",de=s(),q=e("span"),q.textContent="NOT equal",Yt=s(),K=e("li"),ht=e("code"),ht.textContent="?>",pe=s(),I=e("em"),I.textContent="Any/At least one of",fe=s(),B=e("span"),B.textContent="Greater than",ue=s(),P=e("li"),qt=e("code"),qt.textContent="?>=",J=s(),bt=e("em"),bt.textContent="Any/At least one of",me=s(),gt=e("span"),gt.textContent="Greater than or equal",he=s(),x=e("li"),Dt=e("code"),Dt.textContent="?<",it=s(),Ht=e("em"),Ht.textContent="Any/At least one of",be=s(),Mt=e("span"),Mt.textContent="Less than",Zt=s(),V=e("li"),_t=e("code"),_t.textContent="?<=",ge=s(),It=e("em"),It.textContent="Any/At least one of",_e=s(),wt=e("span"),wt.textContent="Less than or equal",we=s(),W=e("li"),G=e("code"),G.textContent="?~",xe=s(),xt=e("em"),xt.textContent="Any/At least one of",te=s(),X=e("span"),X.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for + wildcard match)`,ee=s(),L=e("li"),Y=e("code"),Y.textContent="?!~",E=s(),Bt=e("em"),Bt.textContent="Any/At least one of",$e=s(),Z=e("span"),Z.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for wildcard match)`,v=s(),Gt=e("p"),Gt.innerHTML=`To group and combine several expressions you could use brackets - (...), && (AND) and || (OR) tokens.`,a(b,"class","txt-danger"),a(kt,"class","filter-op svelte-1w7s5nw"),a(E,"class","txt"),a(rt,"class","filter-op svelte-1w7s5nw"),a(et,"class","txt"),a(U,"class","filter-op svelte-1w7s5nw"),a(ct,"class","txt"),a(vt,"class","filter-op svelte-1w7s5nw"),a(dt,"class","txt"),a(N,"class","filter-op svelte-1w7s5nw"),a(Ft,"class","txt"),a(Lt,"class","filter-op svelte-1w7s5nw"),a(At,"class","txt"),a(Tt,"class","filter-op svelte-1w7s5nw"),a(Pt,"class","txt"),a(Rt,"class","filter-op svelte-1w7s5nw"),a(ut,"class","txt"),a(Ot,"class","filter-op svelte-1w7s5nw"),a(St,"class","txt-hint"),a(mt,"class","txt"),a(Et,"class","filter-op svelte-1w7s5nw"),a(Nt,"class","txt-hint"),a(q,"class","txt"),a(ht,"class","filter-op svelte-1w7s5nw"),a(I,"class","txt-hint"),a(B,"class","txt"),a(qt,"class","filter-op svelte-1w7s5nw"),a(bt,"class","txt-hint"),a(gt,"class","txt"),a(Dt,"class","filter-op svelte-1w7s5nw"),a(Ht,"class","txt-hint"),a(Mt,"class","txt"),a(_t,"class","filter-op svelte-1w7s5nw"),a(It,"class","txt-hint"),a(wt,"class","txt"),a(G,"class","filter-op svelte-1w7s5nw"),a(xt,"class","txt-hint"),a(X,"class","txt"),a(Y,"class","filter-op svelte-1w7s5nw"),a(Bt,"class","txt-hint"),a(Z,"class","txt")},m(A,k){u(A,n,k),u(A,o,k),u(A,i,k),t(i,f),t(i,h),t(i,r),t(r,b),t(r,$),t(r,C),t(r,g),t(r,p),t(p,tt),t(tt,kt),t(tt,zt),t(tt,E),t(p,Kt),t(p,H),t(H,rt),t(H,R),t(H,et),t(p,ne),t(p,Q),t(Q,U),t(Q,oe),t(Q,ct),t(p,yt),t(p,lt),t(lt,vt),t(lt,ae),t(lt,dt),t(p,pt),t(p,st),t(st,N),t(st,Jt),t(st,Ft),t(p,y),t(p,nt),t(nt,Lt),t(nt,Vt),t(nt,At),t(p,j),t(p,ot),t(ot,Tt),t(ot,Wt),t(ot,Pt),t(p,F),t(p,ft),t(ft,Rt),t(ft,ie),t(ft,ut),t(p,re),t(p,M),t(M,Ot),t(M,at),t(M,St),t(M,O),t(M,mt),t(p,ce),t(p,z),t(z,Et),t(z,Xt),t(z,Nt),t(z,de),t(z,q),t(p,Yt),t(p,K),t(K,ht),t(K,pe),t(K,I),t(K,fe),t(K,B),t(p,ue),t(p,P),t(P,qt),t(P,J),t(P,bt),t(P,me),t(P,gt),t(p,he),t(p,x),t(x,Dt),t(x,it),t(x,Ht),t(x,be),t(x,Mt),t(p,Zt),t(p,V),t(V,_t),t(V,ge),t(V,It),t(V,_e),t(V,wt),t(p,we),t(p,W),t(W,G),t(W,xe),t(W,xt),t(W,te),t(W,X),t(p,ee),t(p,L),t(L,Y),t(L,S),t(L,Bt),t(L,$e),t(L,Z),u(A,v,k),u(A,Gt,k)},d(A){A&&(m(n),m(o),m(i),m(v),m(Gt))}}}function ml(d){let n,o,i,f,h;function r(g,p){return g[0]?ul:fl}let b=r(d),$=b(d),C=d[0]&&je();return{c(){n=e("button"),$.c(),o=s(),C&&C.c(),i=sl(),a(n,"class","btn btn-sm btn-secondary m-t-10")},m(g,p){u(g,n,p),$.m(n,null),u(g,o,p),C&&C.m(g,p),u(g,i,p),f||(h=ll(n,"click",d[1]),f=!0)},p(g,[p]){b!==(b=r(g))&&($.d(1),$=b(g),$&&($.c(),$.m(n,null))),g[0]?C||(C=je(),C.c(),C.m(i.parentNode,i)):C&&(C.d(1),C=null)},i:Qe,o:Qe,d(g){g&&(m(n),m(o),m(i)),$.d(),C&&C.d(g),f=!1,h()}}}function hl(d,n,o){let i=!1;function f(){o(0,i=!i)}return[i,f]}class bl extends Ze{constructor(n){super(),tl(this,n,hl,ml,el,{})}}function ze(d,n,o){const i=d.slice();return i[7]=n[o],i}function Ke(d,n,o){const i=d.slice();return i[7]=n[o],i}function Je(d,n,o){const i=d.slice();return i[12]=n[o],i[14]=o,i}function Ve(d){let n;return{c(){n=e("p"),n.innerHTML="Requires admin Authorization:TOKEN header",a(n,"class","txt-hint txt-sm txt-right")},m(o,i){u(o,n,i)},d(o){o&&m(n)}}}function We(d){let n,o=d[12]+"",i,f=d[14](...), && (AND) and || (OR) tokens.`,a(b,"class","txt-danger"),a(kt,"class","filter-op svelte-1w7s5nw"),a(S,"class","txt"),a(rt,"class","filter-op svelte-1w7s5nw"),a(et,"class","txt"),a(Q,"class","filter-op svelte-1w7s5nw"),a(ct,"class","txt"),a(vt,"class","filter-op svelte-1w7s5nw"),a(dt,"class","txt"),a(N,"class","filter-op svelte-1w7s5nw"),a(Ft,"class","txt"),a(Lt,"class","filter-op svelte-1w7s5nw"),a(At,"class","txt"),a(Tt,"class","filter-op svelte-1w7s5nw"),a(Pt,"class","txt"),a(Rt,"class","filter-op svelte-1w7s5nw"),a(ut,"class","txt"),a(Ot,"class","filter-op svelte-1w7s5nw"),a(Et,"class","txt-hint"),a(mt,"class","txt"),a(St,"class","filter-op svelte-1w7s5nw"),a(Nt,"class","txt-hint"),a(q,"class","txt"),a(ht,"class","filter-op svelte-1w7s5nw"),a(I,"class","txt-hint"),a(B,"class","txt"),a(qt,"class","filter-op svelte-1w7s5nw"),a(bt,"class","txt-hint"),a(gt,"class","txt"),a(Dt,"class","filter-op svelte-1w7s5nw"),a(Ht,"class","txt-hint"),a(Mt,"class","txt"),a(_t,"class","filter-op svelte-1w7s5nw"),a(It,"class","txt-hint"),a(wt,"class","txt"),a(G,"class","filter-op svelte-1w7s5nw"),a(xt,"class","txt-hint"),a(X,"class","txt"),a(Y,"class","filter-op svelte-1w7s5nw"),a(Bt,"class","txt-hint"),a(Z,"class","txt")},m(A,k){u(A,n,k),u(A,o,k),u(A,i,k),t(i,f),t(i,h),t(i,r),t(r,b),t(r,$),t(r,C),t(r,g),t(r,p),t(p,tt),t(tt,kt),t(tt,zt),t(tt,S),t(p,Kt),t(p,H),t(H,rt),t(H,R),t(H,et),t(p,ne),t(p,U),t(U,Q),t(U,oe),t(U,ct),t(p,yt),t(p,lt),t(lt,vt),t(lt,ae),t(lt,dt),t(p,pt),t(p,st),t(st,N),t(st,Jt),t(st,Ft),t(p,y),t(p,nt),t(nt,Lt),t(nt,Vt),t(nt,At),t(p,j),t(p,ot),t(ot,Tt),t(ot,Wt),t(ot,Pt),t(p,F),t(p,ft),t(ft,Rt),t(ft,ie),t(ft,ut),t(p,re),t(p,M),t(M,Ot),t(M,at),t(M,Et),t(M,O),t(M,mt),t(p,ce),t(p,z),t(z,St),t(z,Xt),t(z,Nt),t(z,de),t(z,q),t(p,Yt),t(p,K),t(K,ht),t(K,pe),t(K,I),t(K,fe),t(K,B),t(p,ue),t(p,P),t(P,qt),t(P,J),t(P,bt),t(P,me),t(P,gt),t(p,he),t(p,x),t(x,Dt),t(x,it),t(x,Ht),t(x,be),t(x,Mt),t(p,Zt),t(p,V),t(V,_t),t(V,ge),t(V,It),t(V,_e),t(V,wt),t(p,we),t(p,W),t(W,G),t(W,xe),t(W,xt),t(W,te),t(W,X),t(p,ee),t(p,L),t(L,Y),t(L,E),t(L,Bt),t(L,$e),t(L,Z),u(A,v,k),u(A,Gt,k)},d(A){A&&(m(n),m(o),m(i),m(v),m(Gt))}}}function ml(d){let n,o,i,f,h;function r(g,p){return g[0]?ul:fl}let b=r(d),$=b(d),C=d[0]&&je();return{c(){n=e("button"),$.c(),o=s(),C&&C.c(),i=sl(),a(n,"class","btn btn-sm btn-secondary m-t-10")},m(g,p){u(g,n,p),$.m(n,null),u(g,o,p),C&&C.m(g,p),u(g,i,p),f||(h=ll(n,"click",d[1]),f=!0)},p(g,[p]){b!==(b=r(g))&&($.d(1),$=b(g),$&&($.c(),$.m(n,null))),g[0]?C||(C=je(),C.c(),C.m(i.parentNode,i)):C&&(C.d(1),C=null)},i:Ue,o:Ue,d(g){g&&(m(n),m(o),m(i)),$.d(),C&&C.d(g),f=!1,h()}}}function hl(d,n,o){let i=!1;function f(){o(0,i=!i)}return[i,f]}class bl extends Ze{constructor(n){super(),tl(this,n,hl,ml,el,{})}}function ze(d,n,o){const i=d.slice();return i[7]=n[o],i}function Ke(d,n,o){const i=d.slice();return i[7]=n[o],i}function Je(d,n,o){const i=d.slice();return i[12]=n[o],i[14]=o,i}function Ve(d){let n;return{c(){n=e("p"),n.innerHTML="Requires superuser Authorization:TOKEN header",a(n,"class","txt-hint txt-sm txt-right")},m(o,i){u(o,n,i)},d(o){o&&m(n)}}}function We(d){let n,o=d[12]+"",i,f=d[14]= "2022-01-01 00:00:00" && someField1 != someField2', ); // you can also fetch all records at once via getFullList - final records = await pb.collection('${(Ee=d[0])==null?void 0:Ee.name}').getFullList( + final records = await pb.collection('${(Se=d[0])==null?void 0:Se.name}').getFullList( sort: '-created', ); @@ -56,18 +56,18 @@ import{S as Ze,i as tl,s as el,e,b as s,E as sl,f as a,g as u,r as ll,x as Qe,o ?sort=-created,id `}});let le=se(d[4]),T=[];for(let l=0;l'2022-01-01') - `}}),P=new bl({}),it=new Fe({props:{content:"?expand=relField1,relField2.subRelField"}}),G=new pl({});let ye=se(d[5]);const Ae=l=>l[7].code;for(let l=0;ll[7].code;for(let l=0;lParam Type Description',Ft=s(),y=e("tbody"),nt=e("tr"),nt.innerHTML='page Number The page (aka. offset) of the paginated list (default to 1).',Lt=s(),Vt=e("tr"),Vt.innerHTML='perPage Number Specify the max returned records per page (default to 30).',At=s(),j=e("tr"),ot=e("td"),ot.textContent="sort",Tt=s(),Wt=e("td"),Wt.innerHTML='String',Pt=s(),F=e("td"),ft=_("Specify the records order attribute(s). "),Rt=e("br"),ie=_(` + `}}),P=new bl({}),it=new Fe({props:{content:"?expand=relField1,relField2.subRelField"}}),G=new pl({});let ye=se(d[5]);const Ae=l=>l[7].code;for(let l=0;ll[7].code;for(let l=0;lParam Type Description',Ft=s(),y=e("tbody"),nt=e("tr"),nt.innerHTML='page Number The page (aka. offset) of the paginated list (default to 1).',Lt=s(),Vt=e("tr"),Vt.innerHTML='perPage Number Specify the max returned records per page (default to 30).',At=s(),j=e("tr"),ot=e("td"),ot.textContent="sort",Tt=s(),Wt=e("td"),Wt.innerHTML='String',Pt=s(),F=e("td"),ft=_("Specify the records order attribute(s). "),Rt=e("br"),ie=_(` Add `),ut=e("code"),ut.textContent="-",re=_(" / "),M=e("code"),M.textContent="+",Ot=_(` (default) in front of the attribute for DESC / ASC order. Ex.: - `),Qt(at.$$.fragment),St=s(),O=e("p"),mt=e("strong"),mt.textContent="Supported record sort fields:",ce=s(),z=e("br"),Et=s(),Xt=e("code"),Xt.textContent="@random",Nt=_(`, + `),Ut(at.$$.fragment),Et=s(),O=e("p"),mt=e("strong"),mt.textContent="Supported record sort fields:",ce=s(),z=e("br"),St=s(),Xt=e("code"),Xt.textContent="@random",Nt=_(`, `);for(let l=0;lString',pe=s(),I=e("td"),fe=_(`Filter the returned records. Ex.: - `),Qt(B.$$.fragment),ue=s(),Qt(P.$$.fragment),qt=s(),J=e("tr"),bt=e("td"),bt.textContent="expand",me=s(),gt=e("td"),gt.innerHTML='String',he=s(),x=e("td"),Dt=_(`Auto expand record relations. Ex.: - `),Qt(it.$$.fragment),Ht=_(` + `),Ut(B.$$.fragment),ue=s(),Ut(P.$$.fragment),qt=s(),J=e("tr"),bt=e("td"),bt.textContent="expand",me=s(),gt=e("td"),gt.innerHTML='String',he=s(),x=e("td"),Dt=_(`Auto expand record relations. Ex.: + `),Ut(it.$$.fragment),Ht=_(` Supports up to 6-levels depth nested relations expansion. `),be=e("br"),Mt=_(` The expanded relations will be appended to each individual record under the `),Zt=e("code"),Zt.textContent="expand",V=_(" property (eg. "),_t=e("code"),_t.textContent='"expand": {"relField1": {...}, ...}',ge=_(`). `),It=e("br"),_e=_(` - Only the relations to which the request user has permissions to `),wt=e("strong"),wt.textContent="view",we=_(" will be expanded."),W=s(),Qt(G.$$.fragment),xe=s(),xt=e("tr"),xt.innerHTML=`skipTotal Boolean If it is set the total counts query will be skipped and the response fields + Only the relations to which the request user has permissions to `),wt=e("strong"),wt.textContent="view",we=_(" will be expanded."),W=s(),Ut(G.$$.fragment),xe=s(),xt=e("tr"),xt.innerHTML=`skipTotal Boolean If it is set the total counts query will be skipped and the response fields totalItems and totalPages will have -1 value.
This could drastically speed up the search queries when the total counters are not needed or cursor @@ -76,7 +76,7 @@ import{S as Ze,i as tl,s as el,e,b as s,E as sl,f as a,g as u,r as ll,x as Qe,o For optimization purposes, it is set by default for the getFirstListItem() and - getFullList() SDKs methods.`,te=s(),X=e("div"),X.textContent="Responses",ee=s(),L=e("div"),Y=e("div");for(let l=0;lgetFullList() SDKs methods.`,te=s(),X=e("div"),X.textContent="Responses",ee=s(),L=e("div"),Y=e("div");for(let l=0;lo(2,b=g.code);return d.$$set=g=>{"collection"in g&&o(0,r=g.collection)},d.$$.update=()=>{d.$$.dirty&1&&o(4,i=ve.getAllCollectionIdentifiers(r)),d.$$.dirty&1&&o(1,f=(r==null?void 0:r.listRule)===null),d.$$.dirty&3&&r!=null&&r.id&&($.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[ve.dummyCollectionRecord(r),ve.dummyCollectionRecord(r)]},null,2)}),$.push({code:400,body:` + `),S.$set(w),(!A||c&1)&&yt!==(yt=l[0].name+"")&&ke(lt,yt),l[1]?k||(k=Ve(),k.c(),k.m(R,null)):k&&(k.d(1),k=null),c&16){le=se(l[4]);let D;for(D=0;Do(2,b=g.code);return d.$$set=g=>{"collection"in g&&o(0,r=g.collection)},d.$$.update=()=>{d.$$.dirty&1&&o(4,i=ve.getAllCollectionIdentifiers(r)),d.$$.dirty&1&&o(1,f=(r==null?void 0:r.listRule)===null),d.$$.dirty&3&&r!=null&&r.id&&($.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[ve.dummyCollectionRecord(r),ve.dummyCollectionRecord(r)]},null,2)}),$.push({code:400,body:` { "code": 400, "message": "Something went wrong while processing your request. Invalid filter.", @@ -130,7 +130,7 @@ import{S as Ze,i as tl,s as el,e,b as s,E as sl,f as a,g as u,r as ll,x as Qe,o `}),f&&$.push({code:403,body:` { "code": 403, - "message": "Only admins can access this action.", + "message": "Only superusers can access this action.", "data": {} } - `}))},o(3,h=ve.getApiExampleUrl(cl.baseUrl)),[r,f,b,h,i,$,C]}class Cl extends Ze{constructor(n){super(),tl(this,n,_l,gl,el,{collection:0})}}export{Cl as default}; + `}))},o(3,h=ve.getApiExampleUrl(dl.baseURL)),[r,f,b,h,i,$,C]}class $l extends Ze{constructor(n){super(),tl(this,n,_l,gl,el,{collection:0})}}export{$l as default}; diff --git a/ui/dist/assets/ListExternalAuthsDocs-DQacf2gi.js b/ui/dist/assets/ListExternalAuthsDocs-DQacf2gi.js deleted file mode 100644 index 6dfdeb29..00000000 --- a/ui/dist/assets/ListExternalAuthsDocs-DQacf2gi.js +++ /dev/null @@ -1,88 +0,0 @@ -import{S as ze,i as Qe,s as Ue,O as F,e as i,v,b as m,c as pe,f as b,g as c,h as a,m as ue,w as N,P as Oe,Q as je,k as Fe,R as Ne,n as Ge,t as G,a as K,o as d,d as me,C as Ke,A as Je,q as J,r as Ve,N as Xe}from"./index-Bp3jGQ0J.js";import{S as Ye}from"./SdkTabs-DxNNd6Sw.js";import{F as Ze}from"./FieldsQueryParam-zDO3HzQv.js";function De(o,l,s){const n=o.slice();return n[5]=l[s],n}function He(o,l,s){const n=o.slice();return n[5]=l[s],n}function Re(o,l){let s,n=l[5].code+"",f,_,r,u;function h(){return l[4](l[5])}return{key:o,first:null,c(){s=i("button"),f=v(n),_=m(),b(s,"class","tab-item"),J(s,"active",l[1]===l[5].code),this.first=s},m(w,y){c(w,s,y),a(s,f),a(s,_),r||(u=Ve(s,"click",h),r=!0)},p(w,y){l=w,y&4&&n!==(n=l[5].code+"")&&N(f,n),y&6&&J(s,"active",l[1]===l[5].code)},d(w){w&&d(s),r=!1,u()}}}function We(o,l){let s,n,f,_;return n=new Xe({props:{content:l[5].body}}),{key:o,first:null,c(){s=i("div"),pe(n.$$.fragment),f=m(),b(s,"class","tab-item"),J(s,"active",l[1]===l[5].code),this.first=s},m(r,u){c(r,s,u),ue(n,s,null),a(s,f),_=!0},p(r,u){l=r;const h={};u&4&&(h.content=l[5].body),n.$set(h),(!_||u&6)&&J(s,"active",l[1]===l[5].code)},i(r){_||(G(n.$$.fragment,r),_=!0)},o(r){K(n.$$.fragment,r),_=!1},d(r){r&&d(s),me(n)}}}function xe(o){var Te,Se,Ee,Ie;let l,s,n=o[0].name+"",f,_,r,u,h,w,y,R=o[0].name+"",V,be,fe,X,Y,P,Z,I,x,$,W,he,z,A,_e,ee,Q=o[0].name+"",te,ke,le,ve,ge,U,se,q,ae,B,oe,L,ne,C,ie,$e,ce,E,de,M,re,T,O,g=[],we=new Map,ye,D,k=[],Pe=new Map,S;P=new Ye({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(Te=o[0])==null?void 0:Te.name}').authWithPassword('test@example.com', '123456'); - - const result = await pb.collection('${(Se=o[0])==null?void 0:Se.name}').listExternalAuths( - pb.authStore.model.id - ); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(Ee=o[0])==null?void 0:Ee.name}').authWithPassword('test@example.com', '123456'); - - final result = await pb.collection('${(Ie=o[0])==null?void 0:Ie.name}').listExternalAuths( - pb.authStore.model.id, - ); - `}}),E=new Ze({});let j=F(o[2]);const Ae=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eAuthorization:TOKEN header",se=m(),q=i("div"),q.textContent="Path Parameters",ae=m(),B=i("table"),B.innerHTML='Param Type Description id String ID of the auth record.',oe=m(),L=i("div"),L.textContent="Query parameters",ne=m(),C=i("table"),ie=i("thead"),ie.innerHTML='Param Type Description',$e=m(),ce=i("tbody"),pe(E.$$.fragment),de=m(),M=i("div"),M.textContent="Responses",re=m(),T=i("div"),O=i("div");for(let e=0;es(1,_=h.code);return o.$$set=h=>{"collection"in h&&s(0,f=h.collection)},o.$$.update=()=>{o.$$.dirty&1&&s(2,r=[{code:200,body:` - [ - { - "id": "8171022dc95a4e8", - "created": "2022-09-01 10:24:18.434", - "updated": "2022-09-01 10:24:18.889", - "recordId": "e22581b6f1d44ea", - "collectionId": "${f.id}", - "provider": "google", - "providerId": "2da15468800514p", - }, - { - "id": "171022dc895a4e8", - "created": "2022-09-01 10:24:18.434", - "updated": "2022-09-01 10:24:18.889", - "recordId": "e22581b6f1d44ea", - "collectionId": "${f.id}", - "provider": "twitter", - "providerId": "720688005140514", - } - ] - `},{code:401,body:` - { - "code": 401, - "message": "The request requires valid record authorization token to be set.", - "data": {} - } - `},{code:403,body:` - { - "code": 403, - "message": "The authorized record model is not allowed to perform this action.", - "data": {} - } - `},{code:404,body:` - { - "code": 404, - "message": "The requested resource wasn't found.", - "data": {} - } - `}])},s(3,n=Ke.getApiExampleUrl(Je.baseUrl)),[f,_,r,n,u]}class at extends ze{constructor(l){super(),Qe(this,l,et,xe,Ue,{collection:0})}}export{at as default}; diff --git a/ui/dist/assets/PageAdminConfirmPasswordReset-C3mHsOYN.js b/ui/dist/assets/PageAdminConfirmPasswordReset-C3mHsOYN.js deleted file mode 100644 index f8a522b2..00000000 --- a/ui/dist/assets/PageAdminConfirmPasswordReset-C3mHsOYN.js +++ /dev/null @@ -1,2 +0,0 @@ -import{S as E,i as G,s as I,F as K,c as F,m as R,t as B,a as N,d as T,C as M,p as H,e as _,v as P,b as h,f,q as J,g as b,h as c,r as j,u as O,j as Q,l as U,o as w,z as V,A as L,B as X,D as Y,w as Z,y as q}from"./index-Bp3jGQ0J.js";function W(i){let e,n,s;return{c(){e=P("for "),n=_("strong"),s=P(i[3]),f(n,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,n,t),c(n,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&(w(e),w(n))}}}function x(i){let e,n,s,l,t,u,p,d;return{c(){e=_("label"),n=P("New password"),l=h(),t=_("input"),f(e,"for",s=i[8]),f(t,"type","password"),f(t,"id",u=i[8]),t.required=!0,t.autofocus=!0},m(r,a){b(r,e,a),c(e,n),b(r,l,a),b(r,t,a),q(t,i[0]),t.focus(),p||(d=j(t,"input",i[6]),p=!0)},p(r,a){a&256&&s!==(s=r[8])&&f(e,"for",s),a&256&&u!==(u=r[8])&&f(t,"id",u),a&1&&t.value!==r[0]&&q(t,r[0])},d(r){r&&(w(e),w(l),w(t)),p=!1,d()}}}function ee(i){let e,n,s,l,t,u,p,d;return{c(){e=_("label"),n=P("New password confirm"),l=h(),t=_("input"),f(e,"for",s=i[8]),f(t,"type","password"),f(t,"id",u=i[8]),t.required=!0},m(r,a){b(r,e,a),c(e,n),b(r,l,a),b(r,t,a),q(t,i[1]),p||(d=j(t,"input",i[7]),p=!0)},p(r,a){a&256&&s!==(s=r[8])&&f(e,"for",s),a&256&&u!==(u=r[8])&&f(t,"id",u),a&2&&t.value!==r[1]&&q(t,r[1])},d(r){r&&(w(e),w(l),w(t)),p=!1,d()}}}function te(i){let e,n,s,l,t,u,p,d,r,a,g,S,k,v,C,A,y,m=i[3]&&W(i);return u=new H({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:o})=>({8:o}),({uniqueId:o})=>o?256:0]},$$scope:{ctx:i}}}),d=new H({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:o})=>({8:o}),({uniqueId:o})=>o?256:0]},$$scope:{ctx:i}}}),{c(){e=_("form"),n=_("div"),s=_("h4"),l=P(`Reset your admin password - `),m&&m.c(),t=h(),F(u.$$.fragment),p=h(),F(d.$$.fragment),r=h(),a=_("button"),g=_("span"),g.textContent="Set new password",S=h(),k=_("div"),v=_("a"),v.textContent="Back to login",f(s,"class","m-b-xs"),f(n,"class","content txt-center m-b-sm"),f(g,"class","txt"),f(a,"type","submit"),f(a,"class","btn btn-lg btn-block"),a.disabled=i[2],J(a,"btn-loading",i[2]),f(e,"class","m-b-base"),f(v,"href","/login"),f(v,"class","link-hint"),f(k,"class","content txt-center")},m(o,$){b(o,e,$),c(e,n),c(n,s),c(s,l),m&&m.m(s,null),c(e,t),R(u,e,null),c(e,p),R(d,e,null),c(e,r),c(e,a),c(a,g),b(o,S,$),b(o,k,$),c(k,v),C=!0,A||(y=[j(e,"submit",O(i[4])),Q(U.call(null,v))],A=!0)},p(o,$){o[3]?m?m.p(o,$):(m=W(o),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:o}),u.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:o}),d.$set(D),(!C||$&4)&&(a.disabled=o[2]),(!C||$&4)&&J(a,"btn-loading",o[2])},i(o){C||(B(u.$$.fragment,o),B(d.$$.fragment,o),C=!0)},o(o){N(u.$$.fragment,o),N(d.$$.fragment,o),C=!1},d(o){o&&(w(e),w(S),w(k)),m&&m.d(),T(u),T(d),A=!1,V(y)}}}function se(i){let e,n;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:i}}}),{c(){F(e.$$.fragment)},m(s,l){R(e,s,l),n=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){n||(B(e.$$.fragment,s),n=!0)},o(s){N(e.$$.fragment,s),n=!1},d(s){T(e,s)}}}function le(i,e,n){let s,{params:l}=e,t="",u="",p=!1;async function d(){if(!p){n(2,p=!0);try{await L.admins.confirmPasswordReset(l==null?void 0:l.token,t,u),X("Successfully set a new admin password."),Y("/")}catch(g){L.error(g)}n(2,p=!1)}}function r(){t=this.value,n(0,t)}function a(){u=this.value,n(1,u)}return i.$$set=g=>{"params"in g&&n(5,l=g.params)},i.$$.update=()=>{i.$$.dirty&32&&n(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,u,p,s,d,l,r,a]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default}; diff --git a/ui/dist/assets/PageAdminRequestPasswordReset-ByYh3pEr.js b/ui/dist/assets/PageAdminRequestPasswordReset-ByYh3pEr.js deleted file mode 100644 index 75377566..00000000 --- a/ui/dist/assets/PageAdminRequestPasswordReset-ByYh3pEr.js +++ /dev/null @@ -1 +0,0 @@ -import{S as H,i as M,s as T,F as j,c as L,m as R,t as w,a as y,d as S,b as v,e as _,f as p,g,h as d,j as B,l as N,k as z,n as D,o as k,A as C,p as G,q as F,r as E,u as I,v as h,w as J,x as P,y as A}from"./index-Bp3jGQ0J.js";function K(u){let e,s,n,l,t,i,c,m,r,a,b,f;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:o})=>({5:o}),({uniqueId:o})=>o?32:0]},$$scope:{ctx:u}}}),{c(){e=_("form"),s=_("div"),s.innerHTML='

Forgotten admin password

Enter the email associated with your account and we’ll send you a recovery link:

',n=v(),L(l.$$.fragment),t=v(),i=_("button"),c=_("i"),m=v(),r=_("span"),r.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(c,"class","ri-mail-send-line"),p(r,"class","txt"),p(i,"type","submit"),p(i,"class","btn btn-lg btn-block"),i.disabled=u[1],F(i,"btn-loading",u[1]),p(e,"class","m-b-base")},m(o,$){g(o,e,$),d(e,s),d(e,n),R(l,e,null),d(e,t),d(e,i),d(i,c),d(i,m),d(i,r),a=!0,b||(f=E(e,"submit",I(u[3])),b=!0)},p(o,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:o}),l.$set(q),(!a||$&2)&&(i.disabled=o[1]),(!a||$&2)&&F(i,"btn-loading",o[1])},i(o){a||(w(l.$$.fragment,o),a=!0)},o(o){y(l.$$.fragment,o),a=!1},d(o){o&&k(e),S(l),b=!1,f()}}}function O(u){let e,s,n,l,t,i,c,m,r;return{c(){e=_("div"),s=_("div"),s.innerHTML='',n=v(),l=_("div"),t=_("p"),i=h("Check "),c=_("strong"),m=h(u[0]),r=h(" for the recovery link."),p(s,"class","icon"),p(c,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){g(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,i),d(t,c),d(c,m),d(t,r)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&k(e)}}}function Q(u){let e,s,n,l,t,i,c,m;return{c(){e=_("label"),s=h("Email"),l=v(),t=_("input"),p(e,"for",n=u[5]),p(t,"type","email"),p(t,"id",i=u[5]),t.required=!0,t.autofocus=!0},m(r,a){g(r,e,a),d(e,s),g(r,l,a),g(r,t,a),A(t,u[0]),t.focus(),c||(m=E(t,"input",u[4]),c=!0)},p(r,a){a&32&&n!==(n=r[5])&&p(e,"for",n),a&32&&i!==(i=r[5])&&p(t,"id",i),a&1&&t.value!==r[0]&&A(t,r[0])},d(r){r&&(k(e),k(l),k(t)),c=!1,m()}}}function U(u){let e,s,n,l,t,i,c,m;const r=[O,K],a=[];function b(f,o){return f[2]?0:1}return e=b(u),s=a[e]=r[e](u),{c(){s.c(),n=v(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(f,o){a[e].m(f,o),g(f,n,o),g(f,l,o),d(l,t),i=!0,c||(m=B(N.call(null,t)),c=!0)},p(f,o){let $=e;e=b(f),e===$?a[e].p(f,o):(z(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(f,o):(s=a[e]=r[e](f),s.c()),w(s,1),s.m(n.parentNode,n))},i(f){i||(w(s),i=!0)},o(f){y(s),i=!1},d(f){f&&(k(n),k(l)),a[e].d(f),c=!1,m()}}}function V(u){let e,s;return e=new j({props:{$$slots:{default:[U]},$$scope:{ctx:u}}}),{c(){L(e.$$.fragment)},m(n,l){R(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(u,e,s){let n="",l=!1,t=!1;async function i(){if(!l){s(1,l=!0);try{await C.admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.error(m)}s(1,l=!1)}}function c(){n=this.value,s(0,n)}return[n,l,t,i,c]}class Y extends H{constructor(e){super(),M(this,e,W,V,T,{})}}export{Y as default}; diff --git a/ui/dist/assets/PageOAuth2RedirectFailure-Bj9LYgF_.js b/ui/dist/assets/PageOAuth2RedirectFailure-Bj9LYgF_.js new file mode 100644 index 00000000..344d7033 --- /dev/null +++ b/ui/dist/assets/PageOAuth2RedirectFailure-Bj9LYgF_.js @@ -0,0 +1 @@ +import{S as r,i as c,s as l,e as u,f as p,g as h,y as n,o as d,J as f,K as m,L as g,M as o}from"./index-B-F-pko3.js";function _(s){let t;return{c(){t=u("div"),t.innerHTML='

Auth failed.

You can close this window and go back to the app to try again.
',p(t,"class","content txt-hint txt-center p-base")},m(e,a){h(e,t,a)},p:n,i:n,o:n,d(e){e&&d(t)}}}function b(s,t,e){let a;return f(s,o,i=>e(0,a=i)),m(o,a="OAuth2 auth failed",a),g(()=>{window.close()}),[]}class v extends r{constructor(t){super(),c(this,t,b,_,l,{})}}export{v as default}; diff --git a/ui/dist/assets/PageOAuth2RedirectFailure-C67m4ehZ.js b/ui/dist/assets/PageOAuth2RedirectFailure-C67m4ehZ.js deleted file mode 100644 index eb7bf8a5..00000000 --- a/ui/dist/assets/PageOAuth2RedirectFailure-C67m4ehZ.js +++ /dev/null @@ -1 +0,0 @@ -import{S as o,i,s as c,e as r,f as l,g as u,x as a,o as d,I as h}from"./index-Bp3jGQ0J.js";function f(n){let t;return{c(){t=r("div"),t.innerHTML='

Auth failed.

You can close this window and go back to the app to try again.
',l(t,"class","content txt-hint txt-center p-base")},m(e,s){u(e,t,s)},p:a,i:a,o:a,d(e){e&&d(t)}}}function p(n){return h(()=>{window.close()}),[]}class x extends o{constructor(t){super(),i(this,t,p,f,c,{})}}export{x as default}; diff --git a/ui/dist/assets/PageOAuth2RedirectSuccess-B4oH6pbq.js b/ui/dist/assets/PageOAuth2RedirectSuccess-B4oH6pbq.js new file mode 100644 index 00000000..e5b9662c --- /dev/null +++ b/ui/dist/assets/PageOAuth2RedirectSuccess-B4oH6pbq.js @@ -0,0 +1 @@ +import{S as i,i as r,s as u,e as l,f as p,g as h,y as n,o as d,J as m,K as f,L as g,M as o}from"./index-B-F-pko3.js";function _(a){let t;return{c(){t=l("div"),t.innerHTML='

Auth completed.

You can close this window and go back to the app.
',p(t,"class","content txt-hint txt-center p-base")},m(e,s){h(e,t,s)},p:n,i:n,o:n,d(e){e&&d(t)}}}function b(a,t,e){let s;return m(a,o,c=>e(0,s=c)),f(o,s="OAuth2 auth completed",s),g(()=>{window.close()}),[]}class v extends i{constructor(t){super(),r(this,t,b,_,u,{})}}export{v as default}; diff --git a/ui/dist/assets/PageOAuth2RedirectSuccess-DEJwxR01.js b/ui/dist/assets/PageOAuth2RedirectSuccess-DEJwxR01.js deleted file mode 100644 index 44cdca76..00000000 --- a/ui/dist/assets/PageOAuth2RedirectSuccess-DEJwxR01.js +++ /dev/null @@ -1 +0,0 @@ -import{S as o,i as c,s as i,e as r,f as u,g as l,x as s,o as d,I as h}from"./index-Bp3jGQ0J.js";function p(n){let t;return{c(){t=r("div"),t.innerHTML='

Auth completed.

You can close this window and go back to the app.
',u(t,"class","content txt-hint txt-center p-base")},m(e,a){l(e,t,a)},p:s,i:s,o:s,d(e){e&&d(t)}}}function f(n){return h(()=>{window.close()}),[]}class x extends o{constructor(t){super(),c(this,t,f,p,i,{})}}export{x as default}; diff --git a/ui/dist/assets/PageRecordConfirmEmailChange-GhjOHgK4.js b/ui/dist/assets/PageRecordConfirmEmailChange-DoQqYIxa.js similarity index 84% rename from ui/dist/assets/PageRecordConfirmEmailChange-GhjOHgK4.js rename to ui/dist/assets/PageRecordConfirmEmailChange-DoQqYIxa.js index f691566f..9f34f187 100644 --- a/ui/dist/assets/PageRecordConfirmEmailChange-GhjOHgK4.js +++ b/ui/dist/assets/PageRecordConfirmEmailChange-DoQqYIxa.js @@ -1,2 +1,2 @@ -import{S as G,i as I,s as J,F as M,c as S,m as A,t as h,a as v,d as L,C as N,E as R,g as _,k as W,n as Y,o as b,G as j,H as z,A as B,p as D,e as m,v as y,b as C,f as p,q as T,h as g,r as P,u as K,x as E,w as O,y as F}from"./index-Bp3jGQ0J.js";function Q(i){let e,t,n,l,s,o,f,a,r,u,k,$,d=i[3]&&H(i);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[V,({uniqueId:c})=>({8:c}),({uniqueId:c})=>c?256:0]},$$scope:{ctx:i}}}),{c(){e=m("form"),t=m("div"),n=m("h5"),l=y(`Type your password to confirm changing your email address - `),d&&d.c(),s=C(),S(o.$$.fragment),f=C(),a=m("button"),r=m("span"),r.textContent="Confirm new email",p(t,"class","content txt-center m-b-base"),p(r,"class","txt"),p(a,"type","submit"),p(a,"class","btn btn-lg btn-block"),a.disabled=i[1],T(a,"btn-loading",i[1])},m(c,w){_(c,e,w),g(e,t),g(t,n),g(n,l),d&&d.m(n,null),g(e,s),A(o,e,null),g(e,f),g(e,a),g(a,r),u=!0,k||($=P(e,"submit",K(i[4])),k=!0)},p(c,w){c[3]?d?d.p(c,w):(d=H(c),d.c(),d.m(n,null)):d&&(d.d(1),d=null);const q={};w&769&&(q.$$scope={dirty:w,ctx:c}),o.$set(q),(!u||w&2)&&(a.disabled=c[1]),(!u||w&2)&&T(a,"btn-loading",c[1])},i(c){u||(h(o.$$.fragment,c),u=!0)},o(c){v(o.$$.fragment,c),u=!1},d(c){c&&b(e),d&&d.d(),L(o),k=!1,$()}}}function U(i){let e,t,n,l,s;return{c(){e=m("div"),e.innerHTML='

Successfully changed the user email address.

You can now sign in with your new email address.

',t=C(),n=m("button"),n.textContent="Close",p(e,"class","alert alert-success"),p(n,"type","button"),p(n,"class","btn btn-transparent btn-block")},m(o,f){_(o,e,f),_(o,t,f),_(o,n,f),l||(s=P(n,"click",i[6]),l=!0)},p:E,i:E,o:E,d(o){o&&(b(e),b(t),b(n)),l=!1,s()}}}function H(i){let e,t,n;return{c(){e=y("to "),t=m("strong"),n=y(i[3]),p(t,"class","txt-nowrap")},m(l,s){_(l,e,s),_(l,t,s),g(t,n)},p(l,s){s&8&&O(n,l[3])},d(l){l&&(b(e),b(t))}}}function V(i){let e,t,n,l,s,o,f,a;return{c(){e=m("label"),t=y("Password"),l=C(),s=m("input"),p(e,"for",n=i[8]),p(s,"type","password"),p(s,"id",o=i[8]),s.required=!0,s.autofocus=!0},m(r,u){_(r,e,u),g(e,t),_(r,l,u),_(r,s,u),F(s,i[0]),s.focus(),f||(a=P(s,"input",i[7]),f=!0)},p(r,u){u&256&&n!==(n=r[8])&&p(e,"for",n),u&256&&o!==(o=r[8])&&p(s,"id",o),u&1&&s.value!==r[0]&&F(s,r[0])},d(r){r&&(b(e),b(l),b(s)),f=!1,a()}}}function X(i){let e,t,n,l;const s=[U,Q],o=[];function f(a,r){return a[2]?0:1}return e=f(i),t=o[e]=s[e](i),{c(){t.c(),n=R()},m(a,r){o[e].m(a,r),_(a,n,r),l=!0},p(a,r){let u=e;e=f(a),e===u?o[e].p(a,r):(W(),v(o[u],1,1,()=>{o[u]=null}),Y(),t=o[e],t?t.p(a,r):(t=o[e]=s[e](a),t.c()),h(t,1),t.m(n.parentNode,n))},i(a){l||(h(t),l=!0)},o(a){v(t),l=!1},d(a){a&&b(n),o[e].d(a)}}}function Z(i){let e,t;return e=new M({props:{nobranding:!0,$$slots:{default:[X]},$$scope:{ctx:i}}}),{c(){S(e.$$.fragment)},m(n,l){A(e,n,l),t=!0},p(n,[l]){const s={};l&527&&(s.$$scope={dirty:l,ctx:n}),e.$set(s)},i(n){t||(h(e.$$.fragment,n),t=!0)},o(n){v(e.$$.fragment,n),t=!1},d(n){L(e,n)}}}function x(i,e,t){let n,{params:l}=e,s="",o=!1,f=!1;async function a(){if(o)return;t(1,o=!0);const k=new j("../");try{const $=z(l==null?void 0:l.token);await k.collection($.collectionId).confirmEmailChange(l==null?void 0:l.token,s),t(2,f=!0)}catch($){B.error($)}t(1,o=!1)}const r=()=>window.close();function u(){s=this.value,t(0,s)}return i.$$set=k=>{"params"in k&&t(5,l=k.params)},i.$$.update=()=>{i.$$.dirty&32&&t(3,n=N.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[s,o,f,n,a,l,r,u]}class te extends G{constructor(e){super(),I(this,e,x,Z,J,{params:5})}}export{te as default}; +import{S as G,i as I,s as J,F as M,c as S,m as L,t as h,a as v,d as z,C as N,E as R,g as _,k as W,n as Y,o as b,G as j,H as A,p as B,q as D,e as m,w as y,b as C,f as p,r as T,h as g,u as P,v as K,y as E,x as O,z as F}from"./index-B-F-pko3.js";function Q(i){let e,t,n,l,s,o,f,a,r,u,k,$,d=i[3]&&H(i);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[V,({uniqueId:c})=>({8:c}),({uniqueId:c})=>c?256:0]},$$scope:{ctx:i}}}),{c(){e=m("form"),t=m("div"),n=m("h5"),l=y(`Type your password to confirm changing your email address + `),d&&d.c(),s=C(),S(o.$$.fragment),f=C(),a=m("button"),r=m("span"),r.textContent="Confirm new email",p(t,"class","content txt-center m-b-base"),p(r,"class","txt"),p(a,"type","submit"),p(a,"class","btn btn-lg btn-block"),a.disabled=i[1],T(a,"btn-loading",i[1])},m(c,w){_(c,e,w),g(e,t),g(t,n),g(n,l),d&&d.m(n,null),g(e,s),L(o,e,null),g(e,f),g(e,a),g(a,r),u=!0,k||($=P(e,"submit",K(i[4])),k=!0)},p(c,w){c[3]?d?d.p(c,w):(d=H(c),d.c(),d.m(n,null)):d&&(d.d(1),d=null);const q={};w&769&&(q.$$scope={dirty:w,ctx:c}),o.$set(q),(!u||w&2)&&(a.disabled=c[1]),(!u||w&2)&&T(a,"btn-loading",c[1])},i(c){u||(h(o.$$.fragment,c),u=!0)},o(c){v(o.$$.fragment,c),u=!1},d(c){c&&b(e),d&&d.d(),z(o),k=!1,$()}}}function U(i){let e,t,n,l,s;return{c(){e=m("div"),e.innerHTML='

Successfully changed the user email address.

You can now sign in with your new email address.

',t=C(),n=m("button"),n.textContent="Close",p(e,"class","alert alert-success"),p(n,"type","button"),p(n,"class","btn btn-transparent btn-block")},m(o,f){_(o,e,f),_(o,t,f),_(o,n,f),l||(s=P(n,"click",i[6]),l=!0)},p:E,i:E,o:E,d(o){o&&(b(e),b(t),b(n)),l=!1,s()}}}function H(i){let e,t,n;return{c(){e=y("to "),t=m("strong"),n=y(i[3]),p(t,"class","txt-nowrap")},m(l,s){_(l,e,s),_(l,t,s),g(t,n)},p(l,s){s&8&&O(n,l[3])},d(l){l&&(b(e),b(t))}}}function V(i){let e,t,n,l,s,o,f,a;return{c(){e=m("label"),t=y("Password"),l=C(),s=m("input"),p(e,"for",n=i[8]),p(s,"type","password"),p(s,"id",o=i[8]),s.required=!0,s.autofocus=!0},m(r,u){_(r,e,u),g(e,t),_(r,l,u),_(r,s,u),F(s,i[0]),s.focus(),f||(a=P(s,"input",i[7]),f=!0)},p(r,u){u&256&&n!==(n=r[8])&&p(e,"for",n),u&256&&o!==(o=r[8])&&p(s,"id",o),u&1&&s.value!==r[0]&&F(s,r[0])},d(r){r&&(b(e),b(l),b(s)),f=!1,a()}}}function X(i){let e,t,n,l;const s=[U,Q],o=[];function f(a,r){return a[2]?0:1}return e=f(i),t=o[e]=s[e](i),{c(){t.c(),n=R()},m(a,r){o[e].m(a,r),_(a,n,r),l=!0},p(a,r){let u=e;e=f(a),e===u?o[e].p(a,r):(W(),v(o[u],1,1,()=>{o[u]=null}),Y(),t=o[e],t?t.p(a,r):(t=o[e]=s[e](a),t.c()),h(t,1),t.m(n.parentNode,n))},i(a){l||(h(t),l=!0)},o(a){v(t),l=!1},d(a){a&&b(n),o[e].d(a)}}}function Z(i){let e,t;return e=new M({props:{nobranding:!0,$$slots:{default:[X]},$$scope:{ctx:i}}}),{c(){S(e.$$.fragment)},m(n,l){L(e,n,l),t=!0},p(n,[l]){const s={};l&527&&(s.$$scope={dirty:l,ctx:n}),e.$set(s)},i(n){t||(h(e.$$.fragment,n),t=!0)},o(n){v(e.$$.fragment,n),t=!1},d(n){z(e,n)}}}function x(i,e,t){let n,{params:l}=e,s="",o=!1,f=!1;async function a(){if(o)return;t(1,o=!0);const k=new j("../");try{const $=A(l==null?void 0:l.token);await k.collection($.collectionId).confirmEmailChange(l==null?void 0:l.token,s),t(2,f=!0)}catch($){B.error($)}t(1,o=!1)}const r=()=>window.close();function u(){s=this.value,t(0,s)}return i.$$set=k=>{"params"in k&&t(5,l=k.params)},i.$$.update=()=>{i.$$.dirty&32&&t(3,n=N.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[s,o,f,n,a,l,r,u]}class te extends G{constructor(e){super(),I(this,e,x,Z,J,{params:5})}}export{te as default}; diff --git a/ui/dist/assets/PageRecordConfirmPasswordReset-DiHRjH5i.js b/ui/dist/assets/PageRecordConfirmPasswordReset-BZpXiFAY.js similarity index 91% rename from ui/dist/assets/PageRecordConfirmPasswordReset-DiHRjH5i.js rename to ui/dist/assets/PageRecordConfirmPasswordReset-BZpXiFAY.js index 2a824fc7..e7c919ca 100644 --- a/ui/dist/assets/PageRecordConfirmPasswordReset-DiHRjH5i.js +++ b/ui/dist/assets/PageRecordConfirmPasswordReset-BZpXiFAY.js @@ -1,2 +1,2 @@ -import{S as J,i as M,s as W,F as Y,c as H,m as N,t as P,a as y,d as T,C as j,E as z,g as _,k as B,n as D,o as m,G as K,H as O,A as Q,p as E,e as b,v as q,b as C,f as p,q as G,h as w,r as S,u as U,x as F,w as V,y as R}from"./index-Bp3jGQ0J.js";function X(a){let e,l,s,n,t,o,c,r,i,u,v,g,k,h,d=a[4]&&I(a);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:a}}}),r=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:a}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=q(`Reset your user password - `),d&&d.c(),t=C(),H(o.$$.fragment),c=C(),H(r.$$.fragment),i=C(),u=b("button"),v=b("span"),v.textContent="Set new password",p(l,"class","content txt-center m-b-base"),p(v,"class","txt"),p(u,"type","submit"),p(u,"class","btn btn-lg btn-block"),u.disabled=a[2],G(u,"btn-loading",a[2])},m(f,$){_(f,e,$),w(e,l),w(l,s),w(s,n),d&&d.m(s,null),w(e,t),N(o,e,null),w(e,c),N(r,e,null),w(e,i),w(e,u),w(u,v),g=!0,k||(h=S(e,"submit",U(a[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=I(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const A={};$&3073&&(A.$$scope={dirty:$,ctx:f}),o.$set(A);const L={};$&3074&&(L.$$scope={dirty:$,ctx:f}),r.$set(L),(!g||$&4)&&(u.disabled=f[2]),(!g||$&4)&&G(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(r.$$.fragment,f),g=!0)},o(f){y(o.$$.fragment,f),y(r.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),T(o),T(r),k=!1,h()}}}function Z(a){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML='

Successfully changed the user password.

You can now sign in with your new password.

',l=C(),s=b("button"),s.textContent="Close",p(e,"class","alert alert-success"),p(s,"type","button"),p(s,"class","btn btn-transparent btn-block")},m(o,c){_(o,e,c),_(o,l,c),_(o,s,c),n||(t=S(s,"click",a[7]),n=!0)},p:F,i:F,o:F,d(o){o&&(m(e),m(l),m(s)),n=!1,t()}}}function I(a){let e,l,s;return{c(){e=q("for "),l=b("strong"),s=q(a[4])},m(n,t){_(n,e,t),_(n,l,t),w(l,s)},p(n,t){t&16&&V(s,n[4])},d(n){n&&(m(e),m(l))}}}function x(a){let e,l,s,n,t,o,c,r;return{c(){e=b("label"),l=q("New password"),n=C(),t=b("input"),p(e,"for",s=a[10]),p(t,"type","password"),p(t,"id",o=a[10]),t.required=!0,t.autofocus=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),R(t,a[0]),t.focus(),c||(r=S(t,"input",a[8]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&(m(e),m(n),m(t)),c=!1,r()}}}function ee(a){let e,l,s,n,t,o,c,r;return{c(){e=b("label"),l=q("New password confirm"),n=C(),t=b("input"),p(e,"for",s=a[10]),p(t,"type","password"),p(t,"id",o=a[10]),t.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),R(t,a[1]),c||(r=S(t,"input",a[9]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&2&&t.value!==i[1]&&R(t,i[1])},d(i){i&&(m(e),m(n),m(t)),c=!1,r()}}}function te(a){let e,l,s,n;const t=[Z,X],o=[];function c(r,i){return r[3]?0:1}return e=c(a),l=o[e]=t[e](a),{c(){l.c(),s=z()},m(r,i){o[e].m(r,i),_(r,s,i),n=!0},p(r,i){let u=e;e=c(r),e===u?o[e].p(r,i):(B(),y(o[u],1,1,()=>{o[u]=null}),D(),l=o[e],l?l.p(r,i):(l=o[e]=t[e](r),l.c()),P(l,1),l.m(s.parentNode,s))},i(r){n||(P(l),n=!0)},o(r){y(l),n=!1},d(r){r&&m(s),o[e].d(r)}}}function se(a){let e,l;return e=new Y({props:{nobranding:!0,$$slots:{default:[te]},$$scope:{ctx:a}}}),{c(){H(e.$$.fragment)},m(s,n){N(e,s,n),l=!0},p(s,[n]){const t={};n&2079&&(t.$$scope={dirty:n,ctx:s}),e.$set(t)},i(s){l||(P(e.$$.fragment,s),l=!0)},o(s){y(e.$$.fragment,s),l=!1},d(s){T(e,s)}}}function le(a,e,l){let s,{params:n}=e,t="",o="",c=!1,r=!1;async function i(){if(c)return;l(2,c=!0);const k=new K("../");try{const h=O(n==null?void 0:n.token);await k.collection(h.collectionId).confirmPasswordReset(n==null?void 0:n.token,t,o),l(3,r=!0)}catch(h){Q.error(h)}l(2,c=!1)}const u=()=>window.close();function v(){t=this.value,l(0,t)}function g(){o=this.value,l(1,o)}return a.$$set=k=>{"params"in k&&l(6,n=k.params)},a.$$.update=()=>{a.$$.dirty&64&&l(4,s=j.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,c,r,s,i,n,u,v,g]}class oe extends J{constructor(e){super(),M(this,e,le,se,W,{params:6})}}export{oe as default}; +import{S as J,i as M,s as W,F as Y,c as H,m as N,t as P,a as y,d as T,C as j,E as A,g as _,k as B,n as D,o as m,G as K,H as O,p as Q,q as E,e as b,w as q,b as C,f as p,r as G,h as w,u as S,v as U,y as F,x as V,z as R}from"./index-B-F-pko3.js";function X(a){let e,l,s,n,t,o,c,r,i,u,v,g,k,h,d=a[4]&&I(a);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:a}}}),r=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:a}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=q(`Reset your user password + `),d&&d.c(),t=C(),H(o.$$.fragment),c=C(),H(r.$$.fragment),i=C(),u=b("button"),v=b("span"),v.textContent="Set new password",p(l,"class","content txt-center m-b-base"),p(v,"class","txt"),p(u,"type","submit"),p(u,"class","btn btn-lg btn-block"),u.disabled=a[2],G(u,"btn-loading",a[2])},m(f,$){_(f,e,$),w(e,l),w(l,s),w(s,n),d&&d.m(s,null),w(e,t),N(o,e,null),w(e,c),N(r,e,null),w(e,i),w(e,u),w(u,v),g=!0,k||(h=S(e,"submit",U(a[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=I(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const L={};$&3073&&(L.$$scope={dirty:$,ctx:f}),o.$set(L);const z={};$&3074&&(z.$$scope={dirty:$,ctx:f}),r.$set(z),(!g||$&4)&&(u.disabled=f[2]),(!g||$&4)&&G(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(r.$$.fragment,f),g=!0)},o(f){y(o.$$.fragment,f),y(r.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),T(o),T(r),k=!1,h()}}}function Z(a){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML='

Successfully changed the user password.

You can now sign in with your new password.

',l=C(),s=b("button"),s.textContent="Close",p(e,"class","alert alert-success"),p(s,"type","button"),p(s,"class","btn btn-transparent btn-block")},m(o,c){_(o,e,c),_(o,l,c),_(o,s,c),n||(t=S(s,"click",a[7]),n=!0)},p:F,i:F,o:F,d(o){o&&(m(e),m(l),m(s)),n=!1,t()}}}function I(a){let e,l,s;return{c(){e=q("for "),l=b("strong"),s=q(a[4])},m(n,t){_(n,e,t),_(n,l,t),w(l,s)},p(n,t){t&16&&V(s,n[4])},d(n){n&&(m(e),m(l))}}}function x(a){let e,l,s,n,t,o,c,r;return{c(){e=b("label"),l=q("New password"),n=C(),t=b("input"),p(e,"for",s=a[10]),p(t,"type","password"),p(t,"id",o=a[10]),t.required=!0,t.autofocus=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),R(t,a[0]),t.focus(),c||(r=S(t,"input",a[8]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&(m(e),m(n),m(t)),c=!1,r()}}}function ee(a){let e,l,s,n,t,o,c,r;return{c(){e=b("label"),l=q("New password confirm"),n=C(),t=b("input"),p(e,"for",s=a[10]),p(t,"type","password"),p(t,"id",o=a[10]),t.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),R(t,a[1]),c||(r=S(t,"input",a[9]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&2&&t.value!==i[1]&&R(t,i[1])},d(i){i&&(m(e),m(n),m(t)),c=!1,r()}}}function te(a){let e,l,s,n;const t=[Z,X],o=[];function c(r,i){return r[3]?0:1}return e=c(a),l=o[e]=t[e](a),{c(){l.c(),s=A()},m(r,i){o[e].m(r,i),_(r,s,i),n=!0},p(r,i){let u=e;e=c(r),e===u?o[e].p(r,i):(B(),y(o[u],1,1,()=>{o[u]=null}),D(),l=o[e],l?l.p(r,i):(l=o[e]=t[e](r),l.c()),P(l,1),l.m(s.parentNode,s))},i(r){n||(P(l),n=!0)},o(r){y(l),n=!1},d(r){r&&m(s),o[e].d(r)}}}function se(a){let e,l;return e=new Y({props:{nobranding:!0,$$slots:{default:[te]},$$scope:{ctx:a}}}),{c(){H(e.$$.fragment)},m(s,n){N(e,s,n),l=!0},p(s,[n]){const t={};n&2079&&(t.$$scope={dirty:n,ctx:s}),e.$set(t)},i(s){l||(P(e.$$.fragment,s),l=!0)},o(s){y(e.$$.fragment,s),l=!1},d(s){T(e,s)}}}function le(a,e,l){let s,{params:n}=e,t="",o="",c=!1,r=!1;async function i(){if(c)return;l(2,c=!0);const k=new K("../");try{const h=O(n==null?void 0:n.token);await k.collection(h.collectionId).confirmPasswordReset(n==null?void 0:n.token,t,o),l(3,r=!0)}catch(h){Q.error(h)}l(2,c=!1)}const u=()=>window.close();function v(){t=this.value,l(0,t)}function g(){o=this.value,l(1,o)}return a.$$set=k=>{"params"in k&&l(6,n=k.params)},a.$$.update=()=>{a.$$.dirty&64&&l(4,s=j.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,c,r,s,i,n,u,v,g]}class oe extends J{constructor(e){super(),M(this,e,le,se,W,{params:6})}}export{oe as default}; diff --git a/ui/dist/assets/PageRecordConfirmVerification-BEu14spu.js b/ui/dist/assets/PageRecordConfirmVerification-BEu14spu.js deleted file mode 100644 index 205c84b1..00000000 --- a/ui/dist/assets/PageRecordConfirmVerification-BEu14spu.js +++ /dev/null @@ -1 +0,0 @@ -import{S as v,i as y,s as g,F as w,c as x,m as C,t as $,a as H,d as L,G as P,H as T,E as M,g as r,o as a,e as f,b as _,f as d,r as b,x as p}from"./index-Bp3jGQ0J.js";function S(c){let t,s,e,n,l;return{c(){t=f("div"),t.innerHTML='

Invalid or expired verification token.

',s=_(),e=f("button"),e.textContent="Close",d(t,"class","alert alert-danger"),d(e,"type","button"),d(e,"class","btn btn-transparent btn-block")},m(i,o){r(i,t,o),r(i,s,o),r(i,e,o),n||(l=b(e,"click",c[4]),n=!0)},p,d(i){i&&(a(t),a(s),a(e)),n=!1,l()}}}function h(c){let t,s,e,n,l;return{c(){t=f("div"),t.innerHTML='

Successfully verified email address.

',s=_(),e=f("button"),e.textContent="Close",d(t,"class","alert alert-success"),d(e,"type","button"),d(e,"class","btn btn-transparent btn-block")},m(i,o){r(i,t,o),r(i,s,o),r(i,e,o),n||(l=b(e,"click",c[3]),n=!0)},p,d(i){i&&(a(t),a(s),a(e)),n=!1,l()}}}function F(c){let t;return{c(){t=f("div"),t.innerHTML='
Please wait...
',d(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function I(c){let t;function s(l,i){return l[1]?F:l[0]?h:S}let e=s(c),n=e(c);return{c(){n.c(),t=M()},m(l,i){n.m(l,i),r(l,t,i)},p(l,i){e===(e=s(l))&&n?n.p(l,i):(n.d(1),n=e(l),n&&(n.c(),n.m(t.parentNode,t)))},d(l){l&&a(t),n.d(l)}}}function V(c){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[I]},$$scope:{ctx:c}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const l={};n&67&&(l.$$scope={dirty:n,ctx:e}),t.$set(l)},i(e){s||($(t.$$.fragment,e),s=!0)},o(e){H(t.$$.fragment,e),s=!1},d(e){L(t,e)}}}function q(c,t,s){let{params:e}=t,n=!1,l=!1;i();async function i(){s(1,l=!0);const u=new P("../");try{const m=T(e==null?void 0:e.token);await u.collection(m.collectionId).confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch{s(0,n=!1)}s(1,l=!1)}const o=()=>window.close(),k=()=>window.close();return c.$$set=u=>{"params"in u&&s(2,e=u.params)},[n,l,e,o,k]}class G extends v{constructor(t){super(),y(this,t,q,V,g,{params:2})}}export{G as default}; diff --git a/ui/dist/assets/PageRecordConfirmVerification-O66HL0pH.js b/ui/dist/assets/PageRecordConfirmVerification-O66HL0pH.js new file mode 100644 index 00000000..5feb74ff --- /dev/null +++ b/ui/dist/assets/PageRecordConfirmVerification-O66HL0pH.js @@ -0,0 +1 @@ +import{S as P,i as R,s as L,F as M,c as S,m as V,t as q,a as E,d as F,G as w,H as y,I as N,E as g,g as r,o as a,p as G,e as u,b as v,f,u as k,y as m,r as C,h as j}from"./index-B-F-pko3.js";function z(o){let e,l,n;function t(i,d){return i[4]?K:J}let s=t(o),c=s(o);return{c(){e=u("div"),e.innerHTML='

Invalid or expired verification token.

',l=v(),c.c(),n=g(),f(e,"class","alert alert-danger")},m(i,d){r(i,e,d),r(i,l,d),c.m(i,d),r(i,n,d)},p(i,d){s===(s=t(i))&&c?c.p(i,d):(c.d(1),c=s(i),c&&(c.c(),c.m(n.parentNode,n)))},d(i){i&&(a(e),a(l),a(n)),c.d(i)}}}function A(o){let e,l,n,t,s;return{c(){e=u("div"),e.innerHTML='

Please check your email for the new verification link.

',l=v(),n=u("button"),n.textContent="Close",f(e,"class","alert alert-success"),f(n,"type","button"),f(n,"class","btn btn-transparent btn-block")},m(c,i){r(c,e,i),r(c,l,i),r(c,n,i),t||(s=k(n,"click",o[8]),t=!0)},p:m,d(c){c&&(a(e),a(l),a(n)),t=!1,s()}}}function B(o){let e,l,n,t,s;return{c(){e=u("div"),e.innerHTML='

Successfully verified email address.

',l=v(),n=u("button"),n.textContent="Close",f(e,"class","alert alert-success"),f(n,"type","button"),f(n,"class","btn btn-transparent btn-block")},m(c,i){r(c,e,i),r(c,l,i),r(c,n,i),t||(s=k(n,"click",o[7]),t=!0)},p:m,d(c){c&&(a(e),a(l),a(n)),t=!1,s()}}}function D(o){let e;return{c(){e=u("div"),e.innerHTML='
Please wait...
',f(e,"class","txt-center")},m(l,n){r(l,e,n)},p:m,d(l){l&&a(e)}}}function J(o){let e,l,n;return{c(){e=u("button"),e.textContent="Close",f(e,"type","button"),f(e,"class","btn btn-transparent btn-block")},m(t,s){r(t,e,s),l||(n=k(e,"click",o[9]),l=!0)},p:m,d(t){t&&a(e),l=!1,n()}}}function K(o){let e,l,n,t;return{c(){e=u("button"),l=u("span"),l.textContent="Resend",f(l,"class","txt"),f(e,"type","button"),f(e,"class","btn btn-transparent btn-block"),e.disabled=o[3],C(e,"btn-loading",o[3])},m(s,c){r(s,e,c),j(e,l),n||(t=k(e,"click",o[5]),n=!0)},p(s,c){c&8&&(e.disabled=s[3]),c&8&&C(e,"btn-loading",s[3])},d(s){s&&a(e),n=!1,t()}}}function O(o){let e;function l(s,c){return s[1]?D:s[0]?B:s[2]?A:z}let n=l(o),t=n(o);return{c(){t.c(),e=g()},m(s,c){t.m(s,c),r(s,e,c)},p(s,c){n===(n=l(s))&&t?t.p(s,c):(t.d(1),t=n(s),t&&(t.c(),t.m(e.parentNode,e)))},d(s){s&&a(e),t.d(s)}}}function Q(o){let e,l;return e=new M({props:{nobranding:!0,$$slots:{default:[O]},$$scope:{ctx:o}}}),{c(){S(e.$$.fragment)},m(n,t){V(e,n,t),l=!0},p(n,[t]){const s={};t&2079&&(s.$$scope={dirty:t,ctx:n}),e.$set(s)},i(n){l||(q(e.$$.fragment,n),l=!0)},o(n){E(e.$$.fragment,n),l=!1},d(n){F(e,n)}}}function U(o,e,l){let n,{params:t}=e,s=!1,c=!1,i=!1,d=!1;x();async function x(){if(c)return;l(1,c=!0);const b=new w("../");try{const p=y(t==null?void 0:t.token);await b.collection(p.collectionId).confirmVerification(t==null?void 0:t.token),l(0,s=!0)}catch{l(0,s=!1)}l(1,c=!1)}async function T(){const b=y(t==null?void 0:t.token);if(d||!b.collectionId||!b.email)return;l(3,d=!0);const p=new w("../");try{const _=y(t==null?void 0:t.token);await p.collection(_.collectionId).requestVerification(_.email),l(2,i=!0)}catch(_){G.error(_),l(2,i=!1)}l(3,d=!1)}const h=()=>window.close(),H=()=>window.close(),I=()=>window.close();return o.$$set=b=>{"params"in b&&l(6,t=b.params)},o.$$.update=()=>{o.$$.dirty&64&&l(4,n=(t==null?void 0:t.token)&&N(t.token))},[s,c,i,d,n,T,t,h,H,I]}class X extends P{constructor(e){super(),R(this,e,U,Q,L,{params:6})}}export{X as default}; diff --git a/ui/dist/assets/PageSuperuserConfirmPasswordReset-jxr0GY89.js b/ui/dist/assets/PageSuperuserConfirmPasswordReset-jxr0GY89.js new file mode 100644 index 00000000..353deb21 --- /dev/null +++ b/ui/dist/assets/PageSuperuserConfirmPasswordReset-jxr0GY89.js @@ -0,0 +1,2 @@ +import{S as E,i as G,s as I,F as K,c as R,m as B,t as N,a as T,d as j,C as M,q as J,e as _,w as P,b as k,f,r as L,g as b,h as c,u as z,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as q}from"./index-B-F-pko3.js";function y(r){let e,n,s;return{c(){e=P("for "),n=_("strong"),s=P(r[3]),f(n,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,n,t),c(n,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&(w(e),w(n))}}}function x(r){let e,n,s,l,t,i,p,d;return{c(){e=_("label"),n=P("New password"),l=k(),t=_("input"),f(e,"for",s=r[8]),f(t,"type","password"),f(t,"id",i=r[8]),t.required=!0,t.autofocus=!0},m(u,a){b(u,e,a),c(e,n),b(u,l,a),b(u,t,a),q(t,r[0]),t.focus(),p||(d=z(t,"input",r[6]),p=!0)},p(u,a){a&256&&s!==(s=u[8])&&f(e,"for",s),a&256&&i!==(i=u[8])&&f(t,"id",i),a&1&&t.value!==u[0]&&q(t,u[0])},d(u){u&&(w(e),w(l),w(t)),p=!1,d()}}}function ee(r){let e,n,s,l,t,i,p,d;return{c(){e=_("label"),n=P("New password confirm"),l=k(),t=_("input"),f(e,"for",s=r[8]),f(t,"type","password"),f(t,"id",i=r[8]),t.required=!0},m(u,a){b(u,e,a),c(e,n),b(u,l,a),b(u,t,a),q(t,r[1]),p||(d=z(t,"input",r[7]),p=!0)},p(u,a){a&256&&s!==(s=u[8])&&f(e,"for",s),a&256&&i!==(i=u[8])&&f(t,"id",i),a&2&&t.value!==u[1]&&q(t,u[1])},d(u){u&&(w(e),w(l),w(t)),p=!1,d()}}}function te(r){let e,n,s,l,t,i,p,d,u,a,g,S,C,v,h,F,A,m=r[3]&&y(r);return i=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:o})=>({8:o}),({uniqueId:o})=>o?256:0]},$$scope:{ctx:r}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:o})=>({8:o}),({uniqueId:o})=>o?256:0]},$$scope:{ctx:r}}}),{c(){e=_("form"),n=_("div"),s=_("h4"),l=P(`Reset your superuser password + `),m&&m.c(),t=k(),R(i.$$.fragment),p=k(),R(d.$$.fragment),u=k(),a=_("button"),g=_("span"),g.textContent="Set new password",S=k(),C=_("div"),v=_("a"),v.textContent="Back to login",f(s,"class","m-b-xs"),f(n,"class","content txt-center m-b-sm"),f(g,"class","txt"),f(a,"type","submit"),f(a,"class","btn btn-lg btn-block"),a.disabled=r[2],L(a,"btn-loading",r[2]),f(e,"class","m-b-base"),f(v,"href","/login"),f(v,"class","link-hint"),f(C,"class","content txt-center")},m(o,$){b(o,e,$),c(e,n),c(n,s),c(s,l),m&&m.m(s,null),c(e,t),B(i,e,null),c(e,p),B(d,e,null),c(e,u),c(e,a),c(a,g),b(o,S,$),b(o,C,$),c(C,v),h=!0,F||(A=[z(e,"submit",O(r[4])),Q(U.call(null,v))],F=!0)},p(o,$){o[3]?m?m.p(o,$):(m=y(o),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const D={};$&769&&(D.$$scope={dirty:$,ctx:o}),i.$set(D);const H={};$&770&&(H.$$scope={dirty:$,ctx:o}),d.$set(H),(!h||$&4)&&(a.disabled=o[2]),(!h||$&4)&&L(a,"btn-loading",o[2])},i(o){h||(N(i.$$.fragment,o),N(d.$$.fragment,o),h=!0)},o(o){T(i.$$.fragment,o),T(d.$$.fragment,o),h=!1},d(o){o&&(w(e),w(S),w(C)),m&&m.d(),j(i),j(d),F=!1,V(A)}}}function se(r){let e,n;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:r}}}),{c(){R(e.$$.fragment)},m(s,l){B(e,s,l),n=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){n||(N(e.$$.fragment,s),n=!0)},o(s){T(e.$$.fragment,s),n=!1},d(s){j(e,s)}}}function le(r,e,n){let s,{params:l}=e,t="",i="",p=!1;async function d(){if(!p){n(2,p=!0);try{await W.collection("_superusers").confirmPasswordReset(l==null?void 0:l.token,t,i),X("Successfully set a new superuser password."),Y("/")}catch(g){W.error(g)}n(2,p=!1)}}function u(){t=this.value,n(0,t)}function a(){i=this.value,n(1,i)}return r.$$set=g=>{"params"in g&&n(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&n(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,i,p,s,d,l,u,a]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default}; diff --git a/ui/dist/assets/PageSuperuserRequestPasswordReset-S4Qbbggx.js b/ui/dist/assets/PageSuperuserRequestPasswordReset-S4Qbbggx.js new file mode 100644 index 00000000..dff1c87c --- /dev/null +++ b/ui/dist/assets/PageSuperuserRequestPasswordReset-S4Qbbggx.js @@ -0,0 +1 @@ +import{S as M,i as T,s as j,F as z,c as L,m as R,t as w,a as y,d as E,b as v,e as m,f as p,g,h as d,j as B,l as N,k as A,n as D,o as k,p as C,q as G,r as F,u as H,v as I,w as h,x as J,y as P,z as S}from"./index-B-F-pko3.js";function K(u){let e,s,n,l,t,r,c,_,i,a,b,f;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:o})=>({5:o}),({uniqueId:o})=>o?32:0]},$$scope:{ctx:u}}}),{c(){e=m("form"),s=m("div"),s.innerHTML='

Forgotten superuser password

Enter the email associated with your account and we’ll send you a recovery link:

',n=v(),L(l.$$.fragment),t=v(),r=m("button"),c=m("i"),_=v(),i=m("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(c,"class","ri-mail-send-line"),p(i,"class","txt"),p(r,"type","submit"),p(r,"class","btn btn-lg btn-block"),r.disabled=u[1],F(r,"btn-loading",u[1]),p(e,"class","m-b-base")},m(o,$){g(o,e,$),d(e,s),d(e,n),R(l,e,null),d(e,t),d(e,r),d(r,c),d(r,_),d(r,i),a=!0,b||(f=H(e,"submit",I(u[3])),b=!0)},p(o,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:o}),l.$set(q),(!a||$&2)&&(r.disabled=o[1]),(!a||$&2)&&F(r,"btn-loading",o[1])},i(o){a||(w(l.$$.fragment,o),a=!0)},o(o){y(l.$$.fragment,o),a=!1},d(o){o&&k(e),E(l),b=!1,f()}}}function O(u){let e,s,n,l,t,r,c,_,i;return{c(){e=m("div"),s=m("div"),s.innerHTML='',n=v(),l=m("div"),t=m("p"),r=h("Check "),c=m("strong"),_=h(u[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(c,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){g(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,r),d(t,c),d(c,_),d(t,i)},p(a,b){b&1&&J(_,a[0])},i:P,o:P,d(a){a&&k(e)}}}function Q(u){let e,s,n,l,t,r,c,_;return{c(){e=m("label"),s=h("Email"),l=v(),t=m("input"),p(e,"for",n=u[5]),p(t,"type","email"),p(t,"id",r=u[5]),t.required=!0,t.autofocus=!0},m(i,a){g(i,e,a),d(e,s),g(i,l,a),g(i,t,a),S(t,u[0]),t.focus(),c||(_=H(t,"input",u[4]),c=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&r!==(r=i[5])&&p(t,"id",r),a&1&&t.value!==i[0]&&S(t,i[0])},d(i){i&&(k(e),k(l),k(t)),c=!1,_()}}}function U(u){let e,s,n,l,t,r,c,_;const i=[O,K],a=[];function b(f,o){return f[2]?0:1}return e=b(u),s=a[e]=i[e](u),{c(){s.c(),n=v(),l=m("div"),t=m("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(f,o){a[e].m(f,o),g(f,n,o),g(f,l,o),d(l,t),r=!0,c||(_=B(N.call(null,t)),c=!0)},p(f,o){let $=e;e=b(f),e===$?a[e].p(f,o):(A(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(f,o):(s=a[e]=i[e](f),s.c()),w(s,1),s.m(n.parentNode,n))},i(f){r||(w(s),r=!0)},o(f){y(s),r=!1},d(f){f&&(k(n),k(l)),a[e].d(f),c=!1,_()}}}function V(u){let e,s;return e=new z({props:{$$slots:{default:[U]},$$scope:{ctx:u}}}),{c(){L(e.$$.fragment)},m(n,l){R(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){E(e,n)}}}function W(u,e,s){let n="",l=!1,t=!1;async function r(){if(!l){s(1,l=!0);try{await C.collection("_superusers").requestPasswordReset(n),s(2,t=!0)}catch(_){C.error(_)}s(1,l=!1)}}function c(){n=this.value,s(0,n)}return[n,l,t,r,c]}class Y extends M{constructor(e){super(),T(this,e,W,V,j,{})}}export{Y as default}; diff --git a/ui/dist/assets/PasswordResetDocs-BWqelyHu.js b/ui/dist/assets/PasswordResetDocs-BWqelyHu.js new file mode 100644 index 00000000..3ecd5717 --- /dev/null +++ b/ui/dist/assets/PasswordResetDocs-BWqelyHu.js @@ -0,0 +1,100 @@ +import{S as se,i as ne,s as oe,T as U,e as p,b as S,w as D,f as k,g as b,h as u,x as z,U as ee,V as ye,k as te,W as Te,n as le,t as V,a as X,o as v,r as H,u as ae,R as Ee,c as J,m as Z,d as x,Q as Ce,X as fe,C as qe,p as Oe,Y as pe}from"./index-B-F-pko3.js";function me(o,t,e){const n=o.slice();return n[4]=t[e],n}function _e(o,t,e){const n=o.slice();return n[4]=t[e],n}function he(o,t){let e,n=t[4].code+"",d,c,r,a;function f(){return t[3](t[4])}return{key:o,first:null,c(){e=p("button"),d=D(n),c=S(),k(e,"class","tab-item"),H(e,"active",t[1]===t[4].code),this.first=e},m(g,y){b(g,e,y),u(e,d),u(e,c),r||(a=ae(e,"click",f),r=!0)},p(g,y){t=g,y&4&&n!==(n=t[4].code+"")&&z(d,n),y&6&&H(e,"active",t[1]===t[4].code)},d(g){g&&v(e),r=!1,a()}}}function be(o,t){let e,n,d,c;return n=new Ee({props:{content:t[4].body}}),{key:o,first:null,c(){e=p("div"),J(n.$$.fragment),d=S(),k(e,"class","tab-item"),H(e,"active",t[1]===t[4].code),this.first=e},m(r,a){b(r,e,a),Z(n,e,null),u(e,d),c=!0},p(r,a){t=r;const f={};a&4&&(f.content=t[4].body),n.$set(f),(!c||a&6)&&H(e,"active",t[1]===t[4].code)},i(r){c||(V(n.$$.fragment,r),c=!0)},o(r){X(n.$$.fragment,r),c=!1},d(r){r&&v(e),x(n)}}}function We(o){let t,e,n,d,c,r,a,f=o[0].name+"",g,y,I,q,Q,N,L,O,W,T,C,R=[],M=new Map,j,A,h=[],K=new Map,E,P=U(o[2]);const B=l=>l[4].code;for(let l=0;ll[4].code;for(let l=0;lParam Type Description
Required token
String The token from the password reset request email.
Required password
String The new password to set.
Required passwordConfirm
String The new password confirmation.',L=S(),O=p("div"),O.textContent="Responses",W=S(),T=p("div"),C=p("div");for(let l=0;le(1,d=a.code);return o.$$set=a=>{"collection"in a&&e(0,n=a.collection)},e(2,c=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "token": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}]),[n,d,c,r]}class Ae extends se{constructor(t){super(),ne(this,t,Ne,We,oe,{collection:0})}}function ve(o,t,e){const n=o.slice();return n[4]=t[e],n}function ke(o,t,e){const n=o.slice();return n[4]=t[e],n}function ge(o,t){let e,n=t[4].code+"",d,c,r,a;function f(){return t[3](t[4])}return{key:o,first:null,c(){e=p("button"),d=D(n),c=S(),k(e,"class","tab-item"),H(e,"active",t[1]===t[4].code),this.first=e},m(g,y){b(g,e,y),u(e,d),u(e,c),r||(a=ae(e,"click",f),r=!0)},p(g,y){t=g,y&4&&n!==(n=t[4].code+"")&&z(d,n),y&6&&H(e,"active",t[1]===t[4].code)},d(g){g&&v(e),r=!1,a()}}}function we(o,t){let e,n,d,c;return n=new Ee({props:{content:t[4].body}}),{key:o,first:null,c(){e=p("div"),J(n.$$.fragment),d=S(),k(e,"class","tab-item"),H(e,"active",t[1]===t[4].code),this.first=e},m(r,a){b(r,e,a),Z(n,e,null),u(e,d),c=!0},p(r,a){t=r;const f={};a&4&&(f.content=t[4].body),n.$set(f),(!c||a&6)&&H(e,"active",t[1]===t[4].code)},i(r){c||(V(n.$$.fragment,r),c=!0)},o(r){X(n.$$.fragment,r),c=!1},d(r){r&&v(e),x(n)}}}function De(o){let t,e,n,d,c,r,a,f=o[0].name+"",g,y,I,q,Q,N,L,O,W,T,C,R=[],M=new Map,j,A,h=[],K=new Map,E,P=U(o[2]);const B=l=>l[4].code;for(let l=0;ll[4].code;for(let l=0;lParam Type Description
Required email
String The auth record email address to send the password reset request (if exists).',L=S(),O=p("div"),O.textContent="Responses",W=S(),T=p("div"),C=p("div");for(let l=0;le(1,d=a.code);return o.$$set=a=>{"collection"in a&&e(0,n=a.collection)},e(2,c=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "email": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}]),[n,d,c,r]}class Be extends se{constructor(t){super(),ne(this,t,Me,De,oe,{collection:0})}}function $e(o,t,e){const n=o.slice();return n[5]=t[e],n[7]=e,n}function Re(o,t,e){const n=o.slice();return n[5]=t[e],n[7]=e,n}function Pe(o){let t,e,n,d,c;function r(){return o[4](o[7])}return{c(){t=p("button"),e=p("div"),e.textContent=`${o[5].title}`,n=S(),k(e,"class","txt"),k(t,"class","tab-item"),H(t,"active",o[1]==o[7])},m(a,f){b(a,t,f),u(t,e),u(t,n),d||(c=ae(t,"click",r),d=!0)},p(a,f){o=a,f&2&&H(t,"active",o[1]==o[7])},d(a){a&&v(t),d=!1,c()}}}function Se(o){let t,e,n,d;var c=o[5].component;function r(a,f){return{props:{collection:a[0]}}}return c&&(e=pe(c,r(o))),{c(){t=p("div"),e&&J(e.$$.fragment),n=S(),k(t,"class","tab-item"),H(t,"active",o[1]==o[7])},m(a,f){b(a,t,f),e&&Z(e,t,null),u(t,n),d=!0},p(a,f){if(c!==(c=a[5].component)){if(e){te();const g=e;X(g.$$.fragment,1,0,()=>{x(g,1)}),le()}c?(e=pe(c,r(a)),J(e.$$.fragment),V(e.$$.fragment,1),Z(e,t,n)):e=null}else if(c){const g={};f&1&&(g.collection=a[0]),e.$set(g)}(!d||f&2)&&H(t,"active",a[1]==a[7])},i(a){d||(e&&V(e.$$.fragment,a),d=!0)},o(a){e&&X(e.$$.fragment,a),d=!1},d(a){a&&v(t),e&&x(e)}}}function Fe(o){var l,s,_,ie;let t,e,n=o[0].name+"",d,c,r,a,f,g,y,I=o[0].name+"",q,Q,N,L,O,W,T,C,R,M,j,A,h,K;W=new Ce({props:{js:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${o[2]}'); + + ... + + await pb.collection('${(l=o[0])==null?void 0:l.name}').requestPasswordReset('test@example.com'); + + // --- + // (optional) in your custom confirmation page: + // --- + + // note: after this call all previously issued auth tokens are invalidated + await pb.collection('${(s=o[0])==null?void 0:s.name}').confirmPasswordReset( + 'RESET_TOKEN', + 'NEW_PASSWORD', + 'NEW_PASSWORD_CONFIRM', + ); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${o[2]}'); + + ... + + await pb.collection('${(_=o[0])==null?void 0:_.name}').requestPasswordReset('test@example.com'); + + // --- + // (optional) in your custom confirmation page: + // --- + + // note: after this call all previously issued auth tokens are invalidated + await pb.collection('${(ie=o[0])==null?void 0:ie.name}').confirmPasswordReset( + 'RESET_TOKEN', + 'NEW_PASSWORD', + 'NEW_PASSWORD_CONFIRM', + ); + `}});let E=U(o[3]),P=[];for(let i=0;iX(m[i],1,1,()=>{m[i]=null});return{c(){t=p("h3"),e=D("Password reset ("),d=D(n),c=D(")"),r=S(),a=p("div"),f=p("p"),g=D("Sends "),y=p("strong"),q=D(I),Q=D(" password reset email request."),N=S(),L=p("p"),L.textContent=`On successful password reset all previously issued auth tokens for the specific record will be + automatically invalidated.`,O=S(),J(W.$$.fragment),T=S(),C=p("h6"),C.textContent="API details",R=S(),M=p("div"),j=p("div");for(let i=0;ie(1,r=f);return o.$$set=f=>{"collection"in f&&e(0,d=f.collection)},e(2,n=qe.getApiExampleUrl(Oe.baseURL)),[d,r,n,c,a]}class Ue extends se{constructor(t){super(),ne(this,t,Ie,Fe,oe,{collection:0})}}export{Ue as default}; diff --git a/ui/dist/assets/RealtimeApiDocs-Bz63T_FK.js b/ui/dist/assets/RealtimeApiDocs-BCmrhp7T.js similarity index 63% rename from ui/dist/assets/RealtimeApiDocs-Bz63T_FK.js rename to ui/dist/assets/RealtimeApiDocs-BCmrhp7T.js index 2106cbbd..e49e5be9 100644 --- a/ui/dist/assets/RealtimeApiDocs-Bz63T_FK.js +++ b/ui/dist/assets/RealtimeApiDocs-BCmrhp7T.js @@ -1,61 +1,61 @@ -import{S as re,i as ae,s as be,N as pe,C as P,e as p,v as y,b as a,c as se,f as u,g as s,h as I,m as ne,w as ue,t as ie,a as ce,o as n,d as le,A as me}from"./index-Bp3jGQ0J.js";import{S as de}from"./SdkTabs-DxNNd6Sw.js";function he(t){var B,U,A,W,H,L,T,q,M,N,j,J;let i,m,c=t[0].name+"",b,d,k,h,D,f,_,l,C,$,S,g,w,v,E,r,R;return l=new de({props:{js:` +import{S as re,i as ae,s as be,Q as pe,R as ue,C as P,e as p,w as y,b as a,c as se,f as u,g as s,h as I,m as ne,x as me,t as ie,a as ce,o as n,d as le,p as de}from"./index-B-F-pko3.js";function he(o){var B,U,W,L,A,H,T,q,M,j,J,N;let i,m,c=o[0].name+"",b,d,k,h,D,f,_,l,C,$,S,g,w,v,E,r,R;return l=new pe({props:{js:` import PocketBase from 'pocketbase'; - const pb = new PocketBase('${t[1]}'); + const pb = new PocketBase('${o[1]}'); ... // (Optionally) authenticate await pb.collection('users').authWithPassword('test@example.com', '123456'); - // Subscribe to changes in any ${(B=t[0])==null?void 0:B.name} record - pb.collection('${(U=t[0])==null?void 0:U.name}').subscribe('*', function (e) { + // Subscribe to changes in any ${(B=o[0])==null?void 0:B.name} record + pb.collection('${(U=o[0])==null?void 0:U.name}').subscribe('*', function (e) { console.log(e.action); console.log(e.record); - }, { /* other options like expand, custom headers, etc. */ }); + }, { /* other options like: filter, expand, custom headers, etc. */ }); // Subscribe to changes only in the specified record - pb.collection('${(A=t[0])==null?void 0:A.name}').subscribe('RECORD_ID', function (e) { + pb.collection('${(W=o[0])==null?void 0:W.name}').subscribe('RECORD_ID', function (e) { console.log(e.action); console.log(e.record); - }, { /* other options like expand, custom headers, etc. */ }); + }, { /* other options like: filter, expand, custom headers, etc. */ }); // Unsubscribe - pb.collection('${(W=t[0])==null?void 0:W.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions - pb.collection('${(H=t[0])==null?void 0:H.name}').unsubscribe('*'); // remove all '*' topic subscriptions - pb.collection('${(L=t[0])==null?void 0:L.name}').unsubscribe(); // remove all subscriptions in the collection + pb.collection('${(L=o[0])==null?void 0:L.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions + pb.collection('${(A=o[0])==null?void 0:A.name}').unsubscribe('*'); // remove all '*' topic subscriptions + pb.collection('${(H=o[0])==null?void 0:H.name}').unsubscribe(); // remove all subscriptions in the collection `,dart:` import 'package:pocketbase/pocketbase.dart'; - final pb = PocketBase('${t[1]}'); + final pb = PocketBase('${o[1]}'); ... // (Optionally) authenticate await pb.collection('users').authWithPassword('test@example.com', '123456'); - // Subscribe to changes in any ${(T=t[0])==null?void 0:T.name} record - pb.collection('${(q=t[0])==null?void 0:q.name}').subscribe('*', (e) { + // Subscribe to changes in any ${(T=o[0])==null?void 0:T.name} record + pb.collection('${(q=o[0])==null?void 0:q.name}').subscribe('*', (e) { print(e.action); print(e.record); - }, /* other options like expand, custom headers, etc. */); + }, /* other options like: filter, expand, custom headers, etc. */); // Subscribe to changes only in the specified record - pb.collection('${(M=t[0])==null?void 0:M.name}').subscribe('RECORD_ID', (e) { + pb.collection('${(M=o[0])==null?void 0:M.name}').subscribe('RECORD_ID', (e) { print(e.action); print(e.record); - }, /* other options like expand, custom headers, etc. */); + }, /* other options like: filter, expand, custom headers, etc. */); // Unsubscribe - pb.collection('${(N=t[0])==null?void 0:N.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions - pb.collection('${(j=t[0])==null?void 0:j.name}').unsubscribe('*'); // remove all '*' topic subscriptions - pb.collection('${(J=t[0])==null?void 0:J.name}').unsubscribe(); // remove all subscriptions in the collection - `}}),r=new pe({props:{content:JSON.stringify({action:"create",record:P.dummyCollectionRecord(t[0])},null,2).replace('"action": "create"','"action": "create" // create, update or delete')}}),{c(){i=p("h3"),m=y("Realtime ("),b=y(c),d=y(")"),k=a(),h=p("div"),h.innerHTML=`

Subscribe to realtime changes via Server-Sent Events (SSE).

Events are sent for create, update + pb.collection('${(j=o[0])==null?void 0:j.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions + pb.collection('${(J=o[0])==null?void 0:J.name}').unsubscribe('*'); // remove all '*' topic subscriptions + pb.collection('${(N=o[0])==null?void 0:N.name}').unsubscribe(); // remove all subscriptions in the collection + `}}),r=new ue({props:{content:JSON.stringify({action:"create",record:P.dummyCollectionRecord(o[0])},null,2).replace('"action": "create"','"action": "create" // create, update or delete')}}),{c(){i=p("h3"),m=y("Realtime ("),b=y(c),d=y(")"),k=a(),h=p("div"),h.innerHTML=`

Subscribe to realtime changes via Server-Sent Events (SSE).

Events are sent for create, update and delete record operations (see "Event data format" section below).

`,D=a(),f=p("div"),f.innerHTML=`

You could subscribe to a single record or to an entire collection.

When you subscribe to a single record, the collection's ViewRule will be used to determine whether the subscriber has access to receive the event message.

When you subscribe to an entire collection, the collection's ListRule will be used to determine whether the subscriber has access to receive the - event message.

`,_=a(),se(l.$$.fragment),C=a(),$=p("h6"),$.textContent="API details",S=a(),g=p("div"),g.innerHTML='SSE

/api/realtime

',w=a(),v=p("div"),v.textContent="Event data format",E=a(),se(r.$$.fragment),u(i,"class","m-b-sm"),u(h,"class","content txt-lg m-b-sm"),u(f,"class","alert alert-info m-t-10 m-b-sm"),u($,"class","m-b-xs"),u(g,"class","alert"),u(v,"class","section-title")},m(e,o){s(e,i,o),I(i,m),I(i,b),I(i,d),s(e,k,o),s(e,h,o),s(e,D,o),s(e,f,o),s(e,_,o),ne(l,e,o),s(e,C,o),s(e,$,o),s(e,S,o),s(e,g,o),s(e,w,o),s(e,v,o),s(e,E,o),ne(r,e,o),R=!0},p(e,[o]){var Y,z,F,G,K,Q,X,Z,x,ee,oe,te;(!R||o&1)&&c!==(c=e[0].name+"")&&ue(b,c);const O={};o&3&&(O.js=` + event message.

`,_=a(),se(l.$$.fragment),C=a(),$=p("h6"),$.textContent="API details",S=a(),g=p("div"),g.innerHTML='SSE

/api/realtime

',w=a(),v=p("div"),v.textContent="Event data format",E=a(),se(r.$$.fragment),u(i,"class","m-b-sm"),u(h,"class","content txt-lg m-b-sm"),u(f,"class","alert alert-info m-t-10 m-b-sm"),u($,"class","m-b-xs"),u(g,"class","alert"),u(v,"class","section-title")},m(e,t){s(e,i,t),I(i,m),I(i,b),I(i,d),s(e,k,t),s(e,h,t),s(e,D,t),s(e,f,t),s(e,_,t),ne(l,e,t),s(e,C,t),s(e,$,t),s(e,S,t),s(e,g,t),s(e,w,t),s(e,v,t),s(e,E,t),ne(r,e,t),R=!0},p(e,[t]){var V,Y,z,F,G,K,X,Z,x,ee,te,oe;(!R||t&1)&&c!==(c=e[0].name+"")&&me(b,c);const O={};t&3&&(O.js=` import PocketBase from 'pocketbase'; const pb = new PocketBase('${e[1]}'); @@ -65,23 +65,23 @@ import{S as re,i as ae,s as be,N as pe,C as P,e as p,v as y,b as a,c as se,f as // (Optionally) authenticate await pb.collection('users').authWithPassword('test@example.com', '123456'); - // Subscribe to changes in any ${(Y=e[0])==null?void 0:Y.name} record - pb.collection('${(z=e[0])==null?void 0:z.name}').subscribe('*', function (e) { + // Subscribe to changes in any ${(V=e[0])==null?void 0:V.name} record + pb.collection('${(Y=e[0])==null?void 0:Y.name}').subscribe('*', function (e) { console.log(e.action); console.log(e.record); - }, { /* other options like expand, custom headers, etc. */ }); + }, { /* other options like: filter, expand, custom headers, etc. */ }); // Subscribe to changes only in the specified record - pb.collection('${(F=e[0])==null?void 0:F.name}').subscribe('RECORD_ID', function (e) { + pb.collection('${(z=e[0])==null?void 0:z.name}').subscribe('RECORD_ID', function (e) { console.log(e.action); console.log(e.record); - }, { /* other options like expand, custom headers, etc. */ }); + }, { /* other options like: filter, expand, custom headers, etc. */ }); // Unsubscribe - pb.collection('${(G=e[0])==null?void 0:G.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions - pb.collection('${(K=e[0])==null?void 0:K.name}').unsubscribe('*'); // remove all '*' topic subscriptions - pb.collection('${(Q=e[0])==null?void 0:Q.name}').unsubscribe(); // remove all subscriptions in the collection - `),o&3&&(O.dart=` + pb.collection('${(F=e[0])==null?void 0:F.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions + pb.collection('${(G=e[0])==null?void 0:G.name}').unsubscribe('*'); // remove all '*' topic subscriptions + pb.collection('${(K=e[0])==null?void 0:K.name}').unsubscribe(); // remove all subscriptions in the collection + `),t&3&&(O.dart=` import 'package:pocketbase/pocketbase.dart'; final pb = PocketBase('${e[1]}'); @@ -95,16 +95,16 @@ import{S as re,i as ae,s as be,N as pe,C as P,e as p,v as y,b as a,c as se,f as pb.collection('${(Z=e[0])==null?void 0:Z.name}').subscribe('*', (e) { print(e.action); print(e.record); - }, /* other options like expand, custom headers, etc. */); + }, /* other options like: filter, expand, custom headers, etc. */); // Subscribe to changes only in the specified record pb.collection('${(x=e[0])==null?void 0:x.name}').subscribe('RECORD_ID', (e) { print(e.action); print(e.record); - }, /* other options like expand, custom headers, etc. */); + }, /* other options like: filter, expand, custom headers, etc. */); // Unsubscribe pb.collection('${(ee=e[0])==null?void 0:ee.name}').unsubscribe('RECORD_ID'); // remove all 'RECORD_ID' subscriptions - pb.collection('${(oe=e[0])==null?void 0:oe.name}').unsubscribe('*'); // remove all '*' topic subscriptions - pb.collection('${(te=e[0])==null?void 0:te.name}').unsubscribe(); // remove all subscriptions in the collection - `),l.$set(O);const V={};o&1&&(V.content=JSON.stringify({action:"create",record:P.dummyCollectionRecord(e[0])},null,2).replace('"action": "create"','"action": "create" // create, update or delete')),r.$set(V)},i(e){R||(ie(l.$$.fragment,e),ie(r.$$.fragment,e),R=!0)},o(e){ce(l.$$.fragment,e),ce(r.$$.fragment,e),R=!1},d(e){e&&(n(i),n(k),n(h),n(D),n(f),n(_),n(C),n($),n(S),n(g),n(w),n(v),n(E)),le(l,e),le(r,e)}}}function fe(t,i,m){let c,{collection:b}=i;return t.$$set=d=>{"collection"in d&&m(0,b=d.collection)},m(1,c=P.getApiExampleUrl(me.baseUrl)),[b,c]}class ve extends re{constructor(i){super(),ae(this,i,fe,he,be,{collection:0})}}export{ve as default}; + pb.collection('${(te=e[0])==null?void 0:te.name}').unsubscribe('*'); // remove all '*' topic subscriptions + pb.collection('${(oe=e[0])==null?void 0:oe.name}').unsubscribe(); // remove all subscriptions in the collection + `),l.$set(O);const Q={};t&1&&(Q.content=JSON.stringify({action:"create",record:P.dummyCollectionRecord(e[0])},null,2).replace('"action": "create"','"action": "create" // create, update or delete')),r.$set(Q)},i(e){R||(ie(l.$$.fragment,e),ie(r.$$.fragment,e),R=!0)},o(e){ce(l.$$.fragment,e),ce(r.$$.fragment,e),R=!1},d(e){e&&(n(i),n(k),n(h),n(D),n(f),n(_),n(C),n($),n(S),n(g),n(w),n(v),n(E)),le(l,e),le(r,e)}}}function fe(o,i,m){let c,{collection:b}=i;return o.$$set=d=>{"collection"in d&&m(0,b=d.collection)},m(1,c=P.getApiExampleUrl(de.baseURL)),[b,c]}class ge extends re{constructor(i){super(),ae(this,i,fe,he,be,{collection:0})}}export{ge as default}; diff --git a/ui/dist/assets/RequestEmailChangeDocs-OulvgXBH.js b/ui/dist/assets/RequestEmailChangeDocs-OulvgXBH.js deleted file mode 100644 index 734d5384..00000000 --- a/ui/dist/assets/RequestEmailChangeDocs-OulvgXBH.js +++ /dev/null @@ -1,64 +0,0 @@ -import{S as Ee,i as Be,s as Se,O as L,e as r,v,b as k,c as Ce,f as b,g as d,h as n,m as ye,w as N,P as ve,Q as Ae,k as Re,R as Me,n as We,t as ee,a as te,o as m,d as Te,C as ze,A as He,q as F,r as Oe,N as Ue}from"./index-Bp3jGQ0J.js";import{S as je}from"./SdkTabs-DxNNd6Sw.js";function we(o,l,a){const s=o.slice();return s[5]=l[a],s}function $e(o,l,a){const s=o.slice();return s[5]=l[a],s}function qe(o,l){let a,s=l[5].code+"",h,f,i,p;function u(){return l[4](l[5])}return{key:o,first:null,c(){a=r("button"),h=v(s),f=k(),b(a,"class","tab-item"),F(a,"active",l[1]===l[5].code),this.first=a},m($,q){d($,a,q),n(a,h),n(a,f),i||(p=Oe(a,"click",u),i=!0)},p($,q){l=$,q&4&&s!==(s=l[5].code+"")&&N(h,s),q&6&&F(a,"active",l[1]===l[5].code)},d($){$&&m(a),i=!1,p()}}}function Pe(o,l){let a,s,h,f;return s=new Ue({props:{content:l[5].body}}),{key:o,first:null,c(){a=r("div"),Ce(s.$$.fragment),h=k(),b(a,"class","tab-item"),F(a,"active",l[1]===l[5].code),this.first=a},m(i,p){d(i,a,p),ye(s,a,null),n(a,h),f=!0},p(i,p){l=i;const u={};p&4&&(u.content=l[5].body),s.$set(u),(!f||p&6)&&F(a,"active",l[1]===l[5].code)},i(i){f||(ee(s.$$.fragment,i),f=!0)},o(i){te(s.$$.fragment,i),f=!1},d(i){i&&m(a),Te(s)}}}function De(o){var pe,ue,be,fe;let l,a,s=o[0].name+"",h,f,i,p,u,$,q,z=o[0].name+"",I,le,K,P,Q,T,G,w,H,ae,O,E,se,J,U=o[0].name+"",V,oe,ne,j,X,B,Y,S,Z,A,x,C,R,g=[],ie=new Map,ce,M,_=[],re=new Map,y;P=new je({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(pe=o[0])==null?void 0:pe.name}').authWithPassword('test@example.com', '1234567890'); - - await pb.collection('${(ue=o[0])==null?void 0:ue.name}').requestEmailChange('new@example.com'); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(be=o[0])==null?void 0:be.name}').authWithPassword('test@example.com', '1234567890'); - - await pb.collection('${(fe=o[0])==null?void 0:fe.name}').requestEmailChange('new@example.com'); - `}});let D=L(o[2]);const de=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eAuthorization:TOKEN header",X=k(),B=r("div"),B.textContent="Body Parameters",Y=k(),S=r("table"),S.innerHTML='Param Type Description
Required newEmail
String The new email address to send the change email request.',Z=k(),A=r("div"),A.textContent="Responses",x=k(),C=r("div"),R=r("div");for(let e=0;ea(1,f=u.code);return o.$$set=u=>{"collection"in u&&a(0,h=u.collection)},a(3,s=ze.getApiExampleUrl(He.baseUrl)),a(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "newEmail": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `},{code:401,body:` - { - "code": 401, - "message": "The request requires valid record authorization token to be set.", - "data": {} - } - `},{code:403,body:` - { - "code": 403, - "message": "The authorized record model is not allowed to perform this action.", - "data": {} - } - `}]),[h,f,i,s,p]}class Ie extends Ee{constructor(l){super(),Be(this,l,Le,De,Se,{collection:0})}}export{Ie as default}; diff --git a/ui/dist/assets/RequestPasswordResetDocs-Ux0BhdtA.js b/ui/dist/assets/RequestPasswordResetDocs-Ux0BhdtA.js deleted file mode 100644 index 836f9e49..00000000 --- a/ui/dist/assets/RequestPasswordResetDocs-Ux0BhdtA.js +++ /dev/null @@ -1,44 +0,0 @@ -import{S as Pe,i as $e,s as qe,O as I,e as r,v as g,b as h,c as ve,f as b,g as d,h as n,m as ge,w as L,P as fe,Q as ye,k as Re,R as Ce,n as Be,t as x,a as ee,o as p,d as we,C as Se,A as Te,q as N,r as Ae,N as Me}from"./index-Bp3jGQ0J.js";import{S as Ue}from"./SdkTabs-DxNNd6Sw.js";function be(o,s,l){const a=o.slice();return a[5]=s[l],a}function _e(o,s,l){const a=o.slice();return a[5]=s[l],a}function ke(o,s){let l,a=s[5].code+"",_,f,i,m;function u(){return s[4](s[5])}return{key:o,first:null,c(){l=r("button"),_=g(a),f=h(),b(l,"class","tab-item"),N(l,"active",s[1]===s[5].code),this.first=l},m(w,P){d(w,l,P),n(l,_),n(l,f),i||(m=Ae(l,"click",u),i=!0)},p(w,P){s=w,P&4&&a!==(a=s[5].code+"")&&L(_,a),P&6&&N(l,"active",s[1]===s[5].code)},d(w){w&&p(l),i=!1,m()}}}function he(o,s){let l,a,_,f;return a=new Me({props:{content:s[5].body}}),{key:o,first:null,c(){l=r("div"),ve(a.$$.fragment),_=h(),b(l,"class","tab-item"),N(l,"active",s[1]===s[5].code),this.first=l},m(i,m){d(i,l,m),ge(a,l,null),n(l,_),f=!0},p(i,m){s=i;const u={};m&4&&(u.content=s[5].body),a.$set(u),(!f||m&6)&&N(l,"active",s[1]===s[5].code)},i(i){f||(x(a.$$.fragment,i),f=!0)},o(i){ee(a.$$.fragment,i),f=!1},d(i){i&&p(l),we(a)}}}function je(o){var de,pe;let s,l,a=o[0].name+"",_,f,i,m,u,w,P,D=o[0].name+"",Q,te,z,$,G,C,J,q,H,se,O,B,le,K,E=o[0].name+"",V,ae,W,S,X,T,Y,A,Z,y,M,v=[],oe=new Map,ne,U,k=[],ie=new Map,R;$=new Ue({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(de=o[0])==null?void 0:de.name}').requestPasswordReset('test@example.com'); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(pe=o[0])==null?void 0:pe.name}').requestPasswordReset('test@example.com'); - `}});let F=I(o[2]);const ce=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description
Required email
String The auth record email address to send the password reset request (if exists).',Y=h(),A=r("div"),A.textContent="Responses",Z=h(),y=r("div"),M=r("div");for(let e=0;el(1,f=u.code);return o.$$set=u=>{"collection"in u&&l(0,_=u.collection)},l(3,a=Se.getApiExampleUrl(Te.baseUrl)),l(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "email": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}]),[_,f,i,a,m]}class Ee extends Pe{constructor(s){super(),$e(this,s,De,je,qe,{collection:0})}}export{Ee as default}; diff --git a/ui/dist/assets/RequestVerificationDocs-CmHx_pVy.js b/ui/dist/assets/RequestVerificationDocs-CmHx_pVy.js deleted file mode 100644 index e0f3992d..00000000 --- a/ui/dist/assets/RequestVerificationDocs-CmHx_pVy.js +++ /dev/null @@ -1,44 +0,0 @@ -import{S as qe,i as we,s as Pe,O as F,e as r,v as g,b as h,c as ve,f as b,g as d,h as n,m as ge,w as I,P as pe,Q as ye,k as Ce,R as Be,n as Se,t as x,a as ee,o as f,d as $e,C as Te,A as Ae,q as L,r as Re,N as Ve}from"./index-Bp3jGQ0J.js";import{S as Me}from"./SdkTabs-DxNNd6Sw.js";function be(o,l,s){const a=o.slice();return a[5]=l[s],a}function _e(o,l,s){const a=o.slice();return a[5]=l[s],a}function ke(o,l){let s,a=l[5].code+"",_,p,i,m;function u(){return l[4](l[5])}return{key:o,first:null,c(){s=r("button"),_=g(a),p=h(),b(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m($,q){d($,s,q),n(s,_),n(s,p),i||(m=Re(s,"click",u),i=!0)},p($,q){l=$,q&4&&a!==(a=l[5].code+"")&&I(_,a),q&6&&L(s,"active",l[1]===l[5].code)},d($){$&&f(s),i=!1,m()}}}function he(o,l){let s,a,_,p;return a=new Ve({props:{content:l[5].body}}),{key:o,first:null,c(){s=r("div"),ve(a.$$.fragment),_=h(),b(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m(i,m){d(i,s,m),ge(a,s,null),n(s,_),p=!0},p(i,m){l=i;const u={};m&4&&(u.content=l[5].body),a.$set(u),(!p||m&6)&&L(s,"active",l[1]===l[5].code)},i(i){p||(x(a.$$.fragment,i),p=!0)},o(i){ee(a.$$.fragment,i),p=!1},d(i){i&&f(s),$e(a)}}}function Ue(o){var de,fe;let l,s,a=o[0].name+"",_,p,i,m,u,$,q,j=o[0].name+"",N,te,Q,w,z,B,G,P,D,le,H,S,se,J,O=o[0].name+"",K,ae,W,T,X,A,Y,R,Z,y,V,v=[],oe=new Map,ne,M,k=[],ie=new Map,C;w=new Me({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${o[3]}'); - - ... - - await pb.collection('${(de=o[0])==null?void 0:de.name}').requestVerification('test@example.com'); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${o[3]}'); - - ... - - await pb.collection('${(fe=o[0])==null?void 0:fe.name}').requestVerification('test@example.com'); - `}});let E=F(o[2]);const ce=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eParam Type Description
Required email
String The auth record email address to send the verification request (if exists).',Y=h(),R=r("div"),R.textContent="Responses",Z=h(),y=r("div"),V=r("div");for(let e=0;es(1,p=u.code);return o.$$set=u=>{"collection"in u&&s(0,_=u.collection)},s(3,a=Te.getApiExampleUrl(Ae.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:` - { - "code": 400, - "message": "Failed to authenticate.", - "data": { - "email": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `}]),[_,p,i,a,m]}class Oe extends qe{constructor(l){super(),we(this,l,je,Ue,Pe,{collection:0})}}export{Oe as default}; diff --git a/ui/dist/assets/SdkTabs-DxNNd6Sw.js b/ui/dist/assets/SdkTabs-DxNNd6Sw.js deleted file mode 100644 index 31959419..00000000 --- a/ui/dist/assets/SdkTabs-DxNNd6Sw.js +++ /dev/null @@ -1 +0,0 @@ -import{S as B,i as F,s as J,O as j,e as v,b as S,f as h,g as y,h as m,P as D,Q as O,k as Q,R as Y,n as z,t as N,a as P,o as C,v as w,q as E,r as A,w as T,N as G,c as H,m as L,d as U}from"./index-Bp3jGQ0J.js";function K(o,e,l){const s=o.slice();return s[6]=e[l],s}function R(o,e,l){const s=o.slice();return s[6]=e[l],s}function q(o,e){let l,s,g=e[6].title+"",r,i,n,k;function c(){return e[5](e[6])}return{key:o,first:null,c(){l=v("button"),s=v("div"),r=w(g),i=S(),h(s,"class","txt"),h(l,"class","tab-item svelte-1maocj6"),E(l,"active",e[1]===e[6].language),this.first=l},m(_,f){y(_,l,f),m(l,s),m(s,r),m(l,i),n||(k=A(l,"click",c),n=!0)},p(_,f){e=_,f&4&&g!==(g=e[6].title+"")&&T(r,g),f&6&&E(l,"active",e[1]===e[6].language)},d(_){_&&C(l),n=!1,k()}}}function I(o,e){let l,s,g,r,i,n,k=e[6].title+"",c,_,f,p,d;return s=new G({props:{language:e[6].language,content:e[6].content}}),{key:o,first:null,c(){l=v("div"),H(s.$$.fragment),g=S(),r=v("div"),i=v("em"),n=v("a"),c=w(k),_=w(" SDK"),p=S(),h(n,"href",f=e[6].url),h(n,"target","_blank"),h(n,"rel","noopener noreferrer"),h(i,"class","txt-sm txt-hint"),h(r,"class","txt-right"),h(l,"class","tab-item svelte-1maocj6"),E(l,"active",e[1]===e[6].language),this.first=l},m(b,t){y(b,l,t),L(s,l,null),m(l,g),m(l,r),m(r,i),m(i,n),m(n,c),m(n,_),m(l,p),d=!0},p(b,t){e=b;const a={};t&4&&(a.language=e[6].language),t&4&&(a.content=e[6].content),s.$set(a),(!d||t&4)&&k!==(k=e[6].title+"")&&T(c,k),(!d||t&4&&f!==(f=e[6].url))&&h(n,"href",f),(!d||t&6)&&E(l,"active",e[1]===e[6].language)},i(b){d||(N(s.$$.fragment,b),d=!0)},o(b){P(s.$$.fragment,b),d=!1},d(b){b&&C(l),U(s)}}}function V(o){let e,l,s=[],g=new Map,r,i,n=[],k=new Map,c,_,f=j(o[2]);const p=t=>t[6].language;for(let t=0;tt[6].language;for(let t=0;tl(1,n=c.language);return o.$$set=c=>{"class"in c&&l(0,g=c.class),"js"in c&&l(3,r=c.js),"dart"in c&&l(4,i=c.dart)},o.$$.update=()=>{o.$$.dirty&2&&n&&localStorage.setItem(M,n),o.$$.dirty&24&&l(2,s=[{title:"JavaScript",language:"javascript",content:r,url:"https://github.com/pocketbase/js-sdk"},{title:"Dart",language:"dart",content:i,url:"https://github.com/pocketbase/dart-sdk"}])},[g,n,s,r,i,k]}class Z extends B{constructor(e){super(),F(this,e,W,V,J,{class:0,js:3,dart:4})}}export{Z as S}; diff --git a/ui/dist/assets/SdkTabs-lBWmLVyw.css b/ui/dist/assets/SdkTabs-lBWmLVyw.css deleted file mode 100644 index 64e6ba64..00000000 --- a/ui/dist/assets/SdkTabs-lBWmLVyw.css +++ /dev/null @@ -1 +0,0 @@ -.sdk-tabs.svelte-1maocj6 .tabs-header .tab-item.svelte-1maocj6{min-width:100px} diff --git a/ui/dist/assets/UnlinkExternalAuthDocs-BcuOuUMj.js b/ui/dist/assets/UnlinkExternalAuthDocs-BcuOuUMj.js deleted file mode 100644 index 3b566860..00000000 --- a/ui/dist/assets/UnlinkExternalAuthDocs-BcuOuUMj.js +++ /dev/null @@ -1,72 +0,0 @@ -import{S as Oe,i as De,s as Me,O as j,e as i,v as g,b as f,c as Be,f as h,g as d,h as a,m as qe,w as I,P as ye,Q as We,k as ze,R as He,n as Le,t as oe,a as ae,o as u,d as Ue,C as Re,A as je,q as N,r as Ie,N as Ne}from"./index-Bp3jGQ0J.js";import{S as Ke}from"./SdkTabs-DxNNd6Sw.js";function Ce(n,l,o){const s=n.slice();return s[5]=l[o],s}function Te(n,l,o){const s=n.slice();return s[5]=l[o],s}function Ee(n,l){let o,s=l[5].code+"",_,b,c,p;function m(){return l[4](l[5])}return{key:n,first:null,c(){o=i("button"),_=g(s),b=f(),h(o,"class","tab-item"),N(o,"active",l[1]===l[5].code),this.first=o},m($,P){d($,o,P),a(o,_),a(o,b),c||(p=Ie(o,"click",m),c=!0)},p($,P){l=$,P&4&&s!==(s=l[5].code+"")&&I(_,s),P&6&&N(o,"active",l[1]===l[5].code)},d($){$&&u(o),c=!1,p()}}}function Se(n,l){let o,s,_,b;return s=new Ne({props:{content:l[5].body}}),{key:n,first:null,c(){o=i("div"),Be(s.$$.fragment),_=f(),h(o,"class","tab-item"),N(o,"active",l[1]===l[5].code),this.first=o},m(c,p){d(c,o,p),qe(s,o,null),a(o,_),b=!0},p(c,p){l=c;const m={};p&4&&(m.content=l[5].body),s.$set(m),(!b||p&6)&&N(o,"active",l[1]===l[5].code)},i(c){b||(oe(s.$$.fragment,c),b=!0)},o(c){ae(s.$$.fragment,c),b=!1},d(c){c&&u(o),Ue(s)}}}function Qe(n){var _e,ke,ge,ve;let l,o,s=n[0].name+"",_,b,c,p,m,$,P,M=n[0].name+"",K,se,ne,Q,F,y,G,E,J,w,W,ie,z,A,ce,V,H=n[0].name+"",X,re,Y,de,Z,ue,L,x,S,ee,B,te,q,le,C,U,v=[],pe=new Map,me,O,k=[],he=new Map,T;y=new Ke({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${n[3]}'); - - ... - - await pb.collection('${(_e=n[0])==null?void 0:_e.name}').authWithPassword('test@example.com', '123456'); - - await pb.collection('${(ke=n[0])==null?void 0:ke.name}').unlinkExternalAuth( - pb.authStore.model.id, - 'google' - ); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${n[3]}'); - - ... - - await pb.collection('${(ge=n[0])==null?void 0:ge.name}').authWithPassword('test@example.com', '123456'); - - await pb.collection('${(ve=n[0])==null?void 0:ve.name}').unlinkExternalAuth( - pb.authStore.model.id, - 'google', - ); - `}});let R=j(n[2]);const be=e=>e[5].code;for(let e=0;ee[5].code;for(let e=0;eAuthorization:TOKEN header",x=f(),S=i("div"),S.textContent="Path Parameters",ee=f(),B=i("table"),B.innerHTML=`Param Type Description id String ID of the auth record. provider String The name of the auth provider to unlink, eg. google, twitter, - github, etc.`,te=f(),q=i("div"),q.textContent="Responses",le=f(),C=i("div"),U=i("div");for(let e=0;eo(1,b=m.code);return n.$$set=m=>{"collection"in m&&o(0,_=m.collection)},o(3,s=Re.getApiExampleUrl(je.baseUrl)),o(2,c=[{code:204,body:"null"},{code:401,body:` - { - "code": 401, - "message": "The request requires valid record authorization token to be set.", - "data": {} - } - `},{code:403,body:` - { - "code": 403, - "message": "The authorized record model is not allowed to perform this action.", - "data": {} - } - `},{code:404,body:` - { - "code": 404, - "message": "The requested resource wasn't found.", - "data": {} - } - `}]),[_,b,c,s,p]}class Ve extends Oe{constructor(l){super(),De(this,l,Fe,Qe,Me,{collection:0})}}export{Ve as default}; diff --git a/ui/dist/assets/UpdateApiDocs-BIFiuRUJ.js b/ui/dist/assets/UpdateApiDocs-BIFiuRUJ.js new file mode 100644 index 00000000..c390a2d3 --- /dev/null +++ b/ui/dist/assets/UpdateApiDocs-BIFiuRUJ.js @@ -0,0 +1,90 @@ +import{S as Ot,i as St,s as Mt,Q as $t,C as x,T as ie,R as Tt,e as i,w as h,b as f,c as we,f as k,g as o,h as n,m as ve,x as te,U as Ie,V as bt,k as qt,W as Rt,n as Dt,t as he,a as ye,o as r,d as Ce,p as Ht,r as Te,u as Lt,y as de}from"./index-B-F-pko3.js";import{F as Pt}from"./FieldsQueryParam-CW6KZfgu.js";function mt(d,e,t){const a=d.slice();return a[10]=e[t],a}function _t(d,e,t){const a=d.slice();return a[10]=e[t],a}function ht(d,e,t){const a=d.slice();return a[15]=e[t],a}function yt(d){let e;return{c(){e=i("p"),e.innerHTML=`Note that in case of a password change all previously issued tokens for the current record + will be automatically invalidated and if you want your user to remain signed in you need to + reauthenticate manually after the update call.`},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function kt(d){let e;return{c(){e=i("p"),e.innerHTML="Requires superuser Authorization:TOKEN header",k(e,"class","txt-hint txt-sm txt-right")},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function gt(d){let e,t,a,m,p,c,u,b,O,T,M,D,S,E,q,H,I,U,$,R,L,g,w,v;function Q(_,C){var le,z,ne;return C&1&&(b=null),b==null&&(b=!!((ne=(z=(le=_[0])==null?void 0:le.fields)==null?void 0:z.find(Wt))!=null&&ne.required)),b?Bt:Ft}let W=Q(d,-1),F=W(d);return{c(){e=i("tr"),e.innerHTML='Auth specific fields',t=f(),a=i("tr"),a.innerHTML=`
Optional email
String The auth record email address. +
+ This field can be updated only by superusers or auth records with "Manage" access. +
+ Regular accounts can update their email by calling "Request email change".`,m=f(),p=i("tr"),c=i("td"),u=i("div"),F.c(),O=f(),T=i("span"),T.textContent="emailVisibility",M=f(),D=i("td"),D.innerHTML='Boolean',S=f(),E=i("td"),E.textContent="Whether to show/hide the auth record email when fetching the record data.",q=f(),H=i("tr"),H.innerHTML=`
Optional oldPassword
String Old auth record password. +
+ This field is required only when changing the record password. Superusers and auth records + with "Manage" access can skip this field.`,I=f(),U=i("tr"),U.innerHTML='
Optional password
String New auth record password.',$=f(),R=i("tr"),R.innerHTML='
Optional passwordConfirm
String New auth record password confirmation.',L=f(),g=i("tr"),g.innerHTML=`
Optional verified
Boolean Indicates whether the auth record is verified or not. +
+ This field can be set only by superusers or auth records with "Manage" access.`,w=f(),v=i("tr"),v.innerHTML='Other fields',k(u,"class","inline-flex")},m(_,C){o(_,e,C),o(_,t,C),o(_,a,C),o(_,m,C),o(_,p,C),n(p,c),n(c,u),F.m(u,null),n(u,O),n(u,T),n(p,M),n(p,D),n(p,S),n(p,E),o(_,q,C),o(_,H,C),o(_,I,C),o(_,U,C),o(_,$,C),o(_,R,C),o(_,L,C),o(_,g,C),o(_,w,C),o(_,v,C)},p(_,C){W!==(W=Q(_,C))&&(F.d(1),F=W(_),F&&(F.c(),F.m(u,O)))},d(_){_&&(r(e),r(t),r(a),r(m),r(p),r(q),r(H),r(I),r(U),r($),r(R),r(L),r(g),r(w),r(v)),F.d()}}}function Ft(d){let e;return{c(){e=i("span"),e.textContent="Optional",k(e,"class","label label-warning")},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function Bt(d){let e;return{c(){e=i("span"),e.textContent="Required",k(e,"class","label label-success")},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function Nt(d){let e;return{c(){e=i("span"),e.textContent="Optional",k(e,"class","label label-warning")},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function jt(d){let e;return{c(){e=i("span"),e.textContent="Required",k(e,"class","label label-success")},m(t,a){o(t,e,a)},d(t){t&&r(e)}}}function At(d){let e,t=d[15].maxSelect==1?"id":"ids",a,m;return{c(){e=h("Relation record "),a=h(t),m=h(".")},m(p,c){o(p,e,c),o(p,a,c),o(p,m,c)},p(p,c){c&64&&t!==(t=p[15].maxSelect==1?"id":"ids")&&te(a,t)},d(p){p&&(r(e),r(a),r(m))}}}function Et(d){let e,t,a,m,p;return{c(){e=h("File object."),t=i("br"),a=h(` + Set to `),m=i("code"),m.textContent="null",p=h(" to delete already uploaded file(s).")},m(c,u){o(c,e,u),o(c,t,u),o(c,a,u),o(c,m,u),o(c,p,u)},p:de,d(c){c&&(r(e),r(t),r(a),r(m),r(p))}}}function Ut(d){let e;return{c(){e=h("URL address.")},m(t,a){o(t,e,a)},p:de,d(t){t&&r(e)}}}function It(d){let e;return{c(){e=h("Email address.")},m(t,a){o(t,e,a)},p:de,d(t){t&&r(e)}}}function Jt(d){let e;return{c(){e=h("JSON array or object.")},m(t,a){o(t,e,a)},p:de,d(t){t&&r(e)}}}function Vt(d){let e;return{c(){e=h("Number value.")},m(t,a){o(t,e,a)},p:de,d(t){t&&r(e)}}}function xt(d){let e;return{c(){e=h("Plain text value.")},m(t,a){o(t,e,a)},p:de,d(t){t&&r(e)}}}function wt(d,e){let t,a,m,p,c,u=e[15].name+"",b,O,T,M,D=x.getFieldValueType(e[15])+"",S,E,q,H;function I(w,v){return w[15].required?jt:Nt}let U=I(e),$=U(e);function R(w,v){if(w[15].type==="text")return xt;if(w[15].type==="number")return Vt;if(w[15].type==="json")return Jt;if(w[15].type==="email")return It;if(w[15].type==="url")return Ut;if(w[15].type==="file")return Et;if(w[15].type==="relation")return At}let L=R(e),g=L&&L(e);return{key:d,first:null,c(){t=i("tr"),a=i("td"),m=i("div"),$.c(),p=f(),c=i("span"),b=h(u),O=f(),T=i("td"),M=i("span"),S=h(D),E=f(),q=i("td"),g&&g.c(),H=f(),k(m,"class","inline-flex"),k(M,"class","label"),this.first=t},m(w,v){o(w,t,v),n(t,a),n(a,m),$.m(m,null),n(m,p),n(m,c),n(c,b),n(t,O),n(t,T),n(T,M),n(M,S),n(t,E),n(t,q),g&&g.m(q,null),n(t,H)},p(w,v){e=w,U!==(U=I(e))&&($.d(1),$=U(e),$&&($.c(),$.m(m,p))),v&64&&u!==(u=e[15].name+"")&&te(b,u),v&64&&D!==(D=x.getFieldValueType(e[15])+"")&&te(S,D),L===(L=R(e))&&g?g.p(e,v):(g&&g.d(1),g=L&&L(e),g&&(g.c(),g.m(q,null)))},d(w){w&&r(t),$.d(),g&&g.d()}}}function vt(d,e){let t,a=e[10].code+"",m,p,c,u;function b(){return e[9](e[10])}return{key:d,first:null,c(){t=i("button"),m=h(a),p=f(),k(t,"class","tab-item"),Te(t,"active",e[2]===e[10].code),this.first=t},m(O,T){o(O,t,T),n(t,m),n(t,p),c||(u=Lt(t,"click",b),c=!0)},p(O,T){e=O,T&8&&a!==(a=e[10].code+"")&&te(m,a),T&12&&Te(t,"active",e[2]===e[10].code)},d(O){O&&r(t),c=!1,u()}}}function Ct(d,e){let t,a,m,p;return a=new Tt({props:{content:e[10].body}}),{key:d,first:null,c(){t=i("div"),we(a.$$.fragment),m=f(),k(t,"class","tab-item"),Te(t,"active",e[2]===e[10].code),this.first=t},m(c,u){o(c,t,u),ve(a,t,null),n(t,m),p=!0},p(c,u){e=c;const b={};u&8&&(b.content=e[10].body),a.$set(b),(!p||u&12)&&Te(t,"active",e[2]===e[10].code)},i(c){p||(he(a.$$.fragment,c),p=!0)},o(c){ye(a.$$.fragment,c),p=!1},d(c){c&&r(t),Ce(a)}}}function Qt(d){var ct,ut;let e,t,a=d[0].name+"",m,p,c,u,b,O,T,M=d[0].name+"",D,S,E,q,H,I,U,$,R,L,g,w,v,Q,W,F,_,C,le,z=d[0].name+"",ne,Je,Oe,Ve,Se,oe,Me,re,$e,ce,qe,K,Re,xe,Y,De,J=[],Qe=new Map,He,ue,Le,G,Pe,We,pe,X,Fe,ze,Be,Ke,B,Ye,ae,Ge,Xe,Ze,Ne,et,je,tt,Ae,lt,nt,se,Ee,fe,Ue,Z,be,V=[],at=new Map,st,me,N=[],it=new Map,ee,j=d[1]&&yt();R=new $t({props:{js:` +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('${d[5]}'); + +... + +// example update data +const data = ${JSON.stringify(Object.assign({},d[4],x.dummyCollectionSchemaData(d[0],!0)),null,4)}; + +const record = await pb.collection('${(ct=d[0])==null?void 0:ct.name}').update('RECORD_ID', data); + `,dart:` +import 'package:pocketbase/pocketbase.dart'; + +final pb = PocketBase('${d[5]}'); + +... + +// example update body +final body = ${JSON.stringify(Object.assign({},d[4],x.dummyCollectionSchemaData(d[0],!0)),null,2)}; + +final record = await pb.collection('${(ut=d[0])==null?void 0:ut.name}').update('RECORD_ID', body: body); + `}});let A=d[7]&&kt(),P=d[1]&>(d),ke=ie(d[6]);const dt=l=>l[15].name;for(let l=0;ll[10].code;for(let l=0;ll[10].code;for(let l=0;l<_e.length;l+=1){let s=mt(d,_e,l),y=rt(s);it.set(y,N[l]=Ct(y,s))}return{c(){e=i("h3"),t=h("Update ("),m=h(a),p=h(")"),c=f(),u=i("div"),b=i("p"),O=h("Update a single "),T=i("strong"),D=h(M),S=h(" record."),E=f(),q=i("p"),q.innerHTML=`Body parameters could be sent as application/json or + multipart/form-data.`,H=f(),I=i("p"),I.innerHTML=`File upload is supported only via multipart/form-data. +
+ For more info and examples you could check the detailed + Files upload and handling docs + .`,U=f(),j&&j.c(),$=f(),we(R.$$.fragment),L=f(),g=i("h6"),g.textContent="API details",w=f(),v=i("div"),Q=i("strong"),Q.textContent="PATCH",W=f(),F=i("div"),_=i("p"),C=h("/api/collections/"),le=i("strong"),ne=h(z),Je=h("/records/"),Oe=i("strong"),Oe.textContent=":id",Ve=f(),A&&A.c(),Se=f(),oe=i("div"),oe.textContent="Path parameters",Me=f(),re=i("table"),re.innerHTML='Param Type Description id String ID of the record to update.',$e=f(),ce=i("div"),ce.textContent="Body Parameters",qe=f(),K=i("table"),Re=i("thead"),Re.innerHTML='Param Type Description',xe=f(),Y=i("tbody"),P&&P.c(),De=f();for(let l=0;lParam Type Description',We=f(),pe=i("tbody"),X=i("tr"),Fe=i("td"),Fe.textContent="expand",ze=f(),Be=i("td"),Be.innerHTML='String',Ke=f(),B=i("td"),Ye=h(`Auto expand relations when returning the updated record. Ex.: + `),we(ae.$$.fragment),Ge=h(` + Supports up to 6-levels depth nested relations expansion. `),Xe=i("br"),Ze=h(` + The expanded relations will be appended to the record under the + `),Ne=i("code"),Ne.textContent="expand",et=h(" property (eg. "),je=i("code"),je.textContent='"expand": {"relField1": {...}, ...}',tt=h(`). Only + the relations that the user has permissions to `),Ae=i("strong"),Ae.textContent="view",lt=h(" will be expanded."),nt=f(),we(se.$$.fragment),Ee=f(),fe=i("div"),fe.textContent="Responses",Ue=f(),Z=i("div"),be=i("div");for(let l=0;l${JSON.stringify(Object.assign({},l[4],x.dummyCollectionSchemaData(l[0],!0)),null,2)}; + +final record = await pb.collection('${(ft=l[0])==null?void 0:ft.name}').update('RECORD_ID', body: body); + `),R.$set(y),(!ee||s&1)&&z!==(z=l[0].name+"")&&te(ne,z),l[7]?A||(A=kt(),A.c(),A.m(v,null)):A&&(A.d(1),A=null),l[1]?P?P.p(l,s):(P=gt(l),P.c(),P.m(Y,De)):P&&(P.d(1),P=null),s&64&&(ke=ie(l[6]),J=Ie(J,s,dt,1,l,ke,Qe,Y,bt,wt,null,ht)),s&12&&(ge=ie(l[3]),V=Ie(V,s,ot,1,l,ge,at,be,bt,vt,null,_t)),s&12&&(_e=ie(l[3]),qt(),N=Ie(N,s,rt,1,l,_e,it,me,Rt,Ct,null,mt),Dt())},i(l){if(!ee){he(R.$$.fragment,l),he(ae.$$.fragment,l),he(se.$$.fragment,l);for(let s=0;s<_e.length;s+=1)he(N[s]);ee=!0}},o(l){ye(R.$$.fragment,l),ye(ae.$$.fragment,l),ye(se.$$.fragment,l);for(let s=0;sd.name=="emailVisibility";function zt(d,e,t){let a,m,p,c,u,{collection:b}=e,O=200,T=[],M={};const D=S=>t(2,O=S.code);return d.$$set=S=>{"collection"in S&&t(0,b=S.collection)},d.$$.update=()=>{var S,E,q;d.$$.dirty&1&&t(1,a=(b==null?void 0:b.type)==="auth"),d.$$.dirty&1&&t(7,m=(b==null?void 0:b.updateRule)===null),d.$$.dirty&2&&t(8,p=a?["id","password","verified","email","emailVisibility"]:["id"]),d.$$.dirty&257&&t(6,c=((S=b==null?void 0:b.fields)==null?void 0:S.filter(H=>!H.hidden&&H.type!="autodate"&&!p.includes(H.name)))||[]),d.$$.dirty&1&&t(3,T=[{code:200,body:JSON.stringify(x.dummyCollectionRecord(b),null,2)},{code:400,body:` + { + "code": 400, + "message": "Failed to update record.", + "data": { + "${(q=(E=b==null?void 0:b.fields)==null?void 0:E[0])==null?void 0:q.name}": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `},{code:403,body:` + { + "code": 403, + "message": "You are not allowed to perform this request.", + "data": {} + } + `},{code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `}]),d.$$.dirty&2&&(a?t(4,M={password:"87654321",passwordConfirm:"87654321",oldPassword:"12345678"}):t(4,M={}))},t(5,u=x.getApiExampleUrl(Ht.baseURL)),[b,a,O,T,M,u,c,m,p,D]}class Gt extends Ot{constructor(e){super(),St(this,e,zt,Qt,Mt,{collection:0})}}export{Gt as default}; diff --git a/ui/dist/assets/UpdateApiDocs-CYknfZa_.js b/ui/dist/assets/UpdateApiDocs-CYknfZa_.js deleted file mode 100644 index 0cd7875c..00000000 --- a/ui/dist/assets/UpdateApiDocs-CYknfZa_.js +++ /dev/null @@ -1,90 +0,0 @@ -import{S as $t,i as Mt,s as qt,C as I,O as Z,N as Ot,e as r,v as b,b as f,c as he,f as v,g as i,h as s,m as ye,w as J,P as Ee,Q as _t,k as Ht,R as Rt,n as Dt,t as ce,a as pe,o as d,d as ke,A as Lt,q as ve,r as Pt,x as ee}from"./index-Bp3jGQ0J.js";import{S as Ft}from"./SdkTabs-DxNNd6Sw.js";import{F as At}from"./FieldsQueryParam-zDO3HzQv.js";function ht(c,e,t){const n=c.slice();return n[8]=e[t],n}function yt(c,e,t){const n=c.slice();return n[8]=e[t],n}function kt(c,e,t){const n=c.slice();return n[13]=e[t],n}function vt(c){let e;return{c(){e=r("p"),e.innerHTML=`Note that in case of a password change all previously issued tokens for the current record - will be automatically invalidated and if you want your user to remain signed in you need to - reauthenticate manually after the update call.`},m(t,n){i(t,e,n)},d(t){t&&d(e)}}}function gt(c){let e;return{c(){e=r("p"),e.innerHTML="Requires admin Authorization:TOKEN header",v(e,"class","txt-hint txt-sm txt-right")},m(t,n){i(t,e,n)},d(t){t&&d(e)}}}function wt(c){let e,t,n,u,m,o,p,h,w,S,g,$,P,E,M,U,F;return{c(){e=r("tr"),e.innerHTML='Auth fields',t=f(),n=r("tr"),n.innerHTML='
Optional username
String The username of the auth record.',u=f(),m=r("tr"),m.innerHTML=`
Optional email
String The auth record email address. -
- This field can be updated only by admins or auth records with "Manage" access. -
- Regular accounts can update their email by calling "Request email change".`,o=f(),p=r("tr"),p.innerHTML='
Optional emailVisibility
Boolean Whether to show/hide the auth record email when fetching the record data.',h=f(),w=r("tr"),w.innerHTML=`
Optional oldPassword
String Old auth record password. -
- This field is required only when changing the record password. Admins and auth records with - "Manage" access can skip this field.`,S=f(),g=r("tr"),g.innerHTML='
Optional password
String New auth record password.',$=f(),P=r("tr"),P.innerHTML='
Optional passwordConfirm
String New auth record password confirmation.',E=f(),M=r("tr"),M.innerHTML=`
Optional verified
Boolean Indicates whether the auth record is verified or not. -
- This field can be set only by admins or auth records with "Manage" access.`,U=f(),F=r("tr"),F.innerHTML='Schema fields'},m(y,_){i(y,e,_),i(y,t,_),i(y,n,_),i(y,u,_),i(y,m,_),i(y,o,_),i(y,p,_),i(y,h,_),i(y,w,_),i(y,S,_),i(y,g,_),i(y,$,_),i(y,P,_),i(y,E,_),i(y,M,_),i(y,U,_),i(y,F,_)},d(y){y&&(d(e),d(t),d(n),d(u),d(m),d(o),d(p),d(h),d(w),d(S),d(g),d($),d(P),d(E),d(M),d(U),d(F))}}}function Nt(c){let e;return{c(){e=r("span"),e.textContent="Optional",v(e,"class","label label-warning")},m(t,n){i(t,e,n)},d(t){t&&d(e)}}}function Bt(c){let e;return{c(){e=r("span"),e.textContent="Required",v(e,"class","label label-success")},m(t,n){i(t,e,n)},d(t){t&&d(e)}}}function jt(c){var m;let e,t=((m=c[13].options)==null?void 0:m.maxSelect)>1?"ids":"id",n,u;return{c(){e=b("User "),n=b(t),u=b(".")},m(o,p){i(o,e,p),i(o,n,p),i(o,u,p)},p(o,p){var h;p&1&&t!==(t=((h=o[13].options)==null?void 0:h.maxSelect)>1?"ids":"id")&&J(n,t)},d(o){o&&(d(e),d(n),d(u))}}}function Et(c){var m;let e,t=((m=c[13].options)==null?void 0:m.maxSelect)>1?"ids":"id",n,u;return{c(){e=b("Relation record "),n=b(t),u=b(".")},m(o,p){i(o,e,p),i(o,n,p),i(o,u,p)},p(o,p){var h;p&1&&t!==(t=((h=o[13].options)==null?void 0:h.maxSelect)>1?"ids":"id")&&J(n,t)},d(o){o&&(d(e),d(n),d(u))}}}function Ut(c){let e,t,n,u,m;return{c(){e=b("File object."),t=r("br"),n=b(` - Set to `),u=r("code"),u.textContent="null",m=b(" to delete already uploaded file(s).")},m(o,p){i(o,e,p),i(o,t,p),i(o,n,p),i(o,u,p),i(o,m,p)},p:ee,d(o){o&&(d(e),d(t),d(n),d(u),d(m))}}}function It(c){let e;return{c(){e=b("URL address.")},m(t,n){i(t,e,n)},p:ee,d(t){t&&d(e)}}}function Jt(c){let e;return{c(){e=b("Email address.")},m(t,n){i(t,e,n)},p:ee,d(t){t&&d(e)}}}function Vt(c){let e;return{c(){e=b("JSON array or object.")},m(t,n){i(t,e,n)},p:ee,d(t){t&&d(e)}}}function Qt(c){let e;return{c(){e=b("Number value.")},m(t,n){i(t,e,n)},p:ee,d(t){t&&d(e)}}}function xt(c){let e;return{c(){e=b("Plain text value.")},m(t,n){i(t,e,n)},p:ee,d(t){t&&d(e)}}}function Tt(c,e){let t,n,u,m,o,p=e[13].name+"",h,w,S,g,$=I.getFieldValueType(e[13])+"",P,E,M,U;function F(C,O){return C[13].required?Bt:Nt}let y=F(e),_=y(e);function A(C,O){if(C[13].type==="text")return xt;if(C[13].type==="number")return Qt;if(C[13].type==="json")return Vt;if(C[13].type==="email")return Jt;if(C[13].type==="url")return It;if(C[13].type==="file")return Ut;if(C[13].type==="relation")return Et;if(C[13].type==="user")return jt}let N=A(e),T=N&&N(e);return{key:c,first:null,c(){t=r("tr"),n=r("td"),u=r("div"),_.c(),m=f(),o=r("span"),h=b(p),w=f(),S=r("td"),g=r("span"),P=b($),E=f(),M=r("td"),T&&T.c(),U=f(),v(u,"class","inline-flex"),v(g,"class","label"),this.first=t},m(C,O){i(C,t,O),s(t,n),s(n,u),_.m(u,null),s(u,m),s(u,o),s(o,h),s(t,w),s(t,S),s(S,g),s(g,P),s(t,E),s(t,M),T&&T.m(M,null),s(t,U)},p(C,O){e=C,y!==(y=F(e))&&(_.d(1),_=y(e),_&&(_.c(),_.m(u,m))),O&1&&p!==(p=e[13].name+"")&&J(h,p),O&1&&$!==($=I.getFieldValueType(e[13])+"")&&J(P,$),N===(N=A(e))&&T?T.p(e,O):(T&&T.d(1),T=N&&N(e),T&&(T.c(),T.m(M,null)))},d(C){C&&d(t),_.d(),T&&T.d()}}}function Ct(c,e){let t,n=e[8].code+"",u,m,o,p;function h(){return e[7](e[8])}return{key:c,first:null,c(){t=r("button"),u=b(n),m=f(),v(t,"class","tab-item"),ve(t,"active",e[1]===e[8].code),this.first=t},m(w,S){i(w,t,S),s(t,u),s(t,m),o||(p=Pt(t,"click",h),o=!0)},p(w,S){e=w,S&4&&n!==(n=e[8].code+"")&&J(u,n),S&6&&ve(t,"active",e[1]===e[8].code)},d(w){w&&d(t),o=!1,p()}}}function St(c,e){let t,n,u,m;return n=new Ot({props:{content:e[8].body}}),{key:c,first:null,c(){t=r("div"),he(n.$$.fragment),u=f(),v(t,"class","tab-item"),ve(t,"active",e[1]===e[8].code),this.first=t},m(o,p){i(o,t,p),ye(n,t,null),s(t,u),m=!0},p(o,p){e=o;const h={};p&4&&(h.content=e[8].body),n.$set(h),(!m||p&6)&&ve(t,"active",e[1]===e[8].code)},i(o){m||(ce(n.$$.fragment,o),m=!0)},o(o){pe(n.$$.fragment,o),m=!1},d(o){o&&d(t),ke(n)}}}function zt(c){var ct,pt,ut;let e,t,n=c[0].name+"",u,m,o,p,h,w,S,g=c[0].name+"",$,P,E,M,U,F,y,_,A,N,T,C,O,ue,Ue,fe,Y,Ie,ge,me=c[0].name+"",we,Je,Te,Ve,Ce,te,Se,le,Oe,ne,$e,V,Me,Qe,Q,qe,B=[],xe=new Map,He,ae,Re,x,De,ze,se,z,Le,Ke,Pe,We,q,Ye,G,Ge,Xe,Ze,Fe,et,Ae,tt,Ne,lt,nt,X,Be,ie,je,K,de,j=[],at=new Map,st,oe,H=[],it=new Map,W,R=c[6]&&vt();A=new Ft({props:{js:` -import PocketBase from 'pocketbase'; - -const pb = new PocketBase('${c[4]}'); - -... - -// example update data -const data = ${JSON.stringify(Object.assign({},c[3],I.dummyCollectionSchemaData(c[0])),null,4)}; - -const record = await pb.collection('${(ct=c[0])==null?void 0:ct.name}').update('RECORD_ID', data); - `,dart:` -import 'package:pocketbase/pocketbase.dart'; - -final pb = PocketBase('${c[4]}'); - -... - -// example update body -final body = ${JSON.stringify(Object.assign({},c[3],I.dummyCollectionSchemaData(c[0])),null,2)}; - -final record = await pb.collection('${(pt=c[0])==null?void 0:pt.name}').update('RECORD_ID', body: body); - `}});let D=c[5]&>(),L=c[6]&&wt(),be=Z((ut=c[0])==null?void 0:ut.schema);const dt=l=>l[13].name;for(let l=0;ll[8].code;for(let l=0;l<_e.length;l+=1){let a=yt(c,_e,l),k=ot(a);at.set(k,j[l]=Ct(k,a))}let re=Z(c[2]);const rt=l=>l[8].code;for(let l=0;lapplication/json or - multipart/form-data.`,U=f(),F=r("p"),F.innerHTML=`File upload is supported only via multipart/form-data. -
- For more info and examples you could check the detailed - Files upload and handling docs - .`,y=f(),R&&R.c(),_=f(),he(A.$$.fragment),N=f(),T=r("h6"),T.textContent="API details",C=f(),O=r("div"),ue=r("strong"),ue.textContent="PATCH",Ue=f(),fe=r("div"),Y=r("p"),Ie=b("/api/collections/"),ge=r("strong"),we=b(me),Je=b("/records/"),Te=r("strong"),Te.textContent=":id",Ve=f(),D&&D.c(),Ce=f(),te=r("div"),te.textContent="Path parameters",Se=f(),le=r("table"),le.innerHTML='Param Type Description id String ID of the record to update.',Oe=f(),ne=r("div"),ne.textContent="Body Parameters",$e=f(),V=r("table"),Me=r("thead"),Me.innerHTML='Param Type Description',Qe=f(),Q=r("tbody"),L&&L.c(),qe=f();for(let l=0;lParam Type Description',ze=f(),se=r("tbody"),z=r("tr"),Le=r("td"),Le.textContent="expand",Ke=f(),Pe=r("td"),Pe.innerHTML='String',We=f(),q=r("td"),Ye=b(`Auto expand relations when returning the updated record. Ex.: - `),he(G.$$.fragment),Ge=b(` - Supports up to 6-levels depth nested relations expansion. `),Xe=r("br"),Ze=b(` - The expanded relations will be appended to the record under the - `),Fe=r("code"),Fe.textContent="expand",et=b(" property (eg. "),Ae=r("code"),Ae.textContent='"expand": {"relField1": {...}, ...}',tt=b(`). Only - the relations that the user has permissions to `),Ne=r("strong"),Ne.textContent="view",lt=b(" will be expanded."),nt=f(),he(X.$$.fragment),Be=f(),ie=r("div"),ie.textContent="Responses",je=f(),K=r("div"),de=r("div");for(let l=0;l${JSON.stringify(Object.assign({},l[3],I.dummyCollectionSchemaData(l[0])),null,2)}; - -final record = await pb.collection('${(mt=l[0])==null?void 0:mt.name}').update('RECORD_ID', body: body); - `),A.$set(k),(!W||a&1)&&me!==(me=l[0].name+"")&&J(we,me),l[5]?D||(D=gt(),D.c(),D.m(O,null)):D&&(D.d(1),D=null),l[6]?L||(L=wt(),L.c(),L.m(Q,qe)):L&&(L.d(1),L=null),a&1&&(be=Z((bt=l[0])==null?void 0:bt.schema),B=Ee(B,a,dt,1,l,be,xe,Q,_t,Tt,null,kt)),a&6&&(_e=Z(l[2]),j=Ee(j,a,ot,1,l,_e,at,de,_t,Ct,null,yt)),a&6&&(re=Z(l[2]),Ht(),H=Ee(H,a,rt,1,l,re,it,oe,Rt,St,null,ht),Dt())},i(l){if(!W){ce(A.$$.fragment,l),ce(G.$$.fragment,l),ce(X.$$.fragment,l);for(let a=0;at(1,p=g.code);return c.$$set=g=>{"collection"in g&&t(0,o=g.collection)},c.$$.update=()=>{var g,$;c.$$.dirty&1&&t(6,n=(o==null?void 0:o.type)==="auth"),c.$$.dirty&1&&t(5,u=(o==null?void 0:o.updateRule)===null),c.$$.dirty&1&&t(2,h=[{code:200,body:JSON.stringify(I.dummyCollectionRecord(o),null,2)},{code:400,body:` - { - "code": 400, - "message": "Failed to update record.", - "data": { - "${($=(g=o==null?void 0:o.schema)==null?void 0:g[0])==null?void 0:$.name}": { - "code": "validation_required", - "message": "Missing required value." - } - } - } - `},{code:403,body:` - { - "code": 403, - "message": "You are not allowed to perform this request.", - "data": {} - } - `},{code:404,body:` - { - "code": 404, - "message": "The requested resource wasn't found.", - "data": {} - } - `}]),c.$$.dirty&1&&(o.type==="auth"?t(3,w={username:"test_username_update",emailVisibility:!1,password:"87654321",passwordConfirm:"87654321",oldPassword:"12345678"}):t(3,w={}))},t(4,m=I.getApiExampleUrl(Lt.baseUrl)),[o,p,h,w,m,u,n,S]}class Xt extends $t{constructor(e){super(),Mt(this,e,Kt,zt,qt,{collection:0})}}export{Xt as default}; diff --git a/ui/dist/assets/VerificationDocs-BIWtoqhd.js b/ui/dist/assets/VerificationDocs-BIWtoqhd.js new file mode 100644 index 00000000..27b1c54d --- /dev/null +++ b/ui/dist/assets/VerificationDocs-BIWtoqhd.js @@ -0,0 +1,79 @@ +import{S as le,i as ne,s as ie,T as D,e as m,b as T,w as M,f as v,g as b,h as u,x as Y,U as x,V as ye,k as ee,W as Ce,n as te,t as L,a as j,o as h,r as K,u as oe,R as qe,c as G,m as J,d as Z,Q as Ve,X as fe,C as Ie,p as Pe,Y as ue}from"./index-B-F-pko3.js";function de(s,t,e){const o=s.slice();return o[4]=t[e],o}function me(s,t,e){const o=s.slice();return o[4]=t[e],o}function pe(s,t){let e,o=t[4].code+"",f,c,r,a;function d(){return t[3](t[4])}return{key:s,first:null,c(){e=m("button"),f=M(o),c=T(),v(e,"class","tab-item"),K(e,"active",t[1]===t[4].code),this.first=e},m(g,C){b(g,e,C),u(e,f),u(e,c),r||(a=oe(e,"click",d),r=!0)},p(g,C){t=g,C&4&&o!==(o=t[4].code+"")&&Y(f,o),C&6&&K(e,"active",t[1]===t[4].code)},d(g){g&&h(e),r=!1,a()}}}function _e(s,t){let e,o,f,c;return o=new qe({props:{content:t[4].body}}),{key:s,first:null,c(){e=m("div"),G(o.$$.fragment),f=T(),v(e,"class","tab-item"),K(e,"active",t[1]===t[4].code),this.first=e},m(r,a){b(r,e,a),J(o,e,null),u(e,f),c=!0},p(r,a){t=r;const d={};a&4&&(d.content=t[4].body),o.$set(d),(!c||a&6)&&K(e,"active",t[1]===t[4].code)},i(r){c||(L(o.$$.fragment,r),c=!0)},o(r){j(o.$$.fragment,r),c=!1},d(r){r&&h(e),Z(o)}}}function Re(s){let t,e,o,f,c,r,a,d=s[0].name+"",g,C,F,R,H,A,B,O,N,q,V,$=[],Q=new Map,U,P,p=[],y=new Map,I,_=D(s[2]);const X=l=>l[4].code;for(let l=0;l<_.length;l+=1){let i=me(s,_,l),n=X(i);Q.set(n,$[l]=pe(n,i))}let E=D(s[2]);const W=l=>l[4].code;for(let l=0;lParam Type Description
Required token
String The token from the verification request email.',B=T(),O=m("div"),O.textContent="Responses",N=T(),q=m("div"),V=m("div");for(let l=0;l<$.length;l+=1)$[l].c();U=T(),P=m("div");for(let l=0;le(1,f=a.code);return s.$$set=a=>{"collection"in a&&e(0,o=a.collection)},e(2,c=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "token": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}]),[o,f,c,r]}class Be extends le{constructor(t){super(),ne(this,t,Ae,Re,ie,{collection:0})}}function be(s,t,e){const o=s.slice();return o[4]=t[e],o}function he(s,t,e){const o=s.slice();return o[4]=t[e],o}function ve(s,t){let e,o=t[4].code+"",f,c,r,a;function d(){return t[3](t[4])}return{key:s,first:null,c(){e=m("button"),f=M(o),c=T(),v(e,"class","tab-item"),K(e,"active",t[1]===t[4].code),this.first=e},m(g,C){b(g,e,C),u(e,f),u(e,c),r||(a=oe(e,"click",d),r=!0)},p(g,C){t=g,C&4&&o!==(o=t[4].code+"")&&Y(f,o),C&6&&K(e,"active",t[1]===t[4].code)},d(g){g&&h(e),r=!1,a()}}}function ge(s,t){let e,o,f,c;return o=new qe({props:{content:t[4].body}}),{key:s,first:null,c(){e=m("div"),G(o.$$.fragment),f=T(),v(e,"class","tab-item"),K(e,"active",t[1]===t[4].code),this.first=e},m(r,a){b(r,e,a),J(o,e,null),u(e,f),c=!0},p(r,a){t=r;const d={};a&4&&(d.content=t[4].body),o.$set(d),(!c||a&6)&&K(e,"active",t[1]===t[4].code)},i(r){c||(L(o.$$.fragment,r),c=!0)},o(r){j(o.$$.fragment,r),c=!1},d(r){r&&h(e),Z(o)}}}function Oe(s){let t,e,o,f,c,r,a,d=s[0].name+"",g,C,F,R,H,A,B,O,N,q,V,$=[],Q=new Map,U,P,p=[],y=new Map,I,_=D(s[2]);const X=l=>l[4].code;for(let l=0;l<_.length;l+=1){let i=he(s,_,l),n=X(i);Q.set(n,$[l]=ve(n,i))}let E=D(s[2]);const W=l=>l[4].code;for(let l=0;lParam Type Description
Required email
String The auth record email address to send the verification request (if exists).',B=T(),O=m("div"),O.textContent="Responses",N=T(),q=m("div"),V=m("div");for(let l=0;l<$.length;l+=1)$[l].c();U=T(),P=m("div");for(let l=0;le(1,f=a.code);return s.$$set=a=>{"collection"in a&&e(0,o=a.collection)},e(2,c=[{code:204,body:"null"},{code:400,body:` + { + "code": 400, + "message": "Failed to authenticate.", + "data": { + "email": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `}]),[o,f,c,r]}class Me extends le{constructor(t){super(),ne(this,t,Ee,Oe,ie,{collection:0})}}function ke(s,t,e){const o=s.slice();return o[5]=t[e],o[7]=e,o}function $e(s,t,e){const o=s.slice();return o[5]=t[e],o[7]=e,o}function we(s){let t,e,o,f,c;function r(){return s[4](s[7])}return{c(){t=m("button"),e=m("div"),e.textContent=`${s[5].title}`,o=T(),v(e,"class","txt"),v(t,"class","tab-item"),K(t,"active",s[1]==s[7])},m(a,d){b(a,t,d),u(t,e),u(t,o),f||(c=oe(t,"click",r),f=!0)},p(a,d){s=a,d&2&&K(t,"active",s[1]==s[7])},d(a){a&&h(t),f=!1,c()}}}function Te(s){let t,e,o,f;var c=s[5].component;function r(a,d){return{props:{collection:a[0]}}}return c&&(e=ue(c,r(s))),{c(){t=m("div"),e&&G(e.$$.fragment),o=T(),v(t,"class","tab-item"),K(t,"active",s[1]==s[7])},m(a,d){b(a,t,d),e&&J(e,t,null),u(t,o),f=!0},p(a,d){if(c!==(c=a[5].component)){if(e){ee();const g=e;j(g.$$.fragment,1,0,()=>{Z(g,1)}),te()}c?(e=ue(c,r(a)),G(e.$$.fragment),L(e.$$.fragment,1),J(e,t,o)):e=null}else if(c){const g={};d&1&&(g.collection=a[0]),e.$set(g)}(!f||d&2)&&K(t,"active",a[1]==a[7])},i(a){f||(e&&L(e.$$.fragment,a),f=!0)},o(a){e&&j(e.$$.fragment,a),f=!1},d(a){a&&h(t),e&&Z(e)}}}function Ne(s){var E,W,l,i;let t,e,o=s[0].name+"",f,c,r,a,d,g,C,F=s[0].name+"",R,H,A,B,O,N,q,V,$,Q,U,P;B=new Ve({props:{js:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${s[2]}'); + + ... + + await pb.collection('${(E=s[0])==null?void 0:E.name}').requestVerification('test@example.com'); + + // --- + // (optional) in your custom confirmation page: + // --- + + await pb.collection('${(W=s[0])==null?void 0:W.name}').confirmVerification('VERIFICATION_TOKEN'); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${s[2]}'); + + ... + + await pb.collection('${(l=s[0])==null?void 0:l.name}').requestVerification('test@example.com'); + + // --- + // (optional) in your custom confirmation page: + // --- + + await pb.collection('${(i=s[0])==null?void 0:i.name}').confirmVerification('VERIFICATION_TOKEN'); + `}});let p=D(s[3]),y=[];for(let n=0;nj(_[n],1,1,()=>{_[n]=null});return{c(){t=m("h3"),e=M("Account verification ("),f=M(o),c=M(")"),r=T(),a=m("div"),d=m("p"),g=M("Sends "),C=m("strong"),R=M(F),H=M(" account verification request."),A=T(),G(B.$$.fragment),O=T(),N=m("h6"),N.textContent="API details",q=T(),V=m("div"),$=m("div");for(let n=0;ne(1,r=d);return s.$$set=d=>{"collection"in d&&e(0,f=d.collection)},e(2,o=Ie.getApiExampleUrl(Pe.baseURL)),[f,r,o,c,a]}class De extends le{constructor(t){super(),ne(this,t,Se,Ne,ie,{collection:0})}}export{De as default}; diff --git a/ui/dist/assets/ViewApiDocs-B6MdbOQi.js b/ui/dist/assets/ViewApiDocs-B6MdbOQi.js new file mode 100644 index 00000000..077a56ed --- /dev/null +++ b/ui/dist/assets/ViewApiDocs-B6MdbOQi.js @@ -0,0 +1,59 @@ +import{S as lt,i as st,s as nt,Q as ot,R as tt,T as K,e as o,w as _,b,c as W,f as m,g as r,h as l,m as X,x as ve,U as Je,V as at,k as it,W as rt,n as dt,t as Q,a as V,o as d,d as Y,C as Ke,p as ct,r as Z,u as pt}from"./index-B-F-pko3.js";import{F as ut}from"./FieldsQueryParam-CW6KZfgu.js";function We(a,s,n){const i=a.slice();return i[6]=s[n],i}function Xe(a,s,n){const i=a.slice();return i[6]=s[n],i}function Ye(a){let s;return{c(){s=o("p"),s.innerHTML="Requires superuser Authorization:TOKEN header",m(s,"class","txt-hint txt-sm txt-right")},m(n,i){r(n,s,i)},d(n){n&&d(s)}}}function Ze(a,s){let n,i,v;function p(){return s[5](s[6])}return{key:a,first:null,c(){n=o("button"),n.textContent=`${s[6].code} `,m(n,"class","tab-item"),Z(n,"active",s[2]===s[6].code),this.first=n},m(c,f){r(c,n,f),i||(v=pt(n,"click",p),i=!0)},p(c,f){s=c,f&20&&Z(n,"active",s[2]===s[6].code)},d(c){c&&d(n),i=!1,v()}}}function et(a,s){let n,i,v,p;return i=new tt({props:{content:s[6].body}}),{key:a,first:null,c(){n=o("div"),W(i.$$.fragment),v=b(),m(n,"class","tab-item"),Z(n,"active",s[2]===s[6].code),this.first=n},m(c,f){r(c,n,f),X(i,n,null),l(n,v),p=!0},p(c,f){s=c,(!p||f&20)&&Z(n,"active",s[2]===s[6].code)},i(c){p||(Q(i.$$.fragment,c),p=!0)},o(c){V(i.$$.fragment,c),p=!1},d(c){c&&d(n),Y(i)}}}function ft(a){var je,Ne;let s,n,i=a[0].name+"",v,p,c,f,w,C,ee,j=a[0].name+"",te,$e,le,F,se,B,ne,$,N,ye,z,P,we,oe,G=a[0].name+"",ae,Ce,ie,Fe,re,S,de,A,ce,I,pe,R,ue,Re,M,O,fe,Oe,be,Te,h,De,E,Pe,Ee,xe,me,Be,_e,Se,Ae,Ie,he,Me,qe,x,ke,q,ge,T,H,y=[],He=new Map,Le,L,k=[],Ue=new Map,D;F=new ot({props:{js:` + import PocketBase from 'pocketbase'; + + const pb = new PocketBase('${a[3]}'); + + ... + + const record = await pb.collection('${(je=a[0])==null?void 0:je.name}').getOne('RECORD_ID', { + expand: 'relField1,relField2.subRelField', + }); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final pb = PocketBase('${a[3]}'); + + ... + + final record = await pb.collection('${(Ne=a[0])==null?void 0:Ne.name}').getOne('RECORD_ID', + expand: 'relField1,relField2.subRelField', + ); + `}});let g=a[1]&&Ye();E=new tt({props:{content:"?expand=relField1,relField2.subRelField"}}),x=new ut({});let J=K(a[4]);const Qe=e=>e[6].code;for(let e=0;ee[6].code;for(let e=0;eParam Type Description id String ID of the record to view.',ce=b(),I=o("div"),I.textContent="Query parameters",pe=b(),R=o("table"),ue=o("thead"),ue.innerHTML='Param Type Description',Re=b(),M=o("tbody"),O=o("tr"),fe=o("td"),fe.textContent="expand",Oe=b(),be=o("td"),be.innerHTML='String',Te=b(),h=o("td"),De=_(`Auto expand record relations. Ex.: + `),W(E.$$.fragment),Pe=_(` + Supports up to 6-levels depth nested relations expansion. `),Ee=o("br"),xe=_(` + The expanded relations will be appended to the record under the + `),me=o("code"),me.textContent="expand",Be=_(" property (eg. "),_e=o("code"),_e.textContent='"expand": {"relField1": {...}, ...}',Se=_(`). + `),Ae=o("br"),Ie=_(` + Only the relations to which the request user has permissions to `),he=o("strong"),he.textContent="view",Me=_(" will be expanded."),qe=b(),W(x.$$.fragment),ke=b(),q=o("div"),q.textContent="Responses",ge=b(),T=o("div"),H=o("div");for(let e=0;en(2,c=C.code);return a.$$set=C=>{"collection"in C&&n(0,p=C.collection)},a.$$.update=()=>{a.$$.dirty&1&&n(1,i=(p==null?void 0:p.viewRule)===null),a.$$.dirty&3&&p!=null&&p.id&&(f.push({code:200,body:JSON.stringify(Ke.dummyCollectionRecord(p),null,2)}),i&&f.push({code:403,body:` + { + "code": 403, + "message": "Only superusers can access this action.", + "data": {} + } + `}),f.push({code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `}))},n(3,v=Ke.getApiExampleUrl(ct.baseURL)),[p,i,c,v,f,w]}class ht extends lt{constructor(s){super(),st(this,s,bt,ft,nt,{collection:0})}}export{ht as default}; diff --git a/ui/dist/assets/ViewApiDocs-D09kZD3M.js b/ui/dist/assets/ViewApiDocs-D09kZD3M.js deleted file mode 100644 index c1fa0810..00000000 --- a/ui/dist/assets/ViewApiDocs-D09kZD3M.js +++ /dev/null @@ -1,59 +0,0 @@ -import{S as lt,i as nt,s as st,N as tt,O as K,e as o,v as _,b as m,c as W,f as b,g as r,h as l,m as X,w as ve,P as Je,Q as ot,k as at,R as it,n as rt,t as Q,a as U,o as d,d as Y,C as Ke,A as dt,q as Z,r as ct}from"./index-Bp3jGQ0J.js";import{S as pt}from"./SdkTabs-DxNNd6Sw.js";import{F as ut}from"./FieldsQueryParam-zDO3HzQv.js";function We(a,n,s){const i=a.slice();return i[6]=n[s],i}function Xe(a,n,s){const i=a.slice();return i[6]=n[s],i}function Ye(a){let n;return{c(){n=o("p"),n.innerHTML="Requires admin Authorization:TOKEN header",b(n,"class","txt-hint txt-sm txt-right")},m(s,i){r(s,n,i)},d(s){s&&d(n)}}}function Ze(a,n){let s,i,v;function p(){return n[5](n[6])}return{key:a,first:null,c(){s=o("button"),s.textContent=`${n[6].code} `,b(s,"class","tab-item"),Z(s,"active",n[2]===n[6].code),this.first=s},m(c,f){r(c,s,f),i||(v=ct(s,"click",p),i=!0)},p(c,f){n=c,f&20&&Z(s,"active",n[2]===n[6].code)},d(c){c&&d(s),i=!1,v()}}}function et(a,n){let s,i,v,p;return i=new tt({props:{content:n[6].body}}),{key:a,first:null,c(){s=o("div"),W(i.$$.fragment),v=m(),b(s,"class","tab-item"),Z(s,"active",n[2]===n[6].code),this.first=s},m(c,f){r(c,s,f),X(i,s,null),l(s,v),p=!0},p(c,f){n=c,(!p||f&20)&&Z(s,"active",n[2]===n[6].code)},i(c){p||(Q(i.$$.fragment,c),p=!0)},o(c){U(i.$$.fragment,c),p=!1},d(c){c&&d(s),Y(i)}}}function ft(a){var je,Ve;let n,s,i=a[0].name+"",v,p,c,f,w,C,ee,j=a[0].name+"",te,$e,le,F,ne,S,se,$,V,ye,z,T,we,oe,G=a[0].name+"",ae,Ce,ie,Fe,re,B,de,q,ce,x,pe,R,ue,Re,I,O,fe,Oe,me,Pe,h,De,A,Te,Ae,Ee,be,Se,_e,Be,qe,xe,he,Ie,Me,E,ke,M,ge,P,H,y=[],He=new Map,Le,L,k=[],Ne=new Map,D;F=new pt({props:{js:` - import PocketBase from 'pocketbase'; - - const pb = new PocketBase('${a[3]}'); - - ... - - const record = await pb.collection('${(je=a[0])==null?void 0:je.name}').getOne('RECORD_ID', { - expand: 'relField1,relField2.subRelField', - }); - `,dart:` - import 'package:pocketbase/pocketbase.dart'; - - final pb = PocketBase('${a[3]}'); - - ... - - final record = await pb.collection('${(Ve=a[0])==null?void 0:Ve.name}').getOne('RECORD_ID', - expand: 'relField1,relField2.subRelField', - ); - `}});let g=a[1]&&Ye();A=new tt({props:{content:"?expand=relField1,relField2.subRelField"}}),E=new ut({});let J=K(a[4]);const Qe=e=>e[6].code;for(let e=0;ee[6].code;for(let e=0;eParam Type Description id String ID of the record to view.',ce=m(),x=o("div"),x.textContent="Query parameters",pe=m(),R=o("table"),ue=o("thead"),ue.innerHTML='Param Type Description',Re=m(),I=o("tbody"),O=o("tr"),fe=o("td"),fe.textContent="expand",Oe=m(),me=o("td"),me.innerHTML='String',Pe=m(),h=o("td"),De=_(`Auto expand record relations. Ex.: - `),W(A.$$.fragment),Te=_(` - Supports up to 6-levels depth nested relations expansion. `),Ae=o("br"),Ee=_(` - The expanded relations will be appended to the record under the - `),be=o("code"),be.textContent="expand",Se=_(" property (eg. "),_e=o("code"),_e.textContent='"expand": {"relField1": {...}, ...}',Be=_(`). - `),qe=o("br"),xe=_(` - Only the relations to which the request user has permissions to `),he=o("strong"),he.textContent="view",Ie=_(" will be expanded."),Me=m(),W(E.$$.fragment),ke=m(),M=o("div"),M.textContent="Responses",ge=m(),P=o("div"),H=o("div");for(let e=0;es(2,c=C.code);return a.$$set=C=>{"collection"in C&&s(0,p=C.collection)},a.$$.update=()=>{a.$$.dirty&1&&s(1,i=(p==null?void 0:p.viewRule)===null),a.$$.dirty&3&&p!=null&&p.id&&(f.push({code:200,body:JSON.stringify(Ke.dummyCollectionRecord(p),null,2)}),i&&f.push({code:403,body:` - { - "code": 403, - "message": "Only admins can access this action.", - "data": {} - } - `}),f.push({code:404,body:` - { - "code": 404, - "message": "The requested resource wasn't found.", - "data": {} - } - `}))},s(3,v=Ke.getApiExampleUrl(dt.baseUrl)),[p,i,c,v,f,w]}class kt extends lt{constructor(n){super(),nt(this,n,mt,ft,st,{collection:0})}}export{kt as default}; diff --git a/ui/dist/assets/autocomplete.worker-Br7MPIGR.js b/ui/dist/assets/autocomplete.worker-Br7MPIGR.js new file mode 100644 index 00000000..3bdb97c5 --- /dev/null +++ b/ui/dist/assets/autocomplete.worker-Br7MPIGR.js @@ -0,0 +1,4 @@ +(function(){"use strict";class Y extends Error{}class zn extends Y{constructor(e){super(`Invalid DateTime: ${e.toMessage()}`)}}class Pn extends Y{constructor(e){super(`Invalid Interval: ${e.toMessage()}`)}}class Yn extends Y{constructor(e){super(`Invalid Duration: ${e.toMessage()}`)}}class j extends Y{}class lt extends Y{constructor(e){super(`Invalid unit ${e}`)}}class x extends Y{}class U extends Y{constructor(){super("Zone is an abstract class")}}const c="numeric",W="short",v="long",Se={year:c,month:c,day:c},ct={year:c,month:W,day:c},Jn={year:c,month:W,day:c,weekday:W},ft={year:c,month:v,day:c},dt={year:c,month:v,day:c,weekday:v},ht={hour:c,minute:c},mt={hour:c,minute:c,second:c},yt={hour:c,minute:c,second:c,timeZoneName:W},gt={hour:c,minute:c,second:c,timeZoneName:v},pt={hour:c,minute:c,hourCycle:"h23"},wt={hour:c,minute:c,second:c,hourCycle:"h23"},St={hour:c,minute:c,second:c,hourCycle:"h23",timeZoneName:W},kt={hour:c,minute:c,second:c,hourCycle:"h23",timeZoneName:v},Tt={year:c,month:c,day:c,hour:c,minute:c},Ot={year:c,month:c,day:c,hour:c,minute:c,second:c},Et={year:c,month:W,day:c,hour:c,minute:c},Nt={year:c,month:W,day:c,hour:c,minute:c,second:c},Bn={year:c,month:W,day:c,weekday:W,hour:c,minute:c},xt={year:c,month:v,day:c,hour:c,minute:c,timeZoneName:W},bt={year:c,month:v,day:c,hour:c,minute:c,second:c,timeZoneName:W},It={year:c,month:v,day:c,weekday:v,hour:c,minute:c,timeZoneName:v},vt={year:c,month:v,day:c,weekday:v,hour:c,minute:c,second:c,timeZoneName:v};class oe{get type(){throw new U}get name(){throw new U}get ianaName(){return this.name}get isUniversal(){throw new U}offsetName(e,t){throw new U}formatOffset(e,t){throw new U}offset(e){throw new U}equals(e){throw new U}get isValid(){throw new U}}let $e=null;class ke extends oe{static get instance(){return $e===null&&($e=new ke),$e}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(e,{format:t,locale:n}){return Xt(e,t,n)}formatOffset(e,t){return fe(this.offset(e),t)}offset(e){return-new Date(e).getTimezoneOffset()}equals(e){return e.type==="system"}get isValid(){return!0}}let Te={};function Gn(r){return Te[r]||(Te[r]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:r,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),Te[r]}const jn={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function Kn(r,e){const t=r.format(e).replace(/\u200E/g,""),n=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(t),[,s,i,a,o,u,l,f]=n;return[a,s,i,o,u,l,f]}function Hn(r,e){const t=r.formatToParts(e),n=[];for(let s=0;s=0?E:1e3+E,(k-m)/(60*1e3)}equals(e){return e.type==="iana"&&e.name===this.name}get isValid(){return this.valid}}let Mt={};function _n(r,e={}){const t=JSON.stringify([r,e]);let n=Mt[t];return n||(n=new Intl.ListFormat(r,e),Mt[t]=n),n}let Ue={};function Ze(r,e={}){const t=JSON.stringify([r,e]);let n=Ue[t];return n||(n=new Intl.DateTimeFormat(r,e),Ue[t]=n),n}let qe={};function Qn(r,e={}){const t=JSON.stringify([r,e]);let n=qe[t];return n||(n=new Intl.NumberFormat(r,e),qe[t]=n),n}let ze={};function Xn(r,e={}){const{base:t,...n}=e,s=JSON.stringify([r,n]);let i=ze[s];return i||(i=new Intl.RelativeTimeFormat(r,e),ze[s]=i),i}let ue=null;function er(){return ue||(ue=new Intl.DateTimeFormat().resolvedOptions().locale,ue)}let Dt={};function tr(r){let e=Dt[r];if(!e){const t=new Intl.Locale(r);e="getWeekInfo"in t?t.getWeekInfo():t.weekInfo,Dt[r]=e}return e}function nr(r){const e=r.indexOf("-x-");e!==-1&&(r=r.substring(0,e));const t=r.indexOf("-u-");if(t===-1)return[r];{let n,s;try{n=Ze(r).resolvedOptions(),s=r}catch{const u=r.substring(0,t);n=Ze(u).resolvedOptions(),s=u}const{numberingSystem:i,calendar:a}=n;return[s,i,a]}}function rr(r,e,t){return(t||e)&&(r.includes("-u-")||(r+="-u"),t&&(r+=`-ca-${t}`),e&&(r+=`-nu-${e}`)),r}function sr(r){const e=[];for(let t=1;t<=12;t++){const n=y.utc(2009,t,1);e.push(r(n))}return e}function ir(r){const e=[];for(let t=1;t<=7;t++){const n=y.utc(2016,11,13+t);e.push(r(n))}return e}function Ee(r,e,t,n){const s=r.listingMode();return s==="error"?null:s==="en"?t(e):n(e)}function ar(r){return r.numberingSystem&&r.numberingSystem!=="latn"?!1:r.numberingSystem==="latn"||!r.locale||r.locale.startsWith("en")||new Intl.DateTimeFormat(r.intl).resolvedOptions().numberingSystem==="latn"}class or{constructor(e,t,n){this.padTo=n.padTo||0,this.floor=n.floor||!1;const{padTo:s,floor:i,...a}=n;if(!t||Object.keys(a).length>0){const o={useGrouping:!1,...n};n.padTo>0&&(o.minimumIntegerDigits=n.padTo),this.inf=Qn(e,o)}}format(e){if(this.inf){const t=this.floor?Math.floor(e):e;return this.inf.format(t)}else{const t=this.floor?Math.floor(e):He(e,3);return N(t,this.padTo)}}}class ur{constructor(e,t,n){this.opts=n,this.originalZone=void 0;let s;if(this.opts.timeZone)this.dt=e;else if(e.zone.type==="fixed"){const a=-1*(e.offset/60),o=a>=0?`Etc/GMT+${a}`:`Etc/GMT${a}`;e.offset!==0&&$.create(o).valid?(s=o,this.dt=e):(s="UTC",this.dt=e.offset===0?e:e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone)}else e.zone.type==="system"?this.dt=e:e.zone.type==="iana"?(this.dt=e,s=e.zone.name):(s="UTC",this.dt=e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone);const i={...this.opts};i.timeZone=i.timeZone||s,this.dtf=Ze(t,i)}format(){return this.originalZone?this.formatToParts().map(({value:e})=>e).join(""):this.dtf.format(this.dt.toJSDate())}formatToParts(){const e=this.dtf.formatToParts(this.dt.toJSDate());return this.originalZone?e.map(t=>{if(t.type==="timeZoneName"){const n=this.originalZone.offsetName(this.dt.ts,{locale:this.dt.locale,format:this.opts.timeZoneName});return{...t,value:n}}else return t}):e}resolvedOptions(){return this.dtf.resolvedOptions()}}class lr{constructor(e,t,n){this.opts={style:"long",...n},!t&&Kt()&&(this.rtf=Xn(e,n))}format(e,t){return this.rtf?this.rtf.format(e,t):Vr(t,e,this.opts.numeric,this.opts.style!=="long")}formatToParts(e,t){return this.rtf?this.rtf.formatToParts(e,t):[]}}const cr={firstDay:1,minimalDays:4,weekend:[6,7]};class S{static fromOpts(e){return S.create(e.locale,e.numberingSystem,e.outputCalendar,e.weekSettings,e.defaultToEN)}static create(e,t,n,s,i=!1){const a=e||T.defaultLocale,o=a||(i?"en-US":er()),u=t||T.defaultNumberingSystem,l=n||T.defaultOutputCalendar,f=je(s)||T.defaultWeekSettings;return new S(o,u,l,f,a)}static resetCache(){ue=null,Ue={},qe={},ze={}}static fromObject({locale:e,numberingSystem:t,outputCalendar:n,weekSettings:s}={}){return S.create(e,t,n,s)}constructor(e,t,n,s,i){const[a,o,u]=nr(e);this.locale=a,this.numberingSystem=t||o||null,this.outputCalendar=n||u||null,this.weekSettings=s,this.intl=rr(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=i,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=ar(this)),this.fastNumbersCached}listingMode(){const e=this.isEnglish(),t=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return e&&t?"en":"intl"}clone(e){return!e||Object.getOwnPropertyNames(e).length===0?this:S.create(e.locale||this.specifiedLocale,e.numberingSystem||this.numberingSystem,e.outputCalendar||this.outputCalendar,je(e.weekSettings)||this.weekSettings,e.defaultToEN||!1)}redefaultToEN(e={}){return this.clone({...e,defaultToEN:!0})}redefaultToSystem(e={}){return this.clone({...e,defaultToEN:!1})}months(e,t=!1){return Ee(this,e,nn,()=>{const n=t?{month:e,day:"numeric"}:{month:e},s=t?"format":"standalone";return this.monthsCache[s][e]||(this.monthsCache[s][e]=sr(i=>this.extract(i,n,"month"))),this.monthsCache[s][e]})}weekdays(e,t=!1){return Ee(this,e,an,()=>{const n=t?{weekday:e,year:"numeric",month:"long",day:"numeric"}:{weekday:e},s=t?"format":"standalone";return this.weekdaysCache[s][e]||(this.weekdaysCache[s][e]=ir(i=>this.extract(i,n,"weekday"))),this.weekdaysCache[s][e]})}meridiems(){return Ee(this,void 0,()=>on,()=>{if(!this.meridiemCache){const e={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[y.utc(2016,11,13,9),y.utc(2016,11,13,19)].map(t=>this.extract(t,e,"dayperiod"))}return this.meridiemCache})}eras(e){return Ee(this,e,un,()=>{const t={era:e};return this.eraCache[e]||(this.eraCache[e]=[y.utc(-40,1,1),y.utc(2017,1,1)].map(n=>this.extract(n,t,"era"))),this.eraCache[e]})}extract(e,t,n){const s=this.dtFormatter(e,t),i=s.formatToParts(),a=i.find(o=>o.type.toLowerCase()===n);return a?a.value:null}numberFormatter(e={}){return new or(this.intl,e.forceSimple||this.fastNumbers,e)}dtFormatter(e,t={}){return new ur(e,this.intl,t)}relFormatter(e={}){return new lr(this.intl,this.isEnglish(),e)}listFormatter(e={}){return _n(this.intl,e)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")}getWeekSettings(){return this.weekSettings?this.weekSettings:Ht()?tr(this.locale):cr}getStartOfWeek(){return this.getWeekSettings().firstDay}getMinDaysInFirstWeek(){return this.getWeekSettings().minimalDays}getWeekendDays(){return this.getWeekSettings().weekend}equals(e){return this.locale===e.locale&&this.numberingSystem===e.numberingSystem&&this.outputCalendar===e.outputCalendar}toString(){return`Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`}}let Pe=null;class I extends oe{static get utcInstance(){return Pe===null&&(Pe=new I(0)),Pe}static instance(e){return e===0?I.utcInstance:new I(e)}static parseSpecifier(e){if(e){const t=e.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(t)return new I(ve(t[1],t[2]))}return null}constructor(e){super(),this.fixed=e}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${fe(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${fe(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(e,t){return fe(this.fixed,t)}get isUniversal(){return!0}offset(){return this.fixed}equals(e){return e.type==="fixed"&&e.fixed===this.fixed}get isValid(){return!0}}class fr extends oe{constructor(e){super(),this.zoneName=e}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}}function Z(r,e){if(g(r)||r===null)return e;if(r instanceof oe)return r;if(pr(r)){const t=r.toLowerCase();return t==="default"?e:t==="local"||t==="system"?ke.instance:t==="utc"||t==="gmt"?I.utcInstance:I.parseSpecifier(t)||$.create(r)}else return q(r)?I.instance(r):typeof r=="object"&&"offset"in r&&typeof r.offset=="function"?r:new fr(r)}const Ye={arab:"[٠-٩]",arabext:"[۰-۹]",bali:"[᭐-᭙]",beng:"[০-৯]",deva:"[०-९]",fullwide:"[0-9]",gujr:"[૦-૯]",hanidec:"[〇|一|二|三|四|五|六|七|八|九]",khmr:"[០-៩]",knda:"[೦-೯]",laoo:"[໐-໙]",limb:"[᥆-᥏]",mlym:"[൦-൯]",mong:"[᠐-᠙]",mymr:"[၀-၉]",orya:"[୦-୯]",tamldec:"[௦-௯]",telu:"[౦-౯]",thai:"[๐-๙]",tibt:"[༠-༩]",latn:"\\d"},Ft={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},dr=Ye.hanidec.replace(/[\[|\]]/g,"").split("");function hr(r){let e=parseInt(r,10);if(isNaN(e)){e="";for(let t=0;t=i&&n<=a&&(e+=n-i)}}return parseInt(e,10)}else return e}let K={};function mr(){K={}}function C({numberingSystem:r},e=""){const t=r||"latn";return K[t]||(K[t]={}),K[t][e]||(K[t][e]=new RegExp(`${Ye[t]}${e}`)),K[t][e]}let Vt=()=>Date.now(),At="system",Wt=null,Ct=null,Lt=null,Rt=60,$t,Ut=null;class T{static get now(){return Vt}static set now(e){Vt=e}static set defaultZone(e){At=e}static get defaultZone(){return Z(At,ke.instance)}static get defaultLocale(){return Wt}static set defaultLocale(e){Wt=e}static get defaultNumberingSystem(){return Ct}static set defaultNumberingSystem(e){Ct=e}static get defaultOutputCalendar(){return Lt}static set defaultOutputCalendar(e){Lt=e}static get defaultWeekSettings(){return Ut}static set defaultWeekSettings(e){Ut=je(e)}static get twoDigitCutoffYear(){return Rt}static set twoDigitCutoffYear(e){Rt=e%100}static get throwOnInvalid(){return $t}static set throwOnInvalid(e){$t=e}static resetCaches(){S.resetCache(),$.resetCache(),y.resetCache(),mr()}}class L{constructor(e,t){this.reason=e,this.explanation=t}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}}const Zt=[0,31,59,90,120,151,181,212,243,273,304,334],qt=[0,31,60,91,121,152,182,213,244,274,305,335];function F(r,e){return new L("unit out of range",`you specified ${e} (of type ${typeof e}) as a ${r}, which is invalid`)}function Je(r,e,t){const n=new Date(Date.UTC(r,e-1,t));r<100&&r>=0&&n.setUTCFullYear(n.getUTCFullYear()-1900);const s=n.getUTCDay();return s===0?7:s}function zt(r,e,t){return t+(le(r)?qt:Zt)[e-1]}function Pt(r,e){const t=le(r)?qt:Zt,n=t.findIndex(i=>ice(n,e,t)?(l=n+1,u=1):l=n,{weekYear:l,weekNumber:u,weekday:o,...De(r)}}function Yt(r,e=4,t=1){const{weekYear:n,weekNumber:s,weekday:i}=r,a=Be(Je(n,1,e),t),o=_(n);let u=s*7+i-a-7+e,l;u<1?(l=n-1,u+=_(l)):u>o?(l=n+1,u-=_(n)):l=n;const{month:f,day:h}=Pt(l,u);return{year:l,month:f,day:h,...De(r)}}function Ge(r){const{year:e,month:t,day:n}=r,s=zt(e,t,n);return{year:e,ordinal:s,...De(r)}}function Jt(r){const{year:e,ordinal:t}=r,{month:n,day:s}=Pt(e,t);return{year:e,month:n,day:s,...De(r)}}function Bt(r,e){if(!g(r.localWeekday)||!g(r.localWeekNumber)||!g(r.localWeekYear)){if(!g(r.weekday)||!g(r.weekNumber)||!g(r.weekYear))throw new j("Cannot mix locale-based week fields with ISO-based week fields");return g(r.localWeekday)||(r.weekday=r.localWeekday),g(r.localWeekNumber)||(r.weekNumber=r.localWeekNumber),g(r.localWeekYear)||(r.weekYear=r.localWeekYear),delete r.localWeekday,delete r.localWeekNumber,delete r.localWeekYear,{minDaysInFirstWeek:e.getMinDaysInFirstWeek(),startOfWeek:e.getStartOfWeek()}}else return{minDaysInFirstWeek:4,startOfWeek:1}}function yr(r,e=4,t=1){const n=xe(r.weekYear),s=V(r.weekNumber,1,ce(r.weekYear,e,t)),i=V(r.weekday,1,7);return n?s?i?!1:F("weekday",r.weekday):F("week",r.weekNumber):F("weekYear",r.weekYear)}function gr(r){const e=xe(r.year),t=V(r.ordinal,1,_(r.year));return e?t?!1:F("ordinal",r.ordinal):F("year",r.year)}function Gt(r){const e=xe(r.year),t=V(r.month,1,12),n=V(r.day,1,be(r.year,r.month));return e?t?n?!1:F("day",r.day):F("month",r.month):F("year",r.year)}function jt(r){const{hour:e,minute:t,second:n,millisecond:s}=r,i=V(e,0,23)||e===24&&t===0&&n===0&&s===0,a=V(t,0,59),o=V(n,0,59),u=V(s,0,999);return i?a?o?u?!1:F("millisecond",s):F("second",n):F("minute",t):F("hour",e)}function g(r){return typeof r>"u"}function q(r){return typeof r=="number"}function xe(r){return typeof r=="number"&&r%1===0}function pr(r){return typeof r=="string"}function wr(r){return Object.prototype.toString.call(r)==="[object Date]"}function Kt(){try{return typeof Intl<"u"&&!!Intl.RelativeTimeFormat}catch{return!1}}function Ht(){try{return typeof Intl<"u"&&!!Intl.Locale&&("weekInfo"in Intl.Locale.prototype||"getWeekInfo"in Intl.Locale.prototype)}catch{return!1}}function Sr(r){return Array.isArray(r)?r:[r]}function _t(r,e,t){if(r.length!==0)return r.reduce((n,s)=>{const i=[e(s),s];return n&&t(n[0],i[0])===n[0]?n:i},null)[1]}function kr(r,e){return e.reduce((t,n)=>(t[n]=r[n],t),{})}function H(r,e){return Object.prototype.hasOwnProperty.call(r,e)}function je(r){if(r==null)return null;if(typeof r!="object")throw new x("Week settings must be an object");if(!V(r.firstDay,1,7)||!V(r.minimalDays,1,7)||!Array.isArray(r.weekend)||r.weekend.some(e=>!V(e,1,7)))throw new x("Invalid week settings");return{firstDay:r.firstDay,minimalDays:r.minimalDays,weekend:Array.from(r.weekend)}}function V(r,e,t){return xe(r)&&r>=e&&r<=t}function Tr(r,e){return r-e*Math.floor(r/e)}function N(r,e=2){const t=r<0;let n;return t?n="-"+(""+-r).padStart(e,"0"):n=(""+r).padStart(e,"0"),n}function z(r){if(!(g(r)||r===null||r===""))return parseInt(r,10)}function J(r){if(!(g(r)||r===null||r===""))return parseFloat(r)}function Ke(r){if(!(g(r)||r===null||r==="")){const e=parseFloat("0."+r)*1e3;return Math.floor(e)}}function He(r,e,t=!1){const n=10**e;return(t?Math.trunc:Math.round)(r*n)/n}function le(r){return r%4===0&&(r%100!==0||r%400===0)}function _(r){return le(r)?366:365}function be(r,e){const t=Tr(e-1,12)+1,n=r+(e-t)/12;return t===2?le(n)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][t-1]}function Ie(r){let e=Date.UTC(r.year,r.month-1,r.day,r.hour,r.minute,r.second,r.millisecond);return r.year<100&&r.year>=0&&(e=new Date(e),e.setUTCFullYear(r.year,r.month-1,r.day)),+e}function Qt(r,e,t){return-Be(Je(r,1,e),t)+e-1}function ce(r,e=4,t=1){const n=Qt(r,e,t),s=Qt(r+1,e,t);return(_(r)-n+s)/7}function _e(r){return r>99?r:r>T.twoDigitCutoffYear?1900+r:2e3+r}function Xt(r,e,t,n=null){const s=new Date(r),i={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};n&&(i.timeZone=n);const a={timeZoneName:e,...i},o=new Intl.DateTimeFormat(t,a).formatToParts(s).find(u=>u.type.toLowerCase()==="timezonename");return o?o.value:null}function ve(r,e){let t=parseInt(r,10);Number.isNaN(t)&&(t=0);const n=parseInt(e,10)||0,s=t<0||Object.is(t,-0)?-n:n;return t*60+s}function en(r){const e=Number(r);if(typeof r=="boolean"||r===""||Number.isNaN(e))throw new x(`Invalid unit value ${r}`);return e}function Me(r,e){const t={};for(const n in r)if(H(r,n)){const s=r[n];if(s==null)continue;t[e(n)]=en(s)}return t}function fe(r,e){const t=Math.trunc(Math.abs(r/60)),n=Math.trunc(Math.abs(r%60)),s=r>=0?"+":"-";switch(e){case"short":return`${s}${N(t,2)}:${N(n,2)}`;case"narrow":return`${s}${t}${n>0?`:${n}`:""}`;case"techie":return`${s}${N(t,2)}${N(n,2)}`;default:throw new RangeError(`Value format ${e} is out of range for property format`)}}function De(r){return kr(r,["hour","minute","second","millisecond"])}const Or=["January","February","March","April","May","June","July","August","September","October","November","December"],tn=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Er=["J","F","M","A","M","J","J","A","S","O","N","D"];function nn(r){switch(r){case"narrow":return[...Er];case"short":return[...tn];case"long":return[...Or];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}const rn=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],sn=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],Nr=["M","T","W","T","F","S","S"];function an(r){switch(r){case"narrow":return[...Nr];case"short":return[...sn];case"long":return[...rn];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}const on=["AM","PM"],xr=["Before Christ","Anno Domini"],br=["BC","AD"],Ir=["B","A"];function un(r){switch(r){case"narrow":return[...Ir];case"short":return[...br];case"long":return[...xr];default:return null}}function vr(r){return on[r.hour<12?0:1]}function Mr(r,e){return an(e)[r.weekday-1]}function Dr(r,e){return nn(e)[r.month-1]}function Fr(r,e){return un(e)[r.year<0?0:1]}function Vr(r,e,t="always",n=!1){const s={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},i=["hours","minutes","seconds"].indexOf(r)===-1;if(t==="auto"&&i){const h=r==="days";switch(e){case 1:return h?"tomorrow":`next ${s[r][0]}`;case-1:return h?"yesterday":`last ${s[r][0]}`;case 0:return h?"today":`this ${s[r][0]}`}}const a=Object.is(e,-0)||e<0,o=Math.abs(e),u=o===1,l=s[r],f=n?u?l[1]:l[2]||l[1]:u?s[r][0]:r;return a?`${o} ${f} ago`:`in ${o} ${f}`}function ln(r,e){let t="";for(const n of r)n.literal?t+=n.val:t+=e(n.val);return t}const Ar={D:Se,DD:ct,DDD:ft,DDDD:dt,t:ht,tt:mt,ttt:yt,tttt:gt,T:pt,TT:wt,TTT:St,TTTT:kt,f:Tt,ff:Et,fff:xt,ffff:It,F:Ot,FF:Nt,FFF:bt,FFFF:vt};class b{static create(e,t={}){return new b(e,t)}static parseFormat(e){let t=null,n="",s=!1;const i=[];for(let a=0;a0&&i.push({literal:s||/^\s+$/.test(n),val:n}),t=null,n="",s=!s):s||o===t?n+=o:(n.length>0&&i.push({literal:/^\s+$/.test(n),val:n}),n=o,t=o)}return n.length>0&&i.push({literal:s||/^\s+$/.test(n),val:n}),i}static macroTokenToFormatOpts(e){return Ar[e]}constructor(e,t){this.opts=t,this.loc=e,this.systemLoc=null}formatWithSystemDefault(e,t){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(e,{...this.opts,...t}).format()}dtFormatter(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t})}formatDateTime(e,t){return this.dtFormatter(e,t).format()}formatDateTimeParts(e,t){return this.dtFormatter(e,t).formatToParts()}formatInterval(e,t){return this.dtFormatter(e.start,t).dtf.formatRange(e.start.toJSDate(),e.end.toJSDate())}resolvedOptions(e,t){return this.dtFormatter(e,t).resolvedOptions()}num(e,t=0){if(this.opts.forceSimple)return N(e,t);const n={...this.opts};return t>0&&(n.padTo=t),this.loc.numberFormatter(n).format(e)}formatDateTimeFromString(e,t){const n=this.loc.listingMode()==="en",s=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",i=(m,E)=>this.loc.extract(e,m,E),a=m=>e.isOffsetFixed&&e.offset===0&&m.allowZ?"Z":e.isValid?e.zone.formatOffset(e.ts,m.format):"",o=()=>n?vr(e):i({hour:"numeric",hourCycle:"h12"},"dayperiod"),u=(m,E)=>n?Dr(e,m):i(E?{month:m}:{month:m,day:"numeric"},"month"),l=(m,E)=>n?Mr(e,m):i(E?{weekday:m}:{weekday:m,month:"long",day:"numeric"},"weekday"),f=m=>{const E=b.macroTokenToFormatOpts(m);return E?this.formatWithSystemDefault(e,E):m},h=m=>n?Fr(e,m):i({era:m},"era"),k=m=>{switch(m){case"S":return this.num(e.millisecond);case"u":case"SSS":return this.num(e.millisecond,3);case"s":return this.num(e.second);case"ss":return this.num(e.second,2);case"uu":return this.num(Math.floor(e.millisecond/10),2);case"uuu":return this.num(Math.floor(e.millisecond/100));case"m":return this.num(e.minute);case"mm":return this.num(e.minute,2);case"h":return this.num(e.hour%12===0?12:e.hour%12);case"hh":return this.num(e.hour%12===0?12:e.hour%12,2);case"H":return this.num(e.hour);case"HH":return this.num(e.hour,2);case"Z":return a({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return a({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return a({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return e.zone.offsetName(e.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return e.zone.offsetName(e.ts,{format:"long",locale:this.loc.locale});case"z":return e.zoneName;case"a":return o();case"d":return s?i({day:"numeric"},"day"):this.num(e.day);case"dd":return s?i({day:"2-digit"},"day"):this.num(e.day,2);case"c":return this.num(e.weekday);case"ccc":return l("short",!0);case"cccc":return l("long",!0);case"ccccc":return l("narrow",!0);case"E":return this.num(e.weekday);case"EEE":return l("short",!1);case"EEEE":return l("long",!1);case"EEEEE":return l("narrow",!1);case"L":return s?i({month:"numeric",day:"numeric"},"month"):this.num(e.month);case"LL":return s?i({month:"2-digit",day:"numeric"},"month"):this.num(e.month,2);case"LLL":return u("short",!0);case"LLLL":return u("long",!0);case"LLLLL":return u("narrow",!0);case"M":return s?i({month:"numeric"},"month"):this.num(e.month);case"MM":return s?i({month:"2-digit"},"month"):this.num(e.month,2);case"MMM":return u("short",!1);case"MMMM":return u("long",!1);case"MMMMM":return u("narrow",!1);case"y":return s?i({year:"numeric"},"year"):this.num(e.year);case"yy":return s?i({year:"2-digit"},"year"):this.num(e.year.toString().slice(-2),2);case"yyyy":return s?i({year:"numeric"},"year"):this.num(e.year,4);case"yyyyyy":return s?i({year:"numeric"},"year"):this.num(e.year,6);case"G":return h("short");case"GG":return h("long");case"GGGGG":return h("narrow");case"kk":return this.num(e.weekYear.toString().slice(-2),2);case"kkkk":return this.num(e.weekYear,4);case"W":return this.num(e.weekNumber);case"WW":return this.num(e.weekNumber,2);case"n":return this.num(e.localWeekNumber);case"nn":return this.num(e.localWeekNumber,2);case"ii":return this.num(e.localWeekYear.toString().slice(-2),2);case"iiii":return this.num(e.localWeekYear,4);case"o":return this.num(e.ordinal);case"ooo":return this.num(e.ordinal,3);case"q":return this.num(e.quarter);case"qq":return this.num(e.quarter,2);case"X":return this.num(Math.floor(e.ts/1e3));case"x":return this.num(e.ts);default:return f(m)}};return ln(b.parseFormat(t),k)}formatDurationFromString(e,t){const n=u=>{switch(u[0]){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":return"hour";case"d":return"day";case"w":return"week";case"M":return"month";case"y":return"year";default:return null}},s=u=>l=>{const f=n(l);return f?this.num(u.get(f),l.length):l},i=b.parseFormat(t),a=i.reduce((u,{literal:l,val:f})=>l?u:u.concat(f),[]),o=e.shiftTo(...a.map(n).filter(u=>u));return ln(i,s(o))}}const cn=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;function Q(...r){const e=r.reduce((t,n)=>t+n.source,"");return RegExp(`^${e}$`)}function X(...r){return e=>r.reduce(([t,n,s],i)=>{const[a,o,u]=i(e,s);return[{...t,...a},o||n,u]},[{},null,1]).slice(0,2)}function ee(r,...e){if(r==null)return[null,null];for(const[t,n]of e){const s=t.exec(r);if(s)return n(s)}return[null,null]}function fn(...r){return(e,t)=>{const n={};let s;for(s=0;sm!==void 0&&(E||m&&f)?-m:m;return[{years:k(J(t)),months:k(J(n)),weeks:k(J(s)),days:k(J(i)),hours:k(J(a)),minutes:k(J(o)),seconds:k(J(u),u==="-0"),milliseconds:k(Ke(l),h)}]}const Br={GMT:0,EDT:-4*60,EST:-5*60,CDT:-5*60,CST:-6*60,MDT:-6*60,MST:-7*60,PDT:-7*60,PST:-8*60};function et(r,e,t,n,s,i,a){const o={year:e.length===2?_e(z(e)):z(e),month:tn.indexOf(t)+1,day:z(n),hour:z(s),minute:z(i)};return a&&(o.second=z(a)),r&&(o.weekday=r.length>3?rn.indexOf(r)+1:sn.indexOf(r)+1),o}const Gr=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function jr(r){const[,e,t,n,s,i,a,o,u,l,f,h]=r,k=et(e,s,n,t,i,a,o);let m;return u?m=Br[u]:l?m=0:m=ve(f,h),[k,new I(m)]}function Kr(r){return r.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}const Hr=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,_r=/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,Qr=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function yn(r){const[,e,t,n,s,i,a,o]=r;return[et(e,s,n,t,i,a,o),I.utcInstance]}function Xr(r){const[,e,t,n,s,i,a,o]=r;return[et(e,o,t,n,s,i,a),I.utcInstance]}const es=Q(Cr,Xe),ts=Q(Lr,Xe),ns=Q(Rr,Xe),rs=Q(hn),gn=X(zr,ne,de,he),ss=X($r,ne,de,he),is=X(Ur,ne,de,he),as=X(ne,de,he);function os(r){return ee(r,[es,gn],[ts,ss],[ns,is],[rs,as])}function us(r){return ee(Kr(r),[Gr,jr])}function ls(r){return ee(r,[Hr,yn],[_r,yn],[Qr,Xr])}function cs(r){return ee(r,[Yr,Jr])}const fs=X(ne);function ds(r){return ee(r,[Pr,fs])}const hs=Q(Zr,qr),ms=Q(mn),ys=X(ne,de,he);function gs(r){return ee(r,[hs,gn],[ms,ys])}const pn="Invalid Duration",wn={weeks:{days:7,hours:7*24,minutes:7*24*60,seconds:7*24*60*60,milliseconds:7*24*60*60*1e3},days:{hours:24,minutes:24*60,seconds:24*60*60,milliseconds:24*60*60*1e3},hours:{minutes:60,seconds:60*60,milliseconds:60*60*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},ps={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:91*24,minutes:91*24*60,seconds:91*24*60*60,milliseconds:91*24*60*60*1e3},months:{weeks:4,days:30,hours:30*24,minutes:30*24*60,seconds:30*24*60*60,milliseconds:30*24*60*60*1e3},...wn},A=146097/400,re=146097/4800,ws={years:{quarters:4,months:12,weeks:A/7,days:A,hours:A*24,minutes:A*24*60,seconds:A*24*60*60,milliseconds:A*24*60*60*1e3},quarters:{months:3,weeks:A/28,days:A/4,hours:A*24/4,minutes:A*24*60/4,seconds:A*24*60*60/4,milliseconds:A*24*60*60*1e3/4},months:{weeks:re/7,days:re,hours:re*24,minutes:re*24*60,seconds:re*24*60*60,milliseconds:re*24*60*60*1e3},...wn},B=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],Ss=B.slice(0).reverse();function P(r,e,t=!1){const n={values:t?e.values:{...r.values,...e.values||{}},loc:r.loc.clone(e.loc),conversionAccuracy:e.conversionAccuracy||r.conversionAccuracy,matrix:e.matrix||r.matrix};return new p(n)}function Sn(r,e){let t=e.milliseconds??0;for(const n of Ss.slice(1))e[n]&&(t+=e[n]*r[n].milliseconds);return t}function kn(r,e){const t=Sn(r,e)<0?-1:1;B.reduceRight((n,s)=>{if(g(e[s]))return n;if(n){const i=e[n]*t,a=r[s][n],o=Math.floor(i/a);e[s]+=o*t,e[n]-=o*a*t}return s},null),B.reduce((n,s)=>{if(g(e[s]))return n;if(n){const i=e[n]%1;e[n]-=i,e[s]+=i*r[n][s]}return s},null)}function ks(r){const e={};for(const[t,n]of Object.entries(r))n!==0&&(e[t]=n);return e}class p{constructor(e){const t=e.conversionAccuracy==="longterm"||!1;let n=t?ws:ps;e.matrix&&(n=e.matrix),this.values=e.values,this.loc=e.loc||S.create(),this.conversionAccuracy=t?"longterm":"casual",this.invalid=e.invalid||null,this.matrix=n,this.isLuxonDuration=!0}static fromMillis(e,t){return p.fromObject({milliseconds:e},t)}static fromObject(e,t={}){if(e==null||typeof e!="object")throw new x(`Duration.fromObject: argument expected to be an object, got ${e===null?"null":typeof e}`);return new p({values:Me(e,p.normalizeUnit),loc:S.fromObject(t),conversionAccuracy:t.conversionAccuracy,matrix:t.matrix})}static fromDurationLike(e){if(q(e))return p.fromMillis(e);if(p.isDuration(e))return e;if(typeof e=="object")return p.fromObject(e);throw new x(`Unknown duration argument ${e} of type ${typeof e}`)}static fromISO(e,t){const[n]=cs(e);return n?p.fromObject(n,t):p.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static fromISOTime(e,t){const[n]=ds(e);return n?p.fromObject(n,t):p.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static invalid(e,t=null){if(!e)throw new x("need to specify a reason the Duration is invalid");const n=e instanceof L?e:new L(e,t);if(T.throwOnInvalid)throw new Yn(n);return new p({invalid:n})}static normalizeUnit(e){const t={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[e&&e.toLowerCase()];if(!t)throw new lt(e);return t}static isDuration(e){return e&&e.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(e,t={}){const n={...t,floor:t.round!==!1&&t.floor!==!1};return this.isValid?b.create(this.loc,n).formatDurationFromString(this,e):pn}toHuman(e={}){if(!this.isValid)return pn;const t=B.map(n=>{const s=this.values[n];return g(s)?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...e,unit:n.slice(0,-1)}).format(s)}).filter(n=>n);return this.loc.listFormatter({type:"conjunction",style:e.listStyle||"narrow",...e}).format(t)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let e="P";return this.years!==0&&(e+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(e+=this.months+this.quarters*3+"M"),this.weeks!==0&&(e+=this.weeks+"W"),this.days!==0&&(e+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(e+="T"),this.hours!==0&&(e+=this.hours+"H"),this.minutes!==0&&(e+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(e+=He(this.seconds+this.milliseconds/1e3,3)+"S"),e==="P"&&(e+="T0S"),e}toISOTime(e={}){if(!this.isValid)return null;const t=this.toMillis();return t<0||t>=864e5?null:(e={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...e,includeOffset:!1},y.fromMillis(t,{zone:"UTC"}).toISOTime(e))}toJSON(){return this.toISO()}toString(){return this.toISO()}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Duration { values: ${JSON.stringify(this.values)} }`:`Duration { Invalid, reason: ${this.invalidReason} }`}toMillis(){return this.isValid?Sn(this.matrix,this.values):NaN}valueOf(){return this.toMillis()}plus(e){if(!this.isValid)return this;const t=p.fromDurationLike(e),n={};for(const s of B)(H(t.values,s)||H(this.values,s))&&(n[s]=t.get(s)+this.get(s));return P(this,{values:n},!0)}minus(e){if(!this.isValid)return this;const t=p.fromDurationLike(e);return this.plus(t.negate())}mapUnits(e){if(!this.isValid)return this;const t={};for(const n of Object.keys(this.values))t[n]=en(e(this.values[n],n));return P(this,{values:t},!0)}get(e){return this[p.normalizeUnit(e)]}set(e){if(!this.isValid)return this;const t={...this.values,...Me(e,p.normalizeUnit)};return P(this,{values:t})}reconfigure({locale:e,numberingSystem:t,conversionAccuracy:n,matrix:s}={}){const a={loc:this.loc.clone({locale:e,numberingSystem:t}),matrix:s,conversionAccuracy:n};return P(this,a)}as(e){return this.isValid?this.shiftTo(e).get(e):NaN}normalize(){if(!this.isValid)return this;const e=this.toObject();return kn(this.matrix,e),P(this,{values:e},!0)}rescale(){if(!this.isValid)return this;const e=ks(this.normalize().shiftToAll().toObject());return P(this,{values:e},!0)}shiftTo(...e){if(!this.isValid)return this;if(e.length===0)return this;e=e.map(a=>p.normalizeUnit(a));const t={},n={},s=this.toObject();let i;for(const a of B)if(e.indexOf(a)>=0){i=a;let o=0;for(const l in n)o+=this.matrix[l][a]*n[l],n[l]=0;q(s[a])&&(o+=s[a]);const u=Math.trunc(o);t[a]=u,n[a]=(o*1e3-u*1e3)/1e3}else q(s[a])&&(n[a]=s[a]);for(const a in n)n[a]!==0&&(t[i]+=a===i?n[a]:n[a]/this.matrix[i][a]);return kn(this.matrix,t),P(this,{values:t},!0)}shiftToAll(){return this.isValid?this.shiftTo("years","months","weeks","days","hours","minutes","seconds","milliseconds"):this}negate(){if(!this.isValid)return this;const e={};for(const t of Object.keys(this.values))e[t]=this.values[t]===0?0:-this.values[t];return P(this,{values:e},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(e){if(!this.isValid||!e.isValid||!this.loc.equals(e.loc))return!1;function t(n,s){return n===void 0||n===0?s===void 0||s===0:n===s}for(const n of B)if(!t(this.values[n],e.values[n]))return!1;return!0}}const se="Invalid Interval";function Ts(r,e){return!r||!r.isValid?O.invalid("missing or invalid start"):!e||!e.isValid?O.invalid("missing or invalid end"):ee:!1}isBefore(e){return this.isValid?this.e<=e:!1}contains(e){return this.isValid?this.s<=e&&this.e>e:!1}set({start:e,end:t}={}){return this.isValid?O.fromDateTimes(e||this.s,t||this.e):this}splitAt(...e){if(!this.isValid)return[];const t=e.map(ye).filter(a=>this.contains(a)).sort((a,o)=>a.toMillis()-o.toMillis()),n=[];let{s}=this,i=0;for(;s+this.e?this.e:a;n.push(O.fromDateTimes(s,o)),s=o,i+=1}return n}splitBy(e){const t=p.fromDurationLike(e);if(!this.isValid||!t.isValid||t.as("milliseconds")===0)return[];let{s:n}=this,s=1,i;const a=[];for(;nu*s));i=+o>+this.e?this.e:o,a.push(O.fromDateTimes(n,i)),n=i,s+=1}return a}divideEqually(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]}overlaps(e){return this.e>e.s&&this.s=e.e:!1}equals(e){return!this.isValid||!e.isValid?!1:this.s.equals(e.s)&&this.e.equals(e.e)}intersection(e){if(!this.isValid)return this;const t=this.s>e.s?this.s:e.s,n=this.e=n?null:O.fromDateTimes(t,n)}union(e){if(!this.isValid)return this;const t=this.se.e?this.e:e.e;return O.fromDateTimes(t,n)}static merge(e){const[t,n]=e.sort((s,i)=>s.s-i.s).reduce(([s,i],a)=>i?i.overlaps(a)||i.abutsStart(a)?[s,i.union(a)]:[s.concat([i]),a]:[s,a],[[],null]);return n&&t.push(n),t}static xor(e){let t=null,n=0;const s=[],i=e.map(u=>[{time:u.s,type:"s"},{time:u.e,type:"e"}]),a=Array.prototype.concat(...i),o=a.sort((u,l)=>u.time-l.time);for(const u of o)n+=u.type==="s"?1:-1,n===1?t=u.time:(t&&+t!=+u.time&&s.push(O.fromDateTimes(t,u.time)),t=null);return O.merge(s)}difference(...e){return O.xor([this].concat(e)).map(t=>this.intersection(t)).filter(t=>t&&!t.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} – ${this.e.toISO()})`:se}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`:`Interval { Invalid, reason: ${this.invalidReason} }`}toLocaleString(e=Se,t={}){return this.isValid?b.create(this.s.loc.clone(t),e).formatInterval(this):se}toISO(e){return this.isValid?`${this.s.toISO(e)}/${this.e.toISO(e)}`:se}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:se}toISOTime(e){return this.isValid?`${this.s.toISOTime(e)}/${this.e.toISOTime(e)}`:se}toFormat(e,{separator:t=" – "}={}){return this.isValid?`${this.s.toFormat(e)}${t}${this.e.toFormat(e)}`:se}toDuration(e,t){return this.isValid?this.e.diff(this.s,e,t):p.invalid(this.invalidReason)}mapEndpoints(e){return O.fromDateTimes(e(this.s),e(this.e))}}class Fe{static hasDST(e=T.defaultZone){const t=y.now().setZone(e).set({month:12});return!e.isUniversal&&t.offset!==t.set({month:6}).offset}static isValidIANAZone(e){return $.isValidZone(e)}static normalizeZone(e){return Z(e,T.defaultZone)}static getStartOfWeek({locale:e=null,locObj:t=null}={}){return(t||S.create(e)).getStartOfWeek()}static getMinimumDaysInFirstWeek({locale:e=null,locObj:t=null}={}){return(t||S.create(e)).getMinDaysInFirstWeek()}static getWeekendWeekdays({locale:e=null,locObj:t=null}={}){return(t||S.create(e)).getWeekendDays().slice()}static months(e="long",{locale:t=null,numberingSystem:n=null,locObj:s=null,outputCalendar:i="gregory"}={}){return(s||S.create(t,n,i)).months(e)}static monthsFormat(e="long",{locale:t=null,numberingSystem:n=null,locObj:s=null,outputCalendar:i="gregory"}={}){return(s||S.create(t,n,i)).months(e,!0)}static weekdays(e="long",{locale:t=null,numberingSystem:n=null,locObj:s=null}={}){return(s||S.create(t,n,null)).weekdays(e)}static weekdaysFormat(e="long",{locale:t=null,numberingSystem:n=null,locObj:s=null}={}){return(s||S.create(t,n,null)).weekdays(e,!0)}static meridiems({locale:e=null}={}){return S.create(e).meridiems()}static eras(e="short",{locale:t=null}={}){return S.create(t,null,"gregory").eras(e)}static features(){return{relative:Kt(),localeWeek:Ht()}}}function Tn(r,e){const t=s=>s.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),n=t(e)-t(r);return Math.floor(p.fromMillis(n).as("days"))}function Os(r,e,t){const n=[["years",(u,l)=>l.year-u.year],["quarters",(u,l)=>l.quarter-u.quarter+(l.year-u.year)*4],["months",(u,l)=>l.month-u.month+(l.year-u.year)*12],["weeks",(u,l)=>{const f=Tn(u,l);return(f-f%7)/7}],["days",Tn]],s={},i=r;let a,o;for(const[u,l]of n)t.indexOf(u)>=0&&(a=u,s[u]=l(r,e),o=i.plus(s),o>e?(s[u]--,r=i.plus(s),r>e&&(o=r,s[u]--,r=i.plus(s))):r=o);return[r,s,o,a]}function Es(r,e,t,n){let[s,i,a,o]=Os(r,e,t);const u=e-s,l=t.filter(h=>["hours","minutes","seconds","milliseconds"].indexOf(h)>=0);l.length===0&&(a0?p.fromMillis(u,n).shiftTo(...l).plus(f):f}const Ns="missing Intl.DateTimeFormat.formatToParts support";function w(r,e=t=>t){return{regex:r,deser:([t])=>e(hr(t))}}const On="[  ]",En=new RegExp(On,"g");function xs(r){return r.replace(/\./g,"\\.?").replace(En,On)}function Nn(r){return r.replace(/\./g,"").replace(En," ").toLowerCase()}function R(r,e){return r===null?null:{regex:RegExp(r.map(xs).join("|")),deser:([t])=>r.findIndex(n=>Nn(t)===Nn(n))+e}}function xn(r,e){return{regex:r,deser:([,t,n])=>ve(t,n),groups:e}}function Ve(r){return{regex:r,deser:([e])=>e}}function bs(r){return r.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function Is(r,e){const t=C(e),n=C(e,"{2}"),s=C(e,"{3}"),i=C(e,"{4}"),a=C(e,"{6}"),o=C(e,"{1,2}"),u=C(e,"{1,3}"),l=C(e,"{1,6}"),f=C(e,"{1,9}"),h=C(e,"{2,4}"),k=C(e,"{4,6}"),m=D=>({regex:RegExp(bs(D.val)),deser:([ae])=>ae,literal:!0}),M=(D=>{if(r.literal)return m(D);switch(D.val){case"G":return R(e.eras("short"),0);case"GG":return R(e.eras("long"),0);case"y":return w(l);case"yy":return w(h,_e);case"yyyy":return w(i);case"yyyyy":return w(k);case"yyyyyy":return w(a);case"M":return w(o);case"MM":return w(n);case"MMM":return R(e.months("short",!0),1);case"MMMM":return R(e.months("long",!0),1);case"L":return w(o);case"LL":return w(n);case"LLL":return R(e.months("short",!1),1);case"LLLL":return R(e.months("long",!1),1);case"d":return w(o);case"dd":return w(n);case"o":return w(u);case"ooo":return w(s);case"HH":return w(n);case"H":return w(o);case"hh":return w(n);case"h":return w(o);case"mm":return w(n);case"m":return w(o);case"q":return w(o);case"qq":return w(n);case"s":return w(o);case"ss":return w(n);case"S":return w(u);case"SSS":return w(s);case"u":return Ve(f);case"uu":return Ve(o);case"uuu":return w(t);case"a":return R(e.meridiems(),0);case"kkkk":return w(i);case"kk":return w(h,_e);case"W":return w(o);case"WW":return w(n);case"E":case"c":return w(t);case"EEE":return R(e.weekdays("short",!1),1);case"EEEE":return R(e.weekdays("long",!1),1);case"ccc":return R(e.weekdays("short",!0),1);case"cccc":return R(e.weekdays("long",!0),1);case"Z":case"ZZ":return xn(new RegExp(`([+-]${o.source})(?::(${n.source}))?`),2);case"ZZZ":return xn(new RegExp(`([+-]${o.source})(${n.source})?`),2);case"z":return Ve(/[a-z_+-/]{1,256}?/i);case" ":return Ve(/[^\S\n\r]/);default:return m(D)}})(r)||{invalidReason:Ns};return M.token=r,M}const vs={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour12:{numeric:"h","2-digit":"hh"},hour24:{numeric:"H","2-digit":"HH"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"},timeZoneName:{long:"ZZZZZ",short:"ZZZ"}};function Ms(r,e,t){const{type:n,value:s}=r;if(n==="literal"){const u=/^\s+$/.test(s);return{literal:!u,val:u?" ":s}}const i=e[n];let a=n;n==="hour"&&(e.hour12!=null?a=e.hour12?"hour12":"hour24":e.hourCycle!=null?e.hourCycle==="h11"||e.hourCycle==="h12"?a="hour12":a="hour24":a=t.hour12?"hour12":"hour24");let o=vs[a];if(typeof o=="object"&&(o=o[i]),o)return{literal:!1,val:o}}function Ds(r){return[`^${r.map(t=>t.regex).reduce((t,n)=>`${t}(${n.source})`,"")}$`,r]}function Fs(r,e,t){const n=r.match(e);if(n){const s={};let i=1;for(const a in t)if(H(t,a)){const o=t[a],u=o.groups?o.groups+1:1;!o.literal&&o.token&&(s[o.token.val[0]]=o.deser(n.slice(i,i+u))),i+=u}return[n,s]}else return[n,{}]}function Vs(r){const e=i=>{switch(i){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}};let t=null,n;return g(r.z)||(t=$.create(r.z)),g(r.Z)||(t||(t=new I(r.Z)),n=r.Z),g(r.q)||(r.M=(r.q-1)*3+1),g(r.h)||(r.h<12&&r.a===1?r.h+=12:r.h===12&&r.a===0&&(r.h=0)),r.G===0&&r.y&&(r.y=-r.y),g(r.u)||(r.S=Ke(r.u)),[Object.keys(r).reduce((i,a)=>{const o=e(a);return o&&(i[o]=r[a]),i},{}),t,n]}let tt=null;function As(){return tt||(tt=y.fromMillis(1555555555555)),tt}function Ws(r,e){if(r.literal)return r;const t=b.macroTokenToFormatOpts(r.val),n=Mn(t,e);return n==null||n.includes(void 0)?r:n}function bn(r,e){return Array.prototype.concat(...r.map(t=>Ws(t,e)))}class In{constructor(e,t){if(this.locale=e,this.format=t,this.tokens=bn(b.parseFormat(t),e),this.units=this.tokens.map(n=>Is(n,e)),this.disqualifyingUnit=this.units.find(n=>n.invalidReason),!this.disqualifyingUnit){const[n,s]=Ds(this.units);this.regex=RegExp(n,"i"),this.handlers=s}}explainFromTokens(e){if(this.isValid){const[t,n]=Fs(e,this.regex,this.handlers),[s,i,a]=n?Vs(n):[null,null,void 0];if(H(n,"a")&&H(n,"H"))throw new j("Can't include meridiem when specifying 24-hour format");return{input:e,tokens:this.tokens,regex:this.regex,rawMatches:t,matches:n,result:s,zone:i,specificOffset:a}}else return{input:e,tokens:this.tokens,invalidReason:this.invalidReason}}get isValid(){return!this.disqualifyingUnit}get invalidReason(){return this.disqualifyingUnit?this.disqualifyingUnit.invalidReason:null}}function vn(r,e,t){return new In(r,t).explainFromTokens(e)}function Cs(r,e,t){const{result:n,zone:s,specificOffset:i,invalidReason:a}=vn(r,e,t);return[n,s,i,a]}function Mn(r,e){if(!r)return null;const n=b.create(e,r).dtFormatter(As()),s=n.formatToParts(),i=n.resolvedOptions();return s.map(a=>Ms(a,r,i))}const nt="Invalid DateTime",Dn=864e13;function me(r){return new L("unsupported zone",`the zone "${r.name}" is not supported`)}function rt(r){return r.weekData===null&&(r.weekData=Ne(r.c)),r.weekData}function st(r){return r.localWeekData===null&&(r.localWeekData=Ne(r.c,r.loc.getMinDaysInFirstWeek(),r.loc.getStartOfWeek())),r.localWeekData}function G(r,e){const t={ts:r.ts,zone:r.zone,c:r.c,o:r.o,loc:r.loc,invalid:r.invalid};return new y({...t,...e,old:t})}function Fn(r,e,t){let n=r-e*60*1e3;const s=t.offset(n);if(e===s)return[n,e];n-=(s-e)*60*1e3;const i=t.offset(n);return s===i?[n,s]:[r-Math.min(s,i)*60*1e3,Math.max(s,i)]}function Ae(r,e){r+=e*60*1e3;const t=new Date(r);return{year:t.getUTCFullYear(),month:t.getUTCMonth()+1,day:t.getUTCDate(),hour:t.getUTCHours(),minute:t.getUTCMinutes(),second:t.getUTCSeconds(),millisecond:t.getUTCMilliseconds()}}function We(r,e,t){return Fn(Ie(r),e,t)}function Vn(r,e){const t=r.o,n=r.c.year+Math.trunc(e.years),s=r.c.month+Math.trunc(e.months)+Math.trunc(e.quarters)*3,i={...r.c,year:n,month:s,day:Math.min(r.c.day,be(n,s))+Math.trunc(e.days)+Math.trunc(e.weeks)*7},a=p.fromObject({years:e.years-Math.trunc(e.years),quarters:e.quarters-Math.trunc(e.quarters),months:e.months-Math.trunc(e.months),weeks:e.weeks-Math.trunc(e.weeks),days:e.days-Math.trunc(e.days),hours:e.hours,minutes:e.minutes,seconds:e.seconds,milliseconds:e.milliseconds}).as("milliseconds"),o=Ie(i);let[u,l]=Fn(o,t,r.zone);return a!==0&&(u+=a,l=r.zone.offset(u)),{ts:u,o:l}}function ie(r,e,t,n,s,i){const{setZone:a,zone:o}=t;if(r&&Object.keys(r).length!==0||e){const u=e||o,l=y.fromObject(r,{...t,zone:u,specificOffset:i});return a?l:l.setZone(o)}else return y.invalid(new L("unparsable",`the input "${s}" can't be parsed as ${n}`))}function Ce(r,e,t=!0){return r.isValid?b.create(S.create("en-US"),{allowZ:t,forceSimple:!0}).formatDateTimeFromString(r,e):null}function it(r,e){const t=r.c.year>9999||r.c.year<0;let n="";return t&&r.c.year>=0&&(n+="+"),n+=N(r.c.year,t?6:4),e?(n+="-",n+=N(r.c.month),n+="-",n+=N(r.c.day)):(n+=N(r.c.month),n+=N(r.c.day)),n}function An(r,e,t,n,s,i){let a=N(r.c.hour);return e?(a+=":",a+=N(r.c.minute),(r.c.millisecond!==0||r.c.second!==0||!t)&&(a+=":")):a+=N(r.c.minute),(r.c.millisecond!==0||r.c.second!==0||!t)&&(a+=N(r.c.second),(r.c.millisecond!==0||!n)&&(a+=".",a+=N(r.c.millisecond,3))),s&&(r.isOffsetFixed&&r.offset===0&&!i?a+="Z":r.o<0?(a+="-",a+=N(Math.trunc(-r.o/60)),a+=":",a+=N(Math.trunc(-r.o%60))):(a+="+",a+=N(Math.trunc(r.o/60)),a+=":",a+=N(Math.trunc(r.o%60)))),i&&(a+="["+r.zone.ianaName+"]"),a}const Wn={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},Ls={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},Rs={ordinal:1,hour:0,minute:0,second:0,millisecond:0},Cn=["year","month","day","hour","minute","second","millisecond"],$s=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],Us=["year","ordinal","hour","minute","second","millisecond"];function Zs(r){const e={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[r.toLowerCase()];if(!e)throw new lt(r);return e}function Ln(r){switch(r.toLowerCase()){case"localweekday":case"localweekdays":return"localWeekday";case"localweeknumber":case"localweeknumbers":return"localWeekNumber";case"localweekyear":case"localweekyears":return"localWeekYear";default:return Zs(r)}}function qs(r){return Re[r]||(Le===void 0&&(Le=T.now()),Re[r]=r.offset(Le)),Re[r]}function Rn(r,e){const t=Z(e.zone,T.defaultZone);if(!t.isValid)return y.invalid(me(t));const n=S.fromObject(e);let s,i;if(g(r.year))s=T.now();else{for(const u of Cn)g(r[u])&&(r[u]=Wn[u]);const a=Gt(r)||jt(r);if(a)return y.invalid(a);const o=qs(t);[s,i]=We(r,o,t)}return new y({ts:s,zone:t,loc:n,o:i})}function $n(r,e,t){const n=g(t.round)?!0:t.round,s=(a,o)=>(a=He(a,n||t.calendary?0:2,!0),e.loc.clone(t).relFormatter(t).format(a,o)),i=a=>t.calendary?e.hasSame(r,a)?0:e.startOf(a).diff(r.startOf(a),a).get(a):e.diff(r,a).get(a);if(t.unit)return s(i(t.unit),t.unit);for(const a of t.units){const o=i(a);if(Math.abs(o)>=1)return s(o,a)}return s(r>e?-0:0,t.units[t.units.length-1])}function Un(r){let e={},t;return r.length>0&&typeof r[r.length-1]=="object"?(e=r[r.length-1],t=Array.from(r).slice(0,r.length-1)):t=Array.from(r),[e,t]}let Le,Re={};class y{constructor(e){const t=e.zone||T.defaultZone;let n=e.invalid||(Number.isNaN(e.ts)?new L("invalid input"):null)||(t.isValid?null:me(t));this.ts=g(e.ts)?T.now():e.ts;let s=null,i=null;if(!n)if(e.old&&e.old.ts===this.ts&&e.old.zone.equals(t))[s,i]=[e.old.c,e.old.o];else{const o=q(e.o)&&!e.old?e.o:t.offset(this.ts);s=Ae(this.ts,o),n=Number.isNaN(s.year)?new L("invalid input"):null,s=n?null:s,i=n?null:o}this._zone=t,this.loc=e.loc||S.create(),this.invalid=n,this.weekData=null,this.localWeekData=null,this.c=s,this.o=i,this.isLuxonDateTime=!0}static now(){return new y({})}static local(){const[e,t]=Un(arguments),[n,s,i,a,o,u,l]=t;return Rn({year:n,month:s,day:i,hour:a,minute:o,second:u,millisecond:l},e)}static utc(){const[e,t]=Un(arguments),[n,s,i,a,o,u,l]=t;return e.zone=I.utcInstance,Rn({year:n,month:s,day:i,hour:a,minute:o,second:u,millisecond:l},e)}static fromJSDate(e,t={}){const n=wr(e)?e.valueOf():NaN;if(Number.isNaN(n))return y.invalid("invalid input");const s=Z(t.zone,T.defaultZone);return s.isValid?new y({ts:n,zone:s,loc:S.fromObject(t)}):y.invalid(me(s))}static fromMillis(e,t={}){if(q(e))return e<-Dn||e>Dn?y.invalid("Timestamp out of range"):new y({ts:e,zone:Z(t.zone,T.defaultZone),loc:S.fromObject(t)});throw new x(`fromMillis requires a numerical input, but received a ${typeof e} with value ${e}`)}static fromSeconds(e,t={}){if(q(e))return new y({ts:e*1e3,zone:Z(t.zone,T.defaultZone),loc:S.fromObject(t)});throw new x("fromSeconds requires a numerical input")}static fromObject(e,t={}){e=e||{};const n=Z(t.zone,T.defaultZone);if(!n.isValid)return y.invalid(me(n));const s=S.fromObject(t),i=Me(e,Ln),{minDaysInFirstWeek:a,startOfWeek:o}=Bt(i,s),u=T.now(),l=g(t.specificOffset)?n.offset(u):t.specificOffset,f=!g(i.ordinal),h=!g(i.year),k=!g(i.month)||!g(i.day),m=h||k,E=i.weekYear||i.weekNumber;if((m||f)&&E)throw new j("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(k&&f)throw new j("Can't mix ordinal dates with month/day");const M=E||i.weekday&&!m;let D,ae,ge=Ae(u,l);M?(D=$s,ae=Ls,ge=Ne(ge,a,o)):f?(D=Us,ae=Rs,ge=Ge(ge)):(D=Cn,ae=Wn);let Zn=!1;for(const we of D){const Hs=i[we];g(Hs)?Zn?i[we]=ae[we]:i[we]=ge[we]:Zn=!0}const Bs=M?yr(i,a,o):f?gr(i):Gt(i),qn=Bs||jt(i);if(qn)return y.invalid(qn);const Gs=M?Yt(i,a,o):f?Jt(i):i,[js,Ks]=We(Gs,l,n),pe=new y({ts:js,zone:n,o:Ks,loc:s});return i.weekday&&m&&e.weekday!==pe.weekday?y.invalid("mismatched weekday",`you can't specify both a weekday of ${i.weekday} and a date of ${pe.toISO()}`):pe.isValid?pe:y.invalid(pe.invalid)}static fromISO(e,t={}){const[n,s]=os(e);return ie(n,s,t,"ISO 8601",e)}static fromRFC2822(e,t={}){const[n,s]=us(e);return ie(n,s,t,"RFC 2822",e)}static fromHTTP(e,t={}){const[n,s]=ls(e);return ie(n,s,t,"HTTP",t)}static fromFormat(e,t,n={}){if(g(e)||g(t))throw new x("fromFormat requires an input string and a format");const{locale:s=null,numberingSystem:i=null}=n,a=S.fromOpts({locale:s,numberingSystem:i,defaultToEN:!0}),[o,u,l,f]=Cs(a,e,t);return f?y.invalid(f):ie(o,u,n,`format ${t}`,e,l)}static fromString(e,t,n={}){return y.fromFormat(e,t,n)}static fromSQL(e,t={}){const[n,s]=gs(e);return ie(n,s,t,"SQL",e)}static invalid(e,t=null){if(!e)throw new x("need to specify a reason the DateTime is invalid");const n=e instanceof L?e:new L(e,t);if(T.throwOnInvalid)throw new zn(n);return new y({invalid:n})}static isDateTime(e){return e&&e.isLuxonDateTime||!1}static parseFormatForOpts(e,t={}){const n=Mn(e,S.fromObject(t));return n?n.map(s=>s?s.val:null).join(""):null}static expandFormat(e,t={}){return bn(b.parseFormat(e),S.fromObject(t)).map(s=>s.val).join("")}static resetCache(){Le=void 0,Re={}}get(e){return this[e]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?rt(this).weekYear:NaN}get weekNumber(){return this.isValid?rt(this).weekNumber:NaN}get weekday(){return this.isValid?rt(this).weekday:NaN}get isWeekend(){return this.isValid&&this.loc.getWeekendDays().includes(this.weekday)}get localWeekday(){return this.isValid?st(this).weekday:NaN}get localWeekNumber(){return this.isValid?st(this).weekNumber:NaN}get localWeekYear(){return this.isValid?st(this).weekYear:NaN}get ordinal(){return this.isValid?Ge(this.c).ordinal:NaN}get monthShort(){return this.isValid?Fe.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?Fe.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?Fe.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?Fe.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}getPossibleOffsets(){if(!this.isValid||this.isOffsetFixed)return[this];const e=864e5,t=6e4,n=Ie(this.c),s=this.zone.offset(n-e),i=this.zone.offset(n+e),a=this.zone.offset(n-s*t),o=this.zone.offset(n-i*t);if(a===o)return[this];const u=n-a*t,l=n-o*t,f=Ae(u,a),h=Ae(l,o);return f.hour===h.hour&&f.minute===h.minute&&f.second===h.second&&f.millisecond===h.millisecond?[G(this,{ts:u}),G(this,{ts:l})]:[this]}get isInLeapYear(){return le(this.year)}get daysInMonth(){return be(this.year,this.month)}get daysInYear(){return this.isValid?_(this.year):NaN}get weeksInWeekYear(){return this.isValid?ce(this.weekYear):NaN}get weeksInLocalWeekYear(){return this.isValid?ce(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}resolvedLocaleOptions(e={}){const{locale:t,numberingSystem:n,calendar:s}=b.create(this.loc.clone(e),e).resolvedOptions(this);return{locale:t,numberingSystem:n,outputCalendar:s}}toUTC(e=0,t={}){return this.setZone(I.instance(e),t)}toLocal(){return this.setZone(T.defaultZone)}setZone(e,{keepLocalTime:t=!1,keepCalendarTime:n=!1}={}){if(e=Z(e,T.defaultZone),e.equals(this.zone))return this;if(e.isValid){let s=this.ts;if(t||n){const i=e.offset(this.ts),a=this.toObject();[s]=We(a,i,e)}return G(this,{ts:s,zone:e})}else return y.invalid(me(e))}reconfigure({locale:e,numberingSystem:t,outputCalendar:n}={}){const s=this.loc.clone({locale:e,numberingSystem:t,outputCalendar:n});return G(this,{loc:s})}setLocale(e){return this.reconfigure({locale:e})}set(e){if(!this.isValid)return this;const t=Me(e,Ln),{minDaysInFirstWeek:n,startOfWeek:s}=Bt(t,this.loc),i=!g(t.weekYear)||!g(t.weekNumber)||!g(t.weekday),a=!g(t.ordinal),o=!g(t.year),u=!g(t.month)||!g(t.day),l=o||u,f=t.weekYear||t.weekNumber;if((l||a)&&f)throw new j("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(u&&a)throw new j("Can't mix ordinal dates with month/day");let h;i?h=Yt({...Ne(this.c,n,s),...t},n,s):g(t.ordinal)?(h={...this.toObject(),...t},g(t.day)&&(h.day=Math.min(be(h.year,h.month),h.day))):h=Jt({...Ge(this.c),...t});const[k,m]=We(h,this.o,this.zone);return G(this,{ts:k,o:m})}plus(e){if(!this.isValid)return this;const t=p.fromDurationLike(e);return G(this,Vn(this,t))}minus(e){if(!this.isValid)return this;const t=p.fromDurationLike(e).negate();return G(this,Vn(this,t))}startOf(e,{useLocaleWeeks:t=!1}={}){if(!this.isValid)return this;const n={},s=p.normalizeUnit(e);switch(s){case"years":n.month=1;case"quarters":case"months":n.day=1;case"weeks":case"days":n.hour=0;case"hours":n.minute=0;case"minutes":n.second=0;case"seconds":n.millisecond=0;break}if(s==="weeks")if(t){const i=this.loc.getStartOfWeek(),{weekday:a}=this;athis.valueOf(),o=a?this:e,u=a?e:this,l=Es(o,u,i,s);return a?l.negate():l}diffNow(e="milliseconds",t={}){return this.diff(y.now(),e,t)}until(e){return this.isValid?O.fromDateTimes(this,e):this}hasSame(e,t,n){if(!this.isValid)return!1;const s=e.valueOf(),i=this.setZone(e.zone,{keepLocalTime:!0});return i.startOf(t,n)<=s&&s<=i.endOf(t,n)}equals(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)}toRelative(e={}){if(!this.isValid)return null;const t=e.base||y.fromObject({},{zone:this.zone}),n=e.padding?thist.valueOf(),Math.min)}static max(...e){if(!e.every(y.isDateTime))throw new x("max requires all arguments be DateTimes");return _t(e,t=>t.valueOf(),Math.max)}static fromFormatExplain(e,t,n={}){const{locale:s=null,numberingSystem:i=null}=n,a=S.fromOpts({locale:s,numberingSystem:i,defaultToEN:!0});return vn(a,e,t)}static fromStringExplain(e,t,n={}){return y.fromFormatExplain(e,t,n)}static buildFormatParser(e,t={}){const{locale:n=null,numberingSystem:s=null}=t,i=S.fromOpts({locale:n,numberingSystem:s,defaultToEN:!0});return new In(i,e)}static fromFormatParser(e,t,n={}){if(g(e)||g(t))throw new x("fromFormatParser requires an input string and a format parser");const{locale:s=null,numberingSystem:i=null}=n,a=S.fromOpts({locale:s,numberingSystem:i,defaultToEN:!0});if(!a.equals(t.locale))throw new x(`fromFormatParser called with a locale of ${a}, but the format parser was created for ${t.locale}`);const{result:o,zone:u,specificOffset:l,invalidReason:f}=t.explainFromTokens(e);return f?y.invalid(f):ie(o,u,n,`format ${t.format}`,e,l)}static get DATE_SHORT(){return Se}static get DATE_MED(){return ct}static get DATE_MED_WITH_WEEKDAY(){return Jn}static get DATE_FULL(){return ft}static get DATE_HUGE(){return dt}static get TIME_SIMPLE(){return ht}static get TIME_WITH_SECONDS(){return mt}static get TIME_WITH_SHORT_OFFSET(){return yt}static get TIME_WITH_LONG_OFFSET(){return gt}static get TIME_24_SIMPLE(){return pt}static get TIME_24_WITH_SECONDS(){return wt}static get TIME_24_WITH_SHORT_OFFSET(){return St}static get TIME_24_WITH_LONG_OFFSET(){return kt}static get DATETIME_SHORT(){return Tt}static get DATETIME_SHORT_WITH_SECONDS(){return Ot}static get DATETIME_MED(){return Et}static get DATETIME_MED_WITH_SECONDS(){return Nt}static get DATETIME_MED_WITH_WEEKDAY(){return Bn}static get DATETIME_FULL(){return xt}static get DATETIME_FULL_WITH_SECONDS(){return bt}static get DATETIME_HUGE(){return It}static get DATETIME_HUGE_WITH_SECONDS(){return vt}}function ye(r){if(y.isDateTime(r))return r;if(r&&r.valueOf&&q(r.valueOf()))return y.fromJSDate(r);if(r&&typeof r=="object")return y.fromObject(r);throw new x(`Unknown datetime argument: ${r}, of type ${typeof r}`)}const zs=[".jpg",".jpeg",".png",".svg",".gif",".jfif",".webp",".avif"],Ps=[".mp4",".avi",".mov",".3gp",".wmv"],Ys=[".aa",".aac",".m4v",".mp3",".ogg",".oga",".mogg",".amr"],Js=[".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".odp",".odt",".ods",".txt"];class d{static isObject(e){return e!==null&&typeof e=="object"&&e.constructor===Object}static clone(e){return typeof structuredClone<"u"?structuredClone(e):JSON.parse(JSON.stringify(e))}static zeroValue(e){switch(typeof e){case"string":return"";case"number":return 0;case"boolean":return!1;case"object":return e===null?null:Array.isArray(e)?[]:{};case"undefined":return;default:return null}}static isEmpty(e){return e===""||e===null||typeof e>"u"||Array.isArray(e)&&e.length===0||d.isObject(e)&&Object.keys(e).length===0}static isInput(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return t==="input"||t==="select"||t==="textarea"||(e==null?void 0:e.isContentEditable)}static isFocusable(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return d.isInput(e)||t==="button"||t==="a"||t==="details"||(e==null?void 0:e.tabIndex)>=0}static hasNonEmptyProps(e){for(let t in e)if(!d.isEmpty(e[t]))return!0;return!1}static toArray(e,t=!1){return Array.isArray(e)?e.slice():(t||!d.isEmpty(e))&&typeof e<"u"?[e]:[]}static inArray(e,t){e=Array.isArray(e)?e:[];for(let n=e.length-1;n>=0;n--)if(e[n]==t)return!0;return!1}static removeByValue(e,t){e=Array.isArray(e)?e:[];for(let n=e.length-1;n>=0;n--)if(e[n]==t){e.splice(n,1);break}}static pushUnique(e,t){d.inArray(e,t)||e.push(t)}static mergeUnique(e,t){for(let n of t)d.pushUnique(e,n);return e}static findByKey(e,t,n){e=Array.isArray(e)?e:[];for(let s in e)if(e[s][t]==n)return e[s];return null}static groupByKey(e,t){e=Array.isArray(e)?e:[];const n={};for(let s in e)n[e[s][t]]=n[e[s][t]]||[],n[e[s][t]].push(e[s]);return n}static removeByKey(e,t,n){for(let s in e)if(e[s][t]==n){e.splice(s,1);break}}static pushOrReplaceByKey(e,t,n="id"){for(let s=e.length-1;s>=0;s--)if(e[s][n]==t[n]){e[s]=t;return}e.push(t)}static filterDuplicatesByKey(e,t="id"){e=Array.isArray(e)?e:[];const n={};for(const s of e)n[s[t]]=s;return Object.values(n)}static filterRedactedProps(e,t="******"){const n=JSON.parse(JSON.stringify(e||{}));for(let s in n)typeof n[s]=="object"&&n[s]!==null?n[s]=d.filterRedactedProps(n[s],t):n[s]===t&&delete n[s];return n}static getNestedVal(e,t,n=null,s="."){let i=e||{},a=(t||"").split(s);for(const o of a){if(!d.isObject(i)&&!Array.isArray(i)||typeof i[o]>"u")return n;i=i[o]}return i}static setByPath(e,t,n,s="."){if(e===null||typeof e!="object"){console.warn("setByPath: data not an object or array.");return}let i=e,a=t.split(s),o=a.pop();for(const u of a)(!d.isObject(i)&&!Array.isArray(i)||!d.isObject(i[u])&&!Array.isArray(i[u]))&&(i[u]={}),i=i[u];i[o]=n}static deleteByPath(e,t,n="."){let s=e||{},i=(t||"").split(n),a=i.pop();for(const o of i)(!d.isObject(s)&&!Array.isArray(s)||!d.isObject(s[o])&&!Array.isArray(s[o]))&&(s[o]={}),s=s[o];Array.isArray(s)?s.splice(a,1):d.isObject(s)&&delete s[a],i.length>0&&(Array.isArray(s)&&!s.length||d.isObject(s)&&!Object.keys(s).length)&&(Array.isArray(e)&&e.length>0||d.isObject(e)&&Object.keys(e).length>0)&&d.deleteByPath(e,i.join(n),n)}static randomString(e=10){let t="",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let s=0;s"u")return d.randomString(e);const t=new Uint8Array(e);crypto.getRandomValues(t);const n="-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";let s="";for(let i=0;ii.replaceAll("{_PB_ESCAPED_}",t));for(let i of s)i=i.trim(),d.isEmpty(i)||n.push(i);return n}static joinNonEmpty(e,t=", "){e=e||[];const n=[],s=t.length>1?t.trim():t;for(let i of e)i=typeof i=="string"?i.trim():"",d.isEmpty(i)||n.push(i.replaceAll(s,"\\"+s));return n.join(t)}static getInitials(e){if(e=(e||"").split("@")[0].trim(),e.length<=2)return e.toUpperCase();const t=e.split(/[\.\_\-\ ]/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e[0].toUpperCase()}static formattedFileSize(e){const t=e?Math.floor(Math.log(e)/Math.log(1024)):0;return(e/Math.pow(1024,t)).toFixed(2)*1+" "+["B","KB","MB","GB","TB"][t]}static getDateTime(e){if(typeof e=="string"){const t={19:"yyyy-MM-dd HH:mm:ss",23:"yyyy-MM-dd HH:mm:ss.SSS",20:"yyyy-MM-dd HH:mm:ss'Z'",24:"yyyy-MM-dd HH:mm:ss.SSS'Z'"},n=t[e.length]||t[19];return y.fromFormat(e,n,{zone:"UTC"})}return typeof e=="number"?y.fromMillis(e):y.fromJSDate(e)}static formatToUTCDate(e,t="yyyy-MM-dd HH:mm:ss"){return d.getDateTime(e).toUTC().toFormat(t)}static formatToLocalDate(e,t="yyyy-MM-dd HH:mm:ss"){return d.getDateTime(e).toLocal().toFormat(t)}static async copyToClipboard(e){var t;if(e=""+e,!(!e.length||!((t=window==null?void 0:window.navigator)!=null&&t.clipboard)))return window.navigator.clipboard.writeText(e).catch(n=>{console.warn("Failed to copy.",n)})}static download(e,t){const n=document.createElement("a");n.setAttribute("href",e),n.setAttribute("download",t),n.setAttribute("target","_blank"),n.click(),n.remove()}static downloadJson(e,t){t=t.endsWith(".json")?t:t+".json";const n=new Blob([JSON.stringify(e,null,2)],{type:"application/json"}),s=window.URL.createObjectURL(n);d.download(s,t)}static getJWTPayload(e){const t=(e||"").split(".")[1]||"";if(t==="")return{};try{const n=decodeURIComponent(atob(t));return JSON.parse(n)||{}}catch(n){console.warn("Failed to parse JWT payload data.",n)}return{}}static hasImageExtension(e){return e=e||"",!!zs.find(t=>e.toLowerCase().endsWith(t))}static hasVideoExtension(e){return e=e||"",!!Ps.find(t=>e.toLowerCase().endsWith(t))}static hasAudioExtension(e){return e=e||"",!!Ys.find(t=>e.toLowerCase().endsWith(t))}static hasDocumentExtension(e){return e=e||"",!!Js.find(t=>e.toLowerCase().endsWith(t))}static getFileType(e){return d.hasImageExtension(e)?"image":d.hasDocumentExtension(e)?"document":d.hasVideoExtension(e)?"video":d.hasAudioExtension(e)?"audio":"file"}static generateThumb(e,t=100,n=100){return new Promise(s=>{let i=new FileReader;i.onload=function(a){let o=new Image;o.onload=function(){let u=document.createElement("canvas"),l=u.getContext("2d"),f=o.width,h=o.height;return u.width=t,u.height=n,l.drawImage(o,f>h?(f-h)/2:0,0,f>h?h:f,f>h?h:f,0,0,t,n),s(u.toDataURL(e.type))},o.src=a.target.result},i.readAsDataURL(e)})}static addValueToFormData(e,t,n){if(!(typeof n>"u"))if(d.isEmpty(n))e.append(t,"");else if(Array.isArray(n))for(const s of n)d.addValueToFormData(e,t,s);else n instanceof File?e.append(t,n):n instanceof Date?e.append(t,n.toISOString()):d.isObject(n)?e.append(t,JSON.stringify(n)):e.append(t,""+n)}static dummyCollectionRecord(e){return Object.assign({collectionId:e==null?void 0:e.id,collectionName:e==null?void 0:e.name},d.dummyCollectionSchemaData(e))}static dummyCollectionSchemaData(e,t=!1){var i;const n=(e==null?void 0:e.fields)||[],s={};for(const a of n){if(a.hidden||t&&a.primaryKey&&a.autogeneratePattern)continue;let o=null;if(a.type==="number")o=123;else if(a.type==="date"||a.type==="autodate")o="2022-01-01 10:00:00.123Z";else if(a.type=="bool")o=!0;else if(a.type=="email")o="test@example.com";else if(a.type=="url")o="https://example.com";else if(a.type=="json")o="JSON";else if(a.type=="file"){if(t)continue;o="filename.jpg",a.maxSelect!=1&&(o=[o])}else a.type=="select"?(o=(i=a==null?void 0:a.values)==null?void 0:i[0],(a==null?void 0:a.maxSelect)!=1&&(o=[o])):a.type=="relation"?(o="RELATION_RECORD_ID",(a==null?void 0:a.maxSelect)!=1&&(o=[o])):o="test";s[a.name]=o}return s}static getCollectionTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"auth":return"ri-group-line";case"view":return"ri-table-line";default:return"ri-folder-2-line"}}static getFieldTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"primary":return"ri-key-line";case"text":return"ri-text";case"number":return"ri-hashtag";case"date":return"ri-calendar-line";case"bool":return"ri-toggle-line";case"email":return"ri-mail-line";case"url":return"ri-link";case"editor":return"ri-edit-2-line";case"select":return"ri-list-check";case"json":return"ri-braces-line";case"file":return"ri-image-line";case"relation":return"ri-mind-map";case"password":return"ri-lock-password-line";case"autodate":return"ri-calendar-check-line";default:return"ri-star-s-line"}}static getFieldValueType(e){switch(e==null?void 0:e.type){case"bool":return"Boolean";case"number":return"Number";case"file":return"File";case"select":case"relation":return(e==null?void 0:e.maxSelect)==1?"String":"Array";default:return"String"}}static zeroDefaultStr(e){return(e==null?void 0:e.type)==="number"?"0":(e==null?void 0:e.type)==="bool"?"false":(e==null?void 0:e.type)==="json"?'null, "", [], {}':["select","relation","file"].includes(e==null?void 0:e.type)&&(e==null?void 0:e.maxSelect)!=1?"[]":'""'}static getApiExampleUrl(e){return(window.location.href.substring(0,window.location.href.indexOf("/_"))||e||"/").replace("//localhost","//127.0.0.1")}static hasCollectionChanges(e,t,n=!1){if(e=e||{},t=t||{},e.id!=t.id)return!0;for(let l in e)if(l!=="fields"&&JSON.stringify(e[l])!==JSON.stringify(t[l]))return!0;const s=Array.isArray(e.fields)?e.fields:[],i=Array.isArray(t.fields)?t.fields:[],a=s.filter(l=>(l==null?void 0:l.id)&&!d.findByKey(i,"id",l.id)),o=i.filter(l=>(l==null?void 0:l.id)&&!d.findByKey(s,"id",l.id)),u=i.filter(l=>{const f=d.isObject(l)&&d.findByKey(s,"id",l.id);if(!f)return!1;for(let h in f)if(JSON.stringify(l[h])!=JSON.stringify(f[h]))return!0;return!1});return!!(o.length||u.length||n&&a.length)}static sortCollections(e=[]){const t=[],n=[],s=[];for(const a of e)a.type==="auth"?t.push(a):a.type==="base"?n.push(a):s.push(a);function i(a,o){return a.name>o.name?1:a.name0){const o=d.getExpandPresentableRelField(a,t,n-1);o&&(i+="."+o)}return i}return""}static yieldToMain(){return new Promise(e=>{setTimeout(e,0)})}static defaultFlatpickrOptions(){return{dateFormat:"Y-m-d H:i:S",disableMobile:!0,allowInput:!0,enableTime:!0,time_24hr:!0,locale:{firstDayOfWeek:1}}}static defaultEditorOptions(){const e=["DIV","P","A","EM","B","STRONG","H1","H2","H3","H4","H5","H6","TABLE","TR","TD","TH","TBODY","THEAD","TFOOT","BR","HR","Q","SUP","SUB","DEL","IMG","OL","UL","LI","CODE"];function t(s){let i=s.parentNode;for(;s.firstChild;)i.insertBefore(s.firstChild,s);i.removeChild(s)}function n(s){if(s){for(const i of s.children)n(i);e.includes(s.tagName)?(s.removeAttribute("style"),s.removeAttribute("class")):t(s)}}return{branding:!1,promotion:!1,menubar:!1,min_height:270,height:270,max_height:700,autoresize_bottom_margin:30,convert_unsafe_embeds:!0,skin:"pocketbase",content_style:"body { font-size: 14px }",plugins:["autoresize","autolink","lists","link","image","searchreplace","fullscreen","media","table","code","codesample","directionality"],codesample_global_prismjs:!0,codesample_languages:[{text:"HTML/XML",value:"markup"},{text:"CSS",value:"css"},{text:"SQL",value:"sql"},{text:"JavaScript",value:"javascript"},{text:"Go",value:"go"},{text:"Dart",value:"dart"},{text:"Zig",value:"zig"},{text:"Rust",value:"rust"},{text:"Lua",value:"lua"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"},{text:"Markdown",value:"markdown"},{text:"Swift",value:"swift"},{text:"Kotlin",value:"kotlin"},{text:"Elixir",value:"elixir"},{text:"Scala",value:"scala"},{text:"Julia",value:"julia"},{text:"Haskell",value:"haskell"}],toolbar:"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link image_picker table codesample direction | code fullscreen",paste_postprocess:(s,i)=>{n(i.node)},file_picker_types:"image",file_picker_callback:(s,i,a)=>{const o=document.createElement("input");o.setAttribute("type","file"),o.setAttribute("accept","image/*"),o.addEventListener("change",u=>{const l=u.target.files[0],f=new FileReader;f.addEventListener("load",()=>{if(!tinymce)return;const h="blobid"+new Date().getTime(),k=tinymce.activeEditor.editorUpload.blobCache,m=f.result.split(",")[1],E=k.create(h,l,m);k.add(E),s(E.blobUri(),{title:l.name})}),f.readAsDataURL(l)}),o.click()},setup:s=>{s.on("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.code=="KeyS"&&s.formElement&&(a.preventDefault(),a.stopPropagation(),s.formElement.dispatchEvent(new KeyboardEvent("keydown",a)))});const i="tinymce_last_direction";s.on("init",()=>{var o;const a=(o=window==null?void 0:window.localStorage)==null?void 0:o.getItem(i);!s.isDirty()&&s.getContent()==""&&a=="rtl"&&s.execCommand("mceDirectionRTL")}),s.ui.registry.addMenuButton("direction",{icon:"visualchars",fetch:a=>{a([{type:"menuitem",text:"LTR content",icon:"ltr",onAction:()=>{var u;(u=window==null?void 0:window.localStorage)==null||u.setItem(i,"ltr"),s.execCommand("mceDirectionLTR")}},{type:"menuitem",text:"RTL content",icon:"rtl",onAction:()=>{var u;(u=window==null?void 0:window.localStorage)==null||u.setItem(i,"rtl"),s.execCommand("mceDirectionRTL")}}])}}),s.ui.registry.addMenuButton("image_picker",{icon:"image",fetch:a=>{a([{type:"menuitem",text:"From collection",icon:"gallery",onAction:()=>{s.dispatch("collections_file_picker",{})}},{type:"menuitem",text:"Inline",icon:"browse",onAction:()=>{s.execCommand("mceImage")}}])}})}}}static displayValue(e,t,n="N/A"){e=e||{},t=t||[];let s=[];for(const a of t){let o=e[a];typeof o>"u"||(o=d.stringifyValue(o,n),s.push(o))}if(s.length>0)return s.join(", ");const i=["title","name","slug","email","username","nickname","label","heading","message","key","identifier","id"];for(const a of i){let o=d.stringifyValue(e[a],"");if(o)return o}return n}static stringifyValue(e,t="N/A",n=150){if(d.isEmpty(e))return t;if(typeof e=="number")return""+e;if(typeof e=="boolean")return e?"True":"False";if(typeof e=="string")return e=e.indexOf("<")>=0?d.plainText(e):e,d.truncate(e,n)||t;if(Array.isArray(e)&&typeof e[0]!="object")return d.truncate(e.join(","),n);if(typeof e=="object")try{return d.truncate(JSON.stringify(e),n)||t}catch{return t}return e}static extractColumnsFromQuery(e){var a;const t="__GROUP__";e=(e||"").replace(/\([\s\S]+?\)/gm,t).replace(/[\t\r\n]|(?:\s\s)+/g," ");const n=e.match(/select\s+([\s\S]+)\s+from/),s=((a=n==null?void 0:n[1])==null?void 0:a.split(","))||[],i=[];for(let o of s){const u=o.trim().split(" ").pop();u!=""&&u!=t&&i.push(u.replace(/[\'\"\`\[\]\s]/g,""))}return i}static getAllCollectionIdentifiers(e,t=""){if(!e)return[];let n=[t+"id"];if(e.type==="view")for(let i of d.extractColumnsFromQuery(e.viewQuery))d.pushUnique(n,t+i);const s=e.fields||[];for(const i of s)d.pushUnique(n,t+i.name);return n}static getCollectionAutocompleteKeys(e,t,n="",s=0){let i=e.find(o=>o.name==t||o.id==t);if(!i||s>=4)return[];i.fields=i.fields||[];let a=d.getAllCollectionIdentifiers(i,n);for(const o of i.fields){const u=n+o.name;if(o.type=="relation"&&o.collectionId){const l=d.getCollectionAutocompleteKeys(e,o.collectionId,u+".",s+1);l.length&&(a=a.concat(l))}o.maxSelect!=1&&["select","file","relation"].includes(o.type)&&(a.push(u+":each"),a.push(u+":length"))}for(const o of e){o.fields=o.fields||[];for(const u of o.fields)if(u.type=="relation"&&u.collectionId==i.id){const l=n+o.name+"_via_"+u.name,f=d.getCollectionAutocompleteKeys(e,o.id,l+".",s+2);f.length&&(a=a.concat(f))}}return a}static getCollectionJoinAutocompleteKeys(e){const t=[];let n,s;for(const i of e)if(!i.system){n="@collection."+i.name+".",s=d.getCollectionAutocompleteKeys(e,i.name,n);for(const a of s)t.push(a)}return t}static getRequestAutocompleteKeys(e,t){const n=[];n.push("@request.context"),n.push("@request.method"),n.push("@request.query."),n.push("@request.body."),n.push("@request.headers."),n.push("@request.auth.collectionId"),n.push("@request.auth.collectionName");const s=e.filter(i=>i.type==="auth");for(const i of s){if(i.system)continue;const a=d.getCollectionAutocompleteKeys(e,i.id,"@request.auth.");for(const o of a)d.pushUnique(n,o)}if(t){const i=d.getCollectionAutocompleteKeys(e,t,"@request.body.");for(const a of i){n.push(a);const o=a.split(".");o.length===3&&o[2].indexOf(":")===-1&&n.push(a+":isset")}}return n}static parseIndex(e){var u,l,f,h,k;const t={unique:!1,optional:!1,schemaName:"",indexName:"",tableName:"",columns:[],where:""},s=/create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?/gmi.exec((e||"").trim());if((s==null?void 0:s.length)!=7)return t;const i=/^[\"\'\`\[\{}]|[\"\'\`\]\}]$/gm;t.unique=((u=s[1])==null?void 0:u.trim().toLowerCase())==="unique",t.optional=!d.isEmpty((l=s[2])==null?void 0:l.trim());const a=(s[3]||"").split(".");a.length==2?(t.schemaName=a[0].replace(i,""),t.indexName=a[1].replace(i,"")):(t.schemaName="",t.indexName=a[0].replace(i,"")),t.tableName=(s[4]||"").replace(i,"");const o=(s[5]||"").replace(/,(?=[^\(]*\))/gmi,"{PB_TEMP}").split(",");for(let m of o){m=m.trim().replaceAll("{PB_TEMP}",",");const M=/^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$/gmi.exec(m);if((M==null?void 0:M.length)!=4)continue;const D=(h=(f=M[1])==null?void 0:f.trim())==null?void 0:h.replace(i,"");D&&t.columns.push({name:D,collate:M[2]||"",sort:((k=M[3])==null?void 0:k.toUpperCase())||""})}return t.where=s[6]||"",t}static buildIndex(e){let t="CREATE ";e.unique&&(t+="UNIQUE "),t+="INDEX ",e.optional&&(t+="IF NOT EXISTS "),e.schemaName&&(t+=`\`${e.schemaName}\`.`),t+=`\`${e.indexName||"idx_"+d.randomString(10)}\` `,t+=`ON \`${e.tableName}\` (`;const n=e.columns.filter(s=>!!(s!=null&&s.name));return n.length>1&&(t+=` + `),t+=n.map(s=>{let i="";return s.name.includes("(")||s.name.includes(" ")?i+=s.name:i+="`"+s.name+"`",s.collate&&(i+=" COLLATE "+s.collate),s.sort&&(i+=" "+s.sort.toUpperCase()),i}).join(`, + `),n.length>1&&(t+=` +`),t+=")",e.where&&(t+=` WHERE ${e.where}`),t}static replaceIndexTableName(e,t){const n=d.parseIndex(e);return n.tableName=t,d.buildIndex(n)}static replaceIndexColumn(e,t,n){if(t===n)return e;const s=d.parseIndex(e);let i=!1;for(let a of s.columns)a.name===t&&(a.name=n,i=!0);return i?d.buildIndex(s):e}static normalizeSearchFilter(e,t){if(e=(e||"").trim(),!e||!t.length)return e;const n=["=","!=","~","!~",">",">=","<","<="];for(const s of n)if(e.includes(s))return e;return e=isNaN(e)&&e!="true"&&e!="false"?`"${e.replace(/^[\"\'\`]|[\"\'\`]$/gm,"")}"`:e,t.map(s=>`${s}~${e}`).join("||")}static normalizeLogsFilter(e,t=[]){return d.normalizeSearchFilter(e,["level","message","data"].concat(t))}static initSchemaField(e){return Object.assign({id:"",name:"",type:"text",system:!1,hidden:!1,required:!1},e)}static triggerResize(){window.dispatchEvent(new Event("resize"))}static getHashQueryParams(){let e="";const t=window.location.hash.indexOf("?");return t>-1&&(e=window.location.hash.substring(t+1)),Object.fromEntries(new URLSearchParams(e))}static replaceHashQueryParams(e){e=e||{};let t="",n=window.location.hash;const s=n.indexOf("?");s>-1&&(t=n.substring(s+1),n=n.substring(0,s));const i=new URLSearchParams(t);for(let u in e){const l=e[u];l===null?i.delete(u):i.set(u,l)}t=i.toString(),t!=""&&(n+="?"+t);let a=window.location.href;const o=a.indexOf("#");o>-1&&(a=a.substring(0,o)),window.location.replace(a+n)}}const at=11e3;onmessage=r=>{var t,n;if(!r.data.collections)return;const e={};e.baseKeys=d.getCollectionAutocompleteKeys(r.data.collections,(t=r.data.baseCollection)==null?void 0:t.name),e.baseKeys=ut(e.baseKeys.sort(ot),at),r.data.disableRequestKeys||(e.requestKeys=d.getRequestAutocompleteKeys(r.data.collections,(n=r.data.baseCollection)==null?void 0:n.name),e.requestKeys=ut(e.requestKeys.sort(ot),at)),r.data.disableCollectionJoinKeys||(e.collectionJoinKeys=d.getCollectionJoinAutocompleteKeys(r.data.collections),e.collectionJoinKeys=ut(e.collectionJoinKeys.sort(ot),at)),postMessage(e)};function ot(r,e){return r.length-e.length}function ut(r,e){return r.length>e?r.slice(0,e):r}})(); diff --git a/ui/dist/assets/autocomplete.worker-Dy9W6Fpj.js b/ui/dist/assets/autocomplete.worker-Dy9W6Fpj.js deleted file mode 100644 index 68c8bfe7..00000000 --- a/ui/dist/assets/autocomplete.worker-Dy9W6Fpj.js +++ /dev/null @@ -1,4 +0,0 @@ -(function(){"use strict";class P extends Error{}class $n extends P{constructor(e){super(`Invalid DateTime: ${e.toMessage()}`)}}class Zn extends P{constructor(e){super(`Invalid Interval: ${e.toMessage()}`)}}class Un extends P{constructor(e){super(`Invalid Duration: ${e.toMessage()}`)}}class G extends P{}class at extends P{constructor(e){super(`Invalid unit ${e}`)}}class D extends P{}class Z extends P{constructor(){super("Zone is an abstract class")}}const f="numeric",C="short",M="long",ge={year:f,month:f,day:f},ot={year:f,month:C,day:f},qn={year:f,month:C,day:f,weekday:C},ut={year:f,month:M,day:f},lt={year:f,month:M,day:f,weekday:M},ct={hour:f,minute:f},ft={hour:f,minute:f,second:f},dt={hour:f,minute:f,second:f,timeZoneName:C},ht={hour:f,minute:f,second:f,timeZoneName:M},mt={hour:f,minute:f,hourCycle:"h23"},yt={hour:f,minute:f,second:f,hourCycle:"h23"},gt={hour:f,minute:f,second:f,hourCycle:"h23",timeZoneName:C},pt={hour:f,minute:f,second:f,hourCycle:"h23",timeZoneName:M},wt={year:f,month:f,day:f,hour:f,minute:f},St={year:f,month:f,day:f,hour:f,minute:f,second:f},kt={year:f,month:C,day:f,hour:f,minute:f},Tt={year:f,month:C,day:f,hour:f,minute:f,second:f},zn={year:f,month:C,day:f,weekday:C,hour:f,minute:f},Ot={year:f,month:M,day:f,hour:f,minute:f,timeZoneName:C},Nt={year:f,month:M,day:f,hour:f,minute:f,second:f,timeZoneName:C},Et={year:f,month:M,day:f,weekday:M,hour:f,minute:f,timeZoneName:M},xt={year:f,month:M,day:f,weekday:M,hour:f,minute:f,second:f,timeZoneName:M};class ie{get type(){throw new Z}get name(){throw new Z}get ianaName(){return this.name}get isUniversal(){throw new Z}offsetName(e,t){throw new Z}formatOffset(e,t){throw new Z}offset(e){throw new Z}equals(e){throw new Z}get isValid(){throw new Z}}let Ce=null;class pe extends ie{static get instance(){return Ce===null&&(Ce=new pe),Ce}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(e,{format:t,locale:n}){return Kt(e,t,n)}formatOffset(e,t){return le(this.offset(e),t)}offset(e){return-new Date(e).getTimezoneOffset()}equals(e){return e.type==="system"}get isValid(){return!0}}let we={};function Pn(s){return we[s]||(we[s]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:s,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),we[s]}const Yn={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function Jn(s,e){const t=s.format(e).replace(/\u200E/g,""),n=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(t),[,r,i,a,o,u,l,c]=n;return[a,r,i,o,u,l,c]}function Bn(s,e){const t=s.formatToParts(e),n=[];for(let r=0;r=0?N:1e3+N,(p-m)/(60*1e3)}equals(e){return e.type==="iana"&&e.name===this.name}get isValid(){return this.valid}}let bt={};function jn(s,e={}){const t=JSON.stringify([s,e]);let n=bt[t];return n||(n=new Intl.ListFormat(s,e),bt[t]=n),n}let We={};function Le(s,e={}){const t=JSON.stringify([s,e]);let n=We[t];return n||(n=new Intl.DateTimeFormat(s,e),We[t]=n),n}let Re={};function Gn(s,e={}){const t=JSON.stringify([s,e]);let n=Re[t];return n||(n=new Intl.NumberFormat(s,e),Re[t]=n),n}let $e={};function Kn(s,e={}){const{base:t,...n}=e,r=JSON.stringify([s,n]);let i=$e[r];return i||(i=new Intl.RelativeTimeFormat(s,e),$e[r]=i),i}let ae=null;function Hn(){return ae||(ae=new Intl.DateTimeFormat().resolvedOptions().locale,ae)}let It={};function _n(s){let e=It[s];if(!e){const t=new Intl.Locale(s);e="getWeekInfo"in t?t.getWeekInfo():t.weekInfo,It[s]=e}return e}function Qn(s){const e=s.indexOf("-x-");e!==-1&&(s=s.substring(0,e));const t=s.indexOf("-u-");if(t===-1)return[s];{let n,r;try{n=Le(s).resolvedOptions(),r=s}catch{const u=s.substring(0,t);n=Le(u).resolvedOptions(),r=u}const{numberingSystem:i,calendar:a}=n;return[r,i,a]}}function Xn(s,e,t){return(t||e)&&(s.includes("-u-")||(s+="-u"),t&&(s+=`-ca-${t}`),e&&(s+=`-nu-${e}`)),s}function es(s){const e=[];for(let t=1;t<=12;t++){const n=g.utc(2009,t,1);e.push(s(n))}return e}function ts(s){const e=[];for(let t=1;t<=7;t++){const n=g.utc(2016,11,13+t);e.push(s(n))}return e}function ke(s,e,t,n){const r=s.listingMode();return r==="error"?null:r==="en"?t(e):n(e)}function ns(s){return s.numberingSystem&&s.numberingSystem!=="latn"?!1:s.numberingSystem==="latn"||!s.locale||s.locale.startsWith("en")||new Intl.DateTimeFormat(s.intl).resolvedOptions().numberingSystem==="latn"}class ss{constructor(e,t,n){this.padTo=n.padTo||0,this.floor=n.floor||!1;const{padTo:r,floor:i,...a}=n;if(!t||Object.keys(a).length>0){const o={useGrouping:!1,...n};n.padTo>0&&(o.minimumIntegerDigits=n.padTo),this.inf=Gn(e,o)}}format(e){if(this.inf){const t=this.floor?Math.floor(e):e;return this.inf.format(t)}else{const t=this.floor?Math.floor(e):Je(e,3);return b(t,this.padTo)}}}class rs{constructor(e,t,n){this.opts=n,this.originalZone=void 0;let r;if(this.opts.timeZone)this.dt=e;else if(e.zone.type==="fixed"){const a=-1*(e.offset/60),o=a>=0?`Etc/GMT+${a}`:`Etc/GMT${a}`;e.offset!==0&&$.create(o).valid?(r=o,this.dt=e):(r="UTC",this.dt=e.offset===0?e:e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone)}else e.zone.type==="system"?this.dt=e:e.zone.type==="iana"?(this.dt=e,r=e.zone.name):(r="UTC",this.dt=e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone);const i={...this.opts};i.timeZone=i.timeZone||r,this.dtf=Le(t,i)}format(){return this.originalZone?this.formatToParts().map(({value:e})=>e).join(""):this.dtf.format(this.dt.toJSDate())}formatToParts(){const e=this.dtf.formatToParts(this.dt.toJSDate());return this.originalZone?e.map(t=>{if(t.type==="timeZoneName"){const n=this.originalZone.offsetName(this.dt.ts,{locale:this.dt.locale,format:this.opts.timeZoneName});return{...t,value:n}}else return t}):e}resolvedOptions(){return this.dtf.resolvedOptions()}}class is{constructor(e,t,n){this.opts={style:"long",...n},!t&&Jt()&&(this.rtf=Kn(e,n))}format(e,t){return this.rtf?this.rtf.format(e,t):xs(t,e,this.opts.numeric,this.opts.style!=="long")}formatToParts(e,t){return this.rtf?this.rtf.formatToParts(e,t):[]}}const as={firstDay:1,minimalDays:4,weekend:[6,7]};class T{static fromOpts(e){return T.create(e.locale,e.numberingSystem,e.outputCalendar,e.weekSettings,e.defaultToEN)}static create(e,t,n,r,i=!1){const a=e||x.defaultLocale,o=a||(i?"en-US":Hn()),u=t||x.defaultNumberingSystem,l=n||x.defaultOutputCalendar,c=Pe(r)||x.defaultWeekSettings;return new T(o,u,l,c,a)}static resetCache(){ae=null,We={},Re={},$e={}}static fromObject({locale:e,numberingSystem:t,outputCalendar:n,weekSettings:r}={}){return T.create(e,t,n,r)}constructor(e,t,n,r,i){const[a,o,u]=Qn(e);this.locale=a,this.numberingSystem=t||o||null,this.outputCalendar=n||u||null,this.weekSettings=r,this.intl=Xn(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=i,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=ns(this)),this.fastNumbersCached}listingMode(){const e=this.isEnglish(),t=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return e&&t?"en":"intl"}clone(e){return!e||Object.getOwnPropertyNames(e).length===0?this:T.create(e.locale||this.specifiedLocale,e.numberingSystem||this.numberingSystem,e.outputCalendar||this.outputCalendar,Pe(e.weekSettings)||this.weekSettings,e.defaultToEN||!1)}redefaultToEN(e={}){return this.clone({...e,defaultToEN:!0})}redefaultToSystem(e={}){return this.clone({...e,defaultToEN:!1})}months(e,t=!1){return ke(this,e,Qt,()=>{const n=t?{month:e,day:"numeric"}:{month:e},r=t?"format":"standalone";return this.monthsCache[r][e]||(this.monthsCache[r][e]=es(i=>this.extract(i,n,"month"))),this.monthsCache[r][e]})}weekdays(e,t=!1){return ke(this,e,tn,()=>{const n=t?{weekday:e,year:"numeric",month:"long",day:"numeric"}:{weekday:e},r=t?"format":"standalone";return this.weekdaysCache[r][e]||(this.weekdaysCache[r][e]=ts(i=>this.extract(i,n,"weekday"))),this.weekdaysCache[r][e]})}meridiems(){return ke(this,void 0,()=>nn,()=>{if(!this.meridiemCache){const e={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[g.utc(2016,11,13,9),g.utc(2016,11,13,19)].map(t=>this.extract(t,e,"dayperiod"))}return this.meridiemCache})}eras(e){return ke(this,e,sn,()=>{const t={era:e};return this.eraCache[e]||(this.eraCache[e]=[g.utc(-40,1,1),g.utc(2017,1,1)].map(n=>this.extract(n,t,"era"))),this.eraCache[e]})}extract(e,t,n){const r=this.dtFormatter(e,t),i=r.formatToParts(),a=i.find(o=>o.type.toLowerCase()===n);return a?a.value:null}numberFormatter(e={}){return new ss(this.intl,e.forceSimple||this.fastNumbers,e)}dtFormatter(e,t={}){return new rs(e,this.intl,t)}relFormatter(e={}){return new is(this.intl,this.isEnglish(),e)}listFormatter(e={}){return jn(this.intl,e)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")}getWeekSettings(){return this.weekSettings?this.weekSettings:Bt()?_n(this.locale):as}getStartOfWeek(){return this.getWeekSettings().firstDay}getMinDaysInFirstWeek(){return this.getWeekSettings().minimalDays}getWeekendDays(){return this.getWeekSettings().weekend}equals(e){return this.locale===e.locale&&this.numberingSystem===e.numberingSystem&&this.outputCalendar===e.outputCalendar}}let Ze=null;class v extends ie{static get utcInstance(){return Ze===null&&(Ze=new v(0)),Ze}static instance(e){return e===0?v.utcInstance:new v(e)}static parseSpecifier(e){if(e){const t=e.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(t)return new v(xe(t[1],t[2]))}return null}constructor(e){super(),this.fixed=e}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${le(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${le(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(e,t){return le(this.fixed,t)}get isUniversal(){return!0}offset(){return this.fixed}equals(e){return e.type==="fixed"&&e.fixed===this.fixed}get isValid(){return!0}}class os extends ie{constructor(e){super(),this.zoneName=e}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}}function U(s,e){if(y(s)||s===null)return e;if(s instanceof ie)return s;if(cs(s)){const t=s.toLowerCase();return t==="default"?e:t==="local"||t==="system"?pe.instance:t==="utc"||t==="gmt"?v.utcInstance:v.parseSpecifier(t)||$.create(s)}else return Y(s)?v.instance(s):typeof s=="object"&&"offset"in s&&typeof s.offset=="function"?s:new os(s)}let vt=()=>Date.now(),Dt="system",Mt=null,Ft=null,At=null,Vt=60,Ct,Wt=null;class x{static get now(){return vt}static set now(e){vt=e}static set defaultZone(e){Dt=e}static get defaultZone(){return U(Dt,pe.instance)}static get defaultLocale(){return Mt}static set defaultLocale(e){Mt=e}static get defaultNumberingSystem(){return Ft}static set defaultNumberingSystem(e){Ft=e}static get defaultOutputCalendar(){return At}static set defaultOutputCalendar(e){At=e}static get defaultWeekSettings(){return Wt}static set defaultWeekSettings(e){Wt=Pe(e)}static get twoDigitCutoffYear(){return Vt}static set twoDigitCutoffYear(e){Vt=e%100}static get throwOnInvalid(){return Ct}static set throwOnInvalid(e){Ct=e}static resetCaches(){T.resetCache(),$.resetCache()}}class W{constructor(e,t){this.reason=e,this.explanation=t}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}}const Lt=[0,31,59,90,120,151,181,212,243,273,304,334],Rt=[0,31,60,91,121,152,182,213,244,274,305,335];function F(s,e){return new W("unit out of range",`you specified ${e} (of type ${typeof e}) as a ${s}, which is invalid`)}function Ue(s,e,t){const n=new Date(Date.UTC(s,e-1,t));s<100&&s>=0&&n.setUTCFullYear(n.getUTCFullYear()-1900);const r=n.getUTCDay();return r===0?7:r}function $t(s,e,t){return t+(oe(s)?Rt:Lt)[e-1]}function Zt(s,e){const t=oe(s)?Rt:Lt,n=t.findIndex(i=>iue(n,e,t)?(l=n+1,u=1):l=n,{weekYear:l,weekNumber:u,weekday:o,...Ie(s)}}function Ut(s,e=4,t=1){const{weekYear:n,weekNumber:r,weekday:i}=s,a=qe(Ue(n,1,e),t),o=H(n);let u=r*7+i-a-7+e,l;u<1?(l=n-1,u+=H(l)):u>o?(l=n+1,u-=H(n)):l=n;const{month:c,day:h}=Zt(l,u);return{year:l,month:c,day:h,...Ie(s)}}function ze(s){const{year:e,month:t,day:n}=s,r=$t(e,t,n);return{year:e,ordinal:r,...Ie(s)}}function qt(s){const{year:e,ordinal:t}=s,{month:n,day:r}=Zt(e,t);return{year:e,month:n,day:r,...Ie(s)}}function zt(s,e){if(!y(s.localWeekday)||!y(s.localWeekNumber)||!y(s.localWeekYear)){if(!y(s.weekday)||!y(s.weekNumber)||!y(s.weekYear))throw new G("Cannot mix locale-based week fields with ISO-based week fields");return y(s.localWeekday)||(s.weekday=s.localWeekday),y(s.localWeekNumber)||(s.weekNumber=s.localWeekNumber),y(s.localWeekYear)||(s.weekYear=s.localWeekYear),delete s.localWeekday,delete s.localWeekNumber,delete s.localWeekYear,{minDaysInFirstWeek:e.getMinDaysInFirstWeek(),startOfWeek:e.getStartOfWeek()}}else return{minDaysInFirstWeek:4,startOfWeek:1}}function us(s,e=4,t=1){const n=Oe(s.weekYear),r=A(s.weekNumber,1,ue(s.weekYear,e,t)),i=A(s.weekday,1,7);return n?r?i?!1:F("weekday",s.weekday):F("week",s.weekNumber):F("weekYear",s.weekYear)}function ls(s){const e=Oe(s.year),t=A(s.ordinal,1,H(s.year));return e?t?!1:F("ordinal",s.ordinal):F("year",s.year)}function Pt(s){const e=Oe(s.year),t=A(s.month,1,12),n=A(s.day,1,Ne(s.year,s.month));return e?t?n?!1:F("day",s.day):F("month",s.month):F("year",s.year)}function Yt(s){const{hour:e,minute:t,second:n,millisecond:r}=s,i=A(e,0,23)||e===24&&t===0&&n===0&&r===0,a=A(t,0,59),o=A(n,0,59),u=A(r,0,999);return i?a?o?u?!1:F("millisecond",r):F("second",n):F("minute",t):F("hour",e)}function y(s){return typeof s>"u"}function Y(s){return typeof s=="number"}function Oe(s){return typeof s=="number"&&s%1===0}function cs(s){return typeof s=="string"}function fs(s){return Object.prototype.toString.call(s)==="[object Date]"}function Jt(){try{return typeof Intl<"u"&&!!Intl.RelativeTimeFormat}catch{return!1}}function Bt(){try{return typeof Intl<"u"&&!!Intl.Locale&&("weekInfo"in Intl.Locale.prototype||"getWeekInfo"in Intl.Locale.prototype)}catch{return!1}}function ds(s){return Array.isArray(s)?s:[s]}function jt(s,e,t){if(s.length!==0)return s.reduce((n,r)=>{const i=[e(r),r];return n&&t(n[0],i[0])===n[0]?n:i},null)[1]}function hs(s,e){return e.reduce((t,n)=>(t[n]=s[n],t),{})}function K(s,e){return Object.prototype.hasOwnProperty.call(s,e)}function Pe(s){if(s==null)return null;if(typeof s!="object")throw new D("Week settings must be an object");if(!A(s.firstDay,1,7)||!A(s.minimalDays,1,7)||!Array.isArray(s.weekend)||s.weekend.some(e=>!A(e,1,7)))throw new D("Invalid week settings");return{firstDay:s.firstDay,minimalDays:s.minimalDays,weekend:Array.from(s.weekend)}}function A(s,e,t){return Oe(s)&&s>=e&&s<=t}function ms(s,e){return s-e*Math.floor(s/e)}function b(s,e=2){const t=s<0;let n;return t?n="-"+(""+-s).padStart(e,"0"):n=(""+s).padStart(e,"0"),n}function q(s){if(!(y(s)||s===null||s===""))return parseInt(s,10)}function J(s){if(!(y(s)||s===null||s===""))return parseFloat(s)}function Ye(s){if(!(y(s)||s===null||s==="")){const e=parseFloat("0."+s)*1e3;return Math.floor(e)}}function Je(s,e,t=!1){const n=10**e;return(t?Math.trunc:Math.round)(s*n)/n}function oe(s){return s%4===0&&(s%100!==0||s%400===0)}function H(s){return oe(s)?366:365}function Ne(s,e){const t=ms(e-1,12)+1,n=s+(e-t)/12;return t===2?oe(n)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][t-1]}function Ee(s){let e=Date.UTC(s.year,s.month-1,s.day,s.hour,s.minute,s.second,s.millisecond);return s.year<100&&s.year>=0&&(e=new Date(e),e.setUTCFullYear(s.year,s.month-1,s.day)),+e}function Gt(s,e,t){return-qe(Ue(s,1,e),t)+e-1}function ue(s,e=4,t=1){const n=Gt(s,e,t),r=Gt(s+1,e,t);return(H(s)-n+r)/7}function Be(s){return s>99?s:s>x.twoDigitCutoffYear?1900+s:2e3+s}function Kt(s,e,t,n=null){const r=new Date(s),i={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};n&&(i.timeZone=n);const a={timeZoneName:e,...i},o=new Intl.DateTimeFormat(t,a).formatToParts(r).find(u=>u.type.toLowerCase()==="timezonename");return o?o.value:null}function xe(s,e){let t=parseInt(s,10);Number.isNaN(t)&&(t=0);const n=parseInt(e,10)||0,r=t<0||Object.is(t,-0)?-n:n;return t*60+r}function Ht(s){const e=Number(s);if(typeof s=="boolean"||s===""||Number.isNaN(e))throw new D(`Invalid unit value ${s}`);return e}function be(s,e){const t={};for(const n in s)if(K(s,n)){const r=s[n];if(r==null)continue;t[e(n)]=Ht(r)}return t}function le(s,e){const t=Math.trunc(Math.abs(s/60)),n=Math.trunc(Math.abs(s%60)),r=s>=0?"+":"-";switch(e){case"short":return`${r}${b(t,2)}:${b(n,2)}`;case"narrow":return`${r}${t}${n>0?`:${n}`:""}`;case"techie":return`${r}${b(t,2)}${b(n,2)}`;default:throw new RangeError(`Value format ${e} is out of range for property format`)}}function Ie(s){return hs(s,["hour","minute","second","millisecond"])}const ys=["January","February","March","April","May","June","July","August","September","October","November","December"],_t=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],gs=["J","F","M","A","M","J","J","A","S","O","N","D"];function Qt(s){switch(s){case"narrow":return[...gs];case"short":return[..._t];case"long":return[...ys];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}const Xt=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],en=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],ps=["M","T","W","T","F","S","S"];function tn(s){switch(s){case"narrow":return[...ps];case"short":return[...en];case"long":return[...Xt];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}const nn=["AM","PM"],ws=["Before Christ","Anno Domini"],Ss=["BC","AD"],ks=["B","A"];function sn(s){switch(s){case"narrow":return[...ks];case"short":return[...Ss];case"long":return[...ws];default:return null}}function Ts(s){return nn[s.hour<12?0:1]}function Os(s,e){return tn(e)[s.weekday-1]}function Ns(s,e){return Qt(e)[s.month-1]}function Es(s,e){return sn(e)[s.year<0?0:1]}function xs(s,e,t="always",n=!1){const r={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},i=["hours","minutes","seconds"].indexOf(s)===-1;if(t==="auto"&&i){const h=s==="days";switch(e){case 1:return h?"tomorrow":`next ${r[s][0]}`;case-1:return h?"yesterday":`last ${r[s][0]}`;case 0:return h?"today":`this ${r[s][0]}`}}const a=Object.is(e,-0)||e<0,o=Math.abs(e),u=o===1,l=r[s],c=n?u?l[1]:l[2]||l[1]:u?r[s][0]:s;return a?`${o} ${c} ago`:`in ${o} ${c}`}function rn(s,e){let t="";for(const n of s)n.literal?t+=n.val:t+=e(n.val);return t}const bs={D:ge,DD:ot,DDD:ut,DDDD:lt,t:ct,tt:ft,ttt:dt,tttt:ht,T:mt,TT:yt,TTT:gt,TTTT:pt,f:wt,ff:kt,fff:Ot,ffff:Et,F:St,FF:Tt,FFF:Nt,FFFF:xt};class I{static create(e,t={}){return new I(e,t)}static parseFormat(e){let t=null,n="",r=!1;const i=[];for(let a=0;a0&&i.push({literal:r||/^\s+$/.test(n),val:n}),t=null,n="",r=!r):r||o===t?n+=o:(n.length>0&&i.push({literal:/^\s+$/.test(n),val:n}),n=o,t=o)}return n.length>0&&i.push({literal:r||/^\s+$/.test(n),val:n}),i}static macroTokenToFormatOpts(e){return bs[e]}constructor(e,t){this.opts=t,this.loc=e,this.systemLoc=null}formatWithSystemDefault(e,t){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(e,{...this.opts,...t}).format()}dtFormatter(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t})}formatDateTime(e,t){return this.dtFormatter(e,t).format()}formatDateTimeParts(e,t){return this.dtFormatter(e,t).formatToParts()}formatInterval(e,t){return this.dtFormatter(e.start,t).dtf.formatRange(e.start.toJSDate(),e.end.toJSDate())}resolvedOptions(e,t){return this.dtFormatter(e,t).resolvedOptions()}num(e,t=0){if(this.opts.forceSimple)return b(e,t);const n={...this.opts};return t>0&&(n.padTo=t),this.loc.numberFormatter(n).format(e)}formatDateTimeFromString(e,t){const n=this.loc.listingMode()==="en",r=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",i=(m,N)=>this.loc.extract(e,m,N),a=m=>e.isOffsetFixed&&e.offset===0&&m.allowZ?"Z":e.isValid?e.zone.formatOffset(e.ts,m.format):"",o=()=>n?Ts(e):i({hour:"numeric",hourCycle:"h12"},"dayperiod"),u=(m,N)=>n?Ns(e,m):i(N?{month:m}:{month:m,day:"numeric"},"month"),l=(m,N)=>n?Os(e,m):i(N?{weekday:m}:{weekday:m,month:"long",day:"numeric"},"weekday"),c=m=>{const N=I.macroTokenToFormatOpts(m);return N?this.formatWithSystemDefault(e,N):m},h=m=>n?Es(e,m):i({era:m},"era"),p=m=>{switch(m){case"S":return this.num(e.millisecond);case"u":case"SSS":return this.num(e.millisecond,3);case"s":return this.num(e.second);case"ss":return this.num(e.second,2);case"uu":return this.num(Math.floor(e.millisecond/10),2);case"uuu":return this.num(Math.floor(e.millisecond/100));case"m":return this.num(e.minute);case"mm":return this.num(e.minute,2);case"h":return this.num(e.hour%12===0?12:e.hour%12);case"hh":return this.num(e.hour%12===0?12:e.hour%12,2);case"H":return this.num(e.hour);case"HH":return this.num(e.hour,2);case"Z":return a({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return a({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return a({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return e.zone.offsetName(e.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return e.zone.offsetName(e.ts,{format:"long",locale:this.loc.locale});case"z":return e.zoneName;case"a":return o();case"d":return r?i({day:"numeric"},"day"):this.num(e.day);case"dd":return r?i({day:"2-digit"},"day"):this.num(e.day,2);case"c":return this.num(e.weekday);case"ccc":return l("short",!0);case"cccc":return l("long",!0);case"ccccc":return l("narrow",!0);case"E":return this.num(e.weekday);case"EEE":return l("short",!1);case"EEEE":return l("long",!1);case"EEEEE":return l("narrow",!1);case"L":return r?i({month:"numeric",day:"numeric"},"month"):this.num(e.month);case"LL":return r?i({month:"2-digit",day:"numeric"},"month"):this.num(e.month,2);case"LLL":return u("short",!0);case"LLLL":return u("long",!0);case"LLLLL":return u("narrow",!0);case"M":return r?i({month:"numeric"},"month"):this.num(e.month);case"MM":return r?i({month:"2-digit"},"month"):this.num(e.month,2);case"MMM":return u("short",!1);case"MMMM":return u("long",!1);case"MMMMM":return u("narrow",!1);case"y":return r?i({year:"numeric"},"year"):this.num(e.year);case"yy":return r?i({year:"2-digit"},"year"):this.num(e.year.toString().slice(-2),2);case"yyyy":return r?i({year:"numeric"},"year"):this.num(e.year,4);case"yyyyyy":return r?i({year:"numeric"},"year"):this.num(e.year,6);case"G":return h("short");case"GG":return h("long");case"GGGGG":return h("narrow");case"kk":return this.num(e.weekYear.toString().slice(-2),2);case"kkkk":return this.num(e.weekYear,4);case"W":return this.num(e.weekNumber);case"WW":return this.num(e.weekNumber,2);case"n":return this.num(e.localWeekNumber);case"nn":return this.num(e.localWeekNumber,2);case"ii":return this.num(e.localWeekYear.toString().slice(-2),2);case"iiii":return this.num(e.localWeekYear,4);case"o":return this.num(e.ordinal);case"ooo":return this.num(e.ordinal,3);case"q":return this.num(e.quarter);case"qq":return this.num(e.quarter,2);case"X":return this.num(Math.floor(e.ts/1e3));case"x":return this.num(e.ts);default:return c(m)}};return rn(I.parseFormat(t),p)}formatDurationFromString(e,t){const n=u=>{switch(u[0]){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":return"hour";case"d":return"day";case"w":return"week";case"M":return"month";case"y":return"year";default:return null}},r=u=>l=>{const c=n(l);return c?this.num(u.get(c),l.length):l},i=I.parseFormat(t),a=i.reduce((u,{literal:l,val:c})=>l?u:u.concat(c),[]),o=e.shiftTo(...a.map(n).filter(u=>u));return rn(i,r(o))}}const an=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;function _(...s){const e=s.reduce((t,n)=>t+n.source,"");return RegExp(`^${e}$`)}function Q(...s){return e=>s.reduce(([t,n,r],i)=>{const[a,o,u]=i(e,r);return[{...t,...a},o||n,u]},[{},null,1]).slice(0,2)}function X(s,...e){if(s==null)return[null,null];for(const[t,n]of e){const r=t.exec(s);if(r)return n(r)}return[null,null]}function on(...s){return(e,t)=>{const n={};let r;for(r=0;rm!==void 0&&(N||m&&c)?-m:m;return[{years:p(J(t)),months:p(J(n)),weeks:p(J(r)),days:p(J(i)),hours:p(J(a)),minutes:p(J(o)),seconds:p(J(u),u==="-0"),milliseconds:p(Ye(l),h)}]}const Zs={GMT:0,EDT:-4*60,EST:-5*60,CDT:-5*60,CST:-6*60,MDT:-6*60,MST:-7*60,PDT:-7*60,PST:-8*60};function Ke(s,e,t,n,r,i,a){const o={year:e.length===2?Be(q(e)):q(e),month:_t.indexOf(t)+1,day:q(n),hour:q(r),minute:q(i)};return a&&(o.second=q(a)),s&&(o.weekday=s.length>3?Xt.indexOf(s)+1:en.indexOf(s)+1),o}const Us=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function qs(s){const[,e,t,n,r,i,a,o,u,l,c,h]=s,p=Ke(e,r,n,t,i,a,o);let m;return u?m=Zs[u]:l?m=0:m=xe(c,h),[p,new v(m)]}function zs(s){return s.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}const Ps=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,Ys=/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,Js=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function fn(s){const[,e,t,n,r,i,a,o]=s;return[Ke(e,r,n,t,i,a,o),v.utcInstance]}function Bs(s){const[,e,t,n,r,i,a,o]=s;return[Ke(e,o,t,n,r,i,a),v.utcInstance]}const js=_(vs,Ge),Gs=_(Ds,Ge),Ks=_(Ms,Ge),Hs=_(ln),dn=Q(Ws,te,ce,fe),_s=Q(Fs,te,ce,fe),Qs=Q(As,te,ce,fe),Xs=Q(te,ce,fe);function er(s){return X(s,[js,dn],[Gs,_s],[Ks,Qs],[Hs,Xs])}function tr(s){return X(zs(s),[Us,qs])}function nr(s){return X(s,[Ps,fn],[Ys,fn],[Js,Bs])}function sr(s){return X(s,[Rs,$s])}const rr=Q(te);function ir(s){return X(s,[Ls,rr])}const ar=_(Vs,Cs),or=_(cn),ur=Q(te,ce,fe);function lr(s){return X(s,[ar,dn],[or,ur])}const hn="Invalid Duration",mn={weeks:{days:7,hours:7*24,minutes:7*24*60,seconds:7*24*60*60,milliseconds:7*24*60*60*1e3},days:{hours:24,minutes:24*60,seconds:24*60*60,milliseconds:24*60*60*1e3},hours:{minutes:60,seconds:60*60,milliseconds:60*60*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},cr={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:91*24,minutes:91*24*60,seconds:91*24*60*60,milliseconds:91*24*60*60*1e3},months:{weeks:4,days:30,hours:30*24,minutes:30*24*60,seconds:30*24*60*60,milliseconds:30*24*60*60*1e3},...mn},V=146097/400,ne=146097/4800,fr={years:{quarters:4,months:12,weeks:V/7,days:V,hours:V*24,minutes:V*24*60,seconds:V*24*60*60,milliseconds:V*24*60*60*1e3},quarters:{months:3,weeks:V/28,days:V/4,hours:V*24/4,minutes:V*24*60/4,seconds:V*24*60*60/4,milliseconds:V*24*60*60*1e3/4},months:{weeks:ne/7,days:ne,hours:ne*24,minutes:ne*24*60,seconds:ne*24*60*60,milliseconds:ne*24*60*60*1e3},...mn},B=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],dr=B.slice(0).reverse();function z(s,e,t=!1){const n={values:t?e.values:{...s.values,...e.values||{}},loc:s.loc.clone(e.loc),conversionAccuracy:e.conversionAccuracy||s.conversionAccuracy,matrix:e.matrix||s.matrix};return new w(n)}function yn(s,e){let t=e.milliseconds??0;for(const n of dr.slice(1))e[n]&&(t+=e[n]*s[n].milliseconds);return t}function gn(s,e){const t=yn(s,e)<0?-1:1;B.reduceRight((n,r)=>{if(y(e[r]))return n;if(n){const i=e[n]*t,a=s[r][n],o=Math.floor(i/a);e[r]+=o*t,e[n]-=o*a*t}return r},null),B.reduce((n,r)=>{if(y(e[r]))return n;if(n){const i=e[n]%1;e[n]-=i,e[r]+=i*s[n][r]}return r},null)}function hr(s){const e={};for(const[t,n]of Object.entries(s))n!==0&&(e[t]=n);return e}class w{constructor(e){const t=e.conversionAccuracy==="longterm"||!1;let n=t?fr:cr;e.matrix&&(n=e.matrix),this.values=e.values,this.loc=e.loc||T.create(),this.conversionAccuracy=t?"longterm":"casual",this.invalid=e.invalid||null,this.matrix=n,this.isLuxonDuration=!0}static fromMillis(e,t){return w.fromObject({milliseconds:e},t)}static fromObject(e,t={}){if(e==null||typeof e!="object")throw new D(`Duration.fromObject: argument expected to be an object, got ${e===null?"null":typeof e}`);return new w({values:be(e,w.normalizeUnit),loc:T.fromObject(t),conversionAccuracy:t.conversionAccuracy,matrix:t.matrix})}static fromDurationLike(e){if(Y(e))return w.fromMillis(e);if(w.isDuration(e))return e;if(typeof e=="object")return w.fromObject(e);throw new D(`Unknown duration argument ${e} of type ${typeof e}`)}static fromISO(e,t){const[n]=sr(e);return n?w.fromObject(n,t):w.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static fromISOTime(e,t){const[n]=ir(e);return n?w.fromObject(n,t):w.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static invalid(e,t=null){if(!e)throw new D("need to specify a reason the Duration is invalid");const n=e instanceof W?e:new W(e,t);if(x.throwOnInvalid)throw new Un(n);return new w({invalid:n})}static normalizeUnit(e){const t={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[e&&e.toLowerCase()];if(!t)throw new at(e);return t}static isDuration(e){return e&&e.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(e,t={}){const n={...t,floor:t.round!==!1&&t.floor!==!1};return this.isValid?I.create(this.loc,n).formatDurationFromString(this,e):hn}toHuman(e={}){if(!this.isValid)return hn;const t=B.map(n=>{const r=this.values[n];return y(r)?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...e,unit:n.slice(0,-1)}).format(r)}).filter(n=>n);return this.loc.listFormatter({type:"conjunction",style:e.listStyle||"narrow",...e}).format(t)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let e="P";return this.years!==0&&(e+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(e+=this.months+this.quarters*3+"M"),this.weeks!==0&&(e+=this.weeks+"W"),this.days!==0&&(e+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(e+="T"),this.hours!==0&&(e+=this.hours+"H"),this.minutes!==0&&(e+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(e+=Je(this.seconds+this.milliseconds/1e3,3)+"S"),e==="P"&&(e+="T0S"),e}toISOTime(e={}){if(!this.isValid)return null;const t=this.toMillis();return t<0||t>=864e5?null:(e={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...e,includeOffset:!1},g.fromMillis(t,{zone:"UTC"}).toISOTime(e))}toJSON(){return this.toISO()}toString(){return this.toISO()}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Duration { values: ${JSON.stringify(this.values)} }`:`Duration { Invalid, reason: ${this.invalidReason} }`}toMillis(){return this.isValid?yn(this.matrix,this.values):NaN}valueOf(){return this.toMillis()}plus(e){if(!this.isValid)return this;const t=w.fromDurationLike(e),n={};for(const r of B)(K(t.values,r)||K(this.values,r))&&(n[r]=t.get(r)+this.get(r));return z(this,{values:n},!0)}minus(e){if(!this.isValid)return this;const t=w.fromDurationLike(e);return this.plus(t.negate())}mapUnits(e){if(!this.isValid)return this;const t={};for(const n of Object.keys(this.values))t[n]=Ht(e(this.values[n],n));return z(this,{values:t},!0)}get(e){return this[w.normalizeUnit(e)]}set(e){if(!this.isValid)return this;const t={...this.values,...be(e,w.normalizeUnit)};return z(this,{values:t})}reconfigure({locale:e,numberingSystem:t,conversionAccuracy:n,matrix:r}={}){const a={loc:this.loc.clone({locale:e,numberingSystem:t}),matrix:r,conversionAccuracy:n};return z(this,a)}as(e){return this.isValid?this.shiftTo(e).get(e):NaN}normalize(){if(!this.isValid)return this;const e=this.toObject();return gn(this.matrix,e),z(this,{values:e},!0)}rescale(){if(!this.isValid)return this;const e=hr(this.normalize().shiftToAll().toObject());return z(this,{values:e},!0)}shiftTo(...e){if(!this.isValid)return this;if(e.length===0)return this;e=e.map(a=>w.normalizeUnit(a));const t={},n={},r=this.toObject();let i;for(const a of B)if(e.indexOf(a)>=0){i=a;let o=0;for(const l in n)o+=this.matrix[l][a]*n[l],n[l]=0;Y(r[a])&&(o+=r[a]);const u=Math.trunc(o);t[a]=u,n[a]=(o*1e3-u*1e3)/1e3}else Y(r[a])&&(n[a]=r[a]);for(const a in n)n[a]!==0&&(t[i]+=a===i?n[a]:n[a]/this.matrix[i][a]);return gn(this.matrix,t),z(this,{values:t},!0)}shiftToAll(){return this.isValid?this.shiftTo("years","months","weeks","days","hours","minutes","seconds","milliseconds"):this}negate(){if(!this.isValid)return this;const e={};for(const t of Object.keys(this.values))e[t]=this.values[t]===0?0:-this.values[t];return z(this,{values:e},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(e){if(!this.isValid||!e.isValid||!this.loc.equals(e.loc))return!1;function t(n,r){return n===void 0||n===0?r===void 0||r===0:n===r}for(const n of B)if(!t(this.values[n],e.values[n]))return!1;return!0}}const se="Invalid Interval";function mr(s,e){return!s||!s.isValid?E.invalid("missing or invalid start"):!e||!e.isValid?E.invalid("missing or invalid end"):ee:!1}isBefore(e){return this.isValid?this.e<=e:!1}contains(e){return this.isValid?this.s<=e&&this.e>e:!1}set({start:e,end:t}={}){return this.isValid?E.fromDateTimes(e||this.s,t||this.e):this}splitAt(...e){if(!this.isValid)return[];const t=e.map(he).filter(a=>this.contains(a)).sort((a,o)=>a.toMillis()-o.toMillis()),n=[];let{s:r}=this,i=0;for(;r+this.e?this.e:a;n.push(E.fromDateTimes(r,o)),r=o,i+=1}return n}splitBy(e){const t=w.fromDurationLike(e);if(!this.isValid||!t.isValid||t.as("milliseconds")===0)return[];let{s:n}=this,r=1,i;const a=[];for(;nu*r));i=+o>+this.e?this.e:o,a.push(E.fromDateTimes(n,i)),n=i,r+=1}return a}divideEqually(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]}overlaps(e){return this.e>e.s&&this.s=e.e:!1}equals(e){return!this.isValid||!e.isValid?!1:this.s.equals(e.s)&&this.e.equals(e.e)}intersection(e){if(!this.isValid)return this;const t=this.s>e.s?this.s:e.s,n=this.e=n?null:E.fromDateTimes(t,n)}union(e){if(!this.isValid)return this;const t=this.se.e?this.e:e.e;return E.fromDateTimes(t,n)}static merge(e){const[t,n]=e.sort((r,i)=>r.s-i.s).reduce(([r,i],a)=>i?i.overlaps(a)||i.abutsStart(a)?[r,i.union(a)]:[r.concat([i]),a]:[r,a],[[],null]);return n&&t.push(n),t}static xor(e){let t=null,n=0;const r=[],i=e.map(u=>[{time:u.s,type:"s"},{time:u.e,type:"e"}]),a=Array.prototype.concat(...i),o=a.sort((u,l)=>u.time-l.time);for(const u of o)n+=u.type==="s"?1:-1,n===1?t=u.time:(t&&+t!=+u.time&&r.push(E.fromDateTimes(t,u.time)),t=null);return E.merge(r)}difference(...e){return E.xor([this].concat(e)).map(t=>this.intersection(t)).filter(t=>t&&!t.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} – ${this.e.toISO()})`:se}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`:`Interval { Invalid, reason: ${this.invalidReason} }`}toLocaleString(e=ge,t={}){return this.isValid?I.create(this.s.loc.clone(t),e).formatInterval(this):se}toISO(e){return this.isValid?`${this.s.toISO(e)}/${this.e.toISO(e)}`:se}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:se}toISOTime(e){return this.isValid?`${this.s.toISOTime(e)}/${this.e.toISOTime(e)}`:se}toFormat(e,{separator:t=" – "}={}){return this.isValid?`${this.s.toFormat(e)}${t}${this.e.toFormat(e)}`:se}toDuration(e,t){return this.isValid?this.e.diff(this.s,e,t):w.invalid(this.invalidReason)}mapEndpoints(e){return E.fromDateTimes(e(this.s),e(this.e))}}class ve{static hasDST(e=x.defaultZone){const t=g.now().setZone(e).set({month:12});return!e.isUniversal&&t.offset!==t.set({month:6}).offset}static isValidIANAZone(e){return $.isValidZone(e)}static normalizeZone(e){return U(e,x.defaultZone)}static getStartOfWeek({locale:e=null,locObj:t=null}={}){return(t||T.create(e)).getStartOfWeek()}static getMinimumDaysInFirstWeek({locale:e=null,locObj:t=null}={}){return(t||T.create(e)).getMinDaysInFirstWeek()}static getWeekendWeekdays({locale:e=null,locObj:t=null}={}){return(t||T.create(e)).getWeekendDays().slice()}static months(e="long",{locale:t=null,numberingSystem:n=null,locObj:r=null,outputCalendar:i="gregory"}={}){return(r||T.create(t,n,i)).months(e)}static monthsFormat(e="long",{locale:t=null,numberingSystem:n=null,locObj:r=null,outputCalendar:i="gregory"}={}){return(r||T.create(t,n,i)).months(e,!0)}static weekdays(e="long",{locale:t=null,numberingSystem:n=null,locObj:r=null}={}){return(r||T.create(t,n,null)).weekdays(e)}static weekdaysFormat(e="long",{locale:t=null,numberingSystem:n=null,locObj:r=null}={}){return(r||T.create(t,n,null)).weekdays(e,!0)}static meridiems({locale:e=null}={}){return T.create(e).meridiems()}static eras(e="short",{locale:t=null}={}){return T.create(t,null,"gregory").eras(e)}static features(){return{relative:Jt(),localeWeek:Bt()}}}function pn(s,e){const t=r=>r.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),n=t(e)-t(s);return Math.floor(w.fromMillis(n).as("days"))}function yr(s,e,t){const n=[["years",(u,l)=>l.year-u.year],["quarters",(u,l)=>l.quarter-u.quarter+(l.year-u.year)*4],["months",(u,l)=>l.month-u.month+(l.year-u.year)*12],["weeks",(u,l)=>{const c=pn(u,l);return(c-c%7)/7}],["days",pn]],r={},i=s;let a,o;for(const[u,l]of n)t.indexOf(u)>=0&&(a=u,r[u]=l(s,e),o=i.plus(r),o>e?(r[u]--,s=i.plus(r),s>e&&(o=s,r[u]--,s=i.plus(r))):s=o);return[s,r,o,a]}function gr(s,e,t,n){let[r,i,a,o]=yr(s,e,t);const u=e-r,l=t.filter(h=>["hours","minutes","seconds","milliseconds"].indexOf(h)>=0);l.length===0&&(a0?w.fromMillis(u,n).shiftTo(...l).plus(c):c}const He={arab:"[٠-٩]",arabext:"[۰-۹]",bali:"[᭐-᭙]",beng:"[০-৯]",deva:"[०-९]",fullwide:"[0-9]",gujr:"[૦-૯]",hanidec:"[〇|一|二|三|四|五|六|七|八|九]",khmr:"[០-៩]",knda:"[೦-೯]",laoo:"[໐-໙]",limb:"[᥆-᥏]",mlym:"[൦-൯]",mong:"[᠐-᠙]",mymr:"[၀-၉]",orya:"[୦-୯]",tamldec:"[௦-௯]",telu:"[౦-౯]",thai:"[๐-๙]",tibt:"[༠-༩]",latn:"\\d"},wn={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},pr=He.hanidec.replace(/[\[|\]]/g,"").split("");function wr(s){let e=parseInt(s,10);if(isNaN(e)){e="";for(let t=0;t=i&&n<=a&&(e+=n-i)}}return parseInt(e,10)}else return e}function L({numberingSystem:s},e=""){return new RegExp(`${He[s||"latn"]}${e}`)}const Sr="missing Intl.DateTimeFormat.formatToParts support";function S(s,e=t=>t){return{regex:s,deser:([t])=>e(wr(t))}}const Sn="[  ]",kn=new RegExp(Sn,"g");function kr(s){return s.replace(/\./g,"\\.?").replace(kn,Sn)}function Tn(s){return s.replace(/\./g,"").replace(kn," ").toLowerCase()}function R(s,e){return s===null?null:{regex:RegExp(s.map(kr).join("|")),deser:([t])=>s.findIndex(n=>Tn(t)===Tn(n))+e}}function On(s,e){return{regex:s,deser:([,t,n])=>xe(t,n),groups:e}}function De(s){return{regex:s,deser:([e])=>e}}function Tr(s){return s.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function Or(s,e){const t=L(e),n=L(e,"{2}"),r=L(e,"{3}"),i=L(e,"{4}"),a=L(e,"{6}"),o=L(e,"{1,2}"),u=L(e,"{1,3}"),l=L(e,"{1,6}"),c=L(e,"{1,9}"),h=L(e,"{2,4}"),p=L(e,"{4,6}"),m=k=>({regex:RegExp(Tr(k.val)),deser:([re])=>re,literal:!0}),O=(k=>{if(s.literal)return m(k);switch(k.val){case"G":return R(e.eras("short"),0);case"GG":return R(e.eras("long"),0);case"y":return S(l);case"yy":return S(h,Be);case"yyyy":return S(i);case"yyyyy":return S(p);case"yyyyyy":return S(a);case"M":return S(o);case"MM":return S(n);case"MMM":return R(e.months("short",!0),1);case"MMMM":return R(e.months("long",!0),1);case"L":return S(o);case"LL":return S(n);case"LLL":return R(e.months("short",!1),1);case"LLLL":return R(e.months("long",!1),1);case"d":return S(o);case"dd":return S(n);case"o":return S(u);case"ooo":return S(r);case"HH":return S(n);case"H":return S(o);case"hh":return S(n);case"h":return S(o);case"mm":return S(n);case"m":return S(o);case"q":return S(o);case"qq":return S(n);case"s":return S(o);case"ss":return S(n);case"S":return S(u);case"SSS":return S(r);case"u":return De(c);case"uu":return De(o);case"uuu":return S(t);case"a":return R(e.meridiems(),0);case"kkkk":return S(i);case"kk":return S(h,Be);case"W":return S(o);case"WW":return S(n);case"E":case"c":return S(t);case"EEE":return R(e.weekdays("short",!1),1);case"EEEE":return R(e.weekdays("long",!1),1);case"ccc":return R(e.weekdays("short",!0),1);case"cccc":return R(e.weekdays("long",!0),1);case"Z":case"ZZ":return On(new RegExp(`([+-]${o.source})(?::(${n.source}))?`),2);case"ZZZ":return On(new RegExp(`([+-]${o.source})(${n.source})?`),2);case"z":return De(/[a-z_+-/]{1,256}?/i);case" ":return De(/[^\S\n\r]/);default:return m(k)}})(s)||{invalidReason:Sr};return O.token=s,O}const Nr={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour12:{numeric:"h","2-digit":"hh"},hour24:{numeric:"H","2-digit":"HH"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"},timeZoneName:{long:"ZZZZZ",short:"ZZZ"}};function Er(s,e,t){const{type:n,value:r}=s;if(n==="literal"){const u=/^\s+$/.test(r);return{literal:!u,val:u?" ":r}}const i=e[n];let a=n;n==="hour"&&(e.hour12!=null?a=e.hour12?"hour12":"hour24":e.hourCycle!=null?e.hourCycle==="h11"||e.hourCycle==="h12"?a="hour12":a="hour24":a=t.hour12?"hour12":"hour24");let o=Nr[a];if(typeof o=="object"&&(o=o[i]),o)return{literal:!1,val:o}}function xr(s){return[`^${s.map(t=>t.regex).reduce((t,n)=>`${t}(${n.source})`,"")}$`,s]}function br(s,e,t){const n=s.match(e);if(n){const r={};let i=1;for(const a in t)if(K(t,a)){const o=t[a],u=o.groups?o.groups+1:1;!o.literal&&o.token&&(r[o.token.val[0]]=o.deser(n.slice(i,i+u))),i+=u}return[n,r]}else return[n,{}]}function Ir(s){const e=i=>{switch(i){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}};let t=null,n;return y(s.z)||(t=$.create(s.z)),y(s.Z)||(t||(t=new v(s.Z)),n=s.Z),y(s.q)||(s.M=(s.q-1)*3+1),y(s.h)||(s.h<12&&s.a===1?s.h+=12:s.h===12&&s.a===0&&(s.h=0)),s.G===0&&s.y&&(s.y=-s.y),y(s.u)||(s.S=Ye(s.u)),[Object.keys(s).reduce((i,a)=>{const o=e(a);return o&&(i[o]=s[a]),i},{}),t,n]}let _e=null;function vr(){return _e||(_e=g.fromMillis(1555555555555)),_e}function Dr(s,e){if(s.literal)return s;const t=I.macroTokenToFormatOpts(s.val),n=xn(t,e);return n==null||n.includes(void 0)?s:n}function Nn(s,e){return Array.prototype.concat(...s.map(t=>Dr(t,e)))}function En(s,e,t){const n=Nn(I.parseFormat(t),s),r=n.map(a=>Or(a,s)),i=r.find(a=>a.invalidReason);if(i)return{input:e,tokens:n,invalidReason:i.invalidReason};{const[a,o]=xr(r),u=RegExp(a,"i"),[l,c]=br(e,u,o),[h,p,m]=c?Ir(c):[null,null,void 0];if(K(c,"a")&&K(c,"H"))throw new G("Can't include meridiem when specifying 24-hour format");return{input:e,tokens:n,regex:u,rawMatches:l,matches:c,result:h,zone:p,specificOffset:m}}}function Mr(s,e,t){const{result:n,zone:r,specificOffset:i,invalidReason:a}=En(s,e,t);return[n,r,i,a]}function xn(s,e){if(!s)return null;const n=I.create(e,s).dtFormatter(vr()),r=n.formatToParts(),i=n.resolvedOptions();return r.map(a=>Er(a,s,i))}const Qe="Invalid DateTime",bn=864e13;function Me(s){return new W("unsupported zone",`the zone "${s.name}" is not supported`)}function Xe(s){return s.weekData===null&&(s.weekData=Te(s.c)),s.weekData}function et(s){return s.localWeekData===null&&(s.localWeekData=Te(s.c,s.loc.getMinDaysInFirstWeek(),s.loc.getStartOfWeek())),s.localWeekData}function j(s,e){const t={ts:s.ts,zone:s.zone,c:s.c,o:s.o,loc:s.loc,invalid:s.invalid};return new g({...t,...e,old:t})}function In(s,e,t){let n=s-e*60*1e3;const r=t.offset(n);if(e===r)return[n,e];n-=(r-e)*60*1e3;const i=t.offset(n);return r===i?[n,r]:[s-Math.min(r,i)*60*1e3,Math.max(r,i)]}function Fe(s,e){s+=e*60*1e3;const t=new Date(s);return{year:t.getUTCFullYear(),month:t.getUTCMonth()+1,day:t.getUTCDate(),hour:t.getUTCHours(),minute:t.getUTCMinutes(),second:t.getUTCSeconds(),millisecond:t.getUTCMilliseconds()}}function Ae(s,e,t){return In(Ee(s),e,t)}function vn(s,e){const t=s.o,n=s.c.year+Math.trunc(e.years),r=s.c.month+Math.trunc(e.months)+Math.trunc(e.quarters)*3,i={...s.c,year:n,month:r,day:Math.min(s.c.day,Ne(n,r))+Math.trunc(e.days)+Math.trunc(e.weeks)*7},a=w.fromObject({years:e.years-Math.trunc(e.years),quarters:e.quarters-Math.trunc(e.quarters),months:e.months-Math.trunc(e.months),weeks:e.weeks-Math.trunc(e.weeks),days:e.days-Math.trunc(e.days),hours:e.hours,minutes:e.minutes,seconds:e.seconds,milliseconds:e.milliseconds}).as("milliseconds"),o=Ee(i);let[u,l]=In(o,t,s.zone);return a!==0&&(u+=a,l=s.zone.offset(u)),{ts:u,o:l}}function de(s,e,t,n,r,i){const{setZone:a,zone:o}=t;if(s&&Object.keys(s).length!==0||e){const u=e||o,l=g.fromObject(s,{...t,zone:u,specificOffset:i});return a?l:l.setZone(o)}else return g.invalid(new W("unparsable",`the input "${r}" can't be parsed as ${n}`))}function Ve(s,e,t=!0){return s.isValid?I.create(T.create("en-US"),{allowZ:t,forceSimple:!0}).formatDateTimeFromString(s,e):null}function tt(s,e){const t=s.c.year>9999||s.c.year<0;let n="";return t&&s.c.year>=0&&(n+="+"),n+=b(s.c.year,t?6:4),e?(n+="-",n+=b(s.c.month),n+="-",n+=b(s.c.day)):(n+=b(s.c.month),n+=b(s.c.day)),n}function Dn(s,e,t,n,r,i){let a=b(s.c.hour);return e?(a+=":",a+=b(s.c.minute),(s.c.millisecond!==0||s.c.second!==0||!t)&&(a+=":")):a+=b(s.c.minute),(s.c.millisecond!==0||s.c.second!==0||!t)&&(a+=b(s.c.second),(s.c.millisecond!==0||!n)&&(a+=".",a+=b(s.c.millisecond,3))),r&&(s.isOffsetFixed&&s.offset===0&&!i?a+="Z":s.o<0?(a+="-",a+=b(Math.trunc(-s.o/60)),a+=":",a+=b(Math.trunc(-s.o%60))):(a+="+",a+=b(Math.trunc(s.o/60)),a+=":",a+=b(Math.trunc(s.o%60)))),i&&(a+="["+s.zone.ianaName+"]"),a}const Mn={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},Fr={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},Ar={ordinal:1,hour:0,minute:0,second:0,millisecond:0},Fn=["year","month","day","hour","minute","second","millisecond"],Vr=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],Cr=["year","ordinal","hour","minute","second","millisecond"];function Wr(s){const e={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[s.toLowerCase()];if(!e)throw new at(s);return e}function An(s){switch(s.toLowerCase()){case"localweekday":case"localweekdays":return"localWeekday";case"localweeknumber":case"localweeknumbers":return"localWeekNumber";case"localweekyear":case"localweekyears":return"localWeekYear";default:return Wr(s)}}function Vn(s,e){const t=U(e.zone,x.defaultZone),n=T.fromObject(e),r=x.now();let i,a;if(y(s.year))i=r;else{for(const l of Fn)y(s[l])&&(s[l]=Mn[l]);const o=Pt(s)||Yt(s);if(o)return g.invalid(o);const u=t.offset(r);[i,a]=Ae(s,u,t)}return new g({ts:i,zone:t,loc:n,o:a})}function Cn(s,e,t){const n=y(t.round)?!0:t.round,r=(a,o)=>(a=Je(a,n||t.calendary?0:2,!0),e.loc.clone(t).relFormatter(t).format(a,o)),i=a=>t.calendary?e.hasSame(s,a)?0:e.startOf(a).diff(s.startOf(a),a).get(a):e.diff(s,a).get(a);if(t.unit)return r(i(t.unit),t.unit);for(const a of t.units){const o=i(a);if(Math.abs(o)>=1)return r(o,a)}return r(s>e?-0:0,t.units[t.units.length-1])}function Wn(s){let e={},t;return s.length>0&&typeof s[s.length-1]=="object"?(e=s[s.length-1],t=Array.from(s).slice(0,s.length-1)):t=Array.from(s),[e,t]}class g{constructor(e){const t=e.zone||x.defaultZone;let n=e.invalid||(Number.isNaN(e.ts)?new W("invalid input"):null)||(t.isValid?null:Me(t));this.ts=y(e.ts)?x.now():e.ts;let r=null,i=null;if(!n)if(e.old&&e.old.ts===this.ts&&e.old.zone.equals(t))[r,i]=[e.old.c,e.old.o];else{const o=t.offset(this.ts);r=Fe(this.ts,o),n=Number.isNaN(r.year)?new W("invalid input"):null,r=n?null:r,i=n?null:o}this._zone=t,this.loc=e.loc||T.create(),this.invalid=n,this.weekData=null,this.localWeekData=null,this.c=r,this.o=i,this.isLuxonDateTime=!0}static now(){return new g({})}static local(){const[e,t]=Wn(arguments),[n,r,i,a,o,u,l]=t;return Vn({year:n,month:r,day:i,hour:a,minute:o,second:u,millisecond:l},e)}static utc(){const[e,t]=Wn(arguments),[n,r,i,a,o,u,l]=t;return e.zone=v.utcInstance,Vn({year:n,month:r,day:i,hour:a,minute:o,second:u,millisecond:l},e)}static fromJSDate(e,t={}){const n=fs(e)?e.valueOf():NaN;if(Number.isNaN(n))return g.invalid("invalid input");const r=U(t.zone,x.defaultZone);return r.isValid?new g({ts:n,zone:r,loc:T.fromObject(t)}):g.invalid(Me(r))}static fromMillis(e,t={}){if(Y(e))return e<-bn||e>bn?g.invalid("Timestamp out of range"):new g({ts:e,zone:U(t.zone,x.defaultZone),loc:T.fromObject(t)});throw new D(`fromMillis requires a numerical input, but received a ${typeof e} with value ${e}`)}static fromSeconds(e,t={}){if(Y(e))return new g({ts:e*1e3,zone:U(t.zone,x.defaultZone),loc:T.fromObject(t)});throw new D("fromSeconds requires a numerical input")}static fromObject(e,t={}){e=e||{};const n=U(t.zone,x.defaultZone);if(!n.isValid)return g.invalid(Me(n));const r=T.fromObject(t),i=be(e,An),{minDaysInFirstWeek:a,startOfWeek:o}=zt(i,r),u=x.now(),l=y(t.specificOffset)?n.offset(u):t.specificOffset,c=!y(i.ordinal),h=!y(i.year),p=!y(i.month)||!y(i.day),m=h||p,N=i.weekYear||i.weekNumber;if((m||c)&&N)throw new G("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(p&&c)throw new G("Can't mix ordinal dates with month/day");const O=N||i.weekday&&!m;let k,re,me=Fe(u,l);O?(k=Vr,re=Fr,me=Te(me,a,o)):c?(k=Cr,re=Ar,me=ze(me)):(k=Fn,re=Mn);let Ln=!1;for(const ye of k){const Yr=i[ye];y(Yr)?Ln?i[ye]=re[ye]:i[ye]=me[ye]:Ln=!0}const Ur=O?us(i,a,o):c?ls(i):Pt(i),Rn=Ur||Yt(i);if(Rn)return g.invalid(Rn);const qr=O?Ut(i,a,o):c?qt(i):i,[zr,Pr]=Ae(qr,l,n),it=new g({ts:zr,zone:n,o:Pr,loc:r});return i.weekday&&m&&e.weekday!==it.weekday?g.invalid("mismatched weekday",`you can't specify both a weekday of ${i.weekday} and a date of ${it.toISO()}`):it}static fromISO(e,t={}){const[n,r]=er(e);return de(n,r,t,"ISO 8601",e)}static fromRFC2822(e,t={}){const[n,r]=tr(e);return de(n,r,t,"RFC 2822",e)}static fromHTTP(e,t={}){const[n,r]=nr(e);return de(n,r,t,"HTTP",t)}static fromFormat(e,t,n={}){if(y(e)||y(t))throw new D("fromFormat requires an input string and a format");const{locale:r=null,numberingSystem:i=null}=n,a=T.fromOpts({locale:r,numberingSystem:i,defaultToEN:!0}),[o,u,l,c]=Mr(a,e,t);return c?g.invalid(c):de(o,u,n,`format ${t}`,e,l)}static fromString(e,t,n={}){return g.fromFormat(e,t,n)}static fromSQL(e,t={}){const[n,r]=lr(e);return de(n,r,t,"SQL",e)}static invalid(e,t=null){if(!e)throw new D("need to specify a reason the DateTime is invalid");const n=e instanceof W?e:new W(e,t);if(x.throwOnInvalid)throw new $n(n);return new g({invalid:n})}static isDateTime(e){return e&&e.isLuxonDateTime||!1}static parseFormatForOpts(e,t={}){const n=xn(e,T.fromObject(t));return n?n.map(r=>r?r.val:null).join(""):null}static expandFormat(e,t={}){return Nn(I.parseFormat(e),T.fromObject(t)).map(r=>r.val).join("")}get(e){return this[e]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?Xe(this).weekYear:NaN}get weekNumber(){return this.isValid?Xe(this).weekNumber:NaN}get weekday(){return this.isValid?Xe(this).weekday:NaN}get isWeekend(){return this.isValid&&this.loc.getWeekendDays().includes(this.weekday)}get localWeekday(){return this.isValid?et(this).weekday:NaN}get localWeekNumber(){return this.isValid?et(this).weekNumber:NaN}get localWeekYear(){return this.isValid?et(this).weekYear:NaN}get ordinal(){return this.isValid?ze(this.c).ordinal:NaN}get monthShort(){return this.isValid?ve.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?ve.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?ve.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?ve.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}getPossibleOffsets(){if(!this.isValid||this.isOffsetFixed)return[this];const e=864e5,t=6e4,n=Ee(this.c),r=this.zone.offset(n-e),i=this.zone.offset(n+e),a=this.zone.offset(n-r*t),o=this.zone.offset(n-i*t);if(a===o)return[this];const u=n-a*t,l=n-o*t,c=Fe(u,a),h=Fe(l,o);return c.hour===h.hour&&c.minute===h.minute&&c.second===h.second&&c.millisecond===h.millisecond?[j(this,{ts:u}),j(this,{ts:l})]:[this]}get isInLeapYear(){return oe(this.year)}get daysInMonth(){return Ne(this.year,this.month)}get daysInYear(){return this.isValid?H(this.year):NaN}get weeksInWeekYear(){return this.isValid?ue(this.weekYear):NaN}get weeksInLocalWeekYear(){return this.isValid?ue(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}resolvedLocaleOptions(e={}){const{locale:t,numberingSystem:n,calendar:r}=I.create(this.loc.clone(e),e).resolvedOptions(this);return{locale:t,numberingSystem:n,outputCalendar:r}}toUTC(e=0,t={}){return this.setZone(v.instance(e),t)}toLocal(){return this.setZone(x.defaultZone)}setZone(e,{keepLocalTime:t=!1,keepCalendarTime:n=!1}={}){if(e=U(e,x.defaultZone),e.equals(this.zone))return this;if(e.isValid){let r=this.ts;if(t||n){const i=e.offset(this.ts),a=this.toObject();[r]=Ae(a,i,e)}return j(this,{ts:r,zone:e})}else return g.invalid(Me(e))}reconfigure({locale:e,numberingSystem:t,outputCalendar:n}={}){const r=this.loc.clone({locale:e,numberingSystem:t,outputCalendar:n});return j(this,{loc:r})}setLocale(e){return this.reconfigure({locale:e})}set(e){if(!this.isValid)return this;const t=be(e,An),{minDaysInFirstWeek:n,startOfWeek:r}=zt(t,this.loc),i=!y(t.weekYear)||!y(t.weekNumber)||!y(t.weekday),a=!y(t.ordinal),o=!y(t.year),u=!y(t.month)||!y(t.day),l=o||u,c=t.weekYear||t.weekNumber;if((l||a)&&c)throw new G("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(u&&a)throw new G("Can't mix ordinal dates with month/day");let h;i?h=Ut({...Te(this.c,n,r),...t},n,r):y(t.ordinal)?(h={...this.toObject(),...t},y(t.day)&&(h.day=Math.min(Ne(h.year,h.month),h.day))):h=qt({...ze(this.c),...t});const[p,m]=Ae(h,this.o,this.zone);return j(this,{ts:p,o:m})}plus(e){if(!this.isValid)return this;const t=w.fromDurationLike(e);return j(this,vn(this,t))}minus(e){if(!this.isValid)return this;const t=w.fromDurationLike(e).negate();return j(this,vn(this,t))}startOf(e,{useLocaleWeeks:t=!1}={}){if(!this.isValid)return this;const n={},r=w.normalizeUnit(e);switch(r){case"years":n.month=1;case"quarters":case"months":n.day=1;case"weeks":case"days":n.hour=0;case"hours":n.minute=0;case"minutes":n.second=0;case"seconds":n.millisecond=0;break}if(r==="weeks")if(t){const i=this.loc.getStartOfWeek(),{weekday:a}=this;athis.valueOf(),o=a?this:e,u=a?e:this,l=gr(o,u,i,r);return a?l.negate():l}diffNow(e="milliseconds",t={}){return this.diff(g.now(),e,t)}until(e){return this.isValid?E.fromDateTimes(this,e):this}hasSame(e,t,n){if(!this.isValid)return!1;const r=e.valueOf(),i=this.setZone(e.zone,{keepLocalTime:!0});return i.startOf(t,n)<=r&&r<=i.endOf(t,n)}equals(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)}toRelative(e={}){if(!this.isValid)return null;const t=e.base||g.fromObject({},{zone:this.zone}),n=e.padding?thist.valueOf(),Math.min)}static max(...e){if(!e.every(g.isDateTime))throw new D("max requires all arguments be DateTimes");return jt(e,t=>t.valueOf(),Math.max)}static fromFormatExplain(e,t,n={}){const{locale:r=null,numberingSystem:i=null}=n,a=T.fromOpts({locale:r,numberingSystem:i,defaultToEN:!0});return En(a,e,t)}static fromStringExplain(e,t,n={}){return g.fromFormatExplain(e,t,n)}static get DATE_SHORT(){return ge}static get DATE_MED(){return ot}static get DATE_MED_WITH_WEEKDAY(){return qn}static get DATE_FULL(){return ut}static get DATE_HUGE(){return lt}static get TIME_SIMPLE(){return ct}static get TIME_WITH_SECONDS(){return ft}static get TIME_WITH_SHORT_OFFSET(){return dt}static get TIME_WITH_LONG_OFFSET(){return ht}static get TIME_24_SIMPLE(){return mt}static get TIME_24_WITH_SECONDS(){return yt}static get TIME_24_WITH_SHORT_OFFSET(){return gt}static get TIME_24_WITH_LONG_OFFSET(){return pt}static get DATETIME_SHORT(){return wt}static get DATETIME_SHORT_WITH_SECONDS(){return St}static get DATETIME_MED(){return kt}static get DATETIME_MED_WITH_SECONDS(){return Tt}static get DATETIME_MED_WITH_WEEKDAY(){return zn}static get DATETIME_FULL(){return Ot}static get DATETIME_FULL_WITH_SECONDS(){return Nt}static get DATETIME_HUGE(){return Et}static get DATETIME_HUGE_WITH_SECONDS(){return xt}}function he(s){if(g.isDateTime(s))return s;if(s&&s.valueOf&&Y(s.valueOf()))return g.fromJSDate(s);if(s&&typeof s=="object")return g.fromObject(s);throw new D(`Unknown datetime argument: ${s}, of type ${typeof s}`)}const Lr=[".jpg",".jpeg",".png",".svg",".gif",".jfif",".webp",".avif"],Rr=[".mp4",".avi",".mov",".3gp",".wmv"],$r=[".aa",".aac",".m4v",".mp3",".ogg",".oga",".mogg",".amr"],Zr=[".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".odp",".odt",".ods",".txt"];class d{static isObject(e){return e!==null&&typeof e=="object"&&e.constructor===Object}static clone(e){return typeof structuredClone<"u"?structuredClone(e):JSON.parse(JSON.stringify(e))}static zeroValue(e){switch(typeof e){case"string":return"";case"number":return 0;case"boolean":return!1;case"object":return e===null?null:Array.isArray(e)?[]:{};case"undefined":return;default:return null}}static isEmpty(e){return e===""||e===null||typeof e>"u"||Array.isArray(e)&&e.length===0||d.isObject(e)&&Object.keys(e).length===0}static isInput(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return t==="input"||t==="select"||t==="textarea"||(e==null?void 0:e.isContentEditable)}static isFocusable(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return d.isInput(e)||t==="button"||t==="a"||t==="details"||(e==null?void 0:e.tabIndex)>=0}static hasNonEmptyProps(e){for(let t in e)if(!d.isEmpty(e[t]))return!0;return!1}static toArray(e,t=!1){return Array.isArray(e)?e.slice():(t||!d.isEmpty(e))&&typeof e<"u"?[e]:[]}static inArray(e,t){e=Array.isArray(e)?e:[];for(let n=e.length-1;n>=0;n--)if(e[n]==t)return!0;return!1}static removeByValue(e,t){e=Array.isArray(e)?e:[];for(let n=e.length-1;n>=0;n--)if(e[n]==t){e.splice(n,1);break}}static pushUnique(e,t){d.inArray(e,t)||e.push(t)}static findByKey(e,t,n){e=Array.isArray(e)?e:[];for(let r in e)if(e[r][t]==n)return e[r];return null}static groupByKey(e,t){e=Array.isArray(e)?e:[];const n={};for(let r in e)n[e[r][t]]=n[e[r][t]]||[],n[e[r][t]].push(e[r]);return n}static removeByKey(e,t,n){for(let r in e)if(e[r][t]==n){e.splice(r,1);break}}static pushOrReplaceByKey(e,t,n="id"){for(let r=e.length-1;r>=0;r--)if(e[r][n]==t[n]){e[r]=t;return}e.push(t)}static filterDuplicatesByKey(e,t="id"){e=Array.isArray(e)?e:[];const n={};for(const r of e)n[r[t]]=r;return Object.values(n)}static filterRedactedProps(e,t="******"){const n=JSON.parse(JSON.stringify(e||{}));for(let r in n)typeof n[r]=="object"&&n[r]!==null?n[r]=d.filterRedactedProps(n[r],t):n[r]===t&&delete n[r];return n}static getNestedVal(e,t,n=null,r="."){let i=e||{},a=(t||"").split(r);for(const o of a){if(!d.isObject(i)&&!Array.isArray(i)||typeof i[o]>"u")return n;i=i[o]}return i}static setByPath(e,t,n,r="."){if(e===null||typeof e!="object"){console.warn("setByPath: data not an object or array.");return}let i=e,a=t.split(r),o=a.pop();for(const u of a)(!d.isObject(i)&&!Array.isArray(i)||!d.isObject(i[u])&&!Array.isArray(i[u]))&&(i[u]={}),i=i[u];i[o]=n}static deleteByPath(e,t,n="."){let r=e||{},i=(t||"").split(n),a=i.pop();for(const o of i)(!d.isObject(r)&&!Array.isArray(r)||!d.isObject(r[o])&&!Array.isArray(r[o]))&&(r[o]={}),r=r[o];Array.isArray(r)?r.splice(a,1):d.isObject(r)&&delete r[a],i.length>0&&(Array.isArray(r)&&!r.length||d.isObject(r)&&!Object.keys(r).length)&&(Array.isArray(e)&&e.length>0||d.isObject(e)&&Object.keys(e).length>0)&&d.deleteByPath(e,i.join(n),n)}static randomString(e=10){let t="",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let r=0;r"u")return d.randomString(e);const t=new Uint8Array(e);crypto.getRandomValues(t);const n="-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";let r="";for(let i=0;ii.replaceAll("{_PB_ESCAPED_}",t));for(let i of r)i=i.trim(),d.isEmpty(i)||n.push(i);return n}static joinNonEmpty(e,t=", "){e=e||[];const n=[],r=t.length>1?t.trim():t;for(let i of e)i=typeof i=="string"?i.trim():"",d.isEmpty(i)||n.push(i.replaceAll(r,"\\"+r));return n.join(t)}static getInitials(e){if(e=(e||"").split("@")[0].trim(),e.length<=2)return e.toUpperCase();const t=e.split(/[\.\_\-\ ]/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e[0].toUpperCase()}static formattedFileSize(e){const t=e?Math.floor(Math.log(e)/Math.log(1024)):0;return(e/Math.pow(1024,t)).toFixed(2)*1+" "+["B","KB","MB","GB","TB"][t]}static getDateTime(e){if(typeof e=="string"){const t={19:"yyyy-MM-dd HH:mm:ss",23:"yyyy-MM-dd HH:mm:ss.SSS",20:"yyyy-MM-dd HH:mm:ss'Z'",24:"yyyy-MM-dd HH:mm:ss.SSS'Z'"},n=t[e.length]||t[19];return g.fromFormat(e,n,{zone:"UTC"})}return g.fromJSDate(e)}static formatToUTCDate(e,t="yyyy-MM-dd HH:mm:ss"){return d.getDateTime(e).toUTC().toFormat(t)}static formatToLocalDate(e,t="yyyy-MM-dd HH:mm:ss"){return d.getDateTime(e).toLocal().toFormat(t)}static async copyToClipboard(e){var t;if(e=""+e,!(!e.length||!((t=window==null?void 0:window.navigator)!=null&&t.clipboard)))return window.navigator.clipboard.writeText(e).catch(n=>{console.warn("Failed to copy.",n)})}static download(e,t){const n=document.createElement("a");n.setAttribute("href",e),n.setAttribute("download",t),n.setAttribute("target","_blank"),n.click(),n.remove()}static downloadJson(e,t){t=t.endsWith(".json")?t:t+".json";const n=new Blob([JSON.stringify(e,null,2)],{type:"application/json"}),r=window.URL.createObjectURL(n);d.download(r,t)}static getJWTPayload(e){const t=(e||"").split(".")[1]||"";if(t==="")return{};try{const n=decodeURIComponent(atob(t));return JSON.parse(n)||{}}catch(n){console.warn("Failed to parse JWT payload data.",n)}return{}}static hasImageExtension(e){return e=e||"",!!Lr.find(t=>e.toLowerCase().endsWith(t))}static hasVideoExtension(e){return e=e||"",!!Rr.find(t=>e.toLowerCase().endsWith(t))}static hasAudioExtension(e){return e=e||"",!!$r.find(t=>e.toLowerCase().endsWith(t))}static hasDocumentExtension(e){return e=e||"",!!Zr.find(t=>e.toLowerCase().endsWith(t))}static getFileType(e){return d.hasImageExtension(e)?"image":d.hasDocumentExtension(e)?"document":d.hasVideoExtension(e)?"video":d.hasAudioExtension(e)?"audio":"file"}static generateThumb(e,t=100,n=100){return new Promise(r=>{let i=new FileReader;i.onload=function(a){let o=new Image;o.onload=function(){let u=document.createElement("canvas"),l=u.getContext("2d"),c=o.width,h=o.height;return u.width=t,u.height=n,l.drawImage(o,c>h?(c-h)/2:0,0,c>h?h:c,c>h?h:c,0,0,t,n),r(u.toDataURL(e.type))},o.src=a.target.result},i.readAsDataURL(e)})}static addValueToFormData(e,t,n){if(!(typeof n>"u"))if(d.isEmpty(n))e.append(t,"");else if(Array.isArray(n))for(const r of n)d.addValueToFormData(e,t,r);else n instanceof File?e.append(t,n):n instanceof Date?e.append(t,n.toISOString()):d.isObject(n)?e.append(t,JSON.stringify(n)):e.append(t,""+n)}static dummyCollectionRecord(e){var u,l,c,h,p,m,N;const t=(e==null?void 0:e.schema)||[],n=(e==null?void 0:e.type)==="auth",r=(e==null?void 0:e.type)==="view",i={id:"RECORD_ID",collectionId:e==null?void 0:e.id,collectionName:e==null?void 0:e.name};n&&(i.username="username123",i.verified=!1,i.emailVisibility=!0,i.email="test@example.com"),(!r||d.extractColumnsFromQuery((u=e==null?void 0:e.options)==null?void 0:u.query).includes("created"))&&(i.created="2022-01-01 01:00:00.123Z"),(!r||d.extractColumnsFromQuery((l=e==null?void 0:e.options)==null?void 0:l.query).includes("updated"))&&(i.updated="2022-01-01 23:59:59.456Z");for(const O of t){let k=null;O.type==="number"?k=123:O.type==="date"?k="2022-01-01 10:00:00.123Z":O.type==="bool"?k=!0:O.type==="email"?k="test@example.com":O.type==="url"?k="https://example.com":O.type==="json"?k="JSON":O.type==="file"?(k="filename.jpg",((c=O.options)==null?void 0:c.maxSelect)!==1&&(k=[k])):O.type==="select"?(k=(p=(h=O.options)==null?void 0:h.values)==null?void 0:p[0],((m=O.options)==null?void 0:m.maxSelect)!==1&&(k=[k])):O.type==="relation"?(k="RELATION_RECORD_ID",((N=O.options)==null?void 0:N.maxSelect)!==1&&(k=[k])):k="test",i[O.name]=k}return i}static dummyCollectionSchemaData(e){var r,i,a,o;const t=(e==null?void 0:e.schema)||[],n={};for(const u of t){let l=null;if(u.type==="number")l=123;else if(u.type==="date")l="2022-01-01 10:00:00.123Z";else if(u.type==="bool")l=!0;else if(u.type==="email")l="test@example.com";else if(u.type==="url")l="https://example.com";else if(u.type==="json")l="JSON";else{if(u.type==="file")continue;u.type==="select"?(l=(i=(r=u.options)==null?void 0:r.values)==null?void 0:i[0],((a=u.options)==null?void 0:a.maxSelect)!==1&&(l=[l])):u.type==="relation"?(l="RELATION_RECORD_ID",((o=u.options)==null?void 0:o.maxSelect)!==1&&(l=[l])):l="test"}n[u.name]=l}return n}static getCollectionTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"auth":return"ri-group-line";case"view":return"ri-table-line";default:return"ri-folder-2-line"}}static getFieldTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"primary":return"ri-key-line";case"text":return"ri-text";case"number":return"ri-hashtag";case"date":return"ri-calendar-line";case"bool":return"ri-toggle-line";case"email":return"ri-mail-line";case"url":return"ri-link";case"editor":return"ri-edit-2-line";case"select":return"ri-list-check";case"json":return"ri-braces-line";case"file":return"ri-image-line";case"relation":return"ri-mind-map";case"user":return"ri-user-line";default:return"ri-star-s-line"}}static getFieldValueType(e){var t;switch(e==null?void 0:e.type){case"bool":return"Boolean";case"number":return"Number";case"file":return"File";case"select":case"relation":return((t=e==null?void 0:e.options)==null?void 0:t.maxSelect)===1?"String":"Array";default:return"String"}}static zeroDefaultStr(e){var t;return(e==null?void 0:e.type)==="number"?"0":(e==null?void 0:e.type)==="bool"?"false":(e==null?void 0:e.type)==="json"?'null, "", [], {}':["select","relation","file"].includes(e==null?void 0:e.type)&&((t=e==null?void 0:e.options)==null?void 0:t.maxSelect)!=1?"[]":'""'}static getApiExampleUrl(e){return(window.location.href.substring(0,window.location.href.indexOf("/_"))||e||"/").replace("//localhost","//127.0.0.1")}static hasCollectionChanges(e,t,n=!1){if(e=e||{},t=t||{},e.id!=t.id)return!0;for(let l in e)if(l!=="schema"&&JSON.stringify(e[l])!==JSON.stringify(t[l]))return!0;const r=Array.isArray(e.schema)?e.schema:[],i=Array.isArray(t.schema)?t.schema:[],a=r.filter(l=>(l==null?void 0:l.id)&&!d.findByKey(i,"id",l.id)),o=i.filter(l=>(l==null?void 0:l.id)&&!d.findByKey(r,"id",l.id)),u=i.filter(l=>{const c=d.isObject(l)&&d.findByKey(r,"id",l.id);if(!c)return!1;for(let h in c)if(JSON.stringify(l[h])!=JSON.stringify(c[h]))return!0;return!1});return!!(o.length||u.length||n&&a.length)}static sortCollections(e=[]){const t=[],n=[],r=[];for(const a of e)a.type==="auth"?t.push(a):a.type==="base"?n.push(a):r.push(a);function i(a,o){return a.name>o.name?1:a.name{setTimeout(e,0)})}static defaultFlatpickrOptions(){return{dateFormat:"Y-m-d H:i:S",disableMobile:!0,allowInput:!0,enableTime:!0,time_24hr:!0,locale:{firstDayOfWeek:1}}}static defaultEditorOptions(){const e=["DIV","P","A","EM","B","STRONG","H1","H2","H3","H4","H5","H6","TABLE","TR","TD","TH","TBODY","THEAD","TFOOT","BR","HR","Q","SUP","SUB","DEL","IMG","OL","UL","LI","CODE"];function t(r){let i=r.parentNode;for(;r.firstChild;)i.insertBefore(r.firstChild,r);i.removeChild(r)}function n(r){if(r){for(const i of r.children)n(i);e.includes(r.tagName)?(r.removeAttribute("style"),r.removeAttribute("class")):t(r)}}return{branding:!1,promotion:!1,menubar:!1,min_height:270,height:270,max_height:700,autoresize_bottom_margin:30,convert_unsafe_embeds:!0,skin:"pocketbase",content_style:"body { font-size: 14px }",plugins:["autoresize","autolink","lists","link","image","searchreplace","fullscreen","media","table","code","codesample","directionality"],codesample_global_prismjs:!0,codesample_languages:[{text:"HTML/XML",value:"markup"},{text:"CSS",value:"css"},{text:"SQL",value:"sql"},{text:"JavaScript",value:"javascript"},{text:"Go",value:"go"},{text:"Dart",value:"dart"},{text:"Zig",value:"zig"},{text:"Rust",value:"rust"},{text:"Lua",value:"lua"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"},{text:"Markdown",value:"markdown"},{text:"Swift",value:"swift"},{text:"Kotlin",value:"kotlin"},{text:"Elixir",value:"elixir"},{text:"Scala",value:"scala"},{text:"Julia",value:"julia"},{text:"Haskell",value:"haskell"}],toolbar:"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link image_picker table codesample direction | code fullscreen",paste_postprocess:(r,i)=>{n(i.node)},file_picker_types:"image",file_picker_callback:(r,i,a)=>{const o=document.createElement("input");o.setAttribute("type","file"),o.setAttribute("accept","image/*"),o.addEventListener("change",u=>{const l=u.target.files[0],c=new FileReader;c.addEventListener("load",()=>{if(!tinymce)return;const h="blobid"+new Date().getTime(),p=tinymce.activeEditor.editorUpload.blobCache,m=c.result.split(",")[1],N=p.create(h,l,m);p.add(N),r(N.blobUri(),{title:l.name})}),c.readAsDataURL(l)}),o.click()},setup:r=>{r.on("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.code=="KeyS"&&r.formElement&&(a.preventDefault(),a.stopPropagation(),r.formElement.dispatchEvent(new KeyboardEvent("keydown",a)))});const i="tinymce_last_direction";r.on("init",()=>{var o;const a=(o=window==null?void 0:window.localStorage)==null?void 0:o.getItem(i);!r.isDirty()&&r.getContent()==""&&a=="rtl"&&r.execCommand("mceDirectionRTL")}),r.ui.registry.addMenuButton("direction",{icon:"visualchars",fetch:a=>{a([{type:"menuitem",text:"LTR content",icon:"ltr",onAction:()=>{var u;(u=window==null?void 0:window.localStorage)==null||u.setItem(i,"ltr"),r.execCommand("mceDirectionLTR")}},{type:"menuitem",text:"RTL content",icon:"rtl",onAction:()=>{var u;(u=window==null?void 0:window.localStorage)==null||u.setItem(i,"rtl"),r.execCommand("mceDirectionRTL")}}])}}),r.ui.registry.addMenuButton("image_picker",{icon:"image",fetch:a=>{a([{type:"menuitem",text:"From collection",icon:"gallery",onAction:()=>{r.dispatch("collections_file_picker",{})}},{type:"menuitem",text:"Inline",icon:"browse",onAction:()=>{r.execCommand("mceImage")}}])}})}}}static displayValue(e,t,n="N/A"){e=e||{},t=t||[];let r=[];for(const a of t){let o=e[a];typeof o>"u"||(o=d.stringifyValue(o,n),r.push(o))}if(r.length>0)return r.join(", ");const i=["title","name","slug","email","username","nickname","label","heading","message","key","identifier","id"];for(const a of i){let o=d.stringifyValue(e[a],"");if(o)return o}return n}static stringifyValue(e,t="N/A",n=150){if(d.isEmpty(e))return t;if(typeof e=="number")return""+e;if(typeof e=="boolean")return e?"True":"False";if(typeof e=="string")return e=e.indexOf("<")>=0?d.plainText(e):e,d.truncate(e,n)||t;if(Array.isArray(e)&&typeof e[0]!="object")return d.truncate(e.join(","),n);if(typeof e=="object")try{return d.truncate(JSON.stringify(e),n)||t}catch{return t}return e}static extractColumnsFromQuery(e){var a;const t="__GROUP__";e=(e||"").replace(/\([\s\S]+?\)/gm,t).replace(/[\t\r\n]|(?:\s\s)+/g," ");const n=e.match(/select\s+([\s\S]+)\s+from/),r=((a=n==null?void 0:n[1])==null?void 0:a.split(","))||[],i=[];for(let o of r){const u=o.trim().split(" ").pop();u!=""&&u!=t&&i.push(u.replace(/[\'\"\`\[\]\s]/g,""))}return i}static getAllCollectionIdentifiers(e,t=""){if(!e)return[];let n=[t+"id"];if(e.type==="view")for(let i of d.extractColumnsFromQuery(e.options.query))d.pushUnique(n,t+i);else e.type==="auth"?(n.push(t+"username"),n.push(t+"email"),n.push(t+"emailVisibility"),n.push(t+"verified"),n.push(t+"created"),n.push(t+"updated")):(n.push(t+"created"),n.push(t+"updated"));const r=e.schema||[];for(const i of r)d.pushUnique(n,t+i.name);return n}static getCollectionAutocompleteKeys(e,t,n="",r=0){var o,u,l;let i=e.find(c=>c.name==t||c.id==t);if(!i||r>=4)return[];i.schema=i.schema||[];let a=d.getAllCollectionIdentifiers(i,n);for(const c of i.schema){const h=n+c.name;if(c.type=="relation"&&((o=c.options)!=null&&o.collectionId)){const p=d.getCollectionAutocompleteKeys(e,c.options.collectionId,h+".",r+1);p.length&&(a=a.concat(p))}((u=c.options)==null?void 0:u.maxSelect)!=1&&["select","file","relation"].includes(c.type)&&(a.push(h+":each"),a.push(h+":length"))}for(const c of e){c.schema=c.schema||[];for(const h of c.schema)if(h.type=="relation"&&((l=h.options)==null?void 0:l.collectionId)==i.id){const p=n+c.name+"_via_"+h.name,m=d.getCollectionAutocompleteKeys(e,c.id,p+".",r+2);m.length&&(a=a.concat(m))}}return a}static getCollectionJoinAutocompleteKeys(e){const t=[];for(const n of e){const r="@collection."+n.name+".",i=d.getCollectionAutocompleteKeys(e,n.name,r);for(const a of i)t.push(a)}return t}static getRequestAutocompleteKeys(e,t){const n=[];n.push("@request.context"),n.push("@request.method"),n.push("@request.query."),n.push("@request.data."),n.push("@request.headers."),n.push("@request.auth.id"),n.push("@request.auth.collectionId"),n.push("@request.auth.collectionName"),n.push("@request.auth.verified"),n.push("@request.auth.username"),n.push("@request.auth.email"),n.push("@request.auth.emailVisibility"),n.push("@request.auth.created"),n.push("@request.auth.updated");const r=e.filter(i=>i.type==="auth");for(const i of r){const a=d.getCollectionAutocompleteKeys(e,i.id,"@request.auth.");for(const o of a)d.pushUnique(n,o)}if(t){const i=["created","updated"],a=d.getCollectionAutocompleteKeys(e,t,"@request.data.");for(const o of a){n.push(o);const u=o.split(".");u.length===3&&u[2].indexOf(":")===-1&&!i.includes(u[2])&&n.push(o+":isset")}}return n}static parseIndex(e){var u,l,c,h,p;const t={unique:!1,optional:!1,schemaName:"",indexName:"",tableName:"",columns:[],where:""},r=/create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?/gmi.exec((e||"").trim());if((r==null?void 0:r.length)!=7)return t;const i=/^[\"\'\`\[\{}]|[\"\'\`\]\}]$/gm;t.unique=((u=r[1])==null?void 0:u.trim().toLowerCase())==="unique",t.optional=!d.isEmpty((l=r[2])==null?void 0:l.trim());const a=(r[3]||"").split(".");a.length==2?(t.schemaName=a[0].replace(i,""),t.indexName=a[1].replace(i,"")):(t.schemaName="",t.indexName=a[0].replace(i,"")),t.tableName=(r[4]||"").replace(i,"");const o=(r[5]||"").replace(/,(?=[^\(]*\))/gmi,"{PB_TEMP}").split(",");for(let m of o){m=m.trim().replaceAll("{PB_TEMP}",",");const O=/^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$/gmi.exec(m);if((O==null?void 0:O.length)!=4)continue;const k=(h=(c=O[1])==null?void 0:c.trim())==null?void 0:h.replace(i,"");k&&t.columns.push({name:k,collate:O[2]||"",sort:((p=O[3])==null?void 0:p.toUpperCase())||""})}return t.where=r[6]||"",t}static buildIndex(e){let t="CREATE ";e.unique&&(t+="UNIQUE "),t+="INDEX ",e.optional&&(t+="IF NOT EXISTS "),e.schemaName&&(t+=`\`${e.schemaName}\`.`),t+=`\`${e.indexName||"idx_"+d.randomString(7)}\` `,t+=`ON \`${e.tableName}\` (`;const n=e.columns.filter(r=>!!(r!=null&&r.name));return n.length>1&&(t+=` - `),t+=n.map(r=>{let i="";return r.name.includes("(")||r.name.includes(" ")?i+=r.name:i+="`"+r.name+"`",r.collate&&(i+=" COLLATE "+r.collate),r.sort&&(i+=" "+r.sort.toUpperCase()),i}).join(`, - `),n.length>1&&(t+=` -`),t+=")",e.where&&(t+=` WHERE ${e.where}`),t}static replaceIndexTableName(e,t){const n=d.parseIndex(e);return n.tableName=t,d.buildIndex(n)}static replaceIndexColumn(e,t,n){if(t===n)return e;const r=d.parseIndex(e);let i=!1;for(let a of r.columns)a.name===t&&(a.name=n,i=!0);return i?d.buildIndex(r):e}static normalizeSearchFilter(e,t){if(e=(e||"").trim(),!e||!t.length)return e;const n=["=","!=","~","!~",">",">=","<","<="];for(const r of n)if(e.includes(r))return e;return e=isNaN(e)&&e!="true"&&e!="false"?`"${e.replace(/^[\"\'\`]|[\"\'\`]$/gm,"")}"`:e,t.map(r=>`${r}~${e}`).join("||")}static normalizeLogsFilter(e,t=[]){return d.normalizeSearchFilter(e,["level","message","data"].concat(t))}static initCollection(e){return Object.assign({id:"",created:"",updated:"",name:"",type:"base",system:!1,listRule:null,viewRule:null,createRule:null,updateRule:null,deleteRule:null,schema:[],indexes:[],options:{}},e)}static initSchemaField(e){return Object.assign({id:"",name:"",type:"text",system:!1,required:!1,options:{}},e)}static triggerResize(){window.dispatchEvent(new Event("resize"))}static getHashQueryParams(){let e="";const t=window.location.hash.indexOf("?");return t>-1&&(e=window.location.hash.substring(t+1)),Object.fromEntries(new URLSearchParams(e))}static replaceHashQueryParams(e){e=e||{};let t="",n=window.location.hash;const r=n.indexOf("?");r>-1&&(t=n.substring(r+1),n=n.substring(0,r));const i=new URLSearchParams(t);for(let u in e){const l=e[u];l===null?i.delete(u):i.set(u,l)}t=i.toString(),t!=""&&(n+="?"+t);let a=window.location.href;const o=a.indexOf("#");o>-1&&(a=a.substring(0,o)),window.location.replace(a+n)}}const nt=11e3;onmessage=s=>{var t,n;if(!s.data.collections)return;const e={};e.baseKeys=d.getCollectionAutocompleteKeys(s.data.collections,(t=s.data.baseCollection)==null?void 0:t.name),e.baseKeys=rt(e.baseKeys.sort(st),nt),s.data.disableRequestKeys||(e.requestKeys=d.getRequestAutocompleteKeys(s.data.collections,(n=s.data.baseCollection)==null?void 0:n.name),e.requestKeys=rt(e.requestKeys.sort(st),nt)),s.data.disableCollectionJoinKeys||(e.collectionJoinKeys=d.getCollectionJoinAutocompleteKeys(s.data.collections),e.collectionJoinKeys=rt(e.collectionJoinKeys.sort(st),nt)),postMessage(e)};function st(s,e){return s.length-e.length}function rt(s,e){return s.length>e?s.slice(0,e):s}})(); diff --git a/ui/dist/assets/index-B-F-pko3.js b/ui/dist/assets/index-B-F-pko3.js new file mode 100644 index 00000000..ec8dc055 --- /dev/null +++ b/ui/dist/assets/index-B-F-pko3.js @@ -0,0 +1,200 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./FilterAutocompleteInput-DvxlPb20.js","./index-B5ReTu-C.js","./ListApiDocs-C0epAOOQ.js","./FieldsQueryParam-CW6KZfgu.js","./ListApiDocs-DhdAtA7Y.css","./ViewApiDocs-B6MdbOQi.js","./CreateApiDocs-Cvocn8eg.js","./UpdateApiDocs-BIFiuRUJ.js","./AuthMethodsDocs-DkjR8bbt.js","./AuthRefreshDocs-DVyzazkj.js","./AuthWithPasswordDocs-CTYk9AZ_.js","./AuthWithOAuth2Docs-C0rwhR6l.js","./CodeEditor-CPgcqnd5.js"])))=>i.map(i=>d[i]); +var mk=Object.defineProperty;var hk=(n,e,t)=>e in n?mk(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var ct=(n,e,t)=>hk(n,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))i(l);new MutationObserver(l=>{for(const s of l)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function t(l){const s={};return l.integrity&&(s.integrity=l.integrity),l.referrerPolicy&&(s.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?s.credentials="include":l.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function i(l){if(l.ep)return;l.ep=!0;const s=t(l);fetch(l.href,s)}})();function te(){}const io=n=>n;function je(n,e){for(const t in e)n[t]=e[t];return n}function _k(n){return!!n&&(typeof n=="object"||typeof n=="function")&&typeof n.then=="function"}function Ob(n){return n()}function df(){return Object.create(null)}function De(n){n.forEach(Ob)}function Rt(n){return typeof n=="function"}function _e(n,e){return n!=n?e==e:n!==e||n&&typeof n=="object"||typeof n=="function"}let So;function vn(n,e){return n===e?!0:(So||(So=document.createElement("a")),So.href=e,n===So.href)}function gk(n){return Object.keys(n).length===0}function uu(n,...e){if(n==null){for(const i of e)i(void 0);return te}const t=n.subscribe(...e);return t.unsubscribe?()=>t.unsubscribe():t}function Eb(n){let e;return uu(n,t=>e=t)(),e}function Qe(n,e,t){n.$$.on_destroy.push(uu(e,t))}function Lt(n,e,t,i){if(n){const l=Mb(n,e,t,i);return n[0](l)}}function Mb(n,e,t,i){return n[1]&&i?je(t.ctx.slice(),n[1](i(e))):t.ctx}function At(n,e,t,i){if(n[2]&&i){const l=n[2](i(t));if(e.dirty===void 0)return l;if(typeof l=="object"){const s=[],o=Math.max(e.dirty.length,l.length);for(let r=0;r32){const e=[],t=n.ctx.length/32;for(let i=0;iwindow.performance.now():()=>Date.now(),fu=Db?n=>requestAnimationFrame(n):te;const Zl=new Set;function Ib(n){Zl.forEach(e=>{e.c(n)||(Zl.delete(e),e.f())}),Zl.size!==0&&fu(Ib)}function Er(n){let e;return Zl.size===0&&fu(Ib),{promise:new Promise(t=>{Zl.add(e={c:n,f:t})}),abort(){Zl.delete(e)}}}function w(n,e){n.appendChild(e)}function Lb(n){if(!n)return document;const e=n.getRootNode?n.getRootNode():n.ownerDocument;return e&&e.host?e:n.ownerDocument}function bk(n){const e=b("style");return e.textContent="/* empty */",yk(Lb(n),e),e.sheet}function yk(n,e){return w(n.head||n,e),e.sheet}function v(n,e,t){n.insertBefore(e,t||null)}function k(n){n.parentNode&&n.parentNode.removeChild(n)}function pt(n,e){for(let t=0;tn.removeEventListener(e,t,i)}function tt(n){return function(e){return e.preventDefault(),n.call(this,e)}}function On(n){return function(e){return e.stopPropagation(),n.call(this,e)}}function p(n,e,t){t==null?n.removeAttribute(e):n.getAttribute(e)!==t&&n.setAttribute(e,t)}const kk=["width","height"];function Xn(n,e){const t=Object.getOwnPropertyDescriptors(n.__proto__);for(const i in e)e[i]==null?n.removeAttribute(i):i==="style"?n.style.cssText=e[i]:i==="__value"?n.value=n[i]=e[i]:t[i]&&t[i].set&&kk.indexOf(i)===-1?n[i]=e[i]:p(n,i,e[i])}function vk(n){let e;return{p(...t){e=t,e.forEach(i=>n.push(i))},r(){e.forEach(t=>n.splice(n.indexOf(t),1))}}}function St(n){return n===""?null:+n}function wk(n){return Array.from(n.childNodes)}function ue(n,e){e=""+e,n.data!==e&&(n.data=e)}function ce(n,e){n.value=e??""}function Sk(n,e,t,i){t==null?n.style.removeProperty(e):n.style.setProperty(e,t,"")}function x(n,e,t){n.classList.toggle(e,!!t)}function Ab(n,e,{bubbles:t=!1,cancelable:i=!1}={}){return new CustomEvent(n,{detail:e,bubbles:t,cancelable:i})}function jt(n,e){return new n(e)}const fr=new Map;let cr=0;function Tk(n){let e=5381,t=n.length;for(;t--;)e=(e<<5)-e^n.charCodeAt(t);return e>>>0}function $k(n,e){const t={stylesheet:bk(e),rules:{}};return fr.set(n,t),t}function zs(n,e,t,i,l,s,o,r=0){const a=16.666/i;let u=`{ +`;for(let _=0;_<=1;_+=a){const y=e+(t-e)*s(_);u+=_*100+`%{${o(y,1-y)}} +`}const f=u+`100% {${o(t,1-t)}} +}`,c=`__svelte_${Tk(f)}_${r}`,d=Lb(n),{stylesheet:m,rules:h}=fr.get(d)||$k(d,n);h[c]||(h[c]=!0,m.insertRule(`@keyframes ${c} ${f}`,m.cssRules.length));const g=n.style.animation||"";return n.style.animation=`${g?`${g}, `:""}${c} ${i}ms linear ${l}ms 1 both`,cr+=1,c}function Us(n,e){const t=(n.style.animation||"").split(", "),i=t.filter(e?s=>s.indexOf(e)<0:s=>s.indexOf("__svelte")===-1),l=t.length-i.length;l&&(n.style.animation=i.join(", "),cr-=l,cr||Ck())}function Ck(){fu(()=>{cr||(fr.forEach(n=>{const{ownerNode:e}=n.stylesheet;e&&k(e)}),fr.clear())})}function Ok(n,e,t,i){if(!e)return te;const l=n.getBoundingClientRect();if(e.left===l.left&&e.right===l.right&&e.top===l.top&&e.bottom===l.bottom)return te;const{delay:s=0,duration:o=300,easing:r=io,start:a=Or()+s,end:u=a+o,tick:f=te,css:c}=t(n,{from:e,to:l},i);let d=!0,m=!1,h;function g(){c&&(h=zs(n,0,1,o,s,r,c)),s||(m=!0)}function _(){c&&Us(n,h),d=!1}return Er(y=>{if(!m&&y>=a&&(m=!0),m&&y>=u&&(f(1,0),_()),!d)return!1;if(m){const S=y-a,T=0+1*r(S/o);f(T,1-T)}return!0}),g(),f(0,1),_}function Ek(n){const e=getComputedStyle(n);if(e.position!=="absolute"&&e.position!=="fixed"){const{width:t,height:i}=e,l=n.getBoundingClientRect();n.style.position="absolute",n.style.width=t,n.style.height=i,Pb(n,l)}}function Pb(n,e){const t=n.getBoundingClientRect();if(e.left!==t.left||e.top!==t.top){const i=getComputedStyle(n),l=i.transform==="none"?"":i.transform;n.style.transform=`${l} translate(${e.left-t.left}px, ${e.top-t.top}px)`}}let Vs;function Ni(n){Vs=n}function lo(){if(!Vs)throw new Error("Function called outside component initialization");return Vs}function Yt(n){lo().$$.on_mount.push(n)}function Mk(n){lo().$$.after_update.push(n)}function so(n){lo().$$.on_destroy.push(n)}function _t(){const n=lo();return(e,t,{cancelable:i=!1}={})=>{const l=n.$$.callbacks[e];if(l){const s=Ab(e,t,{cancelable:i});return l.slice().forEach(o=>{o.call(n,s)}),!s.defaultPrevented}return!0}}function Pe(n,e){const t=n.$$.callbacks[e.type];t&&t.slice().forEach(i=>i.call(this,e))}const Yl=[],ie=[];let Gl=[];const Pa=[],Nb=Promise.resolve();let Na=!1;function Rb(){Na||(Na=!0,Nb.then(cu))}function fn(){return Rb(),Nb}function nt(n){Gl.push(n)}function $e(n){Pa.push(n)}const Zr=new Set;let jl=0;function cu(){if(jl!==0)return;const n=Vs;do{try{for(;jln.indexOf(i)===-1?e.push(i):t.push(i)),t.forEach(i=>i()),Gl=e}let gs;function du(){return gs||(gs=Promise.resolve(),gs.then(()=>{gs=null})),gs}function Ol(n,e,t){n.dispatchEvent(Ab(`${e?"intro":"outro"}${t}`))}const Xo=new Set;let wi;function re(){wi={r:0,c:[],p:wi}}function ae(){wi.r||De(wi.c),wi=wi.p}function O(n,e){n&&n.i&&(Xo.delete(n),n.i(e))}function D(n,e,t,i){if(n&&n.o){if(Xo.has(n))return;Xo.add(n),wi.c.push(()=>{Xo.delete(n),i&&(t&&n.d(1),i())}),n.o(e)}else i&&i()}const pu={duration:0};function Fb(n,e,t){const i={direction:"in"};let l=e(n,t,i),s=!1,o,r,a=0;function u(){o&&Us(n,o)}function f(){const{delay:d=0,duration:m=300,easing:h=io,tick:g=te,css:_}=l||pu;_&&(o=zs(n,0,1,m,d,h,_,a++)),g(0,1);const y=Or()+d,S=y+m;r&&r.abort(),s=!0,nt(()=>Ol(n,!0,"start")),r=Er(T=>{if(s){if(T>=S)return g(1,0),Ol(n,!0,"end"),u(),s=!1;if(T>=y){const $=h((T-y)/m);g($,1-$)}}return s})}let c=!1;return{start(){c||(c=!0,Us(n),Rt(l)?(l=l(i),du().then(f)):f())},invalidate(){c=!1},end(){s&&(u(),s=!1)}}}function mu(n,e,t){const i={direction:"out"};let l=e(n,t,i),s=!0,o;const r=wi;r.r+=1;let a;function u(){const{delay:f=0,duration:c=300,easing:d=io,tick:m=te,css:h}=l||pu;h&&(o=zs(n,1,0,c,f,d,h));const g=Or()+f,_=g+c;nt(()=>Ol(n,!1,"start")),"inert"in n&&(a=n.inert,n.inert=!0),Er(y=>{if(s){if(y>=_)return m(0,1),Ol(n,!1,"end"),--r.r||De(r.c),!1;if(y>=g){const S=d((y-g)/c);m(1-S,S)}}return s})}return Rt(l)?du().then(()=>{l=l(i),u()}):u(),{end(f){f&&"inert"in n&&(n.inert=a),f&&l.tick&&l.tick(1,0),s&&(o&&Us(n,o),s=!1)}}}function ze(n,e,t,i){let s=e(n,t,{direction:"both"}),o=i?0:1,r=null,a=null,u=null,f;function c(){u&&Us(n,u)}function d(h,g){const _=h.b-o;return g*=Math.abs(_),{a:o,b:h.b,d:_,duration:g,start:h.start,end:h.start+g,group:h.group}}function m(h){const{delay:g=0,duration:_=300,easing:y=io,tick:S=te,css:T}=s||pu,$={start:Or()+g,b:h};h||($.group=wi,wi.r+=1),"inert"in n&&(h?f!==void 0&&(n.inert=f):(f=n.inert,n.inert=!0)),r||a?a=$:(T&&(c(),u=zs(n,o,h,_,g,y,T)),h&&S(0,1),r=d($,_),nt(()=>Ol(n,h,"start")),Er(E=>{if(a&&E>a.start&&(r=d(a,_),a=null,Ol(n,r.b,"start"),T&&(c(),u=zs(n,o,r.b,r.duration,0,y,s.css))),r){if(E>=r.end)S(o=r.b,1-o),Ol(n,r.b,"end"),a||(r.b?c():--r.group.r||De(r.group.c)),r=null;else if(E>=r.start){const M=E-r.start;o=r.a+r.d*y(M/r.duration),S(o,1-o)}}return!!(r||a)}))}return{run(h){Rt(s)?du().then(()=>{s=s({direction:h?"in":"out"}),m(h)}):m(h)},end(){c(),r=a=null}}}function mf(n,e){const t=e.token={};function i(l,s,o,r){if(e.token!==t)return;e.resolved=r;let a=e.ctx;o!==void 0&&(a=a.slice(),a[o]=r);const u=l&&(e.current=l)(a);let f=!1;e.block&&(e.blocks?e.blocks.forEach((c,d)=>{d!==s&&c&&(re(),D(c,1,1,()=>{e.blocks[d]===c&&(e.blocks[d]=null)}),ae())}):e.block.d(1),u.c(),O(u,1),u.m(e.mount(),e.anchor),f=!0),e.block=u,e.blocks&&(e.blocks[s]=u),f&&cu()}if(_k(n)){const l=lo();if(n.then(s=>{Ni(l),i(e.then,1,e.value,s),Ni(null)},s=>{if(Ni(l),i(e.catch,2,e.error,s),Ni(null),!e.hasCatch)throw s}),e.current!==e.pending)return i(e.pending,0),!0}else{if(e.current!==e.then)return i(e.then,1,e.value,n),!0;e.resolved=n}}function Lk(n,e,t){const i=e.slice(),{resolved:l}=n;n.current===n.then&&(i[n.value]=l),n.current===n.catch&&(i[n.error]=l),n.block.p(i,t)}function pe(n){return(n==null?void 0:n.length)!==void 0?n:Array.from(n)}function ci(n,e){n.d(1),e.delete(n.key)}function zt(n,e){D(n,1,1,()=>{e.delete(n.key)})}function Ak(n,e){n.f(),zt(n,e)}function yt(n,e,t,i,l,s,o,r,a,u,f,c){let d=n.length,m=s.length,h=d;const g={};for(;h--;)g[n[h].key]=h;const _=[],y=new Map,S=new Map,T=[];for(h=m;h--;){const L=c(l,s,h),I=t(L);let A=o.get(I);A?T.push(()=>A.p(L,e)):(A=u(I,L),A.c()),y.set(I,_[h]=A),I in g&&S.set(I,Math.abs(h-g[I]))}const $=new Set,E=new Set;function M(L){O(L,1),L.m(r,f),o.set(L.key,L),f=L.first,m--}for(;d&&m;){const L=_[m-1],I=n[d-1],A=L.key,P=I.key;L===I?(f=L.first,d--,m--):y.has(P)?!o.has(A)||$.has(A)?M(L):E.has(P)?d--:S.get(A)>S.get(P)?(E.add(A),M(L)):($.add(P),d--):(a(I,o),d--)}for(;d--;){const L=n[d];y.has(L.key)||a(L,o)}for(;m;)M(_[m-1]);return De(T),_}function kt(n,e){const t={},i={},l={$$scope:1};let s=n.length;for(;s--;){const o=n[s],r=e[s];if(r){for(const a in o)a in r||(i[a]=1);for(const a in r)l[a]||(t[a]=r[a],l[a]=1);n[s]=r}else for(const a in o)l[a]=1}for(const o in i)o in t||(t[o]=void 0);return t}function Ft(n){return typeof n=="object"&&n!==null?n:{}}function ve(n,e,t){const i=n.$$.props[e];i!==void 0&&(n.$$.bound[i]=t,t(n.$$.ctx[i]))}function H(n){n&&n.c()}function F(n,e,t){const{fragment:i,after_update:l}=n.$$;i&&i.m(e,t),nt(()=>{const s=n.$$.on_mount.map(Ob).filter(Rt);n.$$.on_destroy?n.$$.on_destroy.push(...s):De(s),n.$$.on_mount=[]}),l.forEach(nt)}function q(n,e){const t=n.$$;t.fragment!==null&&(Ik(t.after_update),De(t.on_destroy),t.fragment&&t.fragment.d(e),t.on_destroy=t.fragment=null,t.ctx=[])}function Pk(n,e){n.$$.dirty[0]===-1&&(Yl.push(n),Rb(),n.$$.dirty.fill(0)),n.$$.dirty[e/31|0]|=1<{const h=m.length?m[0]:d;return u.ctx&&l(u.ctx[c],u.ctx[c]=h)&&(!u.skip_bound&&u.bound[c]&&u.bound[c](h),f&&Pk(n,c)),d}):[],u.update(),f=!0,De(u.before_update),u.fragment=i?i(u.ctx):!1,e.target){if(e.hydrate){const c=wk(e.target);u.fragment&&u.fragment.l(c),c.forEach(k)}else u.fragment&&u.fragment.c();e.intro&&O(n.$$.fragment),F(n,e.target,e.anchor),cu()}Ni(a)}class ye{constructor(){ct(this,"$$");ct(this,"$$set")}$destroy(){q(this,1),this.$destroy=te}$on(e,t){if(!Rt(t))return te;const i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(t),()=>{const l=i.indexOf(t);l!==-1&&i.splice(l,1)}}$set(e){this.$$set&&!gk(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}const Nk="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(Nk);class Pl extends Error{}class Rk extends Pl{constructor(e){super(`Invalid DateTime: ${e.toMessage()}`)}}class Fk extends Pl{constructor(e){super(`Invalid Interval: ${e.toMessage()}`)}}class qk extends Pl{constructor(e){super(`Invalid Duration: ${e.toMessage()}`)}}class Jl extends Pl{}class qb extends Pl{constructor(e){super(`Invalid unit ${e}`)}}class gn extends Pl{}class Vi extends Pl{constructor(){super("Zone is an abstract class")}}const Ue="numeric",ui="short",Vn="long",dr={year:Ue,month:Ue,day:Ue},Hb={year:Ue,month:ui,day:Ue},Hk={year:Ue,month:ui,day:Ue,weekday:ui},jb={year:Ue,month:Vn,day:Ue},zb={year:Ue,month:Vn,day:Ue,weekday:Vn},Ub={hour:Ue,minute:Ue},Vb={hour:Ue,minute:Ue,second:Ue},Bb={hour:Ue,minute:Ue,second:Ue,timeZoneName:ui},Wb={hour:Ue,minute:Ue,second:Ue,timeZoneName:Vn},Yb={hour:Ue,minute:Ue,hourCycle:"h23"},Kb={hour:Ue,minute:Ue,second:Ue,hourCycle:"h23"},Jb={hour:Ue,minute:Ue,second:Ue,hourCycle:"h23",timeZoneName:ui},Zb={hour:Ue,minute:Ue,second:Ue,hourCycle:"h23",timeZoneName:Vn},Gb={year:Ue,month:Ue,day:Ue,hour:Ue,minute:Ue},Xb={year:Ue,month:Ue,day:Ue,hour:Ue,minute:Ue,second:Ue},Qb={year:Ue,month:ui,day:Ue,hour:Ue,minute:Ue},xb={year:Ue,month:ui,day:Ue,hour:Ue,minute:Ue,second:Ue},jk={year:Ue,month:ui,day:Ue,weekday:ui,hour:Ue,minute:Ue},e0={year:Ue,month:Vn,day:Ue,hour:Ue,minute:Ue,timeZoneName:ui},t0={year:Ue,month:Vn,day:Ue,hour:Ue,minute:Ue,second:Ue,timeZoneName:ui},n0={year:Ue,month:Vn,day:Ue,weekday:Vn,hour:Ue,minute:Ue,timeZoneName:Vn},i0={year:Ue,month:Vn,day:Ue,weekday:Vn,hour:Ue,minute:Ue,second:Ue,timeZoneName:Vn};class oo{get type(){throw new Vi}get name(){throw new Vi}get ianaName(){return this.name}get isUniversal(){throw new Vi}offsetName(e,t){throw new Vi}formatOffset(e,t){throw new Vi}offset(e){throw new Vi}equals(e){throw new Vi}get isValid(){throw new Vi}}let Gr=null;class Mr extends oo{static get instance(){return Gr===null&&(Gr=new Mr),Gr}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(e,{format:t,locale:i}){return d0(e,t,i)}formatOffset(e,t){return Ms(this.offset(e),t)}offset(e){return-new Date(e).getTimezoneOffset()}equals(e){return e.type==="system"}get isValid(){return!0}}let Qo={};function zk(n){return Qo[n]||(Qo[n]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:n,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),Qo[n]}const Uk={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function Vk(n,e){const t=n.format(e).replace(/\u200E/g,""),i=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(t),[,l,s,o,r,a,u,f]=i;return[o,l,s,r,a,u,f]}function Bk(n,e){const t=n.formatToParts(e),i=[];for(let l=0;l=0?h:1e3+h,(d-m)/(60*1e3)}equals(e){return e.type==="iana"&&e.name===this.name}get isValid(){return this.valid}}let hf={};function Wk(n,e={}){const t=JSON.stringify([n,e]);let i=hf[t];return i||(i=new Intl.ListFormat(n,e),hf[t]=i),i}let Ra={};function Fa(n,e={}){const t=JSON.stringify([n,e]);let i=Ra[t];return i||(i=new Intl.DateTimeFormat(n,e),Ra[t]=i),i}let qa={};function Yk(n,e={}){const t=JSON.stringify([n,e]);let i=qa[t];return i||(i=new Intl.NumberFormat(n,e),qa[t]=i),i}let Ha={};function Kk(n,e={}){const{base:t,...i}=e,l=JSON.stringify([n,i]);let s=Ha[l];return s||(s=new Intl.RelativeTimeFormat(n,e),Ha[l]=s),s}let Ts=null;function Jk(){return Ts||(Ts=new Intl.DateTimeFormat().resolvedOptions().locale,Ts)}let _f={};function Zk(n){let e=_f[n];if(!e){const t=new Intl.Locale(n);e="getWeekInfo"in t?t.getWeekInfo():t.weekInfo,_f[n]=e}return e}function Gk(n){const e=n.indexOf("-x-");e!==-1&&(n=n.substring(0,e));const t=n.indexOf("-u-");if(t===-1)return[n];{let i,l;try{i=Fa(n).resolvedOptions(),l=n}catch{const a=n.substring(0,t);i=Fa(a).resolvedOptions(),l=a}const{numberingSystem:s,calendar:o}=i;return[l,s,o]}}function Xk(n,e,t){return(t||e)&&(n.includes("-u-")||(n+="-u"),t&&(n+=`-ca-${t}`),e&&(n+=`-nu-${e}`)),n}function Qk(n){const e=[];for(let t=1;t<=12;t++){const i=Xe.utc(2009,t,1);e.push(n(i))}return e}function xk(n){const e=[];for(let t=1;t<=7;t++){const i=Xe.utc(2016,11,13+t);e.push(n(i))}return e}function $o(n,e,t,i){const l=n.listingMode();return l==="error"?null:l==="en"?t(e):i(e)}function ev(n){return n.numberingSystem&&n.numberingSystem!=="latn"?!1:n.numberingSystem==="latn"||!n.locale||n.locale.startsWith("en")||new Intl.DateTimeFormat(n.intl).resolvedOptions().numberingSystem==="latn"}class tv{constructor(e,t,i){this.padTo=i.padTo||0,this.floor=i.floor||!1;const{padTo:l,floor:s,...o}=i;if(!t||Object.keys(o).length>0){const r={useGrouping:!1,...i};i.padTo>0&&(r.minimumIntegerDigits=i.padTo),this.inf=Yk(e,r)}}format(e){if(this.inf){const t=this.floor?Math.floor(e):e;return this.inf.format(t)}else{const t=this.floor?Math.floor(e):yu(e,3);return xt(t,this.padTo)}}}class nv{constructor(e,t,i){this.opts=i,this.originalZone=void 0;let l;if(this.opts.timeZone)this.dt=e;else if(e.zone.type==="fixed"){const o=-1*(e.offset/60),r=o>=0?`Etc/GMT+${o}`:`Etc/GMT${o}`;e.offset!==0&&Fi.create(r).valid?(l=r,this.dt=e):(l="UTC",this.dt=e.offset===0?e:e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone)}else e.zone.type==="system"?this.dt=e:e.zone.type==="iana"?(this.dt=e,l=e.zone.name):(l="UTC",this.dt=e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone);const s={...this.opts};s.timeZone=s.timeZone||l,this.dtf=Fa(t,s)}format(){return this.originalZone?this.formatToParts().map(({value:e})=>e).join(""):this.dtf.format(this.dt.toJSDate())}formatToParts(){const e=this.dtf.formatToParts(this.dt.toJSDate());return this.originalZone?e.map(t=>{if(t.type==="timeZoneName"){const i=this.originalZone.offsetName(this.dt.ts,{locale:this.dt.locale,format:this.opts.timeZoneName});return{...t,value:i}}else return t}):e}resolvedOptions(){return this.dtf.resolvedOptions()}}class iv{constructor(e,t,i){this.opts={style:"long",...i},!t&&f0()&&(this.rtf=Kk(e,i))}format(e,t){return this.rtf?this.rtf.format(e,t):Cv(t,e,this.opts.numeric,this.opts.style!=="long")}formatToParts(e,t){return this.rtf?this.rtf.formatToParts(e,t):[]}}const lv={firstDay:1,minimalDays:4,weekend:[6,7]};class Et{static fromOpts(e){return Et.create(e.locale,e.numberingSystem,e.outputCalendar,e.weekSettings,e.defaultToEN)}static create(e,t,i,l,s=!1){const o=e||Bt.defaultLocale,r=o||(s?"en-US":Jk()),a=t||Bt.defaultNumberingSystem,u=i||Bt.defaultOutputCalendar,f=ja(l)||Bt.defaultWeekSettings;return new Et(r,a,u,f,o)}static resetCache(){Ts=null,Ra={},qa={},Ha={}}static fromObject({locale:e,numberingSystem:t,outputCalendar:i,weekSettings:l}={}){return Et.create(e,t,i,l)}constructor(e,t,i,l,s){const[o,r,a]=Gk(e);this.locale=o,this.numberingSystem=t||r||null,this.outputCalendar=i||a||null,this.weekSettings=l,this.intl=Xk(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=s,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=ev(this)),this.fastNumbersCached}listingMode(){const e=this.isEnglish(),t=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return e&&t?"en":"intl"}clone(e){return!e||Object.getOwnPropertyNames(e).length===0?this:Et.create(e.locale||this.specifiedLocale,e.numberingSystem||this.numberingSystem,e.outputCalendar||this.outputCalendar,ja(e.weekSettings)||this.weekSettings,e.defaultToEN||!1)}redefaultToEN(e={}){return this.clone({...e,defaultToEN:!0})}redefaultToSystem(e={}){return this.clone({...e,defaultToEN:!1})}months(e,t=!1){return $o(this,e,h0,()=>{const i=t?{month:e,day:"numeric"}:{month:e},l=t?"format":"standalone";return this.monthsCache[l][e]||(this.monthsCache[l][e]=Qk(s=>this.extract(s,i,"month"))),this.monthsCache[l][e]})}weekdays(e,t=!1){return $o(this,e,b0,()=>{const i=t?{weekday:e,year:"numeric",month:"long",day:"numeric"}:{weekday:e},l=t?"format":"standalone";return this.weekdaysCache[l][e]||(this.weekdaysCache[l][e]=xk(s=>this.extract(s,i,"weekday"))),this.weekdaysCache[l][e]})}meridiems(){return $o(this,void 0,()=>y0,()=>{if(!this.meridiemCache){const e={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[Xe.utc(2016,11,13,9),Xe.utc(2016,11,13,19)].map(t=>this.extract(t,e,"dayperiod"))}return this.meridiemCache})}eras(e){return $o(this,e,k0,()=>{const t={era:e};return this.eraCache[e]||(this.eraCache[e]=[Xe.utc(-40,1,1),Xe.utc(2017,1,1)].map(i=>this.extract(i,t,"era"))),this.eraCache[e]})}extract(e,t,i){const l=this.dtFormatter(e,t),s=l.formatToParts(),o=s.find(r=>r.type.toLowerCase()===i);return o?o.value:null}numberFormatter(e={}){return new tv(this.intl,e.forceSimple||this.fastNumbers,e)}dtFormatter(e,t={}){return new nv(e,this.intl,t)}relFormatter(e={}){return new iv(this.intl,this.isEnglish(),e)}listFormatter(e={}){return Wk(this.intl,e)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")}getWeekSettings(){return this.weekSettings?this.weekSettings:c0()?Zk(this.locale):lv}getStartOfWeek(){return this.getWeekSettings().firstDay}getMinDaysInFirstWeek(){return this.getWeekSettings().minimalDays}getWeekendDays(){return this.getWeekSettings().weekend}equals(e){return this.locale===e.locale&&this.numberingSystem===e.numberingSystem&&this.outputCalendar===e.outputCalendar}toString(){return`Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`}}let Xr=null;class Cn extends oo{static get utcInstance(){return Xr===null&&(Xr=new Cn(0)),Xr}static instance(e){return e===0?Cn.utcInstance:new Cn(e)}static parseSpecifier(e){if(e){const t=e.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(t)return new Cn(Lr(t[1],t[2]))}return null}constructor(e){super(),this.fixed=e}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${Ms(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${Ms(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(e,t){return Ms(this.fixed,t)}get isUniversal(){return!0}offset(){return this.fixed}equals(e){return e.type==="fixed"&&e.fixed===this.fixed}get isValid(){return!0}}class sv extends oo{constructor(e){super(),this.zoneName=e}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}}function Ji(n,e){if(it(n)||n===null)return e;if(n instanceof oo)return n;if(cv(n)){const t=n.toLowerCase();return t==="default"?e:t==="local"||t==="system"?Mr.instance:t==="utc"||t==="gmt"?Cn.utcInstance:Cn.parseSpecifier(t)||Fi.create(n)}else return Qi(n)?Cn.instance(n):typeof n=="object"&&"offset"in n&&typeof n.offset=="function"?n:new sv(n)}const hu={arab:"[٠-٩]",arabext:"[۰-۹]",bali:"[᭐-᭙]",beng:"[০-৯]",deva:"[०-९]",fullwide:"[0-9]",gujr:"[૦-૯]",hanidec:"[〇|一|二|三|四|五|六|七|八|九]",khmr:"[០-៩]",knda:"[೦-೯]",laoo:"[໐-໙]",limb:"[᥆-᥏]",mlym:"[൦-൯]",mong:"[᠐-᠙]",mymr:"[၀-၉]",orya:"[୦-୯]",tamldec:"[௦-௯]",telu:"[౦-౯]",thai:"[๐-๙]",tibt:"[༠-༩]",latn:"\\d"},gf={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},ov=hu.hanidec.replace(/[\[|\]]/g,"").split("");function rv(n){let e=parseInt(n,10);if(isNaN(e)){e="";for(let t=0;t=s&&i<=o&&(e+=i-s)}}return parseInt(e,10)}else return e}let Kl={};function av(){Kl={}}function ii({numberingSystem:n},e=""){const t=n||"latn";return Kl[t]||(Kl[t]={}),Kl[t][e]||(Kl[t][e]=new RegExp(`${hu[t]}${e}`)),Kl[t][e]}let bf=()=>Date.now(),yf="system",kf=null,vf=null,wf=null,Sf=60,Tf,$f=null;class Bt{static get now(){return bf}static set now(e){bf=e}static set defaultZone(e){yf=e}static get defaultZone(){return Ji(yf,Mr.instance)}static get defaultLocale(){return kf}static set defaultLocale(e){kf=e}static get defaultNumberingSystem(){return vf}static set defaultNumberingSystem(e){vf=e}static get defaultOutputCalendar(){return wf}static set defaultOutputCalendar(e){wf=e}static get defaultWeekSettings(){return $f}static set defaultWeekSettings(e){$f=ja(e)}static get twoDigitCutoffYear(){return Sf}static set twoDigitCutoffYear(e){Sf=e%100}static get throwOnInvalid(){return Tf}static set throwOnInvalid(e){Tf=e}static resetCaches(){Et.resetCache(),Fi.resetCache(),Xe.resetCache(),av()}}class si{constructor(e,t){this.reason=e,this.explanation=t}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}}const l0=[0,31,59,90,120,151,181,212,243,273,304,334],s0=[0,31,60,91,121,152,182,213,244,274,305,335];function Zn(n,e){return new si("unit out of range",`you specified ${e} (of type ${typeof e}) as a ${n}, which is invalid`)}function _u(n,e,t){const i=new Date(Date.UTC(n,e-1,t));n<100&&n>=0&&i.setUTCFullYear(i.getUTCFullYear()-1900);const l=i.getUTCDay();return l===0?7:l}function o0(n,e,t){return t+(ro(n)?s0:l0)[e-1]}function r0(n,e){const t=ro(n)?s0:l0,i=t.findIndex(s=>sBs(i,e,t)?(u=i+1,a=1):u=i,{weekYear:u,weekNumber:a,weekday:r,...Ar(n)}}function Cf(n,e=4,t=1){const{weekYear:i,weekNumber:l,weekday:s}=n,o=gu(_u(i,1,e),t),r=Xl(i);let a=l*7+s-o-7+e,u;a<1?(u=i-1,a+=Xl(u)):a>r?(u=i+1,a-=Xl(i)):u=i;const{month:f,day:c}=r0(u,a);return{year:u,month:f,day:c,...Ar(n)}}function Qr(n){const{year:e,month:t,day:i}=n,l=o0(e,t,i);return{year:e,ordinal:l,...Ar(n)}}function Of(n){const{year:e,ordinal:t}=n,{month:i,day:l}=r0(e,t);return{year:e,month:i,day:l,...Ar(n)}}function Ef(n,e){if(!it(n.localWeekday)||!it(n.localWeekNumber)||!it(n.localWeekYear)){if(!it(n.weekday)||!it(n.weekNumber)||!it(n.weekYear))throw new Jl("Cannot mix locale-based week fields with ISO-based week fields");return it(n.localWeekday)||(n.weekday=n.localWeekday),it(n.localWeekNumber)||(n.weekNumber=n.localWeekNumber),it(n.localWeekYear)||(n.weekYear=n.localWeekYear),delete n.localWeekday,delete n.localWeekNumber,delete n.localWeekYear,{minDaysInFirstWeek:e.getMinDaysInFirstWeek(),startOfWeek:e.getStartOfWeek()}}else return{minDaysInFirstWeek:4,startOfWeek:1}}function uv(n,e=4,t=1){const i=Dr(n.weekYear),l=Gn(n.weekNumber,1,Bs(n.weekYear,e,t)),s=Gn(n.weekday,1,7);return i?l?s?!1:Zn("weekday",n.weekday):Zn("week",n.weekNumber):Zn("weekYear",n.weekYear)}function fv(n){const e=Dr(n.year),t=Gn(n.ordinal,1,Xl(n.year));return e?t?!1:Zn("ordinal",n.ordinal):Zn("year",n.year)}function a0(n){const e=Dr(n.year),t=Gn(n.month,1,12),i=Gn(n.day,1,mr(n.year,n.month));return e?t?i?!1:Zn("day",n.day):Zn("month",n.month):Zn("year",n.year)}function u0(n){const{hour:e,minute:t,second:i,millisecond:l}=n,s=Gn(e,0,23)||e===24&&t===0&&i===0&&l===0,o=Gn(t,0,59),r=Gn(i,0,59),a=Gn(l,0,999);return s?o?r?a?!1:Zn("millisecond",l):Zn("second",i):Zn("minute",t):Zn("hour",e)}function it(n){return typeof n>"u"}function Qi(n){return typeof n=="number"}function Dr(n){return typeof n=="number"&&n%1===0}function cv(n){return typeof n=="string"}function dv(n){return Object.prototype.toString.call(n)==="[object Date]"}function f0(){try{return typeof Intl<"u"&&!!Intl.RelativeTimeFormat}catch{return!1}}function c0(){try{return typeof Intl<"u"&&!!Intl.Locale&&("weekInfo"in Intl.Locale.prototype||"getWeekInfo"in Intl.Locale.prototype)}catch{return!1}}function pv(n){return Array.isArray(n)?n:[n]}function Mf(n,e,t){if(n.length!==0)return n.reduce((i,l)=>{const s=[e(l),l];return i&&t(i[0],s[0])===i[0]?i:s},null)[1]}function mv(n,e){return e.reduce((t,i)=>(t[i]=n[i],t),{})}function ns(n,e){return Object.prototype.hasOwnProperty.call(n,e)}function ja(n){if(n==null)return null;if(typeof n!="object")throw new gn("Week settings must be an object");if(!Gn(n.firstDay,1,7)||!Gn(n.minimalDays,1,7)||!Array.isArray(n.weekend)||n.weekend.some(e=>!Gn(e,1,7)))throw new gn("Invalid week settings");return{firstDay:n.firstDay,minimalDays:n.minimalDays,weekend:Array.from(n.weekend)}}function Gn(n,e,t){return Dr(n)&&n>=e&&n<=t}function hv(n,e){return n-e*Math.floor(n/e)}function xt(n,e=2){const t=n<0;let i;return t?i="-"+(""+-n).padStart(e,"0"):i=(""+n).padStart(e,"0"),i}function Yi(n){if(!(it(n)||n===null||n===""))return parseInt(n,10)}function hl(n){if(!(it(n)||n===null||n===""))return parseFloat(n)}function bu(n){if(!(it(n)||n===null||n==="")){const e=parseFloat("0."+n)*1e3;return Math.floor(e)}}function yu(n,e,t=!1){const i=10**e;return(t?Math.trunc:Math.round)(n*i)/i}function ro(n){return n%4===0&&(n%100!==0||n%400===0)}function Xl(n){return ro(n)?366:365}function mr(n,e){const t=hv(e-1,12)+1,i=n+(e-t)/12;return t===2?ro(i)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][t-1]}function Ir(n){let e=Date.UTC(n.year,n.month-1,n.day,n.hour,n.minute,n.second,n.millisecond);return n.year<100&&n.year>=0&&(e=new Date(e),e.setUTCFullYear(n.year,n.month-1,n.day)),+e}function Df(n,e,t){return-gu(_u(n,1,e),t)+e-1}function Bs(n,e=4,t=1){const i=Df(n,e,t),l=Df(n+1,e,t);return(Xl(n)-i+l)/7}function za(n){return n>99?n:n>Bt.twoDigitCutoffYear?1900+n:2e3+n}function d0(n,e,t,i=null){const l=new Date(n),s={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};i&&(s.timeZone=i);const o={timeZoneName:e,...s},r=new Intl.DateTimeFormat(t,o).formatToParts(l).find(a=>a.type.toLowerCase()==="timezonename");return r?r.value:null}function Lr(n,e){let t=parseInt(n,10);Number.isNaN(t)&&(t=0);const i=parseInt(e,10)||0,l=t<0||Object.is(t,-0)?-i:i;return t*60+l}function p0(n){const e=Number(n);if(typeof n=="boolean"||n===""||Number.isNaN(e))throw new gn(`Invalid unit value ${n}`);return e}function hr(n,e){const t={};for(const i in n)if(ns(n,i)){const l=n[i];if(l==null)continue;t[e(i)]=p0(l)}return t}function Ms(n,e){const t=Math.trunc(Math.abs(n/60)),i=Math.trunc(Math.abs(n%60)),l=n>=0?"+":"-";switch(e){case"short":return`${l}${xt(t,2)}:${xt(i,2)}`;case"narrow":return`${l}${t}${i>0?`:${i}`:""}`;case"techie":return`${l}${xt(t,2)}${xt(i,2)}`;default:throw new RangeError(`Value format ${e} is out of range for property format`)}}function Ar(n){return mv(n,["hour","minute","second","millisecond"])}const _v=["January","February","March","April","May","June","July","August","September","October","November","December"],m0=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],gv=["J","F","M","A","M","J","J","A","S","O","N","D"];function h0(n){switch(n){case"narrow":return[...gv];case"short":return[...m0];case"long":return[..._v];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}const _0=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],g0=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],bv=["M","T","W","T","F","S","S"];function b0(n){switch(n){case"narrow":return[...bv];case"short":return[...g0];case"long":return[..._0];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}const y0=["AM","PM"],yv=["Before Christ","Anno Domini"],kv=["BC","AD"],vv=["B","A"];function k0(n){switch(n){case"narrow":return[...vv];case"short":return[...kv];case"long":return[...yv];default:return null}}function wv(n){return y0[n.hour<12?0:1]}function Sv(n,e){return b0(e)[n.weekday-1]}function Tv(n,e){return h0(e)[n.month-1]}function $v(n,e){return k0(e)[n.year<0?0:1]}function Cv(n,e,t="always",i=!1){const l={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},s=["hours","minutes","seconds"].indexOf(n)===-1;if(t==="auto"&&s){const c=n==="days";switch(e){case 1:return c?"tomorrow":`next ${l[n][0]}`;case-1:return c?"yesterday":`last ${l[n][0]}`;case 0:return c?"today":`this ${l[n][0]}`}}const o=Object.is(e,-0)||e<0,r=Math.abs(e),a=r===1,u=l[n],f=i?a?u[1]:u[2]||u[1]:a?l[n][0]:n;return o?`${r} ${f} ago`:`in ${r} ${f}`}function If(n,e){let t="";for(const i of n)i.literal?t+=i.val:t+=e(i.val);return t}const Ov={D:dr,DD:Hb,DDD:jb,DDDD:zb,t:Ub,tt:Vb,ttt:Bb,tttt:Wb,T:Yb,TT:Kb,TTT:Jb,TTTT:Zb,f:Gb,ff:Qb,fff:e0,ffff:n0,F:Xb,FF:xb,FFF:t0,FFFF:i0};class yn{static create(e,t={}){return new yn(e,t)}static parseFormat(e){let t=null,i="",l=!1;const s=[];for(let o=0;o0&&s.push({literal:l||/^\s+$/.test(i),val:i}),t=null,i="",l=!l):l||r===t?i+=r:(i.length>0&&s.push({literal:/^\s+$/.test(i),val:i}),i=r,t=r)}return i.length>0&&s.push({literal:l||/^\s+$/.test(i),val:i}),s}static macroTokenToFormatOpts(e){return Ov[e]}constructor(e,t){this.opts=t,this.loc=e,this.systemLoc=null}formatWithSystemDefault(e,t){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(e,{...this.opts,...t}).format()}dtFormatter(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t})}formatDateTime(e,t){return this.dtFormatter(e,t).format()}formatDateTimeParts(e,t){return this.dtFormatter(e,t).formatToParts()}formatInterval(e,t){return this.dtFormatter(e.start,t).dtf.formatRange(e.start.toJSDate(),e.end.toJSDate())}resolvedOptions(e,t){return this.dtFormatter(e,t).resolvedOptions()}num(e,t=0){if(this.opts.forceSimple)return xt(e,t);const i={...this.opts};return t>0&&(i.padTo=t),this.loc.numberFormatter(i).format(e)}formatDateTimeFromString(e,t){const i=this.loc.listingMode()==="en",l=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",s=(m,h)=>this.loc.extract(e,m,h),o=m=>e.isOffsetFixed&&e.offset===0&&m.allowZ?"Z":e.isValid?e.zone.formatOffset(e.ts,m.format):"",r=()=>i?wv(e):s({hour:"numeric",hourCycle:"h12"},"dayperiod"),a=(m,h)=>i?Tv(e,m):s(h?{month:m}:{month:m,day:"numeric"},"month"),u=(m,h)=>i?Sv(e,m):s(h?{weekday:m}:{weekday:m,month:"long",day:"numeric"},"weekday"),f=m=>{const h=yn.macroTokenToFormatOpts(m);return h?this.formatWithSystemDefault(e,h):m},c=m=>i?$v(e,m):s({era:m},"era"),d=m=>{switch(m){case"S":return this.num(e.millisecond);case"u":case"SSS":return this.num(e.millisecond,3);case"s":return this.num(e.second);case"ss":return this.num(e.second,2);case"uu":return this.num(Math.floor(e.millisecond/10),2);case"uuu":return this.num(Math.floor(e.millisecond/100));case"m":return this.num(e.minute);case"mm":return this.num(e.minute,2);case"h":return this.num(e.hour%12===0?12:e.hour%12);case"hh":return this.num(e.hour%12===0?12:e.hour%12,2);case"H":return this.num(e.hour);case"HH":return this.num(e.hour,2);case"Z":return o({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return o({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return o({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return e.zone.offsetName(e.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return e.zone.offsetName(e.ts,{format:"long",locale:this.loc.locale});case"z":return e.zoneName;case"a":return r();case"d":return l?s({day:"numeric"},"day"):this.num(e.day);case"dd":return l?s({day:"2-digit"},"day"):this.num(e.day,2);case"c":return this.num(e.weekday);case"ccc":return u("short",!0);case"cccc":return u("long",!0);case"ccccc":return u("narrow",!0);case"E":return this.num(e.weekday);case"EEE":return u("short",!1);case"EEEE":return u("long",!1);case"EEEEE":return u("narrow",!1);case"L":return l?s({month:"numeric",day:"numeric"},"month"):this.num(e.month);case"LL":return l?s({month:"2-digit",day:"numeric"},"month"):this.num(e.month,2);case"LLL":return a("short",!0);case"LLLL":return a("long",!0);case"LLLLL":return a("narrow",!0);case"M":return l?s({month:"numeric"},"month"):this.num(e.month);case"MM":return l?s({month:"2-digit"},"month"):this.num(e.month,2);case"MMM":return a("short",!1);case"MMMM":return a("long",!1);case"MMMMM":return a("narrow",!1);case"y":return l?s({year:"numeric"},"year"):this.num(e.year);case"yy":return l?s({year:"2-digit"},"year"):this.num(e.year.toString().slice(-2),2);case"yyyy":return l?s({year:"numeric"},"year"):this.num(e.year,4);case"yyyyyy":return l?s({year:"numeric"},"year"):this.num(e.year,6);case"G":return c("short");case"GG":return c("long");case"GGGGG":return c("narrow");case"kk":return this.num(e.weekYear.toString().slice(-2),2);case"kkkk":return this.num(e.weekYear,4);case"W":return this.num(e.weekNumber);case"WW":return this.num(e.weekNumber,2);case"n":return this.num(e.localWeekNumber);case"nn":return this.num(e.localWeekNumber,2);case"ii":return this.num(e.localWeekYear.toString().slice(-2),2);case"iiii":return this.num(e.localWeekYear,4);case"o":return this.num(e.ordinal);case"ooo":return this.num(e.ordinal,3);case"q":return this.num(e.quarter);case"qq":return this.num(e.quarter,2);case"X":return this.num(Math.floor(e.ts/1e3));case"x":return this.num(e.ts);default:return f(m)}};return If(yn.parseFormat(t),d)}formatDurationFromString(e,t){const i=a=>{switch(a[0]){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":return"hour";case"d":return"day";case"w":return"week";case"M":return"month";case"y":return"year";default:return null}},l=a=>u=>{const f=i(u);return f?this.num(a.get(f),u.length):u},s=yn.parseFormat(t),o=s.reduce((a,{literal:u,val:f})=>u?a:a.concat(f),[]),r=e.shiftTo(...o.map(i).filter(a=>a));return If(s,l(r))}}const v0=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;function os(...n){const e=n.reduce((t,i)=>t+i.source,"");return RegExp(`^${e}$`)}function rs(...n){return e=>n.reduce(([t,i,l],s)=>{const[o,r,a]=s(e,l);return[{...t,...o},r||i,a]},[{},null,1]).slice(0,2)}function as(n,...e){if(n==null)return[null,null];for(const[t,i]of e){const l=t.exec(n);if(l)return i(l)}return[null,null]}function w0(...n){return(e,t)=>{const i={};let l;for(l=0;lm!==void 0&&(h||m&&f)?-m:m;return[{years:d(hl(t)),months:d(hl(i)),weeks:d(hl(l)),days:d(hl(s)),hours:d(hl(o)),minutes:d(hl(r)),seconds:d(hl(a),a==="-0"),milliseconds:d(bu(u),c)}]}const jv={GMT:0,EDT:-4*60,EST:-5*60,CDT:-5*60,CST:-6*60,MDT:-6*60,MST:-7*60,PDT:-7*60,PST:-8*60};function wu(n,e,t,i,l,s,o){const r={year:e.length===2?za(Yi(e)):Yi(e),month:m0.indexOf(t)+1,day:Yi(i),hour:Yi(l),minute:Yi(s)};return o&&(r.second=Yi(o)),n&&(r.weekday=n.length>3?_0.indexOf(n)+1:g0.indexOf(n)+1),r}const zv=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function Uv(n){const[,e,t,i,l,s,o,r,a,u,f,c]=n,d=wu(e,l,i,t,s,o,r);let m;return a?m=jv[a]:u?m=0:m=Lr(f,c),[d,new Cn(m)]}function Vv(n){return n.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}const Bv=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,Wv=/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,Yv=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function Lf(n){const[,e,t,i,l,s,o,r]=n;return[wu(e,l,i,t,s,o,r),Cn.utcInstance]}function Kv(n){const[,e,t,i,l,s,o,r]=n;return[wu(e,r,t,i,l,s,o),Cn.utcInstance]}const Jv=os(Mv,vu),Zv=os(Dv,vu),Gv=os(Iv,vu),Xv=os(T0),C0=rs(Rv,us,ao,uo),Qv=rs(Lv,us,ao,uo),xv=rs(Av,us,ao,uo),e2=rs(us,ao,uo);function t2(n){return as(n,[Jv,C0],[Zv,Qv],[Gv,xv],[Xv,e2])}function n2(n){return as(Vv(n),[zv,Uv])}function i2(n){return as(n,[Bv,Lf],[Wv,Lf],[Yv,Kv])}function l2(n){return as(n,[qv,Hv])}const s2=rs(us);function o2(n){return as(n,[Fv,s2])}const r2=os(Pv,Nv),a2=os($0),u2=rs(us,ao,uo);function f2(n){return as(n,[r2,C0],[a2,u2])}const Af="Invalid Duration",O0={weeks:{days:7,hours:7*24,minutes:7*24*60,seconds:7*24*60*60,milliseconds:7*24*60*60*1e3},days:{hours:24,minutes:24*60,seconds:24*60*60,milliseconds:24*60*60*1e3},hours:{minutes:60,seconds:60*60,milliseconds:60*60*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},c2={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:91*24,minutes:91*24*60,seconds:91*24*60*60,milliseconds:91*24*60*60*1e3},months:{weeks:4,days:30,hours:30*24,minutes:30*24*60,seconds:30*24*60*60,milliseconds:30*24*60*60*1e3},...O0},Wn=146097/400,zl=146097/4800,d2={years:{quarters:4,months:12,weeks:Wn/7,days:Wn,hours:Wn*24,minutes:Wn*24*60,seconds:Wn*24*60*60,milliseconds:Wn*24*60*60*1e3},quarters:{months:3,weeks:Wn/28,days:Wn/4,hours:Wn*24/4,minutes:Wn*24*60/4,seconds:Wn*24*60*60/4,milliseconds:Wn*24*60*60*1e3/4},months:{weeks:zl/7,days:zl,hours:zl*24,minutes:zl*24*60,seconds:zl*24*60*60,milliseconds:zl*24*60*60*1e3},...O0},Tl=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],p2=Tl.slice(0).reverse();function Bi(n,e,t=!1){const i={values:t?e.values:{...n.values,...e.values||{}},loc:n.loc.clone(e.loc),conversionAccuracy:e.conversionAccuracy||n.conversionAccuracy,matrix:e.matrix||n.matrix};return new wt(i)}function E0(n,e){let t=e.milliseconds??0;for(const i of p2.slice(1))e[i]&&(t+=e[i]*n[i].milliseconds);return t}function Pf(n,e){const t=E0(n,e)<0?-1:1;Tl.reduceRight((i,l)=>{if(it(e[l]))return i;if(i){const s=e[i]*t,o=n[l][i],r=Math.floor(s/o);e[l]+=r*t,e[i]-=r*o*t}return l},null),Tl.reduce((i,l)=>{if(it(e[l]))return i;if(i){const s=e[i]%1;e[i]-=s,e[l]+=s*n[i][l]}return l},null)}function m2(n){const e={};for(const[t,i]of Object.entries(n))i!==0&&(e[t]=i);return e}class wt{constructor(e){const t=e.conversionAccuracy==="longterm"||!1;let i=t?d2:c2;e.matrix&&(i=e.matrix),this.values=e.values,this.loc=e.loc||Et.create(),this.conversionAccuracy=t?"longterm":"casual",this.invalid=e.invalid||null,this.matrix=i,this.isLuxonDuration=!0}static fromMillis(e,t){return wt.fromObject({milliseconds:e},t)}static fromObject(e,t={}){if(e==null||typeof e!="object")throw new gn(`Duration.fromObject: argument expected to be an object, got ${e===null?"null":typeof e}`);return new wt({values:hr(e,wt.normalizeUnit),loc:Et.fromObject(t),conversionAccuracy:t.conversionAccuracy,matrix:t.matrix})}static fromDurationLike(e){if(Qi(e))return wt.fromMillis(e);if(wt.isDuration(e))return e;if(typeof e=="object")return wt.fromObject(e);throw new gn(`Unknown duration argument ${e} of type ${typeof e}`)}static fromISO(e,t){const[i]=l2(e);return i?wt.fromObject(i,t):wt.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static fromISOTime(e,t){const[i]=o2(e);return i?wt.fromObject(i,t):wt.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static invalid(e,t=null){if(!e)throw new gn("need to specify a reason the Duration is invalid");const i=e instanceof si?e:new si(e,t);if(Bt.throwOnInvalid)throw new qk(i);return new wt({invalid:i})}static normalizeUnit(e){const t={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[e&&e.toLowerCase()];if(!t)throw new qb(e);return t}static isDuration(e){return e&&e.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(e,t={}){const i={...t,floor:t.round!==!1&&t.floor!==!1};return this.isValid?yn.create(this.loc,i).formatDurationFromString(this,e):Af}toHuman(e={}){if(!this.isValid)return Af;const t=Tl.map(i=>{const l=this.values[i];return it(l)?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...e,unit:i.slice(0,-1)}).format(l)}).filter(i=>i);return this.loc.listFormatter({type:"conjunction",style:e.listStyle||"narrow",...e}).format(t)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let e="P";return this.years!==0&&(e+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(e+=this.months+this.quarters*3+"M"),this.weeks!==0&&(e+=this.weeks+"W"),this.days!==0&&(e+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(e+="T"),this.hours!==0&&(e+=this.hours+"H"),this.minutes!==0&&(e+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(e+=yu(this.seconds+this.milliseconds/1e3,3)+"S"),e==="P"&&(e+="T0S"),e}toISOTime(e={}){if(!this.isValid)return null;const t=this.toMillis();return t<0||t>=864e5?null:(e={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...e,includeOffset:!1},Xe.fromMillis(t,{zone:"UTC"}).toISOTime(e))}toJSON(){return this.toISO()}toString(){return this.toISO()}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Duration { values: ${JSON.stringify(this.values)} }`:`Duration { Invalid, reason: ${this.invalidReason} }`}toMillis(){return this.isValid?E0(this.matrix,this.values):NaN}valueOf(){return this.toMillis()}plus(e){if(!this.isValid)return this;const t=wt.fromDurationLike(e),i={};for(const l of Tl)(ns(t.values,l)||ns(this.values,l))&&(i[l]=t.get(l)+this.get(l));return Bi(this,{values:i},!0)}minus(e){if(!this.isValid)return this;const t=wt.fromDurationLike(e);return this.plus(t.negate())}mapUnits(e){if(!this.isValid)return this;const t={};for(const i of Object.keys(this.values))t[i]=p0(e(this.values[i],i));return Bi(this,{values:t},!0)}get(e){return this[wt.normalizeUnit(e)]}set(e){if(!this.isValid)return this;const t={...this.values,...hr(e,wt.normalizeUnit)};return Bi(this,{values:t})}reconfigure({locale:e,numberingSystem:t,conversionAccuracy:i,matrix:l}={}){const o={loc:this.loc.clone({locale:e,numberingSystem:t}),matrix:l,conversionAccuracy:i};return Bi(this,o)}as(e){return this.isValid?this.shiftTo(e).get(e):NaN}normalize(){if(!this.isValid)return this;const e=this.toObject();return Pf(this.matrix,e),Bi(this,{values:e},!0)}rescale(){if(!this.isValid)return this;const e=m2(this.normalize().shiftToAll().toObject());return Bi(this,{values:e},!0)}shiftTo(...e){if(!this.isValid)return this;if(e.length===0)return this;e=e.map(o=>wt.normalizeUnit(o));const t={},i={},l=this.toObject();let s;for(const o of Tl)if(e.indexOf(o)>=0){s=o;let r=0;for(const u in i)r+=this.matrix[u][o]*i[u],i[u]=0;Qi(l[o])&&(r+=l[o]);const a=Math.trunc(r);t[o]=a,i[o]=(r*1e3-a*1e3)/1e3}else Qi(l[o])&&(i[o]=l[o]);for(const o in i)i[o]!==0&&(t[s]+=o===s?i[o]:i[o]/this.matrix[s][o]);return Pf(this.matrix,t),Bi(this,{values:t},!0)}shiftToAll(){return this.isValid?this.shiftTo("years","months","weeks","days","hours","minutes","seconds","milliseconds"):this}negate(){if(!this.isValid)return this;const e={};for(const t of Object.keys(this.values))e[t]=this.values[t]===0?0:-this.values[t];return Bi(this,{values:e},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(e){if(!this.isValid||!e.isValid||!this.loc.equals(e.loc))return!1;function t(i,l){return i===void 0||i===0?l===void 0||l===0:i===l}for(const i of Tl)if(!t(this.values[i],e.values[i]))return!1;return!0}}const Ul="Invalid Interval";function h2(n,e){return!n||!n.isValid?Vt.invalid("missing or invalid start"):!e||!e.isValid?Vt.invalid("missing or invalid end"):ee:!1}isBefore(e){return this.isValid?this.e<=e:!1}contains(e){return this.isValid?this.s<=e&&this.e>e:!1}set({start:e,end:t}={}){return this.isValid?Vt.fromDateTimes(e||this.s,t||this.e):this}splitAt(...e){if(!this.isValid)return[];const t=e.map(bs).filter(o=>this.contains(o)).sort((o,r)=>o.toMillis()-r.toMillis()),i=[];let{s:l}=this,s=0;for(;l+this.e?this.e:o;i.push(Vt.fromDateTimes(l,r)),l=r,s+=1}return i}splitBy(e){const t=wt.fromDurationLike(e);if(!this.isValid||!t.isValid||t.as("milliseconds")===0)return[];let{s:i}=this,l=1,s;const o=[];for(;ia*l));s=+r>+this.e?this.e:r,o.push(Vt.fromDateTimes(i,s)),i=s,l+=1}return o}divideEqually(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]}overlaps(e){return this.e>e.s&&this.s=e.e:!1}equals(e){return!this.isValid||!e.isValid?!1:this.s.equals(e.s)&&this.e.equals(e.e)}intersection(e){if(!this.isValid)return this;const t=this.s>e.s?this.s:e.s,i=this.e=i?null:Vt.fromDateTimes(t,i)}union(e){if(!this.isValid)return this;const t=this.se.e?this.e:e.e;return Vt.fromDateTimes(t,i)}static merge(e){const[t,i]=e.sort((l,s)=>l.s-s.s).reduce(([l,s],o)=>s?s.overlaps(o)||s.abutsStart(o)?[l,s.union(o)]:[l.concat([s]),o]:[l,o],[[],null]);return i&&t.push(i),t}static xor(e){let t=null,i=0;const l=[],s=e.map(a=>[{time:a.s,type:"s"},{time:a.e,type:"e"}]),o=Array.prototype.concat(...s),r=o.sort((a,u)=>a.time-u.time);for(const a of r)i+=a.type==="s"?1:-1,i===1?t=a.time:(t&&+t!=+a.time&&l.push(Vt.fromDateTimes(t,a.time)),t=null);return Vt.merge(l)}difference(...e){return Vt.xor([this].concat(e)).map(t=>this.intersection(t)).filter(t=>t&&!t.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} – ${this.e.toISO()})`:Ul}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`:`Interval { Invalid, reason: ${this.invalidReason} }`}toLocaleString(e=dr,t={}){return this.isValid?yn.create(this.s.loc.clone(t),e).formatInterval(this):Ul}toISO(e){return this.isValid?`${this.s.toISO(e)}/${this.e.toISO(e)}`:Ul}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:Ul}toISOTime(e){return this.isValid?`${this.s.toISOTime(e)}/${this.e.toISOTime(e)}`:Ul}toFormat(e,{separator:t=" – "}={}){return this.isValid?`${this.s.toFormat(e)}${t}${this.e.toFormat(e)}`:Ul}toDuration(e,t){return this.isValid?this.e.diff(this.s,e,t):wt.invalid(this.invalidReason)}mapEndpoints(e){return Vt.fromDateTimes(e(this.s),e(this.e))}}class Co{static hasDST(e=Bt.defaultZone){const t=Xe.now().setZone(e).set({month:12});return!e.isUniversal&&t.offset!==t.set({month:6}).offset}static isValidIANAZone(e){return Fi.isValidZone(e)}static normalizeZone(e){return Ji(e,Bt.defaultZone)}static getStartOfWeek({locale:e=null,locObj:t=null}={}){return(t||Et.create(e)).getStartOfWeek()}static getMinimumDaysInFirstWeek({locale:e=null,locObj:t=null}={}){return(t||Et.create(e)).getMinDaysInFirstWeek()}static getWeekendWeekdays({locale:e=null,locObj:t=null}={}){return(t||Et.create(e)).getWeekendDays().slice()}static months(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null,outputCalendar:s="gregory"}={}){return(l||Et.create(t,i,s)).months(e)}static monthsFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null,outputCalendar:s="gregory"}={}){return(l||Et.create(t,i,s)).months(e,!0)}static weekdays(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null}={}){return(l||Et.create(t,i,null)).weekdays(e)}static weekdaysFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null}={}){return(l||Et.create(t,i,null)).weekdays(e,!0)}static meridiems({locale:e=null}={}){return Et.create(e).meridiems()}static eras(e="short",{locale:t=null}={}){return Et.create(t,null,"gregory").eras(e)}static features(){return{relative:f0(),localeWeek:c0()}}}function Nf(n,e){const t=l=>l.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),i=t(e)-t(n);return Math.floor(wt.fromMillis(i).as("days"))}function _2(n,e,t){const i=[["years",(a,u)=>u.year-a.year],["quarters",(a,u)=>u.quarter-a.quarter+(u.year-a.year)*4],["months",(a,u)=>u.month-a.month+(u.year-a.year)*12],["weeks",(a,u)=>{const f=Nf(a,u);return(f-f%7)/7}],["days",Nf]],l={},s=n;let o,r;for(const[a,u]of i)t.indexOf(a)>=0&&(o=a,l[a]=u(n,e),r=s.plus(l),r>e?(l[a]--,n=s.plus(l),n>e&&(r=n,l[a]--,n=s.plus(l))):n=r);return[n,l,r,o]}function g2(n,e,t,i){let[l,s,o,r]=_2(n,e,t);const a=e-l,u=t.filter(c=>["hours","minutes","seconds","milliseconds"].indexOf(c)>=0);u.length===0&&(o0?wt.fromMillis(a,i).shiftTo(...u).plus(f):f}const b2="missing Intl.DateTimeFormat.formatToParts support";function Tt(n,e=t=>t){return{regex:n,deser:([t])=>e(rv(t))}}const y2=" ",M0=`[ ${y2}]`,D0=new RegExp(M0,"g");function k2(n){return n.replace(/\./g,"\\.?").replace(D0,M0)}function Rf(n){return n.replace(/\./g,"").replace(D0," ").toLowerCase()}function li(n,e){return n===null?null:{regex:RegExp(n.map(k2).join("|")),deser:([t])=>n.findIndex(i=>Rf(t)===Rf(i))+e}}function Ff(n,e){return{regex:n,deser:([,t,i])=>Lr(t,i),groups:e}}function Oo(n){return{regex:n,deser:([e])=>e}}function v2(n){return n.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function w2(n,e){const t=ii(e),i=ii(e,"{2}"),l=ii(e,"{3}"),s=ii(e,"{4}"),o=ii(e,"{6}"),r=ii(e,"{1,2}"),a=ii(e,"{1,3}"),u=ii(e,"{1,6}"),f=ii(e,"{1,9}"),c=ii(e,"{2,4}"),d=ii(e,"{4,6}"),m=_=>({regex:RegExp(v2(_.val)),deser:([y])=>y,literal:!0}),g=(_=>{if(n.literal)return m(_);switch(_.val){case"G":return li(e.eras("short"),0);case"GG":return li(e.eras("long"),0);case"y":return Tt(u);case"yy":return Tt(c,za);case"yyyy":return Tt(s);case"yyyyy":return Tt(d);case"yyyyyy":return Tt(o);case"M":return Tt(r);case"MM":return Tt(i);case"MMM":return li(e.months("short",!0),1);case"MMMM":return li(e.months("long",!0),1);case"L":return Tt(r);case"LL":return Tt(i);case"LLL":return li(e.months("short",!1),1);case"LLLL":return li(e.months("long",!1),1);case"d":return Tt(r);case"dd":return Tt(i);case"o":return Tt(a);case"ooo":return Tt(l);case"HH":return Tt(i);case"H":return Tt(r);case"hh":return Tt(i);case"h":return Tt(r);case"mm":return Tt(i);case"m":return Tt(r);case"q":return Tt(r);case"qq":return Tt(i);case"s":return Tt(r);case"ss":return Tt(i);case"S":return Tt(a);case"SSS":return Tt(l);case"u":return Oo(f);case"uu":return Oo(r);case"uuu":return Tt(t);case"a":return li(e.meridiems(),0);case"kkkk":return Tt(s);case"kk":return Tt(c,za);case"W":return Tt(r);case"WW":return Tt(i);case"E":case"c":return Tt(t);case"EEE":return li(e.weekdays("short",!1),1);case"EEEE":return li(e.weekdays("long",!1),1);case"ccc":return li(e.weekdays("short",!0),1);case"cccc":return li(e.weekdays("long",!0),1);case"Z":case"ZZ":return Ff(new RegExp(`([+-]${r.source})(?::(${i.source}))?`),2);case"ZZZ":return Ff(new RegExp(`([+-]${r.source})(${i.source})?`),2);case"z":return Oo(/[a-z_+-/]{1,256}?/i);case" ":return Oo(/[^\S\n\r]/);default:return m(_)}})(n)||{invalidReason:b2};return g.token=n,g}const S2={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour12:{numeric:"h","2-digit":"hh"},hour24:{numeric:"H","2-digit":"HH"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"},timeZoneName:{long:"ZZZZZ",short:"ZZZ"}};function T2(n,e,t){const{type:i,value:l}=n;if(i==="literal"){const a=/^\s+$/.test(l);return{literal:!a,val:a?" ":l}}const s=e[i];let o=i;i==="hour"&&(e.hour12!=null?o=e.hour12?"hour12":"hour24":e.hourCycle!=null?e.hourCycle==="h11"||e.hourCycle==="h12"?o="hour12":o="hour24":o=t.hour12?"hour12":"hour24");let r=S2[o];if(typeof r=="object"&&(r=r[s]),r)return{literal:!1,val:r}}function $2(n){return[`^${n.map(t=>t.regex).reduce((t,i)=>`${t}(${i.source})`,"")}$`,n]}function C2(n,e,t){const i=n.match(e);if(i){const l={};let s=1;for(const o in t)if(ns(t,o)){const r=t[o],a=r.groups?r.groups+1:1;!r.literal&&r.token&&(l[r.token.val[0]]=r.deser(i.slice(s,s+a))),s+=a}return[i,l]}else return[i,{}]}function O2(n){const e=s=>{switch(s){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}};let t=null,i;return it(n.z)||(t=Fi.create(n.z)),it(n.Z)||(t||(t=new Cn(n.Z)),i=n.Z),it(n.q)||(n.M=(n.q-1)*3+1),it(n.h)||(n.h<12&&n.a===1?n.h+=12:n.h===12&&n.a===0&&(n.h=0)),n.G===0&&n.y&&(n.y=-n.y),it(n.u)||(n.S=bu(n.u)),[Object.keys(n).reduce((s,o)=>{const r=e(o);return r&&(s[r]=n[o]),s},{}),t,i]}let xr=null;function E2(){return xr||(xr=Xe.fromMillis(1555555555555)),xr}function M2(n,e){if(n.literal)return n;const t=yn.macroTokenToFormatOpts(n.val),i=P0(t,e);return i==null||i.includes(void 0)?n:i}function I0(n,e){return Array.prototype.concat(...n.map(t=>M2(t,e)))}class L0{constructor(e,t){if(this.locale=e,this.format=t,this.tokens=I0(yn.parseFormat(t),e),this.units=this.tokens.map(i=>w2(i,e)),this.disqualifyingUnit=this.units.find(i=>i.invalidReason),!this.disqualifyingUnit){const[i,l]=$2(this.units);this.regex=RegExp(i,"i"),this.handlers=l}}explainFromTokens(e){if(this.isValid){const[t,i]=C2(e,this.regex,this.handlers),[l,s,o]=i?O2(i):[null,null,void 0];if(ns(i,"a")&&ns(i,"H"))throw new Jl("Can't include meridiem when specifying 24-hour format");return{input:e,tokens:this.tokens,regex:this.regex,rawMatches:t,matches:i,result:l,zone:s,specificOffset:o}}else return{input:e,tokens:this.tokens,invalidReason:this.invalidReason}}get isValid(){return!this.disqualifyingUnit}get invalidReason(){return this.disqualifyingUnit?this.disqualifyingUnit.invalidReason:null}}function A0(n,e,t){return new L0(n,t).explainFromTokens(e)}function D2(n,e,t){const{result:i,zone:l,specificOffset:s,invalidReason:o}=A0(n,e,t);return[i,l,s,o]}function P0(n,e){if(!n)return null;const i=yn.create(e,n).dtFormatter(E2()),l=i.formatToParts(),s=i.resolvedOptions();return l.map(o=>T2(o,n,s))}const ea="Invalid DateTime",qf=864e13;function $s(n){return new si("unsupported zone",`the zone "${n.name}" is not supported`)}function ta(n){return n.weekData===null&&(n.weekData=pr(n.c)),n.weekData}function na(n){return n.localWeekData===null&&(n.localWeekData=pr(n.c,n.loc.getMinDaysInFirstWeek(),n.loc.getStartOfWeek())),n.localWeekData}function _l(n,e){const t={ts:n.ts,zone:n.zone,c:n.c,o:n.o,loc:n.loc,invalid:n.invalid};return new Xe({...t,...e,old:t})}function N0(n,e,t){let i=n-e*60*1e3;const l=t.offset(i);if(e===l)return[i,e];i-=(l-e)*60*1e3;const s=t.offset(i);return l===s?[i,l]:[n-Math.min(l,s)*60*1e3,Math.max(l,s)]}function Eo(n,e){n+=e*60*1e3;const t=new Date(n);return{year:t.getUTCFullYear(),month:t.getUTCMonth()+1,day:t.getUTCDate(),hour:t.getUTCHours(),minute:t.getUTCMinutes(),second:t.getUTCSeconds(),millisecond:t.getUTCMilliseconds()}}function xo(n,e,t){return N0(Ir(n),e,t)}function Hf(n,e){const t=n.o,i=n.c.year+Math.trunc(e.years),l=n.c.month+Math.trunc(e.months)+Math.trunc(e.quarters)*3,s={...n.c,year:i,month:l,day:Math.min(n.c.day,mr(i,l))+Math.trunc(e.days)+Math.trunc(e.weeks)*7},o=wt.fromObject({years:e.years-Math.trunc(e.years),quarters:e.quarters-Math.trunc(e.quarters),months:e.months-Math.trunc(e.months),weeks:e.weeks-Math.trunc(e.weeks),days:e.days-Math.trunc(e.days),hours:e.hours,minutes:e.minutes,seconds:e.seconds,milliseconds:e.milliseconds}).as("milliseconds"),r=Ir(s);let[a,u]=N0(r,t,n.zone);return o!==0&&(a+=o,u=n.zone.offset(a)),{ts:a,o:u}}function Vl(n,e,t,i,l,s){const{setZone:o,zone:r}=t;if(n&&Object.keys(n).length!==0||e){const a=e||r,u=Xe.fromObject(n,{...t,zone:a,specificOffset:s});return o?u:u.setZone(r)}else return Xe.invalid(new si("unparsable",`the input "${l}" can't be parsed as ${i}`))}function Mo(n,e,t=!0){return n.isValid?yn.create(Et.create("en-US"),{allowZ:t,forceSimple:!0}).formatDateTimeFromString(n,e):null}function ia(n,e){const t=n.c.year>9999||n.c.year<0;let i="";return t&&n.c.year>=0&&(i+="+"),i+=xt(n.c.year,t?6:4),e?(i+="-",i+=xt(n.c.month),i+="-",i+=xt(n.c.day)):(i+=xt(n.c.month),i+=xt(n.c.day)),i}function jf(n,e,t,i,l,s){let o=xt(n.c.hour);return e?(o+=":",o+=xt(n.c.minute),(n.c.millisecond!==0||n.c.second!==0||!t)&&(o+=":")):o+=xt(n.c.minute),(n.c.millisecond!==0||n.c.second!==0||!t)&&(o+=xt(n.c.second),(n.c.millisecond!==0||!i)&&(o+=".",o+=xt(n.c.millisecond,3))),l&&(n.isOffsetFixed&&n.offset===0&&!s?o+="Z":n.o<0?(o+="-",o+=xt(Math.trunc(-n.o/60)),o+=":",o+=xt(Math.trunc(-n.o%60))):(o+="+",o+=xt(Math.trunc(n.o/60)),o+=":",o+=xt(Math.trunc(n.o%60)))),s&&(o+="["+n.zone.ianaName+"]"),o}const R0={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},I2={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},L2={ordinal:1,hour:0,minute:0,second:0,millisecond:0},F0=["year","month","day","hour","minute","second","millisecond"],A2=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],P2=["year","ordinal","hour","minute","second","millisecond"];function N2(n){const e={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[n.toLowerCase()];if(!e)throw new qb(n);return e}function zf(n){switch(n.toLowerCase()){case"localweekday":case"localweekdays":return"localWeekday";case"localweeknumber":case"localweeknumbers":return"localWeekNumber";case"localweekyear":case"localweekyears":return"localWeekYear";default:return N2(n)}}function R2(n){return tr[n]||(er===void 0&&(er=Bt.now()),tr[n]=n.offset(er)),tr[n]}function Uf(n,e){const t=Ji(e.zone,Bt.defaultZone);if(!t.isValid)return Xe.invalid($s(t));const i=Et.fromObject(e);let l,s;if(it(n.year))l=Bt.now();else{for(const a of F0)it(n[a])&&(n[a]=R0[a]);const o=a0(n)||u0(n);if(o)return Xe.invalid(o);const r=R2(t);[l,s]=xo(n,r,t)}return new Xe({ts:l,zone:t,loc:i,o:s})}function Vf(n,e,t){const i=it(t.round)?!0:t.round,l=(o,r)=>(o=yu(o,i||t.calendary?0:2,!0),e.loc.clone(t).relFormatter(t).format(o,r)),s=o=>t.calendary?e.hasSame(n,o)?0:e.startOf(o).diff(n.startOf(o),o).get(o):e.diff(n,o).get(o);if(t.unit)return l(s(t.unit),t.unit);for(const o of t.units){const r=s(o);if(Math.abs(r)>=1)return l(r,o)}return l(n>e?-0:0,t.units[t.units.length-1])}function Bf(n){let e={},t;return n.length>0&&typeof n[n.length-1]=="object"?(e=n[n.length-1],t=Array.from(n).slice(0,n.length-1)):t=Array.from(n),[e,t]}let er,tr={};class Xe{constructor(e){const t=e.zone||Bt.defaultZone;let i=e.invalid||(Number.isNaN(e.ts)?new si("invalid input"):null)||(t.isValid?null:$s(t));this.ts=it(e.ts)?Bt.now():e.ts;let l=null,s=null;if(!i)if(e.old&&e.old.ts===this.ts&&e.old.zone.equals(t))[l,s]=[e.old.c,e.old.o];else{const r=Qi(e.o)&&!e.old?e.o:t.offset(this.ts);l=Eo(this.ts,r),i=Number.isNaN(l.year)?new si("invalid input"):null,l=i?null:l,s=i?null:r}this._zone=t,this.loc=e.loc||Et.create(),this.invalid=i,this.weekData=null,this.localWeekData=null,this.c=l,this.o=s,this.isLuxonDateTime=!0}static now(){return new Xe({})}static local(){const[e,t]=Bf(arguments),[i,l,s,o,r,a,u]=t;return Uf({year:i,month:l,day:s,hour:o,minute:r,second:a,millisecond:u},e)}static utc(){const[e,t]=Bf(arguments),[i,l,s,o,r,a,u]=t;return e.zone=Cn.utcInstance,Uf({year:i,month:l,day:s,hour:o,minute:r,second:a,millisecond:u},e)}static fromJSDate(e,t={}){const i=dv(e)?e.valueOf():NaN;if(Number.isNaN(i))return Xe.invalid("invalid input");const l=Ji(t.zone,Bt.defaultZone);return l.isValid?new Xe({ts:i,zone:l,loc:Et.fromObject(t)}):Xe.invalid($s(l))}static fromMillis(e,t={}){if(Qi(e))return e<-qf||e>qf?Xe.invalid("Timestamp out of range"):new Xe({ts:e,zone:Ji(t.zone,Bt.defaultZone),loc:Et.fromObject(t)});throw new gn(`fromMillis requires a numerical input, but received a ${typeof e} with value ${e}`)}static fromSeconds(e,t={}){if(Qi(e))return new Xe({ts:e*1e3,zone:Ji(t.zone,Bt.defaultZone),loc:Et.fromObject(t)});throw new gn("fromSeconds requires a numerical input")}static fromObject(e,t={}){e=e||{};const i=Ji(t.zone,Bt.defaultZone);if(!i.isValid)return Xe.invalid($s(i));const l=Et.fromObject(t),s=hr(e,zf),{minDaysInFirstWeek:o,startOfWeek:r}=Ef(s,l),a=Bt.now(),u=it(t.specificOffset)?i.offset(a):t.specificOffset,f=!it(s.ordinal),c=!it(s.year),d=!it(s.month)||!it(s.day),m=c||d,h=s.weekYear||s.weekNumber;if((m||f)&&h)throw new Jl("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(d&&f)throw new Jl("Can't mix ordinal dates with month/day");const g=h||s.weekday&&!m;let _,y,S=Eo(a,u);g?(_=A2,y=I2,S=pr(S,o,r)):f?(_=P2,y=L2,S=Qr(S)):(_=F0,y=R0);let T=!1;for(const P of _){const R=s[P];it(R)?T?s[P]=y[P]:s[P]=S[P]:T=!0}const $=g?uv(s,o,r):f?fv(s):a0(s),E=$||u0(s);if(E)return Xe.invalid(E);const M=g?Cf(s,o,r):f?Of(s):s,[L,I]=xo(M,u,i),A=new Xe({ts:L,zone:i,o:I,loc:l});return s.weekday&&m&&e.weekday!==A.weekday?Xe.invalid("mismatched weekday",`you can't specify both a weekday of ${s.weekday} and a date of ${A.toISO()}`):A.isValid?A:Xe.invalid(A.invalid)}static fromISO(e,t={}){const[i,l]=t2(e);return Vl(i,l,t,"ISO 8601",e)}static fromRFC2822(e,t={}){const[i,l]=n2(e);return Vl(i,l,t,"RFC 2822",e)}static fromHTTP(e,t={}){const[i,l]=i2(e);return Vl(i,l,t,"HTTP",t)}static fromFormat(e,t,i={}){if(it(e)||it(t))throw new gn("fromFormat requires an input string and a format");const{locale:l=null,numberingSystem:s=null}=i,o=Et.fromOpts({locale:l,numberingSystem:s,defaultToEN:!0}),[r,a,u,f]=D2(o,e,t);return f?Xe.invalid(f):Vl(r,a,i,`format ${t}`,e,u)}static fromString(e,t,i={}){return Xe.fromFormat(e,t,i)}static fromSQL(e,t={}){const[i,l]=f2(e);return Vl(i,l,t,"SQL",e)}static invalid(e,t=null){if(!e)throw new gn("need to specify a reason the DateTime is invalid");const i=e instanceof si?e:new si(e,t);if(Bt.throwOnInvalid)throw new Rk(i);return new Xe({invalid:i})}static isDateTime(e){return e&&e.isLuxonDateTime||!1}static parseFormatForOpts(e,t={}){const i=P0(e,Et.fromObject(t));return i?i.map(l=>l?l.val:null).join(""):null}static expandFormat(e,t={}){return I0(yn.parseFormat(e),Et.fromObject(t)).map(l=>l.val).join("")}static resetCache(){er=void 0,tr={}}get(e){return this[e]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?ta(this).weekYear:NaN}get weekNumber(){return this.isValid?ta(this).weekNumber:NaN}get weekday(){return this.isValid?ta(this).weekday:NaN}get isWeekend(){return this.isValid&&this.loc.getWeekendDays().includes(this.weekday)}get localWeekday(){return this.isValid?na(this).weekday:NaN}get localWeekNumber(){return this.isValid?na(this).weekNumber:NaN}get localWeekYear(){return this.isValid?na(this).weekYear:NaN}get ordinal(){return this.isValid?Qr(this.c).ordinal:NaN}get monthShort(){return this.isValid?Co.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?Co.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?Co.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?Co.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}getPossibleOffsets(){if(!this.isValid||this.isOffsetFixed)return[this];const e=864e5,t=6e4,i=Ir(this.c),l=this.zone.offset(i-e),s=this.zone.offset(i+e),o=this.zone.offset(i-l*t),r=this.zone.offset(i-s*t);if(o===r)return[this];const a=i-o*t,u=i-r*t,f=Eo(a,o),c=Eo(u,r);return f.hour===c.hour&&f.minute===c.minute&&f.second===c.second&&f.millisecond===c.millisecond?[_l(this,{ts:a}),_l(this,{ts:u})]:[this]}get isInLeapYear(){return ro(this.year)}get daysInMonth(){return mr(this.year,this.month)}get daysInYear(){return this.isValid?Xl(this.year):NaN}get weeksInWeekYear(){return this.isValid?Bs(this.weekYear):NaN}get weeksInLocalWeekYear(){return this.isValid?Bs(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}resolvedLocaleOptions(e={}){const{locale:t,numberingSystem:i,calendar:l}=yn.create(this.loc.clone(e),e).resolvedOptions(this);return{locale:t,numberingSystem:i,outputCalendar:l}}toUTC(e=0,t={}){return this.setZone(Cn.instance(e),t)}toLocal(){return this.setZone(Bt.defaultZone)}setZone(e,{keepLocalTime:t=!1,keepCalendarTime:i=!1}={}){if(e=Ji(e,Bt.defaultZone),e.equals(this.zone))return this;if(e.isValid){let l=this.ts;if(t||i){const s=e.offset(this.ts),o=this.toObject();[l]=xo(o,s,e)}return _l(this,{ts:l,zone:e})}else return Xe.invalid($s(e))}reconfigure({locale:e,numberingSystem:t,outputCalendar:i}={}){const l=this.loc.clone({locale:e,numberingSystem:t,outputCalendar:i});return _l(this,{loc:l})}setLocale(e){return this.reconfigure({locale:e})}set(e){if(!this.isValid)return this;const t=hr(e,zf),{minDaysInFirstWeek:i,startOfWeek:l}=Ef(t,this.loc),s=!it(t.weekYear)||!it(t.weekNumber)||!it(t.weekday),o=!it(t.ordinal),r=!it(t.year),a=!it(t.month)||!it(t.day),u=r||a,f=t.weekYear||t.weekNumber;if((u||o)&&f)throw new Jl("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(a&&o)throw new Jl("Can't mix ordinal dates with month/day");let c;s?c=Cf({...pr(this.c,i,l),...t},i,l):it(t.ordinal)?(c={...this.toObject(),...t},it(t.day)&&(c.day=Math.min(mr(c.year,c.month),c.day))):c=Of({...Qr(this.c),...t});const[d,m]=xo(c,this.o,this.zone);return _l(this,{ts:d,o:m})}plus(e){if(!this.isValid)return this;const t=wt.fromDurationLike(e);return _l(this,Hf(this,t))}minus(e){if(!this.isValid)return this;const t=wt.fromDurationLike(e).negate();return _l(this,Hf(this,t))}startOf(e,{useLocaleWeeks:t=!1}={}){if(!this.isValid)return this;const i={},l=wt.normalizeUnit(e);switch(l){case"years":i.month=1;case"quarters":case"months":i.day=1;case"weeks":case"days":i.hour=0;case"hours":i.minute=0;case"minutes":i.second=0;case"seconds":i.millisecond=0;break}if(l==="weeks")if(t){const s=this.loc.getStartOfWeek(),{weekday:o}=this;othis.valueOf(),r=o?this:e,a=o?e:this,u=g2(r,a,s,l);return o?u.negate():u}diffNow(e="milliseconds",t={}){return this.diff(Xe.now(),e,t)}until(e){return this.isValid?Vt.fromDateTimes(this,e):this}hasSame(e,t,i){if(!this.isValid)return!1;const l=e.valueOf(),s=this.setZone(e.zone,{keepLocalTime:!0});return s.startOf(t,i)<=l&&l<=s.endOf(t,i)}equals(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)}toRelative(e={}){if(!this.isValid)return null;const t=e.base||Xe.fromObject({},{zone:this.zone}),i=e.padding?thist.valueOf(),Math.min)}static max(...e){if(!e.every(Xe.isDateTime))throw new gn("max requires all arguments be DateTimes");return Mf(e,t=>t.valueOf(),Math.max)}static fromFormatExplain(e,t,i={}){const{locale:l=null,numberingSystem:s=null}=i,o=Et.fromOpts({locale:l,numberingSystem:s,defaultToEN:!0});return A0(o,e,t)}static fromStringExplain(e,t,i={}){return Xe.fromFormatExplain(e,t,i)}static buildFormatParser(e,t={}){const{locale:i=null,numberingSystem:l=null}=t,s=Et.fromOpts({locale:i,numberingSystem:l,defaultToEN:!0});return new L0(s,e)}static fromFormatParser(e,t,i={}){if(it(e)||it(t))throw new gn("fromFormatParser requires an input string and a format parser");const{locale:l=null,numberingSystem:s=null}=i,o=Et.fromOpts({locale:l,numberingSystem:s,defaultToEN:!0});if(!o.equals(t.locale))throw new gn(`fromFormatParser called with a locale of ${o}, but the format parser was created for ${t.locale}`);const{result:r,zone:a,specificOffset:u,invalidReason:f}=t.explainFromTokens(e);return f?Xe.invalid(f):Vl(r,a,i,`format ${t.format}`,e,u)}static get DATE_SHORT(){return dr}static get DATE_MED(){return Hb}static get DATE_MED_WITH_WEEKDAY(){return Hk}static get DATE_FULL(){return jb}static get DATE_HUGE(){return zb}static get TIME_SIMPLE(){return Ub}static get TIME_WITH_SECONDS(){return Vb}static get TIME_WITH_SHORT_OFFSET(){return Bb}static get TIME_WITH_LONG_OFFSET(){return Wb}static get TIME_24_SIMPLE(){return Yb}static get TIME_24_WITH_SECONDS(){return Kb}static get TIME_24_WITH_SHORT_OFFSET(){return Jb}static get TIME_24_WITH_LONG_OFFSET(){return Zb}static get DATETIME_SHORT(){return Gb}static get DATETIME_SHORT_WITH_SECONDS(){return Xb}static get DATETIME_MED(){return Qb}static get DATETIME_MED_WITH_SECONDS(){return xb}static get DATETIME_MED_WITH_WEEKDAY(){return jk}static get DATETIME_FULL(){return e0}static get DATETIME_FULL_WITH_SECONDS(){return t0}static get DATETIME_HUGE(){return n0}static get DATETIME_HUGE_WITH_SECONDS(){return i0}}function bs(n){if(Xe.isDateTime(n))return n;if(n&&n.valueOf&&Qi(n.valueOf()))return Xe.fromJSDate(n);if(n&&typeof n=="object")return Xe.fromObject(n);throw new gn(`Unknown datetime argument: ${n}, of type ${typeof n}`)}const F2=[".jpg",".jpeg",".png",".svg",".gif",".jfif",".webp",".avif"],q2=[".mp4",".avi",".mov",".3gp",".wmv"],H2=[".aa",".aac",".m4v",".mp3",".ogg",".oga",".mogg",".amr"],j2=[".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".odp",".odt",".ods",".txt"],q0=[{level:-4,label:"DEBUG",class:""},{level:0,label:"INFO",class:"label-success"},{level:4,label:"WARN",class:"label-warning"},{level:8,label:"ERROR",class:"label-danger"}];class z{static isObject(e){return e!==null&&typeof e=="object"&&e.constructor===Object}static clone(e){return typeof structuredClone<"u"?structuredClone(e):JSON.parse(JSON.stringify(e))}static zeroValue(e){switch(typeof e){case"string":return"";case"number":return 0;case"boolean":return!1;case"object":return e===null?null:Array.isArray(e)?[]:{};case"undefined":return;default:return null}}static isEmpty(e){return e===""||e===null||typeof e>"u"||Array.isArray(e)&&e.length===0||z.isObject(e)&&Object.keys(e).length===0}static isInput(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return t==="input"||t==="select"||t==="textarea"||(e==null?void 0:e.isContentEditable)}static isFocusable(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return z.isInput(e)||t==="button"||t==="a"||t==="details"||(e==null?void 0:e.tabIndex)>=0}static hasNonEmptyProps(e){for(let t in e)if(!z.isEmpty(e[t]))return!0;return!1}static toArray(e,t=!1){return Array.isArray(e)?e.slice():(t||!z.isEmpty(e))&&typeof e<"u"?[e]:[]}static inArray(e,t){e=Array.isArray(e)?e:[];for(let i=e.length-1;i>=0;i--)if(e[i]==t)return!0;return!1}static removeByValue(e,t){e=Array.isArray(e)?e:[];for(let i=e.length-1;i>=0;i--)if(e[i]==t){e.splice(i,1);break}}static pushUnique(e,t){z.inArray(e,t)||e.push(t)}static mergeUnique(e,t){for(let i of t)z.pushUnique(e,i);return e}static findByKey(e,t,i){e=Array.isArray(e)?e:[];for(let l in e)if(e[l][t]==i)return e[l];return null}static groupByKey(e,t){e=Array.isArray(e)?e:[];const i={};for(let l in e)i[e[l][t]]=i[e[l][t]]||[],i[e[l][t]].push(e[l]);return i}static removeByKey(e,t,i){for(let l in e)if(e[l][t]==i){e.splice(l,1);break}}static pushOrReplaceByKey(e,t,i="id"){for(let l=e.length-1;l>=0;l--)if(e[l][i]==t[i]){e[l]=t;return}e.push(t)}static filterDuplicatesByKey(e,t="id"){e=Array.isArray(e)?e:[];const i={};for(const l of e)i[l[t]]=l;return Object.values(i)}static filterRedactedProps(e,t="******"){const i=JSON.parse(JSON.stringify(e||{}));for(let l in i)typeof i[l]=="object"&&i[l]!==null?i[l]=z.filterRedactedProps(i[l],t):i[l]===t&&delete i[l];return i}static getNestedVal(e,t,i=null,l="."){let s=e||{},o=(t||"").split(l);for(const r of o){if(!z.isObject(s)&&!Array.isArray(s)||typeof s[r]>"u")return i;s=s[r]}return s}static setByPath(e,t,i,l="."){if(e===null||typeof e!="object"){console.warn("setByPath: data not an object or array.");return}let s=e,o=t.split(l),r=o.pop();for(const a of o)(!z.isObject(s)&&!Array.isArray(s)||!z.isObject(s[a])&&!Array.isArray(s[a]))&&(s[a]={}),s=s[a];s[r]=i}static deleteByPath(e,t,i="."){let l=e||{},s=(t||"").split(i),o=s.pop();for(const r of s)(!z.isObject(l)&&!Array.isArray(l)||!z.isObject(l[r])&&!Array.isArray(l[r]))&&(l[r]={}),l=l[r];Array.isArray(l)?l.splice(o,1):z.isObject(l)&&delete l[o],s.length>0&&(Array.isArray(l)&&!l.length||z.isObject(l)&&!Object.keys(l).length)&&(Array.isArray(e)&&e.length>0||z.isObject(e)&&Object.keys(e).length>0)&&z.deleteByPath(e,s.join(i),i)}static randomString(e=10){let t="",i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let l=0;l"u")return z.randomString(e);const t=new Uint8Array(e);crypto.getRandomValues(t);const i="-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";let l="";for(let s=0;ss.replaceAll("{_PB_ESCAPED_}",t));for(let s of l)s=s.trim(),z.isEmpty(s)||i.push(s);return i}static joinNonEmpty(e,t=", "){e=e||[];const i=[],l=t.length>1?t.trim():t;for(let s of e)s=typeof s=="string"?s.trim():"",z.isEmpty(s)||i.push(s.replaceAll(l,"\\"+l));return i.join(t)}static getInitials(e){if(e=(e||"").split("@")[0].trim(),e.length<=2)return e.toUpperCase();const t=e.split(/[\.\_\-\ ]/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e[0].toUpperCase()}static formattedFileSize(e){const t=e?Math.floor(Math.log(e)/Math.log(1024)):0;return(e/Math.pow(1024,t)).toFixed(2)*1+" "+["B","KB","MB","GB","TB"][t]}static getDateTime(e){if(typeof e=="string"){const t={19:"yyyy-MM-dd HH:mm:ss",23:"yyyy-MM-dd HH:mm:ss.SSS",20:"yyyy-MM-dd HH:mm:ss'Z'",24:"yyyy-MM-dd HH:mm:ss.SSS'Z'"},i=t[e.length]||t[19];return Xe.fromFormat(e,i,{zone:"UTC"})}return typeof e=="number"?Xe.fromMillis(e):Xe.fromJSDate(e)}static formatToUTCDate(e,t="yyyy-MM-dd HH:mm:ss"){return z.getDateTime(e).toUTC().toFormat(t)}static formatToLocalDate(e,t="yyyy-MM-dd HH:mm:ss"){return z.getDateTime(e).toLocal().toFormat(t)}static async copyToClipboard(e){var t;if(e=""+e,!(!e.length||!((t=window==null?void 0:window.navigator)!=null&&t.clipboard)))return window.navigator.clipboard.writeText(e).catch(i=>{console.warn("Failed to copy.",i)})}static download(e,t){const i=document.createElement("a");i.setAttribute("href",e),i.setAttribute("download",t),i.setAttribute("target","_blank"),i.click(),i.remove()}static downloadJson(e,t){t=t.endsWith(".json")?t:t+".json";const i=new Blob([JSON.stringify(e,null,2)],{type:"application/json"}),l=window.URL.createObjectURL(i);z.download(l,t)}static getJWTPayload(e){const t=(e||"").split(".")[1]||"";if(t==="")return{};try{const i=decodeURIComponent(atob(t));return JSON.parse(i)||{}}catch(i){console.warn("Failed to parse JWT payload data.",i)}return{}}static hasImageExtension(e){return e=e||"",!!F2.find(t=>e.toLowerCase().endsWith(t))}static hasVideoExtension(e){return e=e||"",!!q2.find(t=>e.toLowerCase().endsWith(t))}static hasAudioExtension(e){return e=e||"",!!H2.find(t=>e.toLowerCase().endsWith(t))}static hasDocumentExtension(e){return e=e||"",!!j2.find(t=>e.toLowerCase().endsWith(t))}static getFileType(e){return z.hasImageExtension(e)?"image":z.hasDocumentExtension(e)?"document":z.hasVideoExtension(e)?"video":z.hasAudioExtension(e)?"audio":"file"}static generateThumb(e,t=100,i=100){return new Promise(l=>{let s=new FileReader;s.onload=function(o){let r=new Image;r.onload=function(){let a=document.createElement("canvas"),u=a.getContext("2d"),f=r.width,c=r.height;return a.width=t,a.height=i,u.drawImage(r,f>c?(f-c)/2:0,0,f>c?c:f,f>c?c:f,0,0,t,i),l(a.toDataURL(e.type))},r.src=o.target.result},s.readAsDataURL(e)})}static addValueToFormData(e,t,i){if(!(typeof i>"u"))if(z.isEmpty(i))e.append(t,"");else if(Array.isArray(i))for(const l of i)z.addValueToFormData(e,t,l);else i instanceof File?e.append(t,i):i instanceof Date?e.append(t,i.toISOString()):z.isObject(i)?e.append(t,JSON.stringify(i)):e.append(t,""+i)}static dummyCollectionRecord(e){return Object.assign({collectionId:e==null?void 0:e.id,collectionName:e==null?void 0:e.name},z.dummyCollectionSchemaData(e))}static dummyCollectionSchemaData(e,t=!1){var s;const i=(e==null?void 0:e.fields)||[],l={};for(const o of i){if(o.hidden||t&&o.primaryKey&&o.autogeneratePattern)continue;let r=null;if(o.type==="number")r=123;else if(o.type==="date"||o.type==="autodate")r="2022-01-01 10:00:00.123Z";else if(o.type=="bool")r=!0;else if(o.type=="email")r="test@example.com";else if(o.type=="url")r="https://example.com";else if(o.type=="json")r="JSON";else if(o.type=="file"){if(t)continue;r="filename.jpg",o.maxSelect!=1&&(r=[r])}else o.type=="select"?(r=(s=o==null?void 0:o.values)==null?void 0:s[0],(o==null?void 0:o.maxSelect)!=1&&(r=[r])):o.type=="relation"?(r="RELATION_RECORD_ID",(o==null?void 0:o.maxSelect)!=1&&(r=[r])):r="test";l[o.name]=r}return l}static getCollectionTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"auth":return"ri-group-line";case"view":return"ri-table-line";default:return"ri-folder-2-line"}}static getFieldTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"primary":return"ri-key-line";case"text":return"ri-text";case"number":return"ri-hashtag";case"date":return"ri-calendar-line";case"bool":return"ri-toggle-line";case"email":return"ri-mail-line";case"url":return"ri-link";case"editor":return"ri-edit-2-line";case"select":return"ri-list-check";case"json":return"ri-braces-line";case"file":return"ri-image-line";case"relation":return"ri-mind-map";case"password":return"ri-lock-password-line";case"autodate":return"ri-calendar-check-line";default:return"ri-star-s-line"}}static getFieldValueType(e){switch(e==null?void 0:e.type){case"bool":return"Boolean";case"number":return"Number";case"file":return"File";case"select":case"relation":return(e==null?void 0:e.maxSelect)==1?"String":"Array";default:return"String"}}static zeroDefaultStr(e){return(e==null?void 0:e.type)==="number"?"0":(e==null?void 0:e.type)==="bool"?"false":(e==null?void 0:e.type)==="json"?'null, "", [], {}':["select","relation","file"].includes(e==null?void 0:e.type)&&(e==null?void 0:e.maxSelect)!=1?"[]":'""'}static getApiExampleUrl(e){return(window.location.href.substring(0,window.location.href.indexOf("/_"))||e||"/").replace("//localhost","//127.0.0.1")}static hasCollectionChanges(e,t,i=!1){if(e=e||{},t=t||{},e.id!=t.id)return!0;for(let u in e)if(u!=="fields"&&JSON.stringify(e[u])!==JSON.stringify(t[u]))return!0;const l=Array.isArray(e.fields)?e.fields:[],s=Array.isArray(t.fields)?t.fields:[],o=l.filter(u=>(u==null?void 0:u.id)&&!z.findByKey(s,"id",u.id)),r=s.filter(u=>(u==null?void 0:u.id)&&!z.findByKey(l,"id",u.id)),a=s.filter(u=>{const f=z.isObject(u)&&z.findByKey(l,"id",u.id);if(!f)return!1;for(let c in f)if(JSON.stringify(u[c])!=JSON.stringify(f[c]))return!0;return!1});return!!(r.length||a.length||i&&o.length)}static sortCollections(e=[]){const t=[],i=[],l=[];for(const o of e)o.type==="auth"?t.push(o):o.type==="base"?i.push(o):l.push(o);function s(o,r){return o.name>r.name?1:o.name0){const r=z.getExpandPresentableRelField(o,t,i-1);r&&(s+="."+r)}return s}return""}static yieldToMain(){return new Promise(e=>{setTimeout(e,0)})}static defaultFlatpickrOptions(){return{dateFormat:"Y-m-d H:i:S",disableMobile:!0,allowInput:!0,enableTime:!0,time_24hr:!0,locale:{firstDayOfWeek:1}}}static defaultEditorOptions(){const e=["DIV","P","A","EM","B","STRONG","H1","H2","H3","H4","H5","H6","TABLE","TR","TD","TH","TBODY","THEAD","TFOOT","BR","HR","Q","SUP","SUB","DEL","IMG","OL","UL","LI","CODE"];function t(l){let s=l.parentNode;for(;l.firstChild;)s.insertBefore(l.firstChild,l);s.removeChild(l)}function i(l){if(l){for(const s of l.children)i(s);e.includes(l.tagName)?(l.removeAttribute("style"),l.removeAttribute("class")):t(l)}}return{branding:!1,promotion:!1,menubar:!1,min_height:270,height:270,max_height:700,autoresize_bottom_margin:30,convert_unsafe_embeds:!0,skin:"pocketbase",content_style:"body { font-size: 14px }",plugins:["autoresize","autolink","lists","link","image","searchreplace","fullscreen","media","table","code","codesample","directionality"],codesample_global_prismjs:!0,codesample_languages:[{text:"HTML/XML",value:"markup"},{text:"CSS",value:"css"},{text:"SQL",value:"sql"},{text:"JavaScript",value:"javascript"},{text:"Go",value:"go"},{text:"Dart",value:"dart"},{text:"Zig",value:"zig"},{text:"Rust",value:"rust"},{text:"Lua",value:"lua"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"},{text:"Markdown",value:"markdown"},{text:"Swift",value:"swift"},{text:"Kotlin",value:"kotlin"},{text:"Elixir",value:"elixir"},{text:"Scala",value:"scala"},{text:"Julia",value:"julia"},{text:"Haskell",value:"haskell"}],toolbar:"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link image_picker table codesample direction | code fullscreen",paste_postprocess:(l,s)=>{i(s.node)},file_picker_types:"image",file_picker_callback:(l,s,o)=>{const r=document.createElement("input");r.setAttribute("type","file"),r.setAttribute("accept","image/*"),r.addEventListener("change",a=>{const u=a.target.files[0],f=new FileReader;f.addEventListener("load",()=>{if(!tinymce)return;const c="blobid"+new Date().getTime(),d=tinymce.activeEditor.editorUpload.blobCache,m=f.result.split(",")[1],h=d.create(c,u,m);d.add(h),l(h.blobUri(),{title:u.name})}),f.readAsDataURL(u)}),r.click()},setup:l=>{l.on("keydown",o=>{(o.ctrlKey||o.metaKey)&&o.code=="KeyS"&&l.formElement&&(o.preventDefault(),o.stopPropagation(),l.formElement.dispatchEvent(new KeyboardEvent("keydown",o)))});const s="tinymce_last_direction";l.on("init",()=>{var r;const o=(r=window==null?void 0:window.localStorage)==null?void 0:r.getItem(s);!l.isDirty()&&l.getContent()==""&&o=="rtl"&&l.execCommand("mceDirectionRTL")}),l.ui.registry.addMenuButton("direction",{icon:"visualchars",fetch:o=>{o([{type:"menuitem",text:"LTR content",icon:"ltr",onAction:()=>{var a;(a=window==null?void 0:window.localStorage)==null||a.setItem(s,"ltr"),l.execCommand("mceDirectionLTR")}},{type:"menuitem",text:"RTL content",icon:"rtl",onAction:()=>{var a;(a=window==null?void 0:window.localStorage)==null||a.setItem(s,"rtl"),l.execCommand("mceDirectionRTL")}}])}}),l.ui.registry.addMenuButton("image_picker",{icon:"image",fetch:o=>{o([{type:"menuitem",text:"From collection",icon:"gallery",onAction:()=>{l.dispatch("collections_file_picker",{})}},{type:"menuitem",text:"Inline",icon:"browse",onAction:()=>{l.execCommand("mceImage")}}])}})}}}static displayValue(e,t,i="N/A"){e=e||{},t=t||[];let l=[];for(const o of t){let r=e[o];typeof r>"u"||(r=z.stringifyValue(r,i),l.push(r))}if(l.length>0)return l.join(", ");const s=["title","name","slug","email","username","nickname","label","heading","message","key","identifier","id"];for(const o of s){let r=z.stringifyValue(e[o],"");if(r)return r}return i}static stringifyValue(e,t="N/A",i=150){if(z.isEmpty(e))return t;if(typeof e=="number")return""+e;if(typeof e=="boolean")return e?"True":"False";if(typeof e=="string")return e=e.indexOf("<")>=0?z.plainText(e):e,z.truncate(e,i)||t;if(Array.isArray(e)&&typeof e[0]!="object")return z.truncate(e.join(","),i);if(typeof e=="object")try{return z.truncate(JSON.stringify(e),i)||t}catch{return t}return e}static extractColumnsFromQuery(e){var o;const t="__GROUP__";e=(e||"").replace(/\([\s\S]+?\)/gm,t).replace(/[\t\r\n]|(?:\s\s)+/g," ");const i=e.match(/select\s+([\s\S]+)\s+from/),l=((o=i==null?void 0:i[1])==null?void 0:o.split(","))||[],s=[];for(let r of l){const a=r.trim().split(" ").pop();a!=""&&a!=t&&s.push(a.replace(/[\'\"\`\[\]\s]/g,""))}return s}static getAllCollectionIdentifiers(e,t=""){if(!e)return[];let i=[t+"id"];if(e.type==="view")for(let s of z.extractColumnsFromQuery(e.viewQuery))z.pushUnique(i,t+s);const l=e.fields||[];for(const s of l)z.pushUnique(i,t+s.name);return i}static getCollectionAutocompleteKeys(e,t,i="",l=0){let s=e.find(r=>r.name==t||r.id==t);if(!s||l>=4)return[];s.fields=s.fields||[];let o=z.getAllCollectionIdentifiers(s,i);for(const r of s.fields){const a=i+r.name;if(r.type=="relation"&&r.collectionId){const u=z.getCollectionAutocompleteKeys(e,r.collectionId,a+".",l+1);u.length&&(o=o.concat(u))}r.maxSelect!=1&&["select","file","relation"].includes(r.type)&&(o.push(a+":each"),o.push(a+":length"))}for(const r of e){r.fields=r.fields||[];for(const a of r.fields)if(a.type=="relation"&&a.collectionId==s.id){const u=i+r.name+"_via_"+a.name,f=z.getCollectionAutocompleteKeys(e,r.id,u+".",l+2);f.length&&(o=o.concat(f))}}return o}static getCollectionJoinAutocompleteKeys(e){const t=[];let i,l;for(const s of e)if(!s.system){i="@collection."+s.name+".",l=z.getCollectionAutocompleteKeys(e,s.name,i);for(const o of l)t.push(o)}return t}static getRequestAutocompleteKeys(e,t){const i=[];i.push("@request.context"),i.push("@request.method"),i.push("@request.query."),i.push("@request.body."),i.push("@request.headers."),i.push("@request.auth.collectionId"),i.push("@request.auth.collectionName");const l=e.filter(s=>s.type==="auth");for(const s of l){if(s.system)continue;const o=z.getCollectionAutocompleteKeys(e,s.id,"@request.auth.");for(const r of o)z.pushUnique(i,r)}if(t){const s=z.getCollectionAutocompleteKeys(e,t,"@request.body.");for(const o of s){i.push(o);const r=o.split(".");r.length===3&&r[2].indexOf(":")===-1&&i.push(o+":isset")}}return i}static parseIndex(e){var a,u,f,c,d;const t={unique:!1,optional:!1,schemaName:"",indexName:"",tableName:"",columns:[],where:""},l=/create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?/gmi.exec((e||"").trim());if((l==null?void 0:l.length)!=7)return t;const s=/^[\"\'\`\[\{}]|[\"\'\`\]\}]$/gm;t.unique=((a=l[1])==null?void 0:a.trim().toLowerCase())==="unique",t.optional=!z.isEmpty((u=l[2])==null?void 0:u.trim());const o=(l[3]||"").split(".");o.length==2?(t.schemaName=o[0].replace(s,""),t.indexName=o[1].replace(s,"")):(t.schemaName="",t.indexName=o[0].replace(s,"")),t.tableName=(l[4]||"").replace(s,"");const r=(l[5]||"").replace(/,(?=[^\(]*\))/gmi,"{PB_TEMP}").split(",");for(let m of r){m=m.trim().replaceAll("{PB_TEMP}",",");const g=/^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$/gmi.exec(m);if((g==null?void 0:g.length)!=4)continue;const _=(c=(f=g[1])==null?void 0:f.trim())==null?void 0:c.replace(s,"");_&&t.columns.push({name:_,collate:g[2]||"",sort:((d=g[3])==null?void 0:d.toUpperCase())||""})}return t.where=l[6]||"",t}static buildIndex(e){let t="CREATE ";e.unique&&(t+="UNIQUE "),t+="INDEX ",e.optional&&(t+="IF NOT EXISTS "),e.schemaName&&(t+=`\`${e.schemaName}\`.`),t+=`\`${e.indexName||"idx_"+z.randomString(10)}\` `,t+=`ON \`${e.tableName}\` (`;const i=e.columns.filter(l=>!!(l!=null&&l.name));return i.length>1&&(t+=` + `),t+=i.map(l=>{let s="";return l.name.includes("(")||l.name.includes(" ")?s+=l.name:s+="`"+l.name+"`",l.collate&&(s+=" COLLATE "+l.collate),l.sort&&(s+=" "+l.sort.toUpperCase()),s}).join(`, + `),i.length>1&&(t+=` +`),t+=")",e.where&&(t+=` WHERE ${e.where}`),t}static replaceIndexTableName(e,t){const i=z.parseIndex(e);return i.tableName=t,z.buildIndex(i)}static replaceIndexColumn(e,t,i){if(t===i)return e;const l=z.parseIndex(e);let s=!1;for(let o of l.columns)o.name===t&&(o.name=i,s=!0);return s?z.buildIndex(l):e}static normalizeSearchFilter(e,t){if(e=(e||"").trim(),!e||!t.length)return e;const i=["=","!=","~","!~",">",">=","<","<="];for(const l of i)if(e.includes(l))return e;return e=isNaN(e)&&e!="true"&&e!="false"?`"${e.replace(/^[\"\'\`]|[\"\'\`]$/gm,"")}"`:e,t.map(l=>`${l}~${e}`).join("||")}static normalizeLogsFilter(e,t=[]){return z.normalizeSearchFilter(e,["level","message","data"].concat(t))}static initSchemaField(e){return Object.assign({id:"",name:"",type:"text",system:!1,hidden:!1,required:!1},e)}static triggerResize(){window.dispatchEvent(new Event("resize"))}static getHashQueryParams(){let e="";const t=window.location.hash.indexOf("?");return t>-1&&(e=window.location.hash.substring(t+1)),Object.fromEntries(new URLSearchParams(e))}static replaceHashQueryParams(e){e=e||{};let t="",i=window.location.hash;const l=i.indexOf("?");l>-1&&(t=i.substring(l+1),i=i.substring(0,l));const s=new URLSearchParams(t);for(let a in e){const u=e[a];u===null?s.delete(a):s.set(a,u)}t=s.toString(),t!=""&&(i+="?"+t);let o=window.location.href;const r=o.indexOf("#");r>-1&&(o=o.substring(0,r)),window.location.replace(o+i)}}let Ua,gl;const Va="app-tooltip";function Wf(n){return typeof n=="string"?{text:n,position:"bottom",hideOnClick:null}:n||{}}function xi(){return gl=gl||document.querySelector("."+Va),gl||(gl=document.createElement("div"),gl.classList.add(Va),document.body.appendChild(gl)),gl}function H0(n,e){let t=xi();if(!t.classList.contains("active")||!(e!=null&&e.text)){Ba();return}t.textContent=e.text,t.className=Va+" active",e.class&&t.classList.add(e.class),e.position&&t.classList.add(e.position),t.style.top="0px",t.style.left="0px";let i=t.offsetHeight,l=t.offsetWidth,s=n.getBoundingClientRect(),o=0,r=0,a=5;e.position=="left"?(o=s.top+s.height/2-i/2,r=s.left-l-a):e.position=="right"?(o=s.top+s.height/2-i/2,r=s.right+a):e.position=="top"?(o=s.top-i-a,r=s.left+s.width/2-l/2):e.position=="top-left"?(o=s.top-i-a,r=s.left):e.position=="top-right"?(o=s.top-i-a,r=s.right-l):e.position=="bottom-left"?(o=s.top+s.height+a,r=s.left):e.position=="bottom-right"?(o=s.top+s.height+a,r=s.right-l):(o=s.top+s.height+a,r=s.left+s.width/2-l/2),r+l>document.documentElement.clientWidth&&(r=document.documentElement.clientWidth-l),r=r>=0?r:0,o+i>document.documentElement.clientHeight&&(o=document.documentElement.clientHeight-i),o=o>=0?o:0,t.style.top=o+"px",t.style.left=r+"px"}function Ba(){clearTimeout(Ua),xi().classList.remove("active"),xi().activeNode=void 0}function z2(n,e){xi().activeNode=n,clearTimeout(Ua),Ua=setTimeout(()=>{xi().classList.add("active"),H0(n,e)},isNaN(e.delay)?0:e.delay)}function He(n,e){let t=Wf(e);function i(){z2(n,t)}function l(){Ba()}return n.addEventListener("mouseenter",i),n.addEventListener("mouseleave",l),n.addEventListener("blur",l),(t.hideOnClick===!0||t.hideOnClick===null&&z.isFocusable(n))&&n.addEventListener("click",l),xi(),{update(s){var o,r;t=Wf(s),(r=(o=xi())==null?void 0:o.activeNode)!=null&&r.contains(n)&&H0(n,t)},destroy(){var s,o;(o=(s=xi())==null?void 0:s.activeNode)!=null&&o.contains(n)&&Ba(),n.removeEventListener("mouseenter",i),n.removeEventListener("mouseleave",l),n.removeEventListener("blur",l),n.removeEventListener("click",l)}}}function Pr(n){const e=n-1;return e*e*e+1}function Ws(n,{delay:e=0,duration:t=400,easing:i=io}={}){const l=+getComputedStyle(n).opacity;return{delay:e,duration:t,easing:i,css:s=>`opacity: ${s*l}`}}function Fn(n,{delay:e=0,duration:t=400,easing:i=Pr,x:l=0,y:s=0,opacity:o=0}={}){const r=getComputedStyle(n),a=+r.opacity,u=r.transform==="none"?"":r.transform,f=a*(1-o),[c,d]=pf(l),[m,h]=pf(s);return{delay:e,duration:t,easing:i,css:(g,_)=>` + transform: ${u} translate(${(1-g)*c}${d}, ${(1-g)*m}${h}); + opacity: ${a-f*_}`}}function vt(n,{delay:e=0,duration:t=400,easing:i=Pr,axis:l="y"}={}){const s=getComputedStyle(n),o=+s.opacity,r=l==="y"?"height":"width",a=parseFloat(s[r]),u=l==="y"?["top","bottom"]:["left","right"],f=u.map(y=>`${y[0].toUpperCase()}${y.slice(1)}`),c=parseFloat(s[`padding${f[0]}`]),d=parseFloat(s[`padding${f[1]}`]),m=parseFloat(s[`margin${f[0]}`]),h=parseFloat(s[`margin${f[1]}`]),g=parseFloat(s[`border${f[0]}Width`]),_=parseFloat(s[`border${f[1]}Width`]);return{delay:e,duration:t,easing:i,css:y=>`overflow: hidden;opacity: ${Math.min(y*20,1)*o};${r}: ${y*a}px;padding-${u[0]}: ${y*c}px;padding-${u[1]}: ${y*d}px;margin-${u[0]}: ${y*m}px;margin-${u[1]}: ${y*h}px;border-${u[0]}-width: ${y*g}px;border-${u[1]}-width: ${y*_}px;`}}function Mt(n,{delay:e=0,duration:t=400,easing:i=Pr,start:l=0,opacity:s=0}={}){const o=getComputedStyle(n),r=+o.opacity,a=o.transform==="none"?"":o.transform,u=1-l,f=r*(1-s);return{delay:e,duration:t,easing:i,css:(c,d)=>` + transform: ${a} scale(${1-u*d}); + opacity: ${r-f*d} + `}}const U2=n=>({}),Yf=n=>({}),V2=n=>({}),Kf=n=>({});function Jf(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T=n[4]&&!n[2]&&Zf(n);const $=n[19].header,E=Lt($,n,n[18],Kf);let M=n[4]&&n[2]&&Gf(n);const L=n[19].default,I=Lt(L,n,n[18],null),A=n[19].footer,P=Lt(A,n,n[18],Yf);return{c(){e=b("div"),t=b("div"),l=C(),s=b("div"),o=b("div"),T&&T.c(),r=C(),E&&E.c(),a=C(),M&&M.c(),u=C(),f=b("div"),I&&I.c(),c=C(),d=b("div"),P&&P.c(),p(t,"class","overlay"),p(o,"class","overlay-panel-section panel-header"),p(f,"class","overlay-panel-section panel-content"),p(d,"class","overlay-panel-section panel-footer"),p(s,"class",m="overlay-panel "+n[1]+" "+n[8]),x(s,"popup",n[2]),p(e,"class","overlay-panel-container"),x(e,"padded",n[2]),x(e,"active",n[0])},m(R,N){v(R,e,N),w(e,t),w(e,l),w(e,s),w(s,o),T&&T.m(o,null),w(o,r),E&&E.m(o,null),w(o,a),M&&M.m(o,null),w(s,u),w(s,f),I&&I.m(f,null),n[21](f),w(s,c),w(s,d),P&&P.m(d,null),_=!0,y||(S=[B(t,"click",tt(n[20])),B(f,"scroll",n[22])],y=!0)},p(R,N){n=R,n[4]&&!n[2]?T?(T.p(n,N),N[0]&20&&O(T,1)):(T=Zf(n),T.c(),O(T,1),T.m(o,r)):T&&(re(),D(T,1,1,()=>{T=null}),ae()),E&&E.p&&(!_||N[0]&262144)&&Pt(E,$,n,n[18],_?At($,n[18],N,V2):Nt(n[18]),Kf),n[4]&&n[2]?M?M.p(n,N):(M=Gf(n),M.c(),M.m(o,null)):M&&(M.d(1),M=null),I&&I.p&&(!_||N[0]&262144)&&Pt(I,L,n,n[18],_?At(L,n[18],N,null):Nt(n[18]),null),P&&P.p&&(!_||N[0]&262144)&&Pt(P,A,n,n[18],_?At(A,n[18],N,U2):Nt(n[18]),Yf),(!_||N[0]&258&&m!==(m="overlay-panel "+n[1]+" "+n[8]))&&p(s,"class",m),(!_||N[0]&262)&&x(s,"popup",n[2]),(!_||N[0]&4)&&x(e,"padded",n[2]),(!_||N[0]&1)&&x(e,"active",n[0])},i(R){_||(R&&nt(()=>{_&&(i||(i=ze(t,Ws,{duration:Ki,opacity:0},!0)),i.run(1))}),O(T),O(E,R),O(I,R),O(P,R),R&&nt(()=>{_&&(g&&g.end(1),h=Fb(s,Fn,n[2]?{duration:Ki,y:-10}:{duration:Ki,x:50}),h.start())}),_=!0)},o(R){R&&(i||(i=ze(t,Ws,{duration:Ki,opacity:0},!1)),i.run(0)),D(T),D(E,R),D(I,R),D(P,R),h&&h.invalidate(),R&&(g=mu(s,Fn,n[2]?{duration:Ki,y:10}:{duration:Ki,x:50})),_=!1},d(R){R&&k(e),R&&i&&i.end(),T&&T.d(),E&&E.d(R),M&&M.d(),I&&I.d(R),n[21](null),P&&P.d(R),R&&g&&g.end(),y=!1,De(S)}}}function Zf(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Close"),p(e,"class","overlay-close")},m(o,r){v(o,e,r),i=!0,l||(s=B(e,"click",tt(n[5])),l=!0)},p(o,r){n=o},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Ws,{duration:Ki},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Ws,{duration:Ki},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function Gf(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Close"),p(e,"class","btn btn-sm btn-circle btn-transparent btn-close m-l-auto")},m(l,s){v(l,e,s),t||(i=B(e,"click",tt(n[5])),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function B2(n){let e,t,i,l,s=n[0]&&Jf(n);return{c(){e=b("div"),s&&s.c(),p(e,"class","overlay-panel-wrapper"),p(e,"tabindex","-1")},m(o,r){v(o,e,r),s&&s.m(e,null),n[23](e),t=!0,i||(l=[B(window,"resize",n[10]),B(window,"keydown",n[9])],i=!0)},p(o,r){o[0]?s?(s.p(o,r),r[0]&1&&O(s,1)):(s=Jf(o),s.c(),O(s,1),s.m(e,null)):s&&(re(),D(s,1,1,()=>{s=null}),ae())},i(o){t||(O(s),t=!0)},o(o){D(s),t=!1},d(o){o&&k(e),s&&s.d(),n[23](null),i=!1,De(l)}}}let bl,la=[];function j0(){return bl=bl||document.querySelector(".overlays"),bl||(bl=document.createElement("div"),bl.classList.add("overlays"),document.body.appendChild(bl)),bl}let Ki=150;function Xf(){return 1e3+j0().querySelectorAll(".overlay-panel-container.active").length}function W2(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,{active:o=!1}=e,{popup:r=!1}=e,{overlayClose:a=!0}=e,{btnClose:u=!0}=e,{escClose:f=!0}=e,{beforeOpen:c=void 0}=e,{beforeHide:d=void 0}=e;const m=_t(),h="op_"+z.randomString(10);let g,_,y,S,T="",$=o;function E(){typeof c=="function"&&c()===!1||t(0,o=!0)}function M(){typeof d=="function"&&d()===!1||t(0,o=!1)}function L(){return o}async function I(X){t(17,$=X),X?(y=document.activeElement,m("show"),g==null||g.focus()):(clearTimeout(S),m("hide"),y==null||y.focus()),await fn(),A()}function A(){g&&(o?t(6,g.style.zIndex=Xf(),g):t(6,g.style="",g))}function P(){z.pushUnique(la,h),document.body.classList.add("overlay-active")}function R(){z.removeByValue(la,h),la.length||document.body.classList.remove("overlay-active")}function N(X){o&&f&&X.code=="Escape"&&!z.isInput(X.target)&&g&&g.style.zIndex==Xf()&&(X.preventDefault(),M())}function U(X){o&&j(_)}function j(X,oe){oe&&t(8,T=""),!(!X||S)&&(S=setTimeout(()=>{if(clearTimeout(S),S=null,!X)return;if(X.scrollHeight-X.offsetHeight>0)t(8,T="scrollable");else{t(8,T="");return}X.scrollTop==0?t(8,T+=" scroll-top-reached"):X.scrollTop+X.offsetHeight==X.scrollHeight&&t(8,T+=" scroll-bottom-reached")},100))}Yt(()=>(j0().appendChild(g),()=>{var X;clearTimeout(S),R(),(X=g==null?void 0:g.classList)==null||X.add("hidden"),setTimeout(()=>{g==null||g.remove()},0)}));const V=()=>a?M():!0;function K(X){ie[X?"unshift":"push"](()=>{_=X,t(7,_)})}const J=X=>j(X.target);function ee(X){ie[X?"unshift":"push"](()=>{g=X,t(6,g)})}return n.$$set=X=>{"class"in X&&t(1,s=X.class),"active"in X&&t(0,o=X.active),"popup"in X&&t(2,r=X.popup),"overlayClose"in X&&t(3,a=X.overlayClose),"btnClose"in X&&t(4,u=X.btnClose),"escClose"in X&&t(12,f=X.escClose),"beforeOpen"in X&&t(13,c=X.beforeOpen),"beforeHide"in X&&t(14,d=X.beforeHide),"$$scope"in X&&t(18,l=X.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&131073&&$!=o&&I(o),n.$$.dirty[0]&128&&j(_,!0),n.$$.dirty[0]&64&&g&&A(),n.$$.dirty[0]&1&&(o?P():R())},[o,s,r,a,u,M,g,_,T,N,U,j,f,c,d,E,L,$,l,i,V,K,J,ee]}class ln extends ye{constructor(e){super(),be(this,e,W2,B2,_e,{class:1,active:0,popup:2,overlayClose:3,btnClose:4,escClose:12,beforeOpen:13,beforeHide:14,show:15,hide:5,isActive:16},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[5]}get isActive(){return this.$$.ctx[16]}}const Bl=[];function z0(n,e){return{subscribe:qn(n,e).subscribe}}function qn(n,e=te){let t;const i=new Set;function l(r){if(_e(n,r)&&(n=r,t)){const a=!Bl.length;for(const u of i)u[1](),Bl.push(u,n);if(a){for(let u=0;u{i.delete(u),i.size===0&&t&&(t(),t=null)}}return{set:l,update:s,subscribe:o}}function U0(n,e,t){const i=!Array.isArray(n),l=i?[n]:n;if(!l.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const s=e.length<2;return z0(t,(o,r)=>{let a=!1;const u=[];let f=0,c=te;const d=()=>{if(f)return;c();const h=e(i?u[0]:u,o,r);s?o(h):c=Rt(h)?h:te},m=l.map((h,g)=>uu(h,_=>{u[g]=_,f&=~(1<{f|=1<t(1,i=c));let l,s=!1,o=!1;const r=()=>{t(3,o=!1),l==null||l.hide()},a=async()=>{i!=null&&i.yesCallback&&(t(2,s=!0),await Promise.resolve(i.yesCallback()),t(2,s=!1)),t(3,o=!0),l==null||l.hide()};function u(c){ie[c?"unshift":"push"](()=>{l=c,t(0,l)})}const f=async()=>{!o&&(i!=null&&i.noCallback)&&i.noCallback(),await fn(),t(3,o=!1),V0()};return n.$$.update=()=>{n.$$.dirty&3&&i!=null&&i.text&&(t(3,o=!1),l==null||l.show())},[l,i,s,o,r,a,u,f]}class G2 extends ye{constructor(e){super(),be(this,e,Z2,J2,_e,{})}}function X2(n){let e;return{c(){e=b("textarea"),p(e,"id",n[0]),Sk(e,"visibility","hidden")},m(t,i){v(t,e,i),n[15](e)},p(t,i){i&1&&p(e,"id",t[0])},d(t){t&&k(e),n[15](null)}}}function Q2(n){let e;return{c(){e=b("div"),p(e,"id",n[0])},m(t,i){v(t,e,i),n[14](e)},p(t,i){i&1&&p(e,"id",t[0])},d(t){t&&k(e),n[14](null)}}}function x2(n){let e;function t(s,o){return s[1]?Q2:X2}let i=t(n),l=i(n);return{c(){e=b("div"),l.c(),p(e,"class",n[2])},m(s,o){v(s,e,o),l.m(e,null),n[16](e)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e,null))),o&4&&p(e,"class",s[2])},i:te,o:te,d(s){s&&k(e),l.d(),n[16](null)}}}function ew(){let n={listeners:[],scriptLoaded:!1,injected:!1};function e(i,l,s){n.injected=!0;const o=i.createElement("script");o.referrerPolicy="origin",o.type="application/javascript",o.src=l,o.onload=()=>{s()},i.head&&i.head.appendChild(o)}function t(i,l,s){n.scriptLoaded?s():(n.listeners.push(s),n.injected||e(i,l,()=>{n.listeners.forEach(o=>o()),n.scriptLoaded=!0}))}return{load:t}}let tw=ew();function sa(){return window&&window.tinymce?window.tinymce:null}function nw(n,e,t){let{id:i="tinymce_svelte"+z.randomString(7)}=e,{inline:l=void 0}=e,{disabled:s=!1}=e,{scriptSrc:o="./libs/tinymce/tinymce.min.js"}=e,{conf:r={}}=e,{modelEvents:a="change input undo redo"}=e,{value:u=""}=e,{text:f=""}=e,{cssClass:c="tinymce-wrapper"}=e;const d=["Activate","AddUndo","BeforeAddUndo","BeforeExecCommand","BeforeGetContent","BeforeRenderUI","BeforeSetContent","BeforePaste","Blur","Change","ClearUndos","Click","ContextMenu","Copy","Cut","Dblclick","Deactivate","Dirty","Drag","DragDrop","DragEnd","DragGesture","DragOver","Drop","ExecCommand","Focus","FocusIn","FocusOut","GetContent","Hide","Init","KeyDown","KeyPress","KeyUp","LoadContent","MouseDown","MouseEnter","MouseLeave","MouseMove","MouseOut","MouseOver","MouseUp","NodeChange","ObjectResizeStart","ObjectResized","ObjectSelected","Paste","PostProcess","PostRender","PreProcess","ProgressState","Redo","Remove","Reset","ResizeEditor","SaveContent","SelectionChange","SetAttrib","SetContent","Show","Submit","Undo","VisualAid"],m=(I,A)=>{d.forEach(P=>{I.on(P,R=>{A(P.toLowerCase(),{eventName:P,event:R,editor:I})})})};let h,g,_,y=u,S=s;const T=_t();function $(){const I={...r,target:g,inline:l!==void 0?l:r.inline!==void 0?r.inline:!1,readonly:s,setup:A=>{t(11,_=A),A.on("init",()=>{A.setContent(u),A.on(a,()=>{t(12,y=A.getContent()),y!==u&&(t(5,u=y),t(6,f=A.getContent({format:"text"})))})}),m(A,T),typeof r.setup=="function"&&r.setup(A)}};t(4,g.style.visibility="",g),sa().init(I)}Yt(()=>(sa()!==null?$():tw.load(h.ownerDocument,o,()=>{h&&$()}),()=>{var I,A;try{_&&((I=_.dom)==null||I.unbind(document),(A=sa())==null||A.remove(_))}catch{}}));function E(I){ie[I?"unshift":"push"](()=>{g=I,t(4,g)})}function M(I){ie[I?"unshift":"push"](()=>{g=I,t(4,g)})}function L(I){ie[I?"unshift":"push"](()=>{h=I,t(3,h)})}return n.$$set=I=>{"id"in I&&t(0,i=I.id),"inline"in I&&t(1,l=I.inline),"disabled"in I&&t(7,s=I.disabled),"scriptSrc"in I&&t(8,o=I.scriptSrc),"conf"in I&&t(9,r=I.conf),"modelEvents"in I&&t(10,a=I.modelEvents),"value"in I&&t(5,u=I.value),"text"in I&&t(6,f=I.text),"cssClass"in I&&t(2,c=I.cssClass)},n.$$.update=()=>{var I;if(n.$$.dirty&14496)try{_&&y!==u&&(_.setContent(u),t(6,f=_.getContent({format:"text"}))),_&&s!==S&&(t(13,S=s),typeof((I=_.mode)==null?void 0:I.set)=="function"?_.mode.set(s?"readonly":"design"):_.setMode(s?"readonly":"design"))}catch(A){console.warn("TinyMCE reactive error:",A)}},[i,l,c,h,g,u,f,s,o,r,a,_,y,S,E,M,L]}class Tu extends ye{constructor(e){super(),be(this,e,nw,x2,_e,{id:0,inline:1,disabled:7,scriptSrc:8,conf:9,modelEvents:10,value:5,text:6,cssClass:2})}}function iw(n,{from:e,to:t},i={}){const l=getComputedStyle(n),s=l.transform==="none"?"":l.transform,[o,r]=l.transformOrigin.split(" ").map(parseFloat),a=e.left+e.width*o/t.width-(t.left+o),u=e.top+e.height*r/t.height-(t.top+r),{delay:f=0,duration:c=m=>Math.sqrt(m)*120,easing:d=Pr}=i;return{delay:f,duration:Rt(c)?c(Math.sqrt(a*a+u*u)):c,easing:d,css:(m,h)=>{const g=h*a,_=h*u,y=m+h*e.width/t.width,S=m+h*e.height/t.height;return`transform: ${s} translate(${g}px, ${_}px) scale(${y}, ${S});`}}}const Nr=qn([]);function Ys(n,e=4e3){return Rr(n,"info",e)}function tn(n,e=3e3){return Rr(n,"success",e)}function $i(n,e=4500){return Rr(n,"error",e)}function lw(n,e=4500){return Rr(n,"warning",e)}function Rr(n,e,t){t=t||4e3;const i={message:n,type:e,duration:t,timeout:setTimeout(()=>{B0(i)},t)};Nr.update(l=>($u(l,i.message),z.pushOrReplaceByKey(l,i,"message"),l))}function B0(n){Nr.update(e=>($u(e,n),e))}function Ds(){Nr.update(n=>{for(let e of n)$u(n,e);return[]})}function $u(n,e){let t;typeof e=="string"?t=z.findByKey(n,"message",e):t=e,t&&(clearTimeout(t.timeout),z.removeByKey(n,"message",t.message))}function Qf(n,e,t){const i=n.slice();return i[2]=e[t],i}function sw(n){let e;return{c(){e=b("i"),p(e,"class","ri-alert-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function ow(n){let e;return{c(){e=b("i"),p(e,"class","ri-error-warning-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function rw(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function aw(n){let e;return{c(){e=b("i"),p(e,"class","ri-information-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function xf(n,e){let t,i,l,s,o=e[2].message+"",r,a,u,f,c,d,m,h=te,g,_,y;function S(M,L){return M[2].type==="info"?aw:M[2].type==="success"?rw:M[2].type==="warning"?ow:sw}let T=S(e),$=T(e);function E(){return e[1](e[2])}return{key:n,first:null,c(){t=b("div"),i=b("div"),$.c(),l=C(),s=b("div"),r=Y(o),a=C(),u=b("button"),u.innerHTML='',f=C(),p(i,"class","icon"),p(s,"class","content"),p(u,"type","button"),p(u,"class","close"),p(t,"class","alert txt-break"),x(t,"alert-info",e[2].type=="info"),x(t,"alert-success",e[2].type=="success"),x(t,"alert-danger",e[2].type=="error"),x(t,"alert-warning",e[2].type=="warning"),this.first=t},m(M,L){v(M,t,L),w(t,i),$.m(i,null),w(t,l),w(t,s),w(s,r),w(t,a),w(t,u),w(t,f),g=!0,_||(y=B(u,"click",tt(E)),_=!0)},p(M,L){e=M,T!==(T=S(e))&&($.d(1),$=T(e),$&&($.c(),$.m(i,null))),(!g||L&1)&&o!==(o=e[2].message+"")&&ue(r,o),(!g||L&1)&&x(t,"alert-info",e[2].type=="info"),(!g||L&1)&&x(t,"alert-success",e[2].type=="success"),(!g||L&1)&&x(t,"alert-danger",e[2].type=="error"),(!g||L&1)&&x(t,"alert-warning",e[2].type=="warning")},r(){m=t.getBoundingClientRect()},f(){Ek(t),h(),Pb(t,m)},a(){h(),h=Ok(t,m,iw,{duration:150})},i(M){g||(M&&nt(()=>{g&&(d&&d.end(1),c=Fb(t,vt,{duration:150}),c.start())}),g=!0)},o(M){c&&c.invalidate(),M&&(d=mu(t,Ws,{duration:150})),g=!1},d(M){M&&k(t),$.d(),M&&d&&d.end(),_=!1,y()}}}function uw(n){let e,t=[],i=new Map,l,s=pe(n[0]);const o=r=>r[2].message;for(let r=0;rt(0,i=s)),[i,s=>B0(s)]}class cw extends ye{constructor(e){super(),be(this,e,fw,uw,_e,{})}}function ec(n){let e,t,i;const l=n[18].default,s=Lt(l,n,n[17],null);return{c(){e=b("div"),s&&s.c(),p(e,"class",n[1]),x(e,"active",n[0])},m(o,r){v(o,e,r),s&&s.m(e,null),n[19](e),i=!0},p(o,r){s&&s.p&&(!i||r[0]&131072)&&Pt(s,l,o,o[17],i?At(l,o[17],r,null):Nt(o[17]),null),(!i||r[0]&2)&&p(e,"class",o[1]),(!i||r[0]&3)&&x(e,"active",o[0])},i(o){i||(O(s,o),o&&nt(()=>{i&&(t||(t=ze(e,Fn,{duration:150,y:3},!0)),t.run(1))}),i=!0)},o(o){D(s,o),o&&(t||(t=ze(e,Fn,{duration:150,y:3},!1)),t.run(0)),i=!1},d(o){o&&k(e),s&&s.d(o),n[19](null),o&&t&&t.end()}}}function dw(n){let e,t,i,l,s=n[0]&&ec(n);return{c(){e=b("div"),s&&s.c(),p(e,"class","toggler-container"),p(e,"tabindex","-1"),p(e,"role","menu")},m(o,r){v(o,e,r),s&&s.m(e,null),n[20](e),t=!0,i||(l=[B(window,"click",n[7]),B(window,"mousedown",n[6]),B(window,"keydown",n[5]),B(window,"focusin",n[4])],i=!0)},p(o,r){o[0]?s?(s.p(o,r),r[0]&1&&O(s,1)):(s=ec(o),s.c(),O(s,1),s.m(e,null)):s&&(re(),D(s,1,1,()=>{s=null}),ae())},i(o){t||(O(s),t=!0)},o(o){D(s),t=!1},d(o){o&&k(e),s&&s.d(),n[20](null),i=!1,De(l)}}}function pw(n,e,t){let{$$slots:i={},$$scope:l}=e,{trigger:s=void 0}=e,{active:o=!1}=e,{escClose:r=!0}=e,{autoScroll:a=!0}=e,{closableClass:u="closable"}=e,{class:f=""}=e,c,d,m,h,g,_=!1;const y=_t();function S(X=0){o&&(clearTimeout(g),g=setTimeout(T,X))}function T(){o&&(t(0,o=!1),_=!1,clearTimeout(h),clearTimeout(g))}function $(){clearTimeout(g),clearTimeout(h),!o&&(t(0,o=!0),m!=null&&m.contains(c)||c==null||c.focus(),h=setTimeout(()=>{a&&(d!=null&&d.scrollIntoViewIfNeeded?d==null||d.scrollIntoViewIfNeeded():d!=null&&d.scrollIntoView&&(d==null||d.scrollIntoView({behavior:"smooth",block:"nearest"})))},180))}function E(){o?T():$()}function M(X){return!c||X.classList.contains(u)||c.contains(X)&&X.closest&&X.closest("."+u)}function L(X){I(),c==null||c.addEventListener("click",A),c==null||c.addEventListener("keydown",P),t(16,m=X||(c==null?void 0:c.parentNode)),m==null||m.addEventListener("click",R),m==null||m.addEventListener("keydown",N)}function I(){clearTimeout(h),clearTimeout(g),c==null||c.removeEventListener("click",A),c==null||c.removeEventListener("keydown",P),m==null||m.removeEventListener("click",R),m==null||m.removeEventListener("keydown",N)}function A(X){X.stopPropagation(),M(X.target)&&T()}function P(X){(X.code==="Enter"||X.code==="Space")&&(X.stopPropagation(),M(X.target)&&S(150))}function R(X){X.preventDefault(),X.stopPropagation(),E()}function N(X){(X.code==="Enter"||X.code==="Space")&&(X.preventDefault(),X.stopPropagation(),E())}function U(X){o&&!(m!=null&&m.contains(X.target))&&!(c!=null&&c.contains(X.target))&&E()}function j(X){o&&r&&X.code==="Escape"&&(X.preventDefault(),T())}function V(X){o&&(_=!(c!=null&&c.contains(X.target)))}function K(X){var oe;o&&_&&!(c!=null&&c.contains(X.target))&&!(m!=null&&m.contains(X.target))&&!((oe=X.target)!=null&&oe.closest(".flatpickr-calendar"))&&T()}Yt(()=>(L(),()=>I()));function J(X){ie[X?"unshift":"push"](()=>{d=X,t(3,d)})}function ee(X){ie[X?"unshift":"push"](()=>{c=X,t(2,c)})}return n.$$set=X=>{"trigger"in X&&t(8,s=X.trigger),"active"in X&&t(0,o=X.active),"escClose"in X&&t(9,r=X.escClose),"autoScroll"in X&&t(10,a=X.autoScroll),"closableClass"in X&&t(11,u=X.closableClass),"class"in X&&t(1,f=X.class),"$$scope"in X&&t(17,l=X.$$scope)},n.$$.update=()=>{var X,oe;n.$$.dirty[0]&260&&c&&L(s),n.$$.dirty[0]&65537&&(o?((X=m==null?void 0:m.classList)==null||X.add("active"),m==null||m.setAttribute("aria-expanded",!0),y("show")):((oe=m==null?void 0:m.classList)==null||oe.remove("active"),m==null||m.setAttribute("aria-expanded",!1),y("hide")))},[o,f,c,d,U,j,V,K,s,r,a,u,S,T,$,E,m,l,i,J,ee]}class Hn extends ye{constructor(e){super(),be(this,e,pw,dw,_e,{trigger:8,active:0,escClose:9,autoScroll:10,closableClass:11,class:1,hideWithDelay:12,hide:13,show:14,toggle:15},null,[-1,-1])}get hideWithDelay(){return this.$$.ctx[12]}get hide(){return this.$$.ctx[13]}get show(){return this.$$.ctx[14]}get toggle(){return this.$$.ctx[15]}}const cn=qn(""),_r=qn(""),Dl=qn(!1),Sn=qn({});function Wt(n){Sn.set(n||{})}function fi(n){Sn.update(e=>(z.deleteByPath(e,n),e))}const Fr=qn({});function tc(n){Fr.set(n||{})}class Rn extends Error{constructor(e){var t,i,l,s;super("ClientResponseError"),this.url="",this.status=0,this.response={},this.isAbort=!1,this.originalError=null,Object.setPrototypeOf(this,Rn.prototype),e!==null&&typeof e=="object"&&(this.url=typeof e.url=="string"?e.url:"",this.status=typeof e.status=="number"?e.status:0,this.isAbort=!!e.isAbort,this.originalError=e.originalError,e.response!==null&&typeof e.response=="object"?this.response=e.response:e.data!==null&&typeof e.data=="object"?this.response=e.data:this.response={}),this.originalError||e instanceof Rn||(this.originalError=e),typeof DOMException<"u"&&e instanceof DOMException&&(this.isAbort=!0),this.name="ClientResponseError "+this.status,this.message=(t=this.response)==null?void 0:t.message,this.message||(this.isAbort?this.message="The request was autocancelled. You can find more info in https://github.com/pocketbase/js-sdk#auto-cancellation.":(s=(l=(i=this.originalError)==null?void 0:i.cause)==null?void 0:l.message)!=null&&s.includes("ECONNREFUSED ::1")?this.message="Failed to connect to the PocketBase server. Try changing the SDK URL from localhost to 127.0.0.1 (https://github.com/pocketbase/js-sdk/issues/21).":this.message="Something went wrong while processing your request.")}get data(){return this.response}toJSON(){return{...this}}}const Do=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function mw(n,e){const t={};if(typeof n!="string")return t;const i=Object.assign({},{}).decode||hw;let l=0;for(;l0&&(!t.exp||t.exp-e>Date.now()/1e3))}W0=typeof atob!="function"||gw?n=>{let e=String(n).replace(/=+$/,"");if(e.length%4==1)throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");for(var t,i,l=0,s=0,o="";i=e.charAt(s++);~i&&(t=l%4?64*t+i:i,l++%4)?o+=String.fromCharCode(255&t>>(-2*l&6)):0)i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(i);return o}:atob;const ic="pb_auth";class Ou{constructor(){this.baseToken="",this.baseModel=null,this._onChangeCallbacks=[]}get token(){return this.baseToken}get record(){return this.baseModel}get model(){return this.baseModel}get isValid(){return!Cu(this.token)}get isAdmin(){return xl(this.token).type==="admin"}get isAuthRecord(){return xl(this.token).type==="authRecord"}save(e,t){this.baseToken=e||"",this.baseModel=t||null,this.triggerChange()}clear(){this.baseToken="",this.baseModel=null,this.triggerChange()}loadFromCookie(e,t=ic){const i=mw(e||"")[t]||"";let l={};try{l=JSON.parse(i),(typeof l===null||typeof l!="object"||Array.isArray(l))&&(l={})}catch{}this.save(l.token||"",l.record||l.model||null)}exportToCookie(e,t=ic){var a,u;const i={secure:!0,sameSite:!0,httpOnly:!0,path:"/"},l=xl(this.token);i.expires=l!=null&&l.exp?new Date(1e3*l.exp):new Date("1970-01-01"),e=Object.assign({},i,e);const s={token:this.token,record:this.record?JSON.parse(JSON.stringify(this.record)):null};let o=nc(t,JSON.stringify(s),e);const r=typeof Blob<"u"?new Blob([o]).size:o.length;if(s.record&&r>4096){s.record={id:(a=s.record)==null?void 0:a.id,email:(u=s.record)==null?void 0:u.email};const f=["collectionId","collectionName","verified"];for(const c in this.record)f.includes(c)&&(s.record[c]=this.record[c]);o=nc(t,JSON.stringify(s),e)}return o}onChange(e,t=!1){return this._onChangeCallbacks.push(e),t&&e(this.token,this.record),()=>{for(let i=this._onChangeCallbacks.length-1;i>=0;i--)if(this._onChangeCallbacks[i]==e)return delete this._onChangeCallbacks[i],void this._onChangeCallbacks.splice(i,1)}}triggerChange(){for(const e of this._onChangeCallbacks)e&&e(this.token,this.record)}}class Y0 extends Ou{constructor(e="pocketbase_auth"){super(),this.storageFallback={},this.storageKey=e,this._bindStorageEvent()}get token(){return(this._storageGet(this.storageKey)||{}).token||""}get record(){const e=this._storageGet(this.storageKey)||{};return e.record||e.model||null}get model(){return this.record}save(e,t){this._storageSet(this.storageKey,{token:e,record:t}),super.save(e,t)}clear(){this._storageRemove(this.storageKey),super.clear()}_storageGet(e){if(typeof window<"u"&&(window!=null&&window.localStorage)){const t=window.localStorage.getItem(e)||"";try{return JSON.parse(t)}catch{return t}}return this.storageFallback[e]}_storageSet(e,t){if(typeof window<"u"&&(window!=null&&window.localStorage)){let i=t;typeof t!="string"&&(i=JSON.stringify(t)),window.localStorage.setItem(e,i)}else this.storageFallback[e]=t}_storageRemove(e){var t;typeof window<"u"&&(window!=null&&window.localStorage)&&((t=window.localStorage)==null||t.removeItem(e)),delete this.storageFallback[e]}_bindStorageEvent(){typeof window<"u"&&(window!=null&&window.localStorage)&&window.addEventListener&&window.addEventListener("storage",e=>{if(e.key!=this.storageKey)return;const t=this._storageGet(this.storageKey)||{};super.save(t.token||"",t.record||t.model||null)})}}class ol{constructor(e){this.client=e}}class bw extends ol{async getAll(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/settings",e)}async update(e,t){return t=Object.assign({method:"PATCH",body:e},t),this.client.send("/api/settings",t)}async testS3(e="storage",t){return t=Object.assign({method:"POST",body:{filesystem:e}},t),this.client.send("/api/settings/test/s3",t).then(()=>!0)}async testEmail(e,t,i,l){return l=Object.assign({method:"POST",body:{email:t,template:i,collection:e}},l),this.client.send("/api/settings/test/email",l).then(()=>!0)}async generateAppleClientSecret(e,t,i,l,s,o){return o=Object.assign({method:"POST",body:{clientId:e,teamId:t,keyId:i,privateKey:l,duration:s}},o),this.client.send("/api/settings/apple/generate-client-secret",o)}}const yw=["requestKey","$cancelKey","$autoCancel","fetch","headers","body","query","params","cache","credentials","headers","integrity","keepalive","method","mode","redirect","referrer","referrerPolicy","signal","window"];function Eu(n){if(n){n.query=n.query||{};for(let e in n)yw.includes(e)||(n.query[e]=n[e],delete n[e])}}function K0(n){const e=[];for(const t in n){if(n[t]===null)continue;const i=n[t],l=encodeURIComponent(t);if(Array.isArray(i))for(const s of i)e.push(l+"="+encodeURIComponent(s));else i instanceof Date?e.push(l+"="+encodeURIComponent(i.toISOString())):typeof i!==null&&typeof i=="object"?e.push(l+"="+encodeURIComponent(JSON.stringify(i))):e.push(l+"="+encodeURIComponent(i))}return e.join("&")}class J0 extends ol{constructor(){super(...arguments),this.clientId="",this.eventSource=null,this.subscriptions={},this.lastSentSubscriptions=[],this.maxConnectTimeout=15e3,this.reconnectAttempts=0,this.maxReconnectAttempts=1/0,this.predefinedReconnectIntervals=[200,300,500,1e3,1200,1500,2e3],this.pendingConnects=[]}get isConnected(){return!!this.eventSource&&!!this.clientId&&!this.pendingConnects.length}async subscribe(e,t,i){var o;if(!e)throw new Error("topic must be set.");let l=e;if(i){Eu(i=Object.assign({},i));const r="options="+encodeURIComponent(JSON.stringify({query:i.query,headers:i.headers}));l+=(l.includes("?")?"&":"?")+r}const s=function(r){const a=r;let u;try{u=JSON.parse(a==null?void 0:a.data)}catch{}t(u||{})};return this.subscriptions[l]||(this.subscriptions[l]=[]),this.subscriptions[l].push(s),this.isConnected?this.subscriptions[l].length===1?await this.submitSubscriptions():(o=this.eventSource)==null||o.addEventListener(l,s):await this.connect(),async()=>this.unsubscribeByTopicAndListener(e,s)}async unsubscribe(e){var i;let t=!1;if(e){const l=this.getSubscriptionsByTopic(e);for(let s in l)if(this.hasSubscriptionListeners(s)){for(let o of this.subscriptions[s])(i=this.eventSource)==null||i.removeEventListener(s,o);delete this.subscriptions[s],t||(t=!0)}}else this.subscriptions={};this.hasSubscriptionListeners()?t&&await this.submitSubscriptions():this.disconnect()}async unsubscribeByPrefix(e){var i;let t=!1;for(let l in this.subscriptions)if((l+"?").startsWith(e)){t=!0;for(let s of this.subscriptions[l])(i=this.eventSource)==null||i.removeEventListener(l,s);delete this.subscriptions[l]}t&&(this.hasSubscriptionListeners()?await this.submitSubscriptions():this.disconnect())}async unsubscribeByTopicAndListener(e,t){var s;let i=!1;const l=this.getSubscriptionsByTopic(e);for(let o in l){if(!Array.isArray(this.subscriptions[o])||!this.subscriptions[o].length)continue;let r=!1;for(let a=this.subscriptions[o].length-1;a>=0;a--)this.subscriptions[o][a]===t&&(r=!0,delete this.subscriptions[o][a],this.subscriptions[o].splice(a,1),(s=this.eventSource)==null||s.removeEventListener(o,t));r&&(this.subscriptions[o].length||delete this.subscriptions[o],i||this.hasSubscriptionListeners(o)||(i=!0))}this.hasSubscriptionListeners()?i&&await this.submitSubscriptions():this.disconnect()}hasSubscriptionListeners(e){var t,i;if(this.subscriptions=this.subscriptions||{},e)return!!((t=this.subscriptions[e])!=null&&t.length);for(let l in this.subscriptions)if((i=this.subscriptions[l])!=null&&i.length)return!0;return!1}async submitSubscriptions(){if(this.clientId)return this.addAllSubscriptionListeners(),this.lastSentSubscriptions=this.getNonEmptySubscriptionKeys(),this.client.send("/api/realtime",{method:"POST",body:{clientId:this.clientId,subscriptions:this.lastSentSubscriptions},requestKey:this.getSubscriptionsCancelKey()}).catch(e=>{if(!(e!=null&&e.isAbort))throw e})}getSubscriptionsCancelKey(){return"realtime_"+this.clientId}getSubscriptionsByTopic(e){const t={};e=e.includes("?")?e:e+"?";for(let i in this.subscriptions)(i+"?").startsWith(e)&&(t[i]=this.subscriptions[i]);return t}getNonEmptySubscriptionKeys(){const e=[];for(let t in this.subscriptions)this.subscriptions[t].length&&e.push(t);return e}addAllSubscriptionListeners(){if(this.eventSource){this.removeAllSubscriptionListeners();for(let e in this.subscriptions)for(let t of this.subscriptions[e])this.eventSource.addEventListener(e,t)}}removeAllSubscriptionListeners(){if(this.eventSource)for(let e in this.subscriptions)for(let t of this.subscriptions[e])this.eventSource.removeEventListener(e,t)}async connect(){if(!(this.reconnectAttempts>0))return new Promise((e,t)=>{this.pendingConnects.push({resolve:e,reject:t}),this.pendingConnects.length>1||this.initConnect()})}initConnect(){this.disconnect(!0),clearTimeout(this.connectTimeoutId),this.connectTimeoutId=setTimeout(()=>{this.connectErrorHandler(new Error("EventSource connect took too long."))},this.maxConnectTimeout),this.eventSource=new EventSource(this.client.buildURL("/api/realtime")),this.eventSource.onerror=e=>{this.connectErrorHandler(new Error("Failed to establish realtime connection."))},this.eventSource.addEventListener("PB_CONNECT",e=>{const t=e;this.clientId=t==null?void 0:t.lastEventId,this.submitSubscriptions().then(async()=>{let i=3;for(;this.hasUnsentSubscriptions()&&i>0;)i--,await this.submitSubscriptions()}).then(()=>{for(let l of this.pendingConnects)l.resolve();this.pendingConnects=[],this.reconnectAttempts=0,clearTimeout(this.reconnectTimeoutId),clearTimeout(this.connectTimeoutId);const i=this.getSubscriptionsByTopic("PB_CONNECT");for(let l in i)for(let s of i[l])s(e)}).catch(i=>{this.clientId="",this.connectErrorHandler(i)})})}hasUnsentSubscriptions(){const e=this.getNonEmptySubscriptionKeys();if(e.length!=this.lastSentSubscriptions.length)return!0;for(const t of e)if(!this.lastSentSubscriptions.includes(t))return!0;return!1}connectErrorHandler(e){if(clearTimeout(this.connectTimeoutId),clearTimeout(this.reconnectTimeoutId),!this.clientId&&!this.reconnectAttempts||this.reconnectAttempts>this.maxReconnectAttempts){for(let i of this.pendingConnects)i.reject(new Rn(e));return this.pendingConnects=[],void this.disconnect()}this.disconnect(!0);const t=this.predefinedReconnectIntervals[this.reconnectAttempts]||this.predefinedReconnectIntervals[this.predefinedReconnectIntervals.length-1];this.reconnectAttempts++,this.reconnectTimeoutId=setTimeout(()=>{this.initConnect()},t)}disconnect(e=!1){var t;if(clearTimeout(this.connectTimeoutId),clearTimeout(this.reconnectTimeoutId),this.removeAllSubscriptionListeners(),this.client.cancelRequest(this.getSubscriptionsCancelKey()),(t=this.eventSource)==null||t.close(),this.eventSource=null,this.clientId="",!e){this.reconnectAttempts=0;for(let i of this.pendingConnects)i.resolve();this.pendingConnects=[]}}}class Z0 extends ol{decode(e){return e}async getFullList(e,t){if(typeof e=="number")return this._getFullList(e,t);let i=500;return(t=Object.assign({},e,t)).batch&&(i=t.batch,delete t.batch),this._getFullList(i,t)}async getList(e=1,t=30,i){return(i=Object.assign({method:"GET"},i)).query=Object.assign({page:e,perPage:t},i.query),this.client.send(this.baseCrudPath,i).then(l=>{var s;return l.items=((s=l.items)==null?void 0:s.map(o=>this.decode(o)))||[],l})}async getFirstListItem(e,t){return(t=Object.assign({requestKey:"one_by_filter_"+this.baseCrudPath+"_"+e},t)).query=Object.assign({filter:e,skipTotal:1},t.query),this.getList(1,1,t).then(i=>{var l;if(!((l=i==null?void 0:i.items)!=null&&l.length))throw new Rn({status:404,response:{code:404,message:"The requested resource wasn't found.",data:{}}});return i.items[0]})}async getOne(e,t){if(!e)throw new Rn({url:this.client.buildURL(this.baseCrudPath+"/"),status:404,response:{code:404,message:"Missing required record id.",data:{}}});return t=Object.assign({method:"GET"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),t).then(i=>this.decode(i))}async create(e,t){return t=Object.assign({method:"POST",body:e},t),this.client.send(this.baseCrudPath,t).then(i=>this.decode(i))}async update(e,t,i){return i=Object.assign({method:"PATCH",body:t},i),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),i).then(l=>this.decode(l))}async delete(e,t){return t=Object.assign({method:"DELETE"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),t).then(()=>!0)}_getFullList(e=500,t){(t=t||{}).query=Object.assign({skipTotal:1},t.query);let i=[],l=async s=>this.getList(s,e||500,t).then(o=>{const r=o.items;return i=i.concat(r),r.length==o.perPage?l(s+1):i});return l(1)}}function Wi(n,e,t,i){const l=i!==void 0;return l||t!==void 0?l?(console.warn(n),e.body=Object.assign({},e.body,t),e.query=Object.assign({},e.query,i),e):Object.assign(e,t):e}function oa(n){var e;(e=n._resetAutoRefresh)==null||e.call(n)}class kw extends Z0{constructor(e,t){super(e),this.collectionIdOrName=t}get baseCrudPath(){return this.baseCollectionPath+"/records"}get baseCollectionPath(){return"/api/collections/"+encodeURIComponent(this.collectionIdOrName)}get isSuperusers(){return this.collectionIdOrName=="_superusers"||this.collectionIdOrName=="_pbc_2773867675"}async subscribe(e,t,i){if(!e)throw new Error("Missing topic.");if(!t)throw new Error("Missing subscription callback.");return this.client.realtime.subscribe(this.collectionIdOrName+"/"+e,t,i)}async unsubscribe(e){return e?this.client.realtime.unsubscribe(this.collectionIdOrName+"/"+e):this.client.realtime.unsubscribeByPrefix(this.collectionIdOrName)}async getFullList(e,t){if(typeof e=="number")return super.getFullList(e,t);const i=Object.assign({},e,t);return super.getFullList(i)}async getList(e=1,t=30,i){return super.getList(e,t,i)}async getFirstListItem(e,t){return super.getFirstListItem(e,t)}async getOne(e,t){return super.getOne(e,t)}async create(e,t){return super.create(e,t)}async update(e,t,i){return super.update(e,t,i).then(l=>{var s,o,r;return((s=this.client.authStore.record)==null?void 0:s.id)!==(l==null?void 0:l.id)||((o=this.client.authStore.record)==null?void 0:o.collectionId)!==this.collectionIdOrName&&((r=this.client.authStore.record)==null?void 0:r.collectionName)!==this.collectionIdOrName||this.client.authStore.save(this.client.authStore.token,l),l})}async delete(e,t){return super.delete(e,t).then(i=>{var l,s,o;return!i||((l=this.client.authStore.record)==null?void 0:l.id)!==e||((s=this.client.authStore.record)==null?void 0:s.collectionId)!==this.collectionIdOrName&&((o=this.client.authStore.record)==null?void 0:o.collectionName)!==this.collectionIdOrName||this.client.authStore.clear(),i})}authResponse(e){const t=this.decode((e==null?void 0:e.record)||{});return this.client.authStore.save(e==null?void 0:e.token,t),Object.assign({},e,{token:(e==null?void 0:e.token)||"",record:t})}async listAuthMethods(e){return e=Object.assign({method:"GET",fields:"mfa,otp,password,oauth2"},e),this.client.send(this.baseCollectionPath+"/auth-methods",e)}async authWithPassword(e,t,i){let l;i=Object.assign({method:"POST",body:{identity:e,password:t}},i),this.isSuperusers&&(l=i.autoRefreshThreshold,delete i.autoRefreshThreshold,i.autoRefresh||oa(this.client));let s=await this.client.send(this.baseCollectionPath+"/auth-with-password",i);return s=this.authResponse(s),l&&this.isSuperusers&&function(r,a,u,f){oa(r);const c=r.beforeSend,d=r.authStore.record,m=r.authStore.onChange((h,g)=>{(!h||(g==null?void 0:g.id)!=(d==null?void 0:d.id)||(g!=null&&g.collectionId||d!=null&&d.collectionId)&&(g==null?void 0:g.collectionId)!=(d==null?void 0:d.collectionId))&&oa(r)});r._resetAutoRefresh=function(){m(),r.beforeSend=c,delete r._resetAutoRefresh},r.beforeSend=async(h,g)=>{var T;const _=r.authStore.token;if((T=g.query)!=null&&T.autoRefresh)return c?c(h,g):{url:h,sendOptions:g};let y=r.authStore.isValid;if(y&&Cu(r.authStore.token,a))try{await u()}catch{y=!1}y||await f();const S=g.headers||{};for(let $ in S)if($.toLowerCase()=="authorization"&&_==S[$]&&r.authStore.token){S[$]=r.authStore.token;break}return g.headers=S,c?c(h,g):{url:h,sendOptions:g}}}(this.client,l,()=>this.authRefresh({autoRefresh:!0}),()=>this.authWithPassword(e,t,Object.assign({autoRefresh:!0},i))),s}async authWithOAuth2Code(e,t,i,l,s,o,r){let a={method:"POST",body:{provider:e,code:t,codeVerifier:i,redirectURL:l,createData:s}};return a=Wi("This form of authWithOAuth2Code(provider, code, codeVerifier, redirectURL, createData?, body?, query?) is deprecated. Consider replacing it with authWithOAuth2Code(provider, code, codeVerifier, redirectURL, createData?, options?).",a,o,r),this.client.send(this.baseCollectionPath+"/auth-with-oauth2",a).then(u=>this.authResponse(u))}authWithOAuth2(...e){if(e.length>1||typeof(e==null?void 0:e[0])=="string")return console.warn("PocketBase: This form of authWithOAuth2() is deprecated and may get removed in the future. Please replace with authWithOAuth2Code() OR use the authWithOAuth2() realtime form as shown in https://pocketbase.io/docs/authentication/#oauth2-integration."),this.authWithOAuth2Code((e==null?void 0:e[0])||"",(e==null?void 0:e[1])||"",(e==null?void 0:e[2])||"",(e==null?void 0:e[3])||"",(e==null?void 0:e[4])||{},(e==null?void 0:e[5])||{},(e==null?void 0:e[6])||{});const t=(e==null?void 0:e[0])||{};let i=null;t.urlCallback||(i=lc(void 0));const l=new J0(this.client);function s(){i==null||i.close(),l.unsubscribe()}const o={},r=t.requestKey;return r&&(o.requestKey=r),this.listAuthMethods(o).then(a=>{var d;const u=a.oauth2.providers.find(m=>m.name===t.provider);if(!u)throw new Rn(new Error(`Missing or invalid provider "${t.provider}".`));const f=this.client.buildURL("/api/oauth2-redirect"),c=r?(d=this.client.cancelControllers)==null?void 0:d[r]:void 0;return c&&(c.signal.onabort=()=>{s()}),new Promise(async(m,h)=>{var g;try{await l.subscribe("@oauth2",async T=>{var E;const $=l.clientId;try{if(!T.state||$!==T.state)throw new Error("State parameters don't match.");if(T.error||!T.code)throw new Error("OAuth2 redirect error or missing code: "+T.error);const M=Object.assign({},t);delete M.provider,delete M.scopes,delete M.createData,delete M.urlCallback,(E=c==null?void 0:c.signal)!=null&&E.onabort&&(c.signal.onabort=null);const L=await this.authWithOAuth2Code(u.name,T.code,u.codeVerifier,f,t.createData,M);m(L)}catch(M){h(new Rn(M))}s()});const _={state:l.clientId};(g=t.scopes)!=null&&g.length&&(_.scope=t.scopes.join(" "));const y=this._replaceQueryParams(u.authURL+f,_);await(t.urlCallback||function(T){i?i.location.href=T:i=lc(T)})(y)}catch(_){s(),h(new Rn(_))}})}).catch(a=>{throw s(),a})}async authRefresh(e,t){let i={method:"POST"};return i=Wi("This form of authRefresh(body?, query?) is deprecated. Consider replacing it with authRefresh(options?).",i,e,t),this.client.send(this.baseCollectionPath+"/auth-refresh",i).then(l=>this.authResponse(l))}async requestPasswordReset(e,t,i){let l={method:"POST",body:{email:e}};return l=Wi("This form of requestPasswordReset(email, body?, query?) is deprecated. Consider replacing it with requestPasswordReset(email, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-password-reset",l).then(()=>!0)}async confirmPasswordReset(e,t,i,l,s){let o={method:"POST",body:{token:e,password:t,passwordConfirm:i}};return o=Wi("This form of confirmPasswordReset(token, password, passwordConfirm, body?, query?) is deprecated. Consider replacing it with confirmPasswordReset(token, password, passwordConfirm, options?).",o,l,s),this.client.send(this.baseCollectionPath+"/confirm-password-reset",o).then(()=>!0)}async requestVerification(e,t,i){let l={method:"POST",body:{email:e}};return l=Wi("This form of requestVerification(email, body?, query?) is deprecated. Consider replacing it with requestVerification(email, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-verification",l).then(()=>!0)}async confirmVerification(e,t,i){let l={method:"POST",body:{token:e}};return l=Wi("This form of confirmVerification(token, body?, query?) is deprecated. Consider replacing it with confirmVerification(token, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/confirm-verification",l).then(()=>{const s=xl(e),o=this.client.authStore.record;return o&&!o.verified&&o.id===s.id&&o.collectionId===s.collectionId&&(o.verified=!0,this.client.authStore.save(this.client.authStore.token,o)),!0})}async requestEmailChange(e,t,i){let l={method:"POST",body:{newEmail:e}};return l=Wi("This form of requestEmailChange(newEmail, body?, query?) is deprecated. Consider replacing it with requestEmailChange(newEmail, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-email-change",l).then(()=>!0)}async confirmEmailChange(e,t,i,l){let s={method:"POST",body:{token:e,password:t}};return s=Wi("This form of confirmEmailChange(token, password, body?, query?) is deprecated. Consider replacing it with confirmEmailChange(token, password, options?).",s,i,l),this.client.send(this.baseCollectionPath+"/confirm-email-change",s).then(()=>{const o=xl(e),r=this.client.authStore.record;return r&&r.id===o.id&&r.collectionId===o.collectionId&&this.client.authStore.clear(),!0})}async listExternalAuths(e,t){return this.client.collection("_externalAuths").getFullList(Object.assign({},t,{filter:this.client.filter("recordRef = {:id}",{id:e})}))}async unlinkExternalAuth(e,t,i){const l=await this.client.collection("_externalAuths").getFirstListItem(this.client.filter("recordRef = {:recordId} && provider = {:provider}",{recordId:e,provider:t}));return this.client.collection("_externalAuths").delete(l.id,i).then(()=>!0)}async requestOTP(e,t){return t=Object.assign({method:"POST",body:{email:e}},t),this.client.send(this.baseCollectionPath+"/request-otp",t)}async authWithOTP(e,t,i){return i=Object.assign({method:"POST",body:{otpId:e,password:t}},i),this.client.send(this.baseCollectionPath+"/auth-with-otp",i).then(l=>this.authResponse(l))}async impersonate(e,t,i){(i=Object.assign({method:"POST",body:{duration:t}},i)).headers=i.headers||{},i.headers.Authorization||(i.headers.Authorization=this.client.authStore.token);const l=new fo(this.client.baseURL,new Ou,this.client.lang),s=await l.send(this.baseCollectionPath+"/impersonate/"+encodeURIComponent(e),i);return l.authStore.save(s==null?void 0:s.token,this.decode((s==null?void 0:s.record)||{})),l}_replaceQueryParams(e,t={}){let i=e,l="";e.indexOf("?")>=0&&(i=e.substring(0,e.indexOf("?")),l=e.substring(e.indexOf("?")+1));const s={},o=l.split("&");for(const r of o){if(r=="")continue;const a=r.split("=");s[decodeURIComponent(a[0].replace(/\+/g," "))]=decodeURIComponent((a[1]||"").replace(/\+/g," "))}for(let r in t)t.hasOwnProperty(r)&&(t[r]==null?delete s[r]:s[r]=t[r]);l="";for(let r in s)s.hasOwnProperty(r)&&(l!=""&&(l+="&"),l+=encodeURIComponent(r.replace(/%20/g,"+"))+"="+encodeURIComponent(s[r].replace(/%20/g,"+")));return l!=""?i+"?"+l:i}}function lc(n){if(typeof window>"u"||!(window!=null&&window.open))throw new Rn(new Error("Not in a browser context - please pass a custom urlCallback function."));let e=1024,t=768,i=window.innerWidth,l=window.innerHeight;e=e>i?i:e,t=t>l?l:t;let s=i/2-e/2,o=l/2-t/2;return window.open(n,"popup_window","width="+e+",height="+t+",top="+o+",left="+s+",resizable,menubar=no")}class vw extends Z0{get baseCrudPath(){return"/api/collections"}async import(e,t=!1,i){return i=Object.assign({method:"PUT",body:{collections:e,deleteMissing:t}},i),this.client.send(this.baseCrudPath+"/import",i).then(()=>!0)}async getScaffolds(e){return e=Object.assign({method:"GET"},e),this.client.send(this.baseCrudPath+"/meta/scaffolds",e)}async truncate(e,t){return t=Object.assign({method:"DELETE"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e)+"/truncate",t).then(()=>!0)}}class ww extends ol{async getList(e=1,t=30,i){return(i=Object.assign({method:"GET"},i)).query=Object.assign({page:e,perPage:t},i.query),this.client.send("/api/logs",i)}async getOne(e,t){if(!e)throw new Rn({url:this.client.buildURL("/api/logs/"),status:404,response:{code:404,message:"Missing required log id.",data:{}}});return t=Object.assign({method:"GET"},t),this.client.send("/api/logs/"+encodeURIComponent(e),t)}async getStats(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/logs/stats",e)}}class Sw extends ol{async check(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/health",e)}}class Tw extends ol{getUrl(e,t,i={}){return console.warn("Please replace pb.files.getUrl() with pb.files.getURL()"),this.getURL(e,t,i)}getURL(e,t,i={}){if(!t||!(e!=null&&e.id)||!(e!=null&&e.collectionId)&&!(e!=null&&e.collectionName))return"";const l=[];l.push("api"),l.push("files"),l.push(encodeURIComponent(e.collectionId||e.collectionName)),l.push(encodeURIComponent(e.id)),l.push(encodeURIComponent(t));let s=this.client.buildURL(l.join("/"));if(Object.keys(i).length){i.download===!1&&delete i.download;const o=new URLSearchParams(i);s+=(s.includes("?")?"&":"?")+o}return s}async getToken(e){return e=Object.assign({method:"POST"},e),this.client.send("/api/files/token",e).then(t=>(t==null?void 0:t.token)||"")}}class $w extends ol{async getFullList(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/backups",e)}async create(e,t){return t=Object.assign({method:"POST",body:{name:e}},t),this.client.send("/api/backups",t).then(()=>!0)}async upload(e,t){return t=Object.assign({method:"POST",body:e},t),this.client.send("/api/backups/upload",t).then(()=>!0)}async delete(e,t){return t=Object.assign({method:"DELETE"},t),this.client.send(`/api/backups/${encodeURIComponent(e)}`,t).then(()=>!0)}async restore(e,t){return t=Object.assign({method:"POST"},t),this.client.send(`/api/backups/${encodeURIComponent(e)}/restore`,t).then(()=>!0)}getDownloadUrl(e,t){return console.warn("Please replace pb.backups.getDownloadUrl() with pb.backups.getDownloadURL()"),this.getDownloadURL(e,t)}getDownloadURL(e,t){return this.client.buildURL(`/api/backups/${encodeURIComponent(t)}?token=${encodeURIComponent(e)}`)}}function Wa(n){return typeof Blob<"u"&&n instanceof Blob||typeof File<"u"&&n instanceof File||n!==null&&typeof n=="object"&&n.uri&&(typeof navigator<"u"&&navigator.product==="ReactNative"||typeof global<"u"&&global.HermesInternal)}function sc(n){return n&&(n.constructor.name==="FormData"||typeof FormData<"u"&&n instanceof FormData)}function oc(n){for(const e in n){const t=Array.isArray(n[e])?n[e]:[n[e]];for(const i of t)if(Wa(i))return!0}return!1}class Cw extends ol{constructor(){super(...arguments),this.requests=[],this.subs={}}collection(e){return this.subs[e]||(this.subs[e]=new Ow(this.requests,e)),this.subs[e]}async send(e){const t=new FormData,i=[];for(let l=0;l0&&s.length==l.length){e.files[i]=e.files[i]||[];for(let r of s)e.files[i].push(r)}else if(e.json[i]=o,s.length>0){let r=i;i.startsWith("+")||i.endsWith("+")||(r+="+"),e.files[r]=e.files[r]||[];for(let a of s)e.files[r].push(a)}}else e.json[i]=l}}}class fo{get baseUrl(){return this.baseURL}set baseUrl(e){this.baseURL=e}constructor(e="/",t,i="en-US"){this.cancelControllers={},this.recordServices={},this.enableAutoCancellation=!0,this.baseURL=e,this.lang=i,t?this.authStore=t:typeof window<"u"&&window.Deno?this.authStore=new Ou:this.authStore=new Y0,this.collections=new vw(this),this.files=new Tw(this),this.logs=new ww(this),this.settings=new bw(this),this.realtime=new J0(this),this.health=new Sw(this),this.backups=new $w(this)}get admins(){return this.collection("_superusers")}createBatch(){return new Cw(this)}collection(e){return this.recordServices[e]||(this.recordServices[e]=new kw(this,e)),this.recordServices[e]}autoCancellation(e){return this.enableAutoCancellation=!!e,this}cancelRequest(e){return this.cancelControllers[e]&&(this.cancelControllers[e].abort(),delete this.cancelControllers[e]),this}cancelAllRequests(){for(let e in this.cancelControllers)this.cancelControllers[e].abort();return this.cancelControllers={},this}filter(e,t){if(!t)return e;for(let i in t){let l=t[i];switch(typeof l){case"boolean":case"number":l=""+l;break;case"string":l="'"+l.replace(/'/g,"\\'")+"'";break;default:l=l===null?"null":l instanceof Date?"'"+l.toISOString().replace("T"," ")+"'":"'"+JSON.stringify(l).replace(/'/g,"\\'")+"'"}e=e.replaceAll("{:"+i+"}",l)}return e}getFileUrl(e,t,i={}){return console.warn("Please replace pb.getFileUrl() with pb.files.getURL()"),this.files.getURL(e,t,i)}buildUrl(e){return console.warn("Please replace pb.buildUrl() with pb.buildURL()"),this.buildURL(e)}buildURL(e){var i;let t=this.baseURL;return typeof window>"u"||!window.location||t.startsWith("https://")||t.startsWith("http://")||(t=(i=window.location.origin)!=null&&i.endsWith("/")?window.location.origin.substring(0,window.location.origin.length-1):window.location.origin||"",this.baseURL.startsWith("/")||(t+=window.location.pathname||"/",t+=t.endsWith("/")?"":"/"),t+=this.baseURL),e&&(t+=t.endsWith("/")?"":"/",t+=e.startsWith("/")?e.substring(1):e),t}async send(e,t){t=this.initSendOptions(e,t);let i=this.buildURL(e);if(this.beforeSend){const l=Object.assign({},await this.beforeSend(i,t));l.url!==void 0||l.options!==void 0?(i=l.url||i,t=l.options||t):Object.keys(l).length&&(t=l,console!=null&&console.warn&&console.warn("Deprecated format of beforeSend return: please use `return { url, options }`, instead of `return options`."))}if(t.query!==void 0){const l=K0(t.query);l&&(i+=(i.includes("?")?"&":"?")+l),delete t.query}return this.getHeader(t.headers,"Content-Type")=="application/json"&&t.body&&typeof t.body!="string"&&(t.body=JSON.stringify(t.body)),(t.fetch||fetch)(i,t).then(async l=>{let s={};try{s=await l.json()}catch{}if(this.afterSend&&(s=await this.afterSend(l,s,t)),l.status>=400)throw new Rn({url:l.url,status:l.status,data:s});return s}).catch(l=>{throw new Rn(l)})}initSendOptions(e,t){if((t=Object.assign({method:"GET"},t)).body=function(l){if(typeof FormData>"u"||l===void 0||typeof l!="object"||l===null||sc(l)||!oc(l))return l;const s=new FormData;for(const o in l){const r=l[o];if(typeof r!="object"||oc({data:r})){const a=Array.isArray(r)?r:[r];for(let u of a)s.append(o,u)}else{let a={};a[o]=r,s.append("@jsonPayload",JSON.stringify(a))}}return s}(t.body),Eu(t),t.query=Object.assign({},t.params,t.query),t.requestKey===void 0&&(t.$autoCancel===!1||t.query.$autoCancel===!1?t.requestKey=null:(t.$cancelKey||t.query.$cancelKey)&&(t.requestKey=t.$cancelKey||t.query.$cancelKey)),delete t.$autoCancel,delete t.query.$autoCancel,delete t.$cancelKey,delete t.query.$cancelKey,this.getHeader(t.headers,"Content-Type")!==null||sc(t.body)||(t.headers=Object.assign({},t.headers,{"Content-Type":"application/json"})),this.getHeader(t.headers,"Accept-Language")===null&&(t.headers=Object.assign({},t.headers,{"Accept-Language":this.lang})),this.authStore.token&&this.getHeader(t.headers,"Authorization")===null&&(t.headers=Object.assign({},t.headers,{Authorization:this.authStore.token})),this.enableAutoCancellation&&t.requestKey!==null){const i=t.requestKey||(t.method||"GET")+e;delete t.requestKey,this.cancelRequest(i);const l=new AbortController;this.cancelControllers[i]=l,t.signal=l.signal}return t}getHeader(e,t){e=e||{},t=t.toLowerCase();for(let i in e)if(i.toLowerCase()==t)return e[i];return null}}const En=qn([]),Qn=qn({}),gr=qn(!1),G0=qn({}),Mu=qn({});let Is;typeof BroadcastChannel<"u"&&(Is=new BroadcastChannel("collections"),Is.onmessage=()=>{var n;Du((n=Eb(Qn))==null?void 0:n.id)});function X0(){Is==null||Is.postMessage("reload")}function Ew(n){En.update(e=>{const t=z.findByKey(e,"id",n);return t?Qn.set(t):e.length&&Qn.set(e[0]),e})}function Mw(n){Qn.update(e=>z.isEmpty(e==null?void 0:e.id)||e.id===n.id?n:e),En.update(e=>(z.pushOrReplaceByKey(e,n,"id"),Iu(),X0(),z.sortCollections(e)))}function Dw(n){En.update(e=>(z.removeByKey(e,"id",n.id),Qn.update(t=>t.id===n.id?e[0]:t),Iu(),X0(),e))}async function Du(n=null){gr.set(!0);try{let e=await me.collections.getFullList(200,{sort:"+name"});e=z.sortCollections(e),En.set(e);const t=n&&z.findByKey(e,"id",n);t?Qn.set(t):e.length&&Qn.set(e.find(i=>!i.system)||e[0]),Iu(),Mu.set(await me.collections.getScaffolds())}catch(e){me.error(e)}gr.set(!1)}function Iu(){G0.update(n=>(En.update(e=>{var t;for(let i of e)n[i.id]=!!((t=i.fields)!=null&&t.find(l=>l.type=="file"&&l.protected));return e}),n))}function Q0(n,e){if(n instanceof RegExp)return{keys:!1,pattern:n};var t,i,l,s,o=[],r="",a=n.split("/");for(a[0]||a.shift();l=a.shift();)t=l[0],t==="*"?(o.push("wild"),r+="/(.*)"):t===":"?(i=l.indexOf("?",1),s=l.indexOf(".",1),o.push(l.substring(1,~i?i:~s?s:l.length)),r+=~i&&!~s?"(?:/([^/]+?))?":"/([^/]+?)",~s&&(r+=(~i?"?":"")+"\\"+l.substring(s))):r+="/"+l;return{keys:o,pattern:new RegExp("^"+r+"/?$","i")}}function Iw(n){let e,t,i;const l=[n[2]];var s=n[0];function o(r,a){let u={};for(let f=0;f{q(u,1)}),ae()}s?(e=jt(s,o(r,a)),e.$on("routeEvent",r[7]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,t.parentNode,t)):e=null}else if(s){const u=a&4?kt(l,[Ft(r[2])]):{};e.$set(u)}},i(r){i||(e&&O(e.$$.fragment,r),i=!0)},o(r){e&&D(e.$$.fragment,r),i=!1},d(r){r&&k(t),e&&q(e,r)}}}function Lw(n){let e,t,i;const l=[{params:n[1]},n[2]];var s=n[0];function o(r,a){let u={};for(let f=0;f{q(u,1)}),ae()}s?(e=jt(s,o(r,a)),e.$on("routeEvent",r[6]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,t.parentNode,t)):e=null}else if(s){const u=a&6?kt(l,[a&2&&{params:r[1]},a&4&&Ft(r[2])]):{};e.$set(u)}},i(r){i||(e&&O(e.$$.fragment,r),i=!0)},o(r){e&&D(e.$$.fragment,r),i=!1},d(r){r&&k(t),e&&q(e,r)}}}function Aw(n){let e,t,i,l;const s=[Lw,Iw],o=[];function r(a,u){return a[1]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,[u]){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function rc(){const n=window.location.href.indexOf("#/");let e=n>-1?window.location.href.substr(n+1):"/";const t=e.indexOf("?");let i="";return t>-1&&(i=e.substr(t+1),e=e.substr(0,t)),{location:e,querystring:i}}const qr=z0(null,function(e){e(rc());const t=()=>{e(rc())};return window.addEventListener("hashchange",t,!1),function(){window.removeEventListener("hashchange",t,!1)}});U0(qr,n=>n.location);const Lu=U0(qr,n=>n.querystring),ac=qn(void 0);async function Il(n){if(!n||n.length<1||n.charAt(0)!="/"&&n.indexOf("#/")!==0)throw Error("Invalid parameter location");await fn();const e=(n.charAt(0)=="#"?"":"#")+n;try{const t={...history.state};delete t.__svelte_spa_router_scrollX,delete t.__svelte_spa_router_scrollY,window.history.replaceState(t,void 0,e)}catch{console.warn("Caught exception while replacing the current page. If you're running this in the Svelte REPL, please note that the `replace` method might not work in this environment.")}window.dispatchEvent(new Event("hashchange"))}function Un(n,e){if(e=fc(e),!n||!n.tagName||n.tagName.toLowerCase()!="a")throw Error('Action "link" can only be used with tags');return uc(n,e),{update(t){t=fc(t),uc(n,t)}}}function Pw(n){n?window.scrollTo(n.__svelte_spa_router_scrollX,n.__svelte_spa_router_scrollY):window.scrollTo(0,0)}function uc(n,e){let t=e.href||n.getAttribute("href");if(t&&t.charAt(0)=="/")t="#"+t;else if(!t||t.length<2||t.slice(0,2)!="#/")throw Error('Invalid value for "href" attribute: '+t);n.setAttribute("href",t),n.addEventListener("click",i=>{i.preventDefault(),e.disabled||Nw(i.currentTarget.getAttribute("href"))})}function fc(n){return n&&typeof n=="string"?{href:n}:n||{}}function Nw(n){history.replaceState({...history.state,__svelte_spa_router_scrollX:window.scrollX,__svelte_spa_router_scrollY:window.scrollY},void 0),window.location.hash=n}function Rw(n,e,t){let{routes:i={}}=e,{prefix:l=""}=e,{restoreScrollState:s=!1}=e;class o{constructor(E,M){if(!M||typeof M!="function"&&(typeof M!="object"||M._sveltesparouter!==!0))throw Error("Invalid component object");if(!E||typeof E=="string"&&(E.length<1||E.charAt(0)!="/"&&E.charAt(0)!="*")||typeof E=="object"&&!(E instanceof RegExp))throw Error('Invalid value for "path" argument - strings must start with / or *');const{pattern:L,keys:I}=Q0(E);this.path=E,typeof M=="object"&&M._sveltesparouter===!0?(this.component=M.component,this.conditions=M.conditions||[],this.userData=M.userData,this.props=M.props||{}):(this.component=()=>Promise.resolve(M),this.conditions=[],this.props={}),this._pattern=L,this._keys=I}match(E){if(l){if(typeof l=="string")if(E.startsWith(l))E=E.substr(l.length)||"/";else return null;else if(l instanceof RegExp){const A=E.match(l);if(A&&A[0])E=E.substr(A[0].length)||"/";else return null}}const M=this._pattern.exec(E);if(M===null)return null;if(this._keys===!1)return M;const L={};let I=0;for(;I{r.push(new o(E,$))}):Object.keys(i).forEach($=>{r.push(new o($,i[$]))});let a=null,u=null,f={};const c=_t();async function d($,E){await fn(),c($,E)}let m=null,h=null;s&&(h=$=>{$.state&&($.state.__svelte_spa_router_scrollY||$.state.__svelte_spa_router_scrollX)?m=$.state:m=null},window.addEventListener("popstate",h),Mk(()=>{Pw(m)}));let g=null,_=null;const y=qr.subscribe(async $=>{g=$;let E=0;for(;E{ac.set(u)});return}t(0,a=null),_=null,ac.set(void 0)});so(()=>{y(),h&&window.removeEventListener("popstate",h)});function S($){Pe.call(this,n,$)}function T($){Pe.call(this,n,$)}return n.$$set=$=>{"routes"in $&&t(3,i=$.routes),"prefix"in $&&t(4,l=$.prefix),"restoreScrollState"in $&&t(5,s=$.restoreScrollState)},n.$$.update=()=>{n.$$.dirty&32&&(history.scrollRestoration=s?"manual":"auto")},[a,u,f,i,l,s,S,T]}class Fw extends ye{constructor(e){super(),be(this,e,Rw,Aw,_e,{routes:3,prefix:4,restoreScrollState:5})}}const ra="pb_superuser_file_token";fo.prototype.logout=function(n=!0){this.authStore.clear(),n&&Il("/login")};fo.prototype.error=function(n,e=!0,t=""){if(!n||!(n instanceof Error)||n.isAbort)return;const i=(n==null?void 0:n.status)<<0||400,l=(n==null?void 0:n.data)||{},s=l.message||n.message||t;if(e&&s&&$i(s),z.isEmpty(l.data)||Wt(l.data),i===401)return this.cancelAllRequests(),this.logout();if(i===403)return this.cancelAllRequests(),Il("/")};fo.prototype.getSuperuserFileToken=async function(n=""){let e=!0;if(n){const i=Eb(G0);e=typeof i[n]<"u"?i[n]:!0}if(!e)return"";let t=localStorage.getItem(ra)||"";return(!t||Cu(t,10))&&(t&&localStorage.removeItem(ra),this._superuserFileTokenRequest||(this._superuserFileTokenRequest=this.files.getToken()),t=await this._superuserFileTokenRequest,localStorage.setItem(ra,t),this._superuserFileTokenRequest=null),t};class qw extends Y0{constructor(e="__pb_superuser_auth__"){super(e),this.save(this.token,this.record)}save(e,t){super.save(e,t),(t==null?void 0:t.collectionName)=="_superusers"&&tc(t)}clear(){super.clear(),tc(null)}}const me=new fo("../",new qw);me.authStore.isValid&&me.collection(me.authStore.record.collectionName).authRefresh().catch(n=>{console.warn("Failed to refresh the existing auth token:",n);const e=(n==null?void 0:n.status)<<0;(e==401||e==403)&&me.authStore.clear()});const nr=[];let x0;function ey(n){const e=n.pattern.test(x0);cc(n,n.className,e),cc(n,n.inactiveClassName,!e)}function cc(n,e,t){(e||"").split(" ").forEach(i=>{i&&(n.node.classList.remove(i),t&&n.node.classList.add(i))})}qr.subscribe(n=>{x0=n.location+(n.querystring?"?"+n.querystring:""),nr.map(ey)});function Ri(n,e){if(e&&(typeof e=="string"||typeof e=="object"&&e instanceof RegExp)?e={path:e}:e=e||{},!e.path&&n.hasAttribute("href")&&(e.path=n.getAttribute("href"),e.path&&e.path.length>1&&e.path.charAt(0)=="#"&&(e.path=e.path.substring(1))),e.className||(e.className="active"),!e.path||typeof e.path=="string"&&(e.path.length<1||e.path.charAt(0)!="/"&&e.path.charAt(0)!="*"))throw Error('Invalid value for "path" argument');const{pattern:t}=typeof e.path=="string"?Q0(e.path):{pattern:e.path},i={node:n,className:e.className,inactiveClassName:e.inactiveClassName,pattern:t};return nr.push(i),ey(i),{destroy(){nr.splice(nr.indexOf(i),1)}}}const Hw="modulepreload",jw=function(n,e){return new URL(n,e).href},dc={},Ot=function(e,t,i){let l=Promise.resolve();if(t&&t.length>0){const o=document.getElementsByTagName("link"),r=document.querySelector("meta[property=csp-nonce]"),a=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));l=Promise.allSettled(t.map(u=>{if(u=jw(u,i),u in dc)return;dc[u]=!0;const f=u.endsWith(".css"),c=f?'[rel="stylesheet"]':"";if(!!i)for(let h=o.length-1;h>=0;h--){const g=o[h];if(g.href===u&&(!f||g.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${u}"]${c}`))return;const m=document.createElement("link");if(m.rel=f?"stylesheet":Hw,f||(m.as="script"),m.crossOrigin="",m.href=u,a&&m.setAttribute("nonce",a),document.head.appendChild(m),f)return new Promise((h,g)=>{m.addEventListener("load",h),m.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${u}`)))})}))}function s(o){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=o,window.dispatchEvent(r),!r.defaultPrevented)throw o}return l.then(o=>{for(const r of o||[])r.status==="rejected"&&s(r.reason);return e().catch(s)})},zw=n=>({}),pc=n=>({});function mc(n){let e,t,i,l,s,o;return{c(){e=b("a"),e.innerHTML=' Docs',t=C(),i=b("span"),i.textContent="|",l=C(),s=b("a"),o=b("span"),o.textContent="PocketBase v0.23.0-rc",p(e,"href","https://pocketbase.io/docs/"),p(e,"target","_blank"),p(e,"rel","noopener noreferrer"),p(i,"class","delimiter"),p(o,"class","txt"),p(s,"href","https://github.com/pocketbase/pocketbase/releases"),p(s,"target","_blank"),p(s,"rel","noopener noreferrer"),p(s,"title","Releases")},m(r,a){v(r,e,a),v(r,t,a),v(r,i,a),v(r,l,a),v(r,s,a),w(s,o)},d(r){r&&(k(e),k(t),k(i),k(l),k(s))}}}function Uw(n){var m;let e,t,i,l,s,o,r;const a=n[4].default,u=Lt(a,n,n[3],null),f=n[4].footer,c=Lt(f,n,n[3],pc);let d=((m=n[2])==null?void 0:m.id)&&mc();return{c(){e=b("div"),t=b("main"),u&&u.c(),i=C(),l=b("footer"),c&&c.c(),s=C(),d&&d.c(),p(t,"class","page-content"),p(l,"class","page-footer"),p(e,"class",o="page-wrapper "+n[1]),x(e,"center-content",n[0])},m(h,g){v(h,e,g),w(e,t),u&&u.m(t,null),w(e,i),w(e,l),c&&c.m(l,null),w(l,s),d&&d.m(l,null),r=!0},p(h,[g]){var _;u&&u.p&&(!r||g&8)&&Pt(u,a,h,h[3],r?At(a,h[3],g,null):Nt(h[3]),null),c&&c.p&&(!r||g&8)&&Pt(c,f,h,h[3],r?At(f,h[3],g,zw):Nt(h[3]),pc),(_=h[2])!=null&&_.id?d||(d=mc(),d.c(),d.m(l,null)):d&&(d.d(1),d=null),(!r||g&2&&o!==(o="page-wrapper "+h[1]))&&p(e,"class",o),(!r||g&3)&&x(e,"center-content",h[0])},i(h){r||(O(u,h),O(c,h),r=!0)},o(h){D(u,h),D(c,h),r=!1},d(h){h&&k(e),u&&u.d(h),c&&c.d(h),d&&d.d()}}}function Vw(n,e,t){let i;Qe(n,Fr,a=>t(2,i=a));let{$$slots:l={},$$scope:s}=e,{center:o=!1}=e,{class:r=""}=e;return n.$$set=a=>{"center"in a&&t(0,o=a.center),"class"in a&&t(1,r=a.class),"$$scope"in a&&t(3,s=a.$$scope)},[o,r,i,s,l]}class di extends ye{constructor(e){super(),be(this,e,Vw,Uw,_e,{center:0,class:1})}}function hc(n){let e,t,i;return{c(){e=b("div"),e.innerHTML='',t=C(),i=b("div"),p(e,"class","block txt-center m-b-lg"),p(i,"class","clearfix")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},d(l){l&&(k(e),k(t),k(i))}}}function Bw(n){let e,t,i,l=!n[0]&&hc();const s=n[1].default,o=Lt(s,n,n[2],null);return{c(){e=b("div"),l&&l.c(),t=C(),o&&o.c(),p(e,"class","wrapper wrapper-sm m-b-xl panel-wrapper svelte-lxxzfu")},m(r,a){v(r,e,a),l&&l.m(e,null),w(e,t),o&&o.m(e,null),i=!0},p(r,a){r[0]?l&&(l.d(1),l=null):l||(l=hc(),l.c(),l.m(e,t)),o&&o.p&&(!i||a&4)&&Pt(o,s,r,r[2],i?At(s,r[2],a,null):Nt(r[2]),null)},i(r){i||(O(o,r),i=!0)},o(r){D(o,r),i=!1},d(r){r&&k(e),l&&l.d(),o&&o.d(r)}}}function Ww(n){let e,t;return e=new di({props:{class:"full-page",center:!0,$$slots:{default:[Bw]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&5&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function Yw(n,e,t){let{$$slots:i={},$$scope:l}=e,{nobranding:s=!1}=e;return n.$$set=o=>{"nobranding"in o&&t(0,s=o.nobranding),"$$scope"in o&&t(2,l=o.$$scope)},[s,i,l]}class ty extends ye{constructor(e){super(),be(this,e,Yw,Ww,_e,{nobranding:0})}}function _c(n,e,t){const i=n.slice();return i[12]=e[t],i}const Kw=n=>({}),gc=n=>({uniqueId:n[4]});function Jw(n){let e,t,i=pe(n[3]),l=[];for(let o=0;oD(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;o{s&&(l||(l=ze(t,Mt,{duration:150,start:.7},!0)),l.run(1))}),s=!0)},o(a){a&&(l||(l=ze(t,Mt,{duration:150,start:.7},!1)),l.run(0)),s=!1},d(a){a&&k(e),a&&l&&l.end(),o=!1,r()}}}function bc(n){let e,t,i=br(n[12])+"",l,s,o,r;return{c(){e=b("div"),t=b("pre"),l=Y(i),s=C(),p(e,"class","help-block help-block-error")},m(a,u){v(a,e,u),w(e,t),w(t,l),w(e,s),r=!0},p(a,u){(!r||u&8)&&i!==(i=br(a[12])+"")&&ue(l,i)},i(a){r||(a&&nt(()=>{r&&(o||(o=ze(e,vt,{duration:150},!0)),o.run(1))}),r=!0)},o(a){a&&(o||(o=ze(e,vt,{duration:150},!1)),o.run(0)),r=!1},d(a){a&&k(e),a&&o&&o.end()}}}function Gw(n){let e,t,i,l,s,o,r;const a=n[9].default,u=Lt(a,n,n[8],gc),f=[Zw,Jw],c=[];function d(m,h){return m[0]&&m[3].length?0:1}return i=d(n),l=c[i]=f[i](n),{c(){e=b("div"),u&&u.c(),t=C(),l.c(),p(e,"class",n[1]),x(e,"error",n[3].length)},m(m,h){v(m,e,h),u&&u.m(e,null),w(e,t),c[i].m(e,null),n[11](e),s=!0,o||(r=B(e,"click",n[10]),o=!0)},p(m,[h]){u&&u.p&&(!s||h&256)&&Pt(u,a,m,m[8],s?At(a,m[8],h,Kw):Nt(m[8]),gc);let g=i;i=d(m),i===g?c[i].p(m,h):(re(),D(c[g],1,1,()=>{c[g]=null}),ae(),l=c[i],l?l.p(m,h):(l=c[i]=f[i](m),l.c()),O(l,1),l.m(e,null)),(!s||h&2)&&p(e,"class",m[1]),(!s||h&10)&&x(e,"error",m[3].length)},i(m){s||(O(u,m),O(l),s=!0)},o(m){D(u,m),D(l),s=!1},d(m){m&&k(e),u&&u.d(m),c[i].d(),n[11](null),o=!1,r()}}}const yc="Invalid value";function br(n){return typeof n=="object"?(n==null?void 0:n.message)||(n==null?void 0:n.code)||yc:n||yc}function Xw(n,e,t){let i;Qe(n,Sn,g=>t(7,i=g));let{$$slots:l={},$$scope:s}=e;const o="field_"+z.randomString(7);let{name:r=""}=e,{inlineError:a=!1}=e,{class:u=void 0}=e,f,c=[];function d(){fi(r)}Yt(()=>(f.addEventListener("input",d),f.addEventListener("change",d),()=>{f.removeEventListener("input",d),f.removeEventListener("change",d)}));function m(g){Pe.call(this,n,g)}function h(g){ie[g?"unshift":"push"](()=>{f=g,t(2,f)})}return n.$$set=g=>{"name"in g&&t(5,r=g.name),"inlineError"in g&&t(0,a=g.inlineError),"class"in g&&t(1,u=g.class),"$$scope"in g&&t(8,s=g.$$scope)},n.$$.update=()=>{n.$$.dirty&160&&t(3,c=z.toArray(z.getNestedVal(i,r)))},[a,u,f,c,o,r,d,i,s,l,m,h]}class fe extends ye{constructor(e){super(),be(this,e,Xw,Gw,_e,{name:5,inlineError:0,class:1,changed:6})}get changed(){return this.$$.ctx[6]}}function Qw(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Email"),l=C(),s=b("input"),p(e,"for",i=n[17]),p(s,"type","email"),p(s,"autocomplete","off"),p(s,"id",o=n[17]),s.required=!0,s.autofocus=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[2]),s.focus(),r||(a=B(s,"input",n[9]),r=!0)},p(u,f){f&131072&&i!==(i=u[17])&&p(e,"for",i),f&131072&&o!==(o=u[17])&&p(s,"id",o),f&4&&s.value!==u[2]&&ce(s,u[2])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function xw(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Password"),l=C(),s=b("input"),r=C(),a=b("div"),a.textContent="Recommended at least 10 characters.",p(e,"for",i=n[17]),p(s,"type","password"),p(s,"autocomplete","new-password"),p(s,"minlength","10"),p(s,"id",o=n[17]),s.required=!0,p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[3]),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[10]),u=!0)},p(c,d){d&131072&&i!==(i=c[17])&&p(e,"for",i),d&131072&&o!==(o=c[17])&&p(s,"id",o),d&8&&s.value!==c[3]&&ce(s,c[3])},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function e3(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Password confirm"),l=C(),s=b("input"),p(e,"for",i=n[17]),p(s,"type","password"),p(s,"minlength","10"),p(s,"id",o=n[17]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[4]),r||(a=B(s,"input",n[11]),r=!0)},p(u,f){f&131072&&i!==(i=u[17])&&p(e,"for",i),f&131072&&o!==(o=u[17])&&p(s,"id",o),f&16&&s.value!==u[4]&&ce(s,u[4])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function t3(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T;return l=new fe({props:{class:"form-field required",name:"email",$$slots:{default:[Qw,({uniqueId:$})=>({17:$}),({uniqueId:$})=>$?131072:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field required",name:"password",$$slots:{default:[xw,({uniqueId:$})=>({17:$}),({uniqueId:$})=>$?131072:0]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[e3,({uniqueId:$})=>({17:$}),({uniqueId:$})=>$?131072:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("div"),t.innerHTML="

Create your first superuser account in order to continue

",i=C(),H(l.$$.fragment),s=C(),H(o.$$.fragment),r=C(),H(a.$$.fragment),u=C(),f=b("button"),f.innerHTML='Create superuser and login ',c=C(),d=b("hr"),m=C(),h=b("label"),h.innerHTML=' Or initialize from backup',g=C(),_=b("input"),p(t,"class","content txt-center m-b-base"),p(f,"type","submit"),p(f,"class","btn btn-lg btn-block btn-next"),x(f,"btn-disabled",n[6]),x(f,"btn-loading",n[0]),p(e,"class","block"),p(e,"autocomplete","off"),p(h,"for","backupFileInput"),p(h,"class","btn btn-lg btn-hint btn-transparent btn-block"),x(h,"btn-disabled",n[6]),x(h,"btn-loading",n[1]),p(_,"id","backupFileInput"),p(_,"type","file"),p(_,"class","hidden"),p(_,"accept",".zip")},m($,E){v($,e,E),w(e,t),w(e,i),F(l,e,null),w(e,s),F(o,e,null),w(e,r),F(a,e,null),w(e,u),w(e,f),v($,c,E),v($,d,E),v($,m,E),v($,h,E),v($,g,E),v($,_,E),n[12](_),y=!0,S||(T=[B(e,"submit",tt(n[7])),B(_,"change",n[13])],S=!0)},p($,[E]){const M={};E&393220&&(M.$$scope={dirty:E,ctx:$}),l.$set(M);const L={};E&393224&&(L.$$scope={dirty:E,ctx:$}),o.$set(L);const I={};E&393232&&(I.$$scope={dirty:E,ctx:$}),a.$set(I),(!y||E&64)&&x(f,"btn-disabled",$[6]),(!y||E&1)&&x(f,"btn-loading",$[0]),(!y||E&64)&&x(h,"btn-disabled",$[6]),(!y||E&2)&&x(h,"btn-loading",$[1])},i($){y||(O(l.$$.fragment,$),O(o.$$.fragment,$),O(a.$$.fragment,$),y=!0)},o($){D(l.$$.fragment,$),D(o.$$.fragment,$),D(a.$$.fragment,$),y=!1},d($){$&&(k(e),k(c),k(d),k(m),k(h),k(g),k(_)),q(l),q(o),q(a),n[12](null),S=!1,De(T)}}}function n3(n,e,t){let i;const l=_t();let s="",o="",r="",a=!1,u=!1,f;async function c(){if(!i){t(0,a=!0);try{await me.collection("_superusers").create({email:s,password:o,passwordConfirm:r}),await me.collection("_superusers").authWithPassword(s,o),l("submit")}catch($){me.error($)}t(0,a=!1)}}function d(){f&&t(5,f.value="",f)}function m($){$&&pn(`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the file source. + +Do you really want to upload and initialize "${$.name}"?`,()=>{h($)},()=>{d()})}async function h($){if(!(!$||i)){t(1,u=!0);try{await me.backups.upload({file:$}),await me.backups.restore($.name),Ys("Please wait while extracting the uploaded archive!"),await new Promise(E=>setTimeout(E,2e3)),l("submit")}catch(E){me.error(E)}d(),t(1,u=!1)}}function g(){s=this.value,t(2,s)}function _(){o=this.value,t(3,o)}function y(){r=this.value,t(4,r)}function S($){ie[$?"unshift":"push"](()=>{f=$,t(5,f)})}const T=$=>{var E,M;m((M=(E=$.target)==null?void 0:E.files)==null?void 0:M[0])};return n.$$.update=()=>{n.$$.dirty&3&&t(6,i=a||u)},[a,u,s,o,r,f,i,c,m,g,_,y,S,T]}class i3 extends ye{constructor(e){super(),be(this,e,n3,t3,_e,{})}}function kc(n){let e,t;return e=new ty({props:{$$slots:{default:[l3]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&9&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function l3(n){let e,t;return e=new i3({}),e.$on("submit",n[1]),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p:te,i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function s3(n){let e,t,i=n[0]&&kc(n);return{c(){i&&i.c(),e=ge()},m(l,s){i&&i.m(l,s),v(l,e,s),t=!0},p(l,[s]){l[0]?i?(i.p(l,s),s&1&&O(i,1)):(i=kc(l),i.c(),O(i,1),i.m(e.parentNode,e)):i&&(re(),D(i,1,1,()=>{i=null}),ae())},i(l){t||(O(i),t=!0)},o(l){D(i),t=!1},d(l){l&&k(e),i&&i.d(l)}}}function o3(n,e,t){let i=!1;l();function l(){if(t(0,i=!1),new URLSearchParams(window.location.search).has("pbinstal")){me.logout(!1),t(0,i=!0);return}me.authStore.isValid?Il("/collections"):me.logout()}return[i,async()=>{t(0,i=!1),await fn(),window.location.search=""}]}class r3 extends ye{constructor(e){super(),be(this,e,o3,s3,_e,{})}}function a3(n){let e,t,i,l;return{c(){e=b("input"),p(e,"type","text"),p(e,"id",n[8]),p(e,"placeholder",t=n[0]||n[1])},m(s,o){v(s,e,o),n[13](e),ce(e,n[7]),i||(l=B(e,"input",n[14]),i=!0)},p(s,o){o&3&&t!==(t=s[0]||s[1])&&p(e,"placeholder",t),o&128&&e.value!==s[7]&&ce(e,s[7])},i:te,o:te,d(s){s&&k(e),n[13](null),i=!1,l()}}}function u3(n){let e,t,i,l;function s(a){n[12](a)}var o=n[4];function r(a,u){let f={id:a[8],singleLine:!0,disableRequestKeys:!0,disableCollectionJoinKeys:!0,extraAutocompleteKeys:a[3],baseCollection:a[2],placeholder:a[0]||a[1]};return a[7]!==void 0&&(f.value=a[7]),{props:f}}return o&&(e=jt(o,r(n)),ie.push(()=>ve(e,"value",s)),e.$on("submit",n[10])),{c(){e&&H(e.$$.fragment),i=ge()},m(a,u){e&&F(e,a,u),v(a,i,u),l=!0},p(a,u){if(u&16&&o!==(o=a[4])){if(e){re();const f=e;D(f.$$.fragment,1,0,()=>{q(f,1)}),ae()}o?(e=jt(o,r(a)),ie.push(()=>ve(e,"value",s)),e.$on("submit",a[10]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,i.parentNode,i)):e=null}else if(o){const f={};u&8&&(f.extraAutocompleteKeys=a[3]),u&4&&(f.baseCollection=a[2]),u&3&&(f.placeholder=a[0]||a[1]),!t&&u&128&&(t=!0,f.value=a[7],$e(()=>t=!1)),e.$set(f)}},i(a){l||(e&&O(e.$$.fragment,a),l=!0)},o(a){e&&D(e.$$.fragment,a),l=!1},d(a){a&&k(i),e&&q(e,a)}}}function vc(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Search',p(e,"type","submit"),p(e,"class","btn btn-expanded-sm btn-sm btn-warning")},m(l,s){v(l,e,s),i=!0},i(l){i||(l&&nt(()=>{i&&(t||(t=ze(e,Fn,{duration:150,x:5},!0)),t.run(1))}),i=!0)},o(l){l&&(t||(t=ze(e,Fn,{duration:150,x:5},!1)),t.run(0)),i=!1},d(l){l&&k(e),l&&t&&t.end()}}}function wc(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Clear',p(e,"type","button"),p(e,"class","btn btn-transparent btn-sm btn-hint p-l-xs p-r-xs m-l-10")},m(o,r){v(o,e,r),i=!0,l||(s=B(e,"click",n[15]),l=!0)},p:te,i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Fn,{duration:150,x:5},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Fn,{duration:150,x:5},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function f3(n){let e,t,i,l,s,o,r,a,u,f,c;const d=[u3,a3],m=[];function h(y,S){return y[4]&&!y[5]?0:1}s=h(n),o=m[s]=d[s](n);let g=(n[0].length||n[7].length)&&n[7]!=n[0]&&vc(),_=(n[0].length||n[7].length)&&wc(n);return{c(){e=b("form"),t=b("label"),i=b("i"),l=C(),o.c(),r=C(),g&&g.c(),a=C(),_&&_.c(),p(i,"class","ri-search-line"),p(t,"for",n[8]),p(t,"class","m-l-10 txt-xl"),p(e,"class","searchbar")},m(y,S){v(y,e,S),w(e,t),w(t,i),w(e,l),m[s].m(e,null),w(e,r),g&&g.m(e,null),w(e,a),_&&_.m(e,null),u=!0,f||(c=[B(e,"click",On(n[11])),B(e,"submit",tt(n[10]))],f=!0)},p(y,[S]){let T=s;s=h(y),s===T?m[s].p(y,S):(re(),D(m[T],1,1,()=>{m[T]=null}),ae(),o=m[s],o?o.p(y,S):(o=m[s]=d[s](y),o.c()),O(o,1),o.m(e,r)),(y[0].length||y[7].length)&&y[7]!=y[0]?g?S&129&&O(g,1):(g=vc(),g.c(),O(g,1),g.m(e,a)):g&&(re(),D(g,1,1,()=>{g=null}),ae()),y[0].length||y[7].length?_?(_.p(y,S),S&129&&O(_,1)):(_=wc(y),_.c(),O(_,1),_.m(e,null)):_&&(re(),D(_,1,1,()=>{_=null}),ae())},i(y){u||(O(o),O(g),O(_),u=!0)},o(y){D(o),D(g),D(_),u=!1},d(y){y&&k(e),m[s].d(),g&&g.d(),_&&_.d(),f=!1,De(c)}}}function c3(n,e,t){const i=_t(),l="search_"+z.randomString(7);let{value:s=""}=e,{placeholder:o='Search term or filter like created > "2022-01-01"...'}=e,{autocompleteCollection:r=null}=e,{extraAutocompleteKeys:a=[]}=e,u,f=!1,c,d="";function m(E=!0){t(7,d=""),E&&(c==null||c.focus()),i("clear")}function h(){t(0,s=d),i("submit",s)}async function g(){u||f||(t(5,f=!0),t(4,u=(await Ot(async()=>{const{default:E}=await import("./FilterAutocompleteInput-DvxlPb20.js");return{default:E}},__vite__mapDeps([0,1]),import.meta.url)).default),t(5,f=!1))}Yt(()=>{g()});function _(E){Pe.call(this,n,E)}function y(E){d=E,t(7,d),t(0,s)}function S(E){ie[E?"unshift":"push"](()=>{c=E,t(6,c)})}function T(){d=this.value,t(7,d),t(0,s)}const $=()=>{m(!1),h()};return n.$$set=E=>{"value"in E&&t(0,s=E.value),"placeholder"in E&&t(1,o=E.placeholder),"autocompleteCollection"in E&&t(2,r=E.autocompleteCollection),"extraAutocompleteKeys"in E&&t(3,a=E.extraAutocompleteKeys)},n.$$.update=()=>{n.$$.dirty&1&&typeof s=="string"&&t(7,d=s)},[s,o,r,a,u,f,c,d,l,m,h,_,y,S,T,$]}class Hr extends ye{constructor(e){super(),be(this,e,c3,f3,_e,{value:0,placeholder:1,autocompleteCollection:2,extraAutocompleteKeys:3})}}function d3(n){let e,t,i,l,s,o;return{c(){e=b("button"),t=b("i"),p(t,"class","ri-refresh-line svelte-1bvelc2"),p(e,"type","button"),p(e,"aria-label","Refresh"),p(e,"class",i="btn btn-transparent btn-circle "+n[1]+" svelte-1bvelc2"),x(e,"refreshing",n[2])},m(r,a){v(r,e,a),w(e,t),s||(o=[Me(l=He.call(null,e,n[0])),B(e,"click",n[3])],s=!0)},p(r,[a]){a&2&&i!==(i="btn btn-transparent btn-circle "+r[1]+" svelte-1bvelc2")&&p(e,"class",i),l&&Rt(l.update)&&a&1&&l.update.call(null,r[0]),a&6&&x(e,"refreshing",r[2])},i:te,o:te,d(r){r&&k(e),s=!1,De(o)}}}function p3(n,e,t){const i=_t();let{tooltip:l={text:"Refresh",position:"right"}}=e,{class:s=""}=e,o=null;function r(){i("refresh");const a=l;t(0,l=null),clearTimeout(o),t(2,o=setTimeout(()=>{t(2,o=null),t(0,l=a)},150))}return Yt(()=>()=>clearTimeout(o)),n.$$set=a=>{"tooltip"in a&&t(0,l=a.tooltip),"class"in a&&t(1,s=a.class)},[l,s,o,r]}class Au extends ye{constructor(e){super(),be(this,e,p3,d3,_e,{tooltip:0,class:1})}}const m3=n=>({}),Sc=n=>({}),h3=n=>({}),Tc=n=>({});function _3(n){let e,t,i,l,s,o,r,a;const u=n[11].before,f=Lt(u,n,n[10],Tc),c=n[11].default,d=Lt(c,n,n[10],null),m=n[11].after,h=Lt(m,n,n[10],Sc);return{c(){e=b("div"),f&&f.c(),t=C(),i=b("div"),d&&d.c(),s=C(),h&&h.c(),p(i,"class",l="scroller "+n[0]+" "+n[3]+" svelte-3a0gfs"),p(e,"class","scroller-wrapper svelte-3a0gfs")},m(g,_){v(g,e,_),f&&f.m(e,null),w(e,t),w(e,i),d&&d.m(i,null),n[12](i),w(e,s),h&&h.m(e,null),o=!0,r||(a=[B(window,"resize",n[1]),B(i,"scroll",n[1])],r=!0)},p(g,[_]){f&&f.p&&(!o||_&1024)&&Pt(f,u,g,g[10],o?At(u,g[10],_,h3):Nt(g[10]),Tc),d&&d.p&&(!o||_&1024)&&Pt(d,c,g,g[10],o?At(c,g[10],_,null):Nt(g[10]),null),(!o||_&9&&l!==(l="scroller "+g[0]+" "+g[3]+" svelte-3a0gfs"))&&p(i,"class",l),h&&h.p&&(!o||_&1024)&&Pt(h,m,g,g[10],o?At(m,g[10],_,m3):Nt(g[10]),Sc)},i(g){o||(O(f,g),O(d,g),O(h,g),o=!0)},o(g){D(f,g),D(d,g),D(h,g),o=!1},d(g){g&&k(e),f&&f.d(g),d&&d.d(g),n[12](null),h&&h.d(g),r=!1,De(a)}}}function g3(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=_t();let{class:o=""}=e,{vThreshold:r=0}=e,{hThreshold:a=0}=e,{dispatchOnNoScroll:u=!0}=e,f=null,c="",d=null,m,h,g,_,y;function S(){f&&t(2,f.scrollTop=0,f)}function T(){f&&t(2,f.scrollLeft=0,f)}function $(){f&&(t(3,c=""),g=f.clientWidth+2,_=f.clientHeight+2,m=f.scrollWidth-g,h=f.scrollHeight-_,h>0?(t(3,c+=" v-scroll"),r>=_&&t(4,r=0),f.scrollTop-r<=0&&(t(3,c+=" v-scroll-start"),s("vScrollStart")),f.scrollTop+r>=h&&(t(3,c+=" v-scroll-end"),s("vScrollEnd"))):u&&s("vScrollEnd"),m>0?(t(3,c+=" h-scroll"),a>=g&&t(5,a=0),f.scrollLeft-a<=0&&(t(3,c+=" h-scroll-start"),s("hScrollStart")),f.scrollLeft+a>=m&&(t(3,c+=" h-scroll-end"),s("hScrollEnd"))):u&&s("hScrollEnd"))}function E(){d||(d=setTimeout(()=>{$(),d=null},150))}Yt(()=>(E(),y=new MutationObserver(E),y.observe(f,{attributeFilter:["width","height"],childList:!0,subtree:!0}),()=>{y==null||y.disconnect(),clearTimeout(d)}));function M(L){ie[L?"unshift":"push"](()=>{f=L,t(2,f)})}return n.$$set=L=>{"class"in L&&t(0,o=L.class),"vThreshold"in L&&t(4,r=L.vThreshold),"hThreshold"in L&&t(5,a=L.hThreshold),"dispatchOnNoScroll"in L&&t(6,u=L.dispatchOnNoScroll),"$$scope"in L&&t(10,l=L.$$scope)},[o,E,f,c,r,a,u,S,T,$,l,i,M]}class Pu extends ye{constructor(e){super(),be(this,e,g3,_3,_e,{class:0,vThreshold:4,hThreshold:5,dispatchOnNoScroll:6,resetVerticalScroll:7,resetHorizontalScroll:8,refresh:9,throttleRefresh:1})}get resetVerticalScroll(){return this.$$.ctx[7]}get resetHorizontalScroll(){return this.$$.ctx[8]}get refresh(){return this.$$.ctx[9]}get throttleRefresh(){return this.$$.ctx[1]}}function b3(n){let e,t,i,l,s;const o=n[6].default,r=Lt(o,n,n[5],null);return{c(){e=b("th"),r&&r.c(),p(e,"tabindex","0"),p(e,"title",n[2]),p(e,"class",t="col-sort "+n[1]),x(e,"col-sort-disabled",n[3]),x(e,"sort-active",n[0]==="-"+n[2]||n[0]==="+"+n[2]),x(e,"sort-desc",n[0]==="-"+n[2]),x(e,"sort-asc",n[0]==="+"+n[2])},m(a,u){v(a,e,u),r&&r.m(e,null),i=!0,l||(s=[B(e,"click",n[7]),B(e,"keydown",n[8])],l=!0)},p(a,[u]){r&&r.p&&(!i||u&32)&&Pt(r,o,a,a[5],i?At(o,a[5],u,null):Nt(a[5]),null),(!i||u&4)&&p(e,"title",a[2]),(!i||u&2&&t!==(t="col-sort "+a[1]))&&p(e,"class",t),(!i||u&10)&&x(e,"col-sort-disabled",a[3]),(!i||u&7)&&x(e,"sort-active",a[0]==="-"+a[2]||a[0]==="+"+a[2]),(!i||u&7)&&x(e,"sort-desc",a[0]==="-"+a[2]),(!i||u&7)&&x(e,"sort-asc",a[0]==="+"+a[2])},i(a){i||(O(r,a),i=!0)},o(a){D(r,a),i=!1},d(a){a&&k(e),r&&r.d(a),l=!1,De(s)}}}function y3(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,{name:o}=e,{sort:r=""}=e,{disable:a=!1}=e;function u(){a||("-"+o===r?t(0,r="+"+o):t(0,r="-"+o))}const f=()=>u(),c=d=>{(d.code==="Enter"||d.code==="Space")&&(d.preventDefault(),u())};return n.$$set=d=>{"class"in d&&t(1,s=d.class),"name"in d&&t(2,o=d.name),"sort"in d&&t(0,r=d.sort),"disable"in d&&t(3,a=d.disable),"$$scope"in d&&t(5,l=d.$$scope)},[r,s,o,a,u,l,i,f,c]}class ir extends ye{constructor(e){super(),be(this,e,y3,b3,_e,{class:1,name:2,sort:0,disable:3})}}function k3(n){let e,t=n[0].replace("Z"," UTC")+"",i,l,s;return{c(){e=b("span"),i=Y(t),p(e,"class","txt-nowrap")},m(o,r){v(o,e,r),w(e,i),l||(s=Me(He.call(null,e,n[1])),l=!0)},p(o,[r]){r&1&&t!==(t=o[0].replace("Z"," UTC")+"")&&ue(i,t)},i:te,o:te,d(o){o&&k(e),l=!1,s()}}}function v3(n,e,t){let{date:i}=e;const l={get text(){return z.formatToLocalDate(i,"yyyy-MM-dd HH:mm:ss.SSS")+" Local"}};return n.$$set=s=>{"date"in s&&t(0,i=s.date)},[i,l]}class ny extends ye{constructor(e){super(),be(this,e,v3,k3,_e,{date:0})}}function w3(n){let e,t,i=(n[1]||"UNKN")+"",l,s,o,r,a;return{c(){e=b("div"),t=b("span"),l=Y(i),s=Y(" ("),o=Y(n[0]),r=Y(")"),p(t,"class","txt"),p(e,"class",a="label log-level-label level-"+n[0]+" svelte-ha6hme")},m(u,f){v(u,e,f),w(e,t),w(t,l),w(t,s),w(t,o),w(t,r)},p(u,[f]){f&2&&i!==(i=(u[1]||"UNKN")+"")&&ue(l,i),f&1&&ue(o,u[0]),f&1&&a!==(a="label log-level-label level-"+u[0]+" svelte-ha6hme")&&p(e,"class",a)},i:te,o:te,d(u){u&&k(e)}}}function S3(n,e,t){let i,{level:l}=e;return n.$$set=s=>{"level"in s&&t(0,l=s.level)},n.$$.update=()=>{var s;n.$$.dirty&1&&t(1,i=(s=q0.find(o=>o.level==l))==null?void 0:s.label)},[l,i]}class iy extends ye{constructor(e){super(),be(this,e,S3,w3,_e,{level:0})}}function $c(n,e,t){var o;const i=n.slice();i[32]=e[t];const l=((o=i[32].data)==null?void 0:o.type)=="request";i[33]=l;const s=N3(i[32]);return i[34]=s,i}function Cc(n,e,t){const i=n.slice();return i[37]=e[t],i}function T3(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),t=b("input"),l=C(),s=b("label"),p(t,"type","checkbox"),p(t,"id","checkbox_0"),t.disabled=i=!n[3].length,t.checked=n[8],p(s,"for","checkbox_0"),p(e,"class","form-field")},m(a,u){v(a,e,u),w(e,t),w(e,l),w(e,s),o||(r=B(t,"change",n[19]),o=!0)},p(a,u){u[0]&8&&i!==(i=!a[3].length)&&(t.disabled=i),u[0]&256&&(t.checked=a[8])},d(a){a&&k(e),o=!1,r()}}}function $3(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function C3(n){let e;return{c(){e=b("div"),e.innerHTML=' level',p(e,"class","col-header-content")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function O3(n){let e;return{c(){e=b("div"),e.innerHTML=' message',p(e,"class","col-header-content")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function E3(n){let e;return{c(){e=b("div"),e.innerHTML=` created`,p(e,"class","col-header-content")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function Oc(n){let e;function t(s,o){return s[7]?D3:M3}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function M3(n){var r;let e,t,i,l,s,o=((r=n[0])==null?void 0:r.length)&&Ec(n);return{c(){e=b("tr"),t=b("td"),i=b("h6"),i.textContent="No logs found.",l=C(),o&&o.c(),s=C(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,u){v(a,e,u),w(e,t),w(t,i),w(t,l),o&&o.m(t,null),w(e,s)},p(a,u){var f;(f=a[0])!=null&&f.length?o?o.p(a,u):(o=Ec(a),o.c(),o.m(t,null)):o&&(o.d(1),o=null)},d(a){a&&k(e),o&&o.d()}}}function D3(n){let e;return{c(){e=b("tr"),e.innerHTML=' '},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function Ec(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[26]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function Mc(n){let e,t=pe(n[34]),i=[];for(let l=0;l',P=C(),p(s,"type","checkbox"),p(s,"id",o="checkbox_"+e[32].id),s.checked=r=e[4][e[32].id],p(u,"for",f="checkbox_"+e[32].id),p(l,"class","form-field"),p(i,"class","bulk-select-col min-width"),p(d,"class","col-type-text col-field-level min-width svelte-91v05h"),p(y,"class","txt-ellipsis"),p(_,"class","flex flex-gap-10"),p(g,"class","col-type-text col-field-message svelte-91v05h"),p(M,"class","col-type-date col-field-created"),p(A,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(ee,X){v(ee,t,X),w(t,i),w(i,l),w(l,s),w(l,a),w(l,u),w(t,c),w(t,d),F(m,d,null),w(t,h),w(t,g),w(g,_),w(_,y),w(y,T),w(g,$),V&&V.m(g,null),w(t,E),w(t,M),F(L,M,null),w(t,I),w(t,A),w(t,P),R=!0,N||(U=[B(s,"change",j),B(l,"click",On(e[18])),B(t,"click",K),B(t,"keydown",J)],N=!0)},p(ee,X){e=ee,(!R||X[0]&8&&o!==(o="checkbox_"+e[32].id))&&p(s,"id",o),(!R||X[0]&24&&r!==(r=e[4][e[32].id]))&&(s.checked=r),(!R||X[0]&8&&f!==(f="checkbox_"+e[32].id))&&p(u,"for",f);const oe={};X[0]&8&&(oe.level=e[32].level),m.$set(oe),(!R||X[0]&8)&&S!==(S=e[32].message+"")&&ue(T,S),e[34].length?V?V.p(e,X):(V=Mc(e),V.c(),V.m(g,null)):V&&(V.d(1),V=null);const Se={};X[0]&8&&(Se.date=e[32].created),L.$set(Se)},i(ee){R||(O(m.$$.fragment,ee),O(L.$$.fragment,ee),R=!0)},o(ee){D(m.$$.fragment,ee),D(L.$$.fragment,ee),R=!1},d(ee){ee&&k(t),q(m),V&&V.d(),q(L),N=!1,De(U)}}}function A3(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S=[],T=new Map,$;function E(J,ee){return J[7]?$3:T3}let M=E(n),L=M(n);function I(J){n[20](J)}let A={disable:!0,class:"col-field-level min-width",name:"level",$$slots:{default:[C3]},$$scope:{ctx:n}};n[1]!==void 0&&(A.sort=n[1]),o=new ir({props:A}),ie.push(()=>ve(o,"sort",I));function P(J){n[21](J)}let R={disable:!0,class:"col-type-text col-field-message",name:"data",$$slots:{default:[O3]},$$scope:{ctx:n}};n[1]!==void 0&&(R.sort=n[1]),u=new ir({props:R}),ie.push(()=>ve(u,"sort",P));function N(J){n[22](J)}let U={disable:!0,class:"col-type-date col-field-created",name:"created",$$slots:{default:[E3]},$$scope:{ctx:n}};n[1]!==void 0&&(U.sort=n[1]),d=new ir({props:U}),ie.push(()=>ve(d,"sort",N));let j=pe(n[3]);const V=J=>J[32].id;for(let J=0;Jr=!1)),o.$set(X);const oe={};ee[1]&512&&(oe.$$scope={dirty:ee,ctx:J}),!f&&ee[0]&2&&(f=!0,oe.sort=J[1],$e(()=>f=!1)),u.$set(oe);const Se={};ee[1]&512&&(Se.$$scope={dirty:ee,ctx:J}),!m&&ee[0]&2&&(m=!0,Se.sort=J[1],$e(()=>m=!1)),d.$set(Se),ee[0]&9369&&(j=pe(J[3]),re(),S=yt(S,ee,V,1,J,j,T,y,zt,Ic,null,$c),ae(),!j.length&&K?K.p(J,ee):j.length?K&&(K.d(1),K=null):(K=Oc(J),K.c(),K.m(y,null))),(!$||ee[0]&128)&&x(e,"table-loading",J[7])},i(J){if(!$){O(o.$$.fragment,J),O(u.$$.fragment,J),O(d.$$.fragment,J);for(let ee=0;eeLoad more',p(t,"type","button"),p(t,"class","btn btn-lg btn-secondary btn-expanded"),x(t,"btn-loading",n[7]),x(t,"btn-disabled",n[7]),p(e,"class","block txt-center m-t-sm")},m(s,o){v(s,e,o),w(e,t),i||(l=B(t,"click",n[27]),i=!0)},p(s,o){o[0]&128&&x(t,"btn-loading",s[7]),o[0]&128&&x(t,"btn-disabled",s[7])},d(s){s&&k(e),i=!1,l()}}}function Ac(n){let e,t,i,l,s,o,r=n[5]===1?"log":"logs",a,u,f,c,d,m,h,g,_,y,S;return{c(){e=b("div"),t=b("div"),i=Y("Selected "),l=b("strong"),s=Y(n[5]),o=C(),a=Y(r),u=C(),f=b("button"),f.innerHTML='Reset',c=C(),d=b("div"),m=C(),h=b("button"),h.innerHTML='Download as JSON',p(t,"class","txt"),p(f,"type","button"),p(f,"class","btn btn-xs btn-transparent btn-outline p-l-5 p-r-5"),p(d,"class","flex-fill"),p(h,"type","button"),p(h,"class","btn btn-sm"),p(e,"class","bulkbar svelte-91v05h")},m(T,$){v(T,e,$),w(e,t),w(t,i),w(t,l),w(l,s),w(t,o),w(t,a),w(e,u),w(e,f),w(e,c),w(e,d),w(e,m),w(e,h),_=!0,y||(S=[B(f,"click",n[28]),B(h,"click",n[14])],y=!0)},p(T,$){(!_||$[0]&32)&&ue(s,T[5]),(!_||$[0]&32)&&r!==(r=T[5]===1?"log":"logs")&&ue(a,r)},i(T){_||(T&&nt(()=>{_&&(g||(g=ze(e,Fn,{duration:150,y:5},!0)),g.run(1))}),_=!0)},o(T){T&&(g||(g=ze(e,Fn,{duration:150,y:5},!1)),g.run(0)),_=!1},d(T){T&&k(e),T&&g&&g.end(),y=!1,De(S)}}}function P3(n){let e,t,i,l,s;e=new Pu({props:{class:"table-wrapper",$$slots:{default:[A3]},$$scope:{ctx:n}}});let o=n[3].length&&n[9]&&Lc(n),r=n[5]&&Ac(n);return{c(){H(e.$$.fragment),t=C(),o&&o.c(),i=C(),r&&r.c(),l=ge()},m(a,u){F(e,a,u),v(a,t,u),o&&o.m(a,u),v(a,i,u),r&&r.m(a,u),v(a,l,u),s=!0},p(a,u){const f={};u[0]&411|u[1]&512&&(f.$$scope={dirty:u,ctx:a}),e.$set(f),a[3].length&&a[9]?o?o.p(a,u):(o=Lc(a),o.c(),o.m(i.parentNode,i)):o&&(o.d(1),o=null),a[5]?r?(r.p(a,u),u[0]&32&&O(r,1)):(r=Ac(a),r.c(),O(r,1),r.m(l.parentNode,l)):r&&(re(),D(r,1,1,()=>{r=null}),ae())},i(a){s||(O(e.$$.fragment,a),O(r),s=!0)},o(a){D(e.$$.fragment,a),D(r),s=!1},d(a){a&&(k(t),k(i),k(l)),q(e,a),o&&o.d(a),r&&r.d(a)}}}const Pc=50,aa=/[-:\. ]/gi;function N3(n){let e=[];if(!n.data)return e;if(n.data.type=="request"){const t=["status","execTime","auth","authId","userIP"];for(let i of t)typeof n.data[i]<"u"&&e.push({key:i});n.data.referer&&!n.data.referer.includes(window.location.host)&&e.push({key:"referer"})}else{const t=Object.keys(n.data);for(const i of t)i!="error"&&i!="details"&&e.length<6&&e.push({key:i})}return n.data.error&&e.push({key:"error",label:"label-danger"}),n.data.details&&e.push({key:"details",label:"label-warning"}),e}function R3(n,e,t){let i,l,s;const o=_t();let{filter:r=""}=e,{presets:a=""}=e,{zoom:u={}}=e,{sort:f="-@rowid"}=e,c=[],d=1,m=0,h=!1,g=0,_={};async function y(X=1,oe=!0){t(7,h=!0);const Se=[a,z.normalizeLogsFilter(r)];return u.min&&u.max&&Se.push(`created >= "${u.min}" && created <= "${u.max}"`),me.logs.getList(X,Pc,{sort:f,skipTotal:1,filter:Se.filter(Boolean).join("&&")}).then(async ke=>{var We;X<=1&&S();const Ce=z.toArray(ke.items);if(t(7,h=!1),t(6,d=ke.page),t(17,m=((We=ke.items)==null?void 0:We.length)||0),o("load",c.concat(Ce)),oe){const st=++g;for(;Ce.length&&g==st;){const et=Ce.splice(0,10);for(let Be of et)z.pushOrReplaceByKey(c,Be);t(3,c),await z.yieldToMain()}}else{for(let st of Ce)z.pushOrReplaceByKey(c,st);t(3,c)}}).catch(ke=>{ke!=null&&ke.isAbort||(t(7,h=!1),console.warn(ke),S(),me.error(ke,!Se||(ke==null?void 0:ke.status)!=400))})}function S(){t(3,c=[]),t(4,_={}),t(6,d=1),t(17,m=0)}function T(){s?$():E()}function $(){t(4,_={})}function E(){for(const X of c)t(4,_[X.id]=X,_);t(4,_)}function M(X){_[X.id]?delete _[X.id]:t(4,_[X.id]=X,_),t(4,_)}function L(){const X=Object.values(_).sort((ke,Ce)=>ke.createdCe.created?-1:0);if(!X.length)return;if(X.length==1)return z.downloadJson(X[0],"log_"+X[0].created.replaceAll(aa,"")+".json");const oe=X[0].created.replaceAll(aa,""),Se=X[X.length-1].created.replaceAll(aa,"");return z.downloadJson(X,`${X.length}_logs_${Se}_to_${oe}.json`)}function I(X){Pe.call(this,n,X)}const A=()=>T();function P(X){f=X,t(1,f)}function R(X){f=X,t(1,f)}function N(X){f=X,t(1,f)}const U=X=>M(X),j=X=>o("select",X),V=(X,oe)=>{oe.code==="Enter"&&(oe.preventDefault(),o("select",X))},K=()=>t(0,r=""),J=()=>y(d+1),ee=()=>$();return n.$$set=X=>{"filter"in X&&t(0,r=X.filter),"presets"in X&&t(15,a=X.presets),"zoom"in X&&t(16,u=X.zoom),"sort"in X&&t(1,f=X.sort)},n.$$.update=()=>{n.$$.dirty[0]&98307&&(typeof f<"u"||typeof r<"u"||typeof a<"u"||typeof u<"u")&&(S(),y(1)),n.$$.dirty[0]&131072&&t(9,i=m>=Pc),n.$$.dirty[0]&16&&t(5,l=Object.keys(_).length),n.$$.dirty[0]&40&&t(8,s=c.length&&l===c.length)},[r,f,y,c,_,l,d,h,s,i,o,T,$,M,L,a,u,m,I,A,P,R,N,U,j,V,K,J,ee]}class F3 extends ye{constructor(e){super(),be(this,e,R3,P3,_e,{filter:0,presets:15,zoom:16,sort:1,load:2},null,[-1,-1])}get load(){return this.$$.ctx[2]}}/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function co(n){return n+.5|0}const Zi=(n,e,t)=>Math.max(Math.min(n,t),e);function Cs(n){return Zi(co(n*2.55),0,255)}function el(n){return Zi(co(n*255),0,255)}function Pi(n){return Zi(co(n/2.55)/100,0,1)}function Nc(n){return Zi(co(n*100),0,100)}const Yn={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Ya=[..."0123456789ABCDEF"],q3=n=>Ya[n&15],H3=n=>Ya[(n&240)>>4]+Ya[n&15],Io=n=>(n&240)>>4===(n&15),j3=n=>Io(n.r)&&Io(n.g)&&Io(n.b)&&Io(n.a);function z3(n){var e=n.length,t;return n[0]==="#"&&(e===4||e===5?t={r:255&Yn[n[1]]*17,g:255&Yn[n[2]]*17,b:255&Yn[n[3]]*17,a:e===5?Yn[n[4]]*17:255}:(e===7||e===9)&&(t={r:Yn[n[1]]<<4|Yn[n[2]],g:Yn[n[3]]<<4|Yn[n[4]],b:Yn[n[5]]<<4|Yn[n[6]],a:e===9?Yn[n[7]]<<4|Yn[n[8]]:255})),t}const U3=(n,e)=>n<255?e(n):"";function V3(n){var e=j3(n)?q3:H3;return n?"#"+e(n.r)+e(n.g)+e(n.b)+U3(n.a,e):void 0}const B3=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function ly(n,e,t){const i=e*Math.min(t,1-t),l=(s,o=(s+n/30)%12)=>t-i*Math.max(Math.min(o-3,9-o,1),-1);return[l(0),l(8),l(4)]}function W3(n,e,t){const i=(l,s=(l+n/60)%6)=>t-t*e*Math.max(Math.min(s,4-s,1),0);return[i(5),i(3),i(1)]}function Y3(n,e,t){const i=ly(n,1,.5);let l;for(e+t>1&&(l=1/(e+t),e*=l,t*=l),l=0;l<3;l++)i[l]*=1-e-t,i[l]+=e;return i}function K3(n,e,t,i,l){return n===l?(e-t)/i+(e.5?f/(2-s-o):f/(s+o),a=K3(t,i,l,f,s),a=a*60+.5),[a|0,u||0,r]}function Ru(n,e,t,i){return(Array.isArray(e)?n(e[0],e[1],e[2]):n(e,t,i)).map(el)}function Fu(n,e,t){return Ru(ly,n,e,t)}function J3(n,e,t){return Ru(Y3,n,e,t)}function Z3(n,e,t){return Ru(W3,n,e,t)}function sy(n){return(n%360+360)%360}function G3(n){const e=B3.exec(n);let t=255,i;if(!e)return;e[5]!==i&&(t=e[6]?Cs(+e[5]):el(+e[5]));const l=sy(+e[2]),s=+e[3]/100,o=+e[4]/100;return e[1]==="hwb"?i=J3(l,s,o):e[1]==="hsv"?i=Z3(l,s,o):i=Fu(l,s,o),{r:i[0],g:i[1],b:i[2],a:t}}function X3(n,e){var t=Nu(n);t[0]=sy(t[0]+e),t=Fu(t),n.r=t[0],n.g=t[1],n.b=t[2]}function Q3(n){if(!n)return;const e=Nu(n),t=e[0],i=Nc(e[1]),l=Nc(e[2]);return n.a<255?`hsla(${t}, ${i}%, ${l}%, ${Pi(n.a)})`:`hsl(${t}, ${i}%, ${l}%)`}const Rc={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Fc={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function x3(){const n={},e=Object.keys(Fc),t=Object.keys(Rc);let i,l,s,o,r;for(i=0;i>16&255,s>>8&255,s&255]}return n}let Lo;function eS(n){Lo||(Lo=x3(),Lo.transparent=[0,0,0,0]);const e=Lo[n.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:e.length===4?e[3]:255}}const tS=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function nS(n){const e=tS.exec(n);let t=255,i,l,s;if(e){if(e[7]!==i){const o=+e[7];t=e[8]?Cs(o):Zi(o*255,0,255)}return i=+e[1],l=+e[3],s=+e[5],i=255&(e[2]?Cs(i):Zi(i,0,255)),l=255&(e[4]?Cs(l):Zi(l,0,255)),s=255&(e[6]?Cs(s):Zi(s,0,255)),{r:i,g:l,b:s,a:t}}}function iS(n){return n&&(n.a<255?`rgba(${n.r}, ${n.g}, ${n.b}, ${Pi(n.a)})`:`rgb(${n.r}, ${n.g}, ${n.b})`)}const ua=n=>n<=.0031308?n*12.92:Math.pow(n,1/2.4)*1.055-.055,Wl=n=>n<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4);function lS(n,e,t){const i=Wl(Pi(n.r)),l=Wl(Pi(n.g)),s=Wl(Pi(n.b));return{r:el(ua(i+t*(Wl(Pi(e.r))-i))),g:el(ua(l+t*(Wl(Pi(e.g))-l))),b:el(ua(s+t*(Wl(Pi(e.b))-s))),a:n.a+t*(e.a-n.a)}}function Ao(n,e,t){if(n){let i=Nu(n);i[e]=Math.max(0,Math.min(i[e]+i[e]*t,e===0?360:1)),i=Fu(i),n.r=i[0],n.g=i[1],n.b=i[2]}}function oy(n,e){return n&&Object.assign(e||{},n)}function qc(n){var e={r:0,g:0,b:0,a:255};return Array.isArray(n)?n.length>=3&&(e={r:n[0],g:n[1],b:n[2],a:255},n.length>3&&(e.a=el(n[3]))):(e=oy(n,{r:0,g:0,b:0,a:1}),e.a=el(e.a)),e}function sS(n){return n.charAt(0)==="r"?nS(n):G3(n)}class Ks{constructor(e){if(e instanceof Ks)return e;const t=typeof e;let i;t==="object"?i=qc(e):t==="string"&&(i=z3(e)||eS(e)||sS(e)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var e=oy(this._rgb);return e&&(e.a=Pi(e.a)),e}set rgb(e){this._rgb=qc(e)}rgbString(){return this._valid?iS(this._rgb):void 0}hexString(){return this._valid?V3(this._rgb):void 0}hslString(){return this._valid?Q3(this._rgb):void 0}mix(e,t){if(e){const i=this.rgb,l=e.rgb;let s;const o=t===s?.5:t,r=2*o-1,a=i.a-l.a,u=((r*a===-1?r:(r+a)/(1+r*a))+1)/2;s=1-u,i.r=255&u*i.r+s*l.r+.5,i.g=255&u*i.g+s*l.g+.5,i.b=255&u*i.b+s*l.b+.5,i.a=o*i.a+(1-o)*l.a,this.rgb=i}return this}interpolate(e,t){return e&&(this._rgb=lS(this._rgb,e._rgb,t)),this}clone(){return new Ks(this.rgb)}alpha(e){return this._rgb.a=el(e),this}clearer(e){const t=this._rgb;return t.a*=1-e,this}greyscale(){const e=this._rgb,t=co(e.r*.3+e.g*.59+e.b*.11);return e.r=e.g=e.b=t,this}opaquer(e){const t=this._rgb;return t.a*=1+e,this}negate(){const e=this._rgb;return e.r=255-e.r,e.g=255-e.g,e.b=255-e.b,this}lighten(e){return Ao(this._rgb,2,e),this}darken(e){return Ao(this._rgb,2,-e),this}saturate(e){return Ao(this._rgb,1,e),this}desaturate(e){return Ao(this._rgb,1,-e),this}rotate(e){return X3(this._rgb,e),this}}/*! + * Chart.js v4.4.4 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + */function Ii(){}const oS=(()=>{let n=0;return()=>n++})();function Kt(n){return n===null||typeof n>"u"}function un(n){if(Array.isArray&&Array.isArray(n))return!0;const e=Object.prototype.toString.call(n);return e.slice(0,7)==="[object"&&e.slice(-6)==="Array]"}function bt(n){return n!==null&&Object.prototype.toString.call(n)==="[object Object]"}function wn(n){return(typeof n=="number"||n instanceof Number)&&isFinite(+n)}function _i(n,e){return wn(n)?n:e}function Ct(n,e){return typeof n>"u"?e:n}const rS=(n,e)=>typeof n=="string"&&n.endsWith("%")?parseFloat(n)/100*e:+n;function dt(n,e,t){if(n&&typeof n.call=="function")return n.apply(t,e)}function ht(n,e,t,i){let l,s,o;if(un(n))for(s=n.length,l=0;ln,x:n=>n.x,y:n=>n.y};function fS(n){const e=n.split("."),t=[];let i="";for(const l of e)i+=l,i.endsWith("\\")?i=i.slice(0,-1)+".":(t.push(i),i="");return t}function cS(n){const e=fS(n);return t=>{for(const i of e){if(i==="")break;t=t&&t[i]}return t}}function vr(n,e){return(Hc[e]||(Hc[e]=cS(e)))(n)}function qu(n){return n.charAt(0).toUpperCase()+n.slice(1)}const wr=n=>typeof n<"u",nl=n=>typeof n=="function",jc=(n,e)=>{if(n.size!==e.size)return!1;for(const t of n)if(!e.has(t))return!1;return!0};function dS(n){return n.type==="mouseup"||n.type==="click"||n.type==="contextmenu"}const kn=Math.PI,Ti=2*kn,pS=Ti+kn,Sr=Number.POSITIVE_INFINITY,mS=kn/180,oi=kn/2,yl=kn/4,zc=kn*2/3,Ka=Math.log10,il=Math.sign;function As(n,e,t){return Math.abs(n-e)l-s).pop(),e}function Zs(n){return!isNaN(parseFloat(n))&&isFinite(n)}function _S(n,e){const t=Math.round(n);return t-e<=n&&t+e>=n}function gS(n,e,t){let i,l,s;for(i=0,l=n.length;ia&&u=Math.min(e,t)-i&&n<=Math.max(e,t)+i}function Hu(n,e,t){t=t||(o=>n[o]1;)s=l+i>>1,t(s)?l=s:i=s;return{lo:l,hi:i}}const Cl=(n,e,t,i)=>Hu(n,t,i?l=>{const s=n[l][e];return sn[l][e]Hu(n,t,i=>n[i][e]>=t);function SS(n,e,t){let i=0,l=n.length;for(;ii&&n[l-1]>t;)l--;return i>0||l{const i="_onData"+qu(t),l=n[t];Object.defineProperty(n,t,{configurable:!0,enumerable:!1,value(...s){const o=l.apply(this,s);return n._chartjs.listeners.forEach(r=>{typeof r[i]=="function"&&r[i](...s)}),o}})})}function Bc(n,e){const t=n._chartjs;if(!t)return;const i=t.listeners,l=i.indexOf(e);l!==-1&&i.splice(l,1),!(i.length>0)&&(fy.forEach(s=>{delete n[s]}),delete n._chartjs)}function $S(n){const e=new Set(n);return e.size===n.length?n:Array.from(e)}const cy=function(){return typeof window>"u"?function(n){return n()}:window.requestAnimationFrame}();function dy(n,e){let t=[],i=!1;return function(...l){t=l,i||(i=!0,cy.call(window,()=>{i=!1,n.apply(e,t)}))}}function CS(n,e){let t;return function(...i){return e?(clearTimeout(t),t=setTimeout(n,e,i)):n.apply(this,i),e}}const OS=n=>n==="start"?"left":n==="end"?"right":"center",Wc=(n,e,t)=>n==="start"?e:n==="end"?t:(e+t)/2;function ES(n,e,t){const i=e.length;let l=0,s=i;if(n._sorted){const{iScale:o,_parsed:r}=n,a=o.axis,{min:u,max:f,minDefined:c,maxDefined:d}=o.getUserBounds();c&&(l=ri(Math.min(Cl(r,a,u).lo,t?i:Cl(e,a,o.getPixelForValue(u)).lo),0,i-1)),d?s=ri(Math.max(Cl(r,o.axis,f,!0).hi+1,t?0:Cl(e,a,o.getPixelForValue(f),!0).hi+1),l,i)-l:s=i-l}return{start:l,count:s}}function MS(n){const{xScale:e,yScale:t,_scaleRanges:i}=n,l={xmin:e.min,xmax:e.max,ymin:t.min,ymax:t.max};if(!i)return n._scaleRanges=l,!0;const s=i.xmin!==e.min||i.xmax!==e.max||i.ymin!==t.min||i.ymax!==t.max;return Object.assign(i,l),s}const Po=n=>n===0||n===1,Yc=(n,e,t)=>-(Math.pow(2,10*(n-=1))*Math.sin((n-e)*Ti/t)),Kc=(n,e,t)=>Math.pow(2,-10*n)*Math.sin((n-e)*Ti/t)+1,Ps={linear:n=>n,easeInQuad:n=>n*n,easeOutQuad:n=>-n*(n-2),easeInOutQuad:n=>(n/=.5)<1?.5*n*n:-.5*(--n*(n-2)-1),easeInCubic:n=>n*n*n,easeOutCubic:n=>(n-=1)*n*n+1,easeInOutCubic:n=>(n/=.5)<1?.5*n*n*n:.5*((n-=2)*n*n+2),easeInQuart:n=>n*n*n*n,easeOutQuart:n=>-((n-=1)*n*n*n-1),easeInOutQuart:n=>(n/=.5)<1?.5*n*n*n*n:-.5*((n-=2)*n*n*n-2),easeInQuint:n=>n*n*n*n*n,easeOutQuint:n=>(n-=1)*n*n*n*n+1,easeInOutQuint:n=>(n/=.5)<1?.5*n*n*n*n*n:.5*((n-=2)*n*n*n*n+2),easeInSine:n=>-Math.cos(n*oi)+1,easeOutSine:n=>Math.sin(n*oi),easeInOutSine:n=>-.5*(Math.cos(kn*n)-1),easeInExpo:n=>n===0?0:Math.pow(2,10*(n-1)),easeOutExpo:n=>n===1?1:-Math.pow(2,-10*n)+1,easeInOutExpo:n=>Po(n)?n:n<.5?.5*Math.pow(2,10*(n*2-1)):.5*(-Math.pow(2,-10*(n*2-1))+2),easeInCirc:n=>n>=1?n:-(Math.sqrt(1-n*n)-1),easeOutCirc:n=>Math.sqrt(1-(n-=1)*n),easeInOutCirc:n=>(n/=.5)<1?-.5*(Math.sqrt(1-n*n)-1):.5*(Math.sqrt(1-(n-=2)*n)+1),easeInElastic:n=>Po(n)?n:Yc(n,.075,.3),easeOutElastic:n=>Po(n)?n:Kc(n,.075,.3),easeInOutElastic(n){return Po(n)?n:n<.5?.5*Yc(n*2,.1125,.45):.5+.5*Kc(n*2-1,.1125,.45)},easeInBack(n){return n*n*((1.70158+1)*n-1.70158)},easeOutBack(n){return(n-=1)*n*((1.70158+1)*n+1.70158)+1},easeInOutBack(n){let e=1.70158;return(n/=.5)<1?.5*(n*n*(((e*=1.525)+1)*n-e)):.5*((n-=2)*n*(((e*=1.525)+1)*n+e)+2)},easeInBounce:n=>1-Ps.easeOutBounce(1-n),easeOutBounce(n){return n<1/2.75?7.5625*n*n:n<2/2.75?7.5625*(n-=1.5/2.75)*n+.75:n<2.5/2.75?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375},easeInOutBounce:n=>n<.5?Ps.easeInBounce(n*2)*.5:Ps.easeOutBounce(n*2-1)*.5+.5};function ju(n){if(n&&typeof n=="object"){const e=n.toString();return e==="[object CanvasPattern]"||e==="[object CanvasGradient]"}return!1}function Jc(n){return ju(n)?n:new Ks(n)}function fa(n){return ju(n)?n:new Ks(n).saturate(.5).darken(.1).hexString()}const DS=["x","y","borderWidth","radius","tension"],IS=["color","borderColor","backgroundColor"];function LS(n){n.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),n.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:e=>e!=="onProgress"&&e!=="onComplete"&&e!=="fn"}),n.set("animations",{colors:{type:"color",properties:IS},numbers:{type:"number",properties:DS}}),n.describe("animations",{_fallback:"animation"}),n.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:e=>e|0}}}})}function AS(n){n.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}const Zc=new Map;function PS(n,e){e=e||{};const t=n+JSON.stringify(e);let i=Zc.get(t);return i||(i=new Intl.NumberFormat(n,e),Zc.set(t,i)),i}function py(n,e,t){return PS(e,t).format(n)}const my={values(n){return un(n)?n:""+n},numeric(n,e,t){if(n===0)return"0";const i=this.chart.options.locale;let l,s=n;if(t.length>1){const u=Math.max(Math.abs(t[0].value),Math.abs(t[t.length-1].value));(u<1e-4||u>1e15)&&(l="scientific"),s=NS(n,t)}const o=Ka(Math.abs(s)),r=isNaN(o)?1:Math.max(Math.min(-1*Math.floor(o),20),0),a={notation:l,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(a,this.options.ticks.format),py(n,i,a)},logarithmic(n,e,t){if(n===0)return"0";const i=t[e].significand||n/Math.pow(10,Math.floor(Ka(n)));return[1,2,3,5,10,15].includes(i)||e>.8*t.length?my.numeric.call(this,n,e,t):""}};function NS(n,e){let t=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;return Math.abs(t)>=1&&n!==Math.floor(n)&&(t=n-Math.floor(n)),t}var hy={formatters:my};function RS(n){n.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(e,t)=>t.lineWidth,tickColor:(e,t)=>t.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:hy.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),n.route("scale.ticks","color","","color"),n.route("scale.grid","color","","borderColor"),n.route("scale.border","color","","borderColor"),n.route("scale.title","color","","color"),n.describe("scale",{_fallback:!1,_scriptable:e=>!e.startsWith("before")&&!e.startsWith("after")&&e!=="callback"&&e!=="parser",_indexable:e=>e!=="borderDash"&&e!=="tickBorderDash"&&e!=="dash"}),n.describe("scales",{_fallback:"scale"}),n.describe("scale.ticks",{_scriptable:e=>e!=="backdropPadding"&&e!=="callback",_indexable:e=>e!=="backdropPadding"})}const Ll=Object.create(null),Za=Object.create(null);function Ns(n,e){if(!e)return n;const t=e.split(".");for(let i=0,l=t.length;ii.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(i,l)=>fa(l.backgroundColor),this.hoverBorderColor=(i,l)=>fa(l.borderColor),this.hoverColor=(i,l)=>fa(l.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(e),this.apply(t)}set(e,t){return ca(this,e,t)}get(e){return Ns(this,e)}describe(e,t){return ca(Za,e,t)}override(e,t){return ca(Ll,e,t)}route(e,t,i,l){const s=Ns(this,e),o=Ns(this,i),r="_"+t;Object.defineProperties(s,{[r]:{value:s[t],writable:!0},[t]:{enumerable:!0,get(){const a=this[r],u=o[l];return bt(a)?Object.assign({},u,a):Ct(a,u)},set(a){this[r]=a}}})}apply(e){e.forEach(t=>t(this))}}var nn=new FS({_scriptable:n=>!n.startsWith("on"),_indexable:n=>n!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[LS,AS,RS]);function qS(n){return!n||Kt(n.size)||Kt(n.family)?null:(n.style?n.style+" ":"")+(n.weight?n.weight+" ":"")+n.size+"px "+n.family}function Gc(n,e,t,i,l){let s=e[l];return s||(s=e[l]=n.measureText(l).width,t.push(l)),s>i&&(i=s),i}function kl(n,e,t){const i=n.currentDevicePixelRatio,l=t!==0?Math.max(t/2,.5):0;return Math.round((e-l)*i)/i+l}function Xc(n,e){!e&&!n||(e=e||n.getContext("2d"),e.save(),e.resetTransform(),e.clearRect(0,0,n.width,n.height),e.restore())}function Ga(n,e,t,i){HS(n,e,t,i)}function HS(n,e,t,i,l){let s,o,r,a,u,f,c,d;const m=e.pointStyle,h=e.rotation,g=e.radius;let _=(h||0)*mS;if(m&&typeof m=="object"&&(s=m.toString(),s==="[object HTMLImageElement]"||s==="[object HTMLCanvasElement]")){n.save(),n.translate(t,i),n.rotate(_),n.drawImage(m,-m.width/2,-m.height/2,m.width,m.height),n.restore();return}if(!(isNaN(g)||g<=0)){switch(n.beginPath(),m){default:n.arc(t,i,g,0,Ti),n.closePath();break;case"triangle":f=g,n.moveTo(t+Math.sin(_)*f,i-Math.cos(_)*g),_+=zc,n.lineTo(t+Math.sin(_)*f,i-Math.cos(_)*g),_+=zc,n.lineTo(t+Math.sin(_)*f,i-Math.cos(_)*g),n.closePath();break;case"rectRounded":u=g*.516,a=g-u,o=Math.cos(_+yl)*a,c=Math.cos(_+yl)*a,r=Math.sin(_+yl)*a,d=Math.sin(_+yl)*a,n.arc(t-c,i-r,u,_-kn,_-oi),n.arc(t+d,i-o,u,_-oi,_),n.arc(t+c,i+r,u,_,_+oi),n.arc(t-d,i+o,u,_+oi,_+kn),n.closePath();break;case"rect":if(!h){a=Math.SQRT1_2*g,f=a,n.rect(t-f,i-a,2*f,2*a);break}_+=yl;case"rectRot":c=Math.cos(_)*g,o=Math.cos(_)*g,r=Math.sin(_)*g,d=Math.sin(_)*g,n.moveTo(t-c,i-r),n.lineTo(t+d,i-o),n.lineTo(t+c,i+r),n.lineTo(t-d,i+o),n.closePath();break;case"crossRot":_+=yl;case"cross":c=Math.cos(_)*g,o=Math.cos(_)*g,r=Math.sin(_)*g,d=Math.sin(_)*g,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o);break;case"star":c=Math.cos(_)*g,o=Math.cos(_)*g,r=Math.sin(_)*g,d=Math.sin(_)*g,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o),_+=yl,c=Math.cos(_)*g,o=Math.cos(_)*g,r=Math.sin(_)*g,d=Math.sin(_)*g,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o);break;case"line":o=Math.cos(_)*g,r=Math.sin(_)*g,n.moveTo(t-o,i-r),n.lineTo(t+o,i+r);break;case"dash":n.moveTo(t,i),n.lineTo(t+Math.cos(_)*g,i+Math.sin(_)*g);break;case!1:n.closePath();break}n.fill(),e.borderWidth>0&&n.stroke()}}function Gs(n,e,t){return t=t||.5,!e||n&&n.x>e.left-t&&n.xe.top-t&&n.y0&&s.strokeColor!=="";let a,u;for(n.save(),n.font=l.string,US(n,s),a=0;a+n||0;function _y(n,e){const t={},i=bt(e),l=i?Object.keys(e):e,s=bt(n)?i?o=>Ct(n[o],n[e[o]]):o=>n[o]:()=>n;for(const o of l)t[o]=JS(s(o));return t}function ZS(n){return _y(n,{top:"y",right:"x",bottom:"y",left:"x"})}function lr(n){return _y(n,["topLeft","topRight","bottomLeft","bottomRight"])}function ll(n){const e=ZS(n);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(n,e){n=n||{},e=e||nn.font;let t=Ct(n.size,e.size);typeof t=="string"&&(t=parseInt(t,10));let i=Ct(n.style,e.style);i&&!(""+i).match(YS)&&(console.warn('Invalid font style specified: "'+i+'"'),i=void 0);const l={family:Ct(n.family,e.family),lineHeight:KS(Ct(n.lineHeight,e.lineHeight),t),size:t,style:i,weight:Ct(n.weight,e.weight),string:""};return l.string=qS(l),l}function No(n,e,t,i){let l,s,o;for(l=0,s=n.length;lt&&r===0?0:r+a;return{min:o(i,-Math.abs(s)),max:o(l,s)}}function Nl(n,e){return Object.assign(Object.create(n),e)}function Vu(n,e=[""],t,i,l=()=>n[0]){const s=t||n;typeof i>"u"&&(i=ky("_fallback",n));const o={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:n,_rootScopes:s,_fallback:i,_getTarget:l,override:r=>Vu([r,...n],e,s,i)};return new Proxy(o,{deleteProperty(r,a){return delete r[a],delete r._keys,delete n[0][a],!0},get(r,a){return by(r,a,()=>l4(a,e,n,r))},getOwnPropertyDescriptor(r,a){return Reflect.getOwnPropertyDescriptor(r._scopes[0],a)},getPrototypeOf(){return Reflect.getPrototypeOf(n[0])},has(r,a){return td(r).includes(a)},ownKeys(r){return td(r)},set(r,a,u){const f=r._storage||(r._storage=l());return r[a]=f[a]=u,delete r._keys,!0}})}function is(n,e,t,i){const l={_cacheable:!1,_proxy:n,_context:e,_subProxy:t,_stack:new Set,_descriptors:gy(n,i),setContext:s=>is(n,s,t,i),override:s=>is(n.override(s),e,t,i)};return new Proxy(l,{deleteProperty(s,o){return delete s[o],delete n[o],!0},get(s,o,r){return by(s,o,()=>QS(s,o,r))},getOwnPropertyDescriptor(s,o){return s._descriptors.allKeys?Reflect.has(n,o)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(n,o)},getPrototypeOf(){return Reflect.getPrototypeOf(n)},has(s,o){return Reflect.has(n,o)},ownKeys(){return Reflect.ownKeys(n)},set(s,o,r){return n[o]=r,delete s[o],!0}})}function gy(n,e={scriptable:!0,indexable:!0}){const{_scriptable:t=e.scriptable,_indexable:i=e.indexable,_allKeys:l=e.allKeys}=n;return{allKeys:l,scriptable:t,indexable:i,isScriptable:nl(t)?t:()=>t,isIndexable:nl(i)?i:()=>i}}const XS=(n,e)=>n?n+qu(e):e,Bu=(n,e)=>bt(e)&&n!=="adapters"&&(Object.getPrototypeOf(e)===null||e.constructor===Object);function by(n,e,t){if(Object.prototype.hasOwnProperty.call(n,e)||e==="constructor")return n[e];const i=t();return n[e]=i,i}function QS(n,e,t){const{_proxy:i,_context:l,_subProxy:s,_descriptors:o}=n;let r=i[e];return nl(r)&&o.isScriptable(e)&&(r=xS(e,r,n,t)),un(r)&&r.length&&(r=e4(e,r,n,o.isIndexable)),Bu(e,r)&&(r=is(r,l,s&&s[e],o)),r}function xS(n,e,t,i){const{_proxy:l,_context:s,_subProxy:o,_stack:r}=t;if(r.has(n))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+n);r.add(n);let a=e(s,o||i);return r.delete(n),Bu(n,a)&&(a=Wu(l._scopes,l,n,a)),a}function e4(n,e,t,i){const{_proxy:l,_context:s,_subProxy:o,_descriptors:r}=t;if(typeof s.index<"u"&&i(n))return e[s.index%e.length];if(bt(e[0])){const a=e,u=l._scopes.filter(f=>f!==a);e=[];for(const f of a){const c=Wu(u,l,n,f);e.push(is(c,s,o&&o[n],r))}}return e}function yy(n,e,t){return nl(n)?n(e,t):n}const t4=(n,e)=>n===!0?e:typeof n=="string"?vr(e,n):void 0;function n4(n,e,t,i,l){for(const s of e){const o=t4(t,s);if(o){n.add(o);const r=yy(o._fallback,t,l);if(typeof r<"u"&&r!==t&&r!==i)return r}else if(o===!1&&typeof i<"u"&&t!==i)return null}return!1}function Wu(n,e,t,i){const l=e._rootScopes,s=yy(e._fallback,t,i),o=[...n,...l],r=new Set;r.add(i);let a=ed(r,o,t,s||t,i);return a===null||typeof s<"u"&&s!==t&&(a=ed(r,o,s,a,i),a===null)?!1:Vu(Array.from(r),[""],l,s,()=>i4(e,t,i))}function ed(n,e,t,i,l){for(;t;)t=n4(n,e,t,i,l);return t}function i4(n,e,t){const i=n._getTarget();e in i||(i[e]={});const l=i[e];return un(l)&&bt(t)?t:l||{}}function l4(n,e,t,i){let l;for(const s of e)if(l=ky(XS(s,n),t),typeof l<"u")return Bu(n,l)?Wu(t,i,n,l):l}function ky(n,e){for(const t of e){if(!t)continue;const i=t[n];if(typeof i<"u")return i}}function td(n){let e=n._keys;return e||(e=n._keys=s4(n._scopes)),e}function s4(n){const e=new Set;for(const t of n)for(const i of Object.keys(t).filter(l=>!l.startsWith("_")))e.add(i);return Array.from(e)}const o4=Number.EPSILON||1e-14,ls=(n,e)=>en==="x"?"y":"x";function r4(n,e,t,i){const l=n.skip?e:n,s=e,o=t.skip?e:t,r=Ja(s,l),a=Ja(o,s);let u=r/(r+a),f=a/(r+a);u=isNaN(u)?0:u,f=isNaN(f)?0:f;const c=i*u,d=i*f;return{previous:{x:s.x-c*(o.x-l.x),y:s.y-c*(o.y-l.y)},next:{x:s.x+d*(o.x-l.x),y:s.y+d*(o.y-l.y)}}}function a4(n,e,t){const i=n.length;let l,s,o,r,a,u=ls(n,0);for(let f=0;f!u.skip)),e.cubicInterpolationMode==="monotone")f4(n,l);else{let u=i?n[n.length-1]:n[0];for(s=0,o=n.length;sn.ownerDocument.defaultView.getComputedStyle(n,null);function p4(n,e){return jr(n).getPropertyValue(e)}const m4=["top","right","bottom","left"];function El(n,e,t){const i={};t=t?"-"+t:"";for(let l=0;l<4;l++){const s=m4[l];i[s]=parseFloat(n[e+"-"+s+t])||0}return i.width=i.left+i.right,i.height=i.top+i.bottom,i}const h4=(n,e,t)=>(n>0||e>0)&&(!t||!t.shadowRoot);function _4(n,e){const t=n.touches,i=t&&t.length?t[0]:n,{offsetX:l,offsetY:s}=i;let o=!1,r,a;if(h4(l,s,n.target))r=l,a=s;else{const u=e.getBoundingClientRect();r=i.clientX-u.left,a=i.clientY-u.top,o=!0}return{x:r,y:a,box:o}}function ki(n,e){if("native"in n)return n;const{canvas:t,currentDevicePixelRatio:i}=e,l=jr(t),s=l.boxSizing==="border-box",o=El(l,"padding"),r=El(l,"border","width"),{x:a,y:u,box:f}=_4(n,t),c=o.left+(f&&r.left),d=o.top+(f&&r.top);let{width:m,height:h}=e;return s&&(m-=o.width+r.width,h-=o.height+r.height),{x:Math.round((a-c)/m*t.width/i),y:Math.round((u-d)/h*t.height/i)}}function g4(n,e,t){let i,l;if(e===void 0||t===void 0){const s=n&&Ku(n);if(!s)e=n.clientWidth,t=n.clientHeight;else{const o=s.getBoundingClientRect(),r=jr(s),a=El(r,"border","width"),u=El(r,"padding");e=o.width-u.width-a.width,t=o.height-u.height-a.height,i=Tr(r.maxWidth,s,"clientWidth"),l=Tr(r.maxHeight,s,"clientHeight")}}return{width:e,height:t,maxWidth:i||Sr,maxHeight:l||Sr}}const Fo=n=>Math.round(n*10)/10;function b4(n,e,t,i){const l=jr(n),s=El(l,"margin"),o=Tr(l.maxWidth,n,"clientWidth")||Sr,r=Tr(l.maxHeight,n,"clientHeight")||Sr,a=g4(n,e,t);let{width:u,height:f}=a;if(l.boxSizing==="content-box"){const d=El(l,"border","width"),m=El(l,"padding");u-=m.width+d.width,f-=m.height+d.height}return u=Math.max(0,u-s.width),f=Math.max(0,i?u/i:f-s.height),u=Fo(Math.min(u,o,a.maxWidth)),f=Fo(Math.min(f,r,a.maxHeight)),u&&!f&&(f=Fo(u/2)),(e!==void 0||t!==void 0)&&i&&a.height&&f>a.height&&(f=a.height,u=Fo(Math.floor(f*i))),{width:u,height:f}}function nd(n,e,t){const i=e||1,l=Math.floor(n.height*i),s=Math.floor(n.width*i);n.height=Math.floor(n.height),n.width=Math.floor(n.width);const o=n.canvas;return o.style&&(t||!o.style.height&&!o.style.width)&&(o.style.height=`${n.height}px`,o.style.width=`${n.width}px`),n.currentDevicePixelRatio!==i||o.height!==l||o.width!==s?(n.currentDevicePixelRatio=i,o.height=l,o.width=s,n.ctx.setTransform(i,0,0,i,0,0),!0):!1}const y4=function(){let n=!1;try{const e={get passive(){return n=!0,!1}};Yu()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch{}return n}();function id(n,e){const t=p4(n,e),i=t&&t.match(/^(\d+)(\.\d+)?px$/);return i?+i[1]:void 0}function wl(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:n.y+t*(e.y-n.y)}}function k4(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:i==="middle"?t<.5?n.y:e.y:i==="after"?t<1?n.y:e.y:t>0?e.y:n.y}}function v4(n,e,t,i){const l={x:n.cp2x,y:n.cp2y},s={x:e.cp1x,y:e.cp1y},o=wl(n,l,t),r=wl(l,s,t),a=wl(s,e,t),u=wl(o,r,t),f=wl(r,a,t);return wl(u,f,t)}const w4=function(n,e){return{x(t){return n+n+e-t},setWidth(t){e=t},textAlign(t){return t==="center"?t:t==="right"?"left":"right"},xPlus(t,i){return t-i},leftForLtr(t,i){return t-i}}},S4=function(){return{x(n){return n},setWidth(n){},textAlign(n){return n},xPlus(n,e){return n+e},leftForLtr(n,e){return n}}};function da(n,e,t){return n?w4(e,t):S4()}function T4(n,e){let t,i;(e==="ltr"||e==="rtl")&&(t=n.canvas.style,i=[t.getPropertyValue("direction"),t.getPropertyPriority("direction")],t.setProperty("direction",e,"important"),n.prevTextDirection=i)}function $4(n,e){e!==void 0&&(delete n.prevTextDirection,n.canvas.style.setProperty("direction",e[0],e[1]))}function wy(n){return n==="angle"?{between:ay,compare:kS,normalize:yi}:{between:uy,compare:(e,t)=>e-t,normalize:e=>e}}function ld({start:n,end:e,count:t,loop:i,style:l}){return{start:n%t,end:e%t,loop:i&&(e-n+1)%t===0,style:l}}function C4(n,e,t){const{property:i,start:l,end:s}=t,{between:o,normalize:r}=wy(i),a=e.length;let{start:u,end:f,loop:c}=n,d,m;if(c){for(u+=a,f+=a,d=0,m=a;da(l,T,y)&&r(l,T)!==0,E=()=>r(s,y)===0||a(s,T,y),M=()=>g||$(),L=()=>!g||E();for(let I=f,A=f;I<=c;++I)S=e[I%o],!S.skip&&(y=u(S[i]),y!==T&&(g=a(y,l,s),_===null&&M()&&(_=r(y,l)===0?I:A),_!==null&&L()&&(h.push(ld({start:_,end:I,loop:d,count:o,style:m})),_=null),A=I,T=y));return _!==null&&h.push(ld({start:_,end:c,loop:d,count:o,style:m})),h}function Ty(n,e){const t=[],i=n.segments;for(let l=0;ll&&n[s%e].skip;)s--;return s%=e,{start:l,end:s}}function E4(n,e,t,i){const l=n.length,s=[];let o=e,r=n[e],a;for(a=e+1;a<=t;++a){const u=n[a%l];u.skip||u.stop?r.skip||(i=!1,s.push({start:e%l,end:(a-1)%l,loop:i}),e=o=u.stop?a:null):(o=a,r.skip&&(e=a)),r=u}return o!==null&&s.push({start:e%l,end:o%l,loop:i}),s}function M4(n,e){const t=n.points,i=n.options.spanGaps,l=t.length;if(!l)return[];const s=!!n._loop,{start:o,end:r}=O4(t,l,s,i);if(i===!0)return sd(n,[{start:o,end:r,loop:s}],t,e);const a=rr({chart:e,initial:t.initial,numSteps:o,currentStep:Math.min(i-t.start,o)}))}_refresh(){this._request||(this._running=!0,this._request=cy.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let t=0;this._charts.forEach((i,l)=>{if(!i.running||!i.items.length)return;const s=i.items;let o=s.length-1,r=!1,a;for(;o>=0;--o)a=s[o],a._active?(a._total>i.duration&&(i.duration=a._total),a.tick(e),r=!0):(s[o]=s[s.length-1],s.pop());r&&(l.draw(),this._notify(l,i,e,"progress")),s.length||(i.running=!1,this._notify(l,i,e,"complete"),i.initial=!1),t+=s.length}),this._lastDate=e,t===0&&(this._running=!1)}_getAnims(e){const t=this._charts;let i=t.get(e);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},t.set(e,i)),i}listen(e,t,i){this._getAnims(e).listeners[t].push(i)}add(e,t){!t||!t.length||this._getAnims(e).items.push(...t)}has(e){return this._getAnims(e).items.length>0}start(e){const t=this._charts.get(e);t&&(t.running=!0,t.start=Date.now(),t.duration=t.items.reduce((i,l)=>Math.max(i,l._duration),0),this._refresh())}running(e){if(!this._running)return!1;const t=this._charts.get(e);return!(!t||!t.running||!t.items.length)}stop(e){const t=this._charts.get(e);if(!t||!t.items.length)return;const i=t.items;let l=i.length-1;for(;l>=0;--l)i[l].cancel();t.items=[],this._notify(e,t,Date.now(),"complete")}remove(e){return this._charts.delete(e)}}var Li=new L4;const rd="transparent",A4={boolean(n,e,t){return t>.5?e:n},color(n,e,t){const i=Jc(n||rd),l=i.valid&&Jc(e||rd);return l&&l.valid?l.mix(i,t).hexString():e},number(n,e,t){return n+(e-n)*t}};class P4{constructor(e,t,i,l){const s=t[i];l=No([e.to,l,s,e.from]);const o=No([e.from,s,l]);this._active=!0,this._fn=e.fn||A4[e.type||typeof o],this._easing=Ps[e.easing]||Ps.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=t,this._prop=i,this._from=o,this._to=l,this._promises=void 0}active(){return this._active}update(e,t,i){if(this._active){this._notify(!1);const l=this._target[this._prop],s=i-this._start,o=this._duration-s;this._start=i,this._duration=Math.floor(Math.max(o,e.duration)),this._total+=s,this._loop=!!e.loop,this._to=No([e.to,t,l,e.from]),this._from=No([e.from,l,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){const t=e-this._start,i=this._duration,l=this._prop,s=this._from,o=this._loop,r=this._to;let a;if(this._active=s!==r&&(o||t1?2-a:a,a=this._easing(Math.min(1,Math.max(0,a))),this._target[l]=this._fn(s,r,a)}wait(){const e=this._promises||(this._promises=[]);return new Promise((t,i)=>{e.push({res:t,rej:i})})}_notify(e){const t=e?"res":"rej",i=this._promises||[];for(let l=0;l{const s=e[l];if(!bt(s))return;const o={};for(const r of t)o[r]=s[r];(un(s.properties)&&s.properties||[l]).forEach(r=>{(r===l||!i.has(r))&&i.set(r,o)})})}_animateOptions(e,t){const i=t.options,l=R4(e,i);if(!l)return[];const s=this._createAnimations(l,i);return i.$shared&&N4(e.options.$animations,i).then(()=>{e.options=i},()=>{}),s}_createAnimations(e,t){const i=this._properties,l=[],s=e.$animations||(e.$animations={}),o=Object.keys(t),r=Date.now();let a;for(a=o.length-1;a>=0;--a){const u=o[a];if(u.charAt(0)==="$")continue;if(u==="options"){l.push(...this._animateOptions(e,t));continue}const f=t[u];let c=s[u];const d=i.get(u);if(c)if(d&&c.active()){c.update(d,f,r);continue}else c.cancel();if(!d||!d.duration){e[u]=f;continue}s[u]=c=new P4(d,e,u,f),l.push(c)}return l}update(e,t){if(this._properties.size===0){Object.assign(e,t);return}const i=this._createAnimations(e,t);if(i.length)return Li.add(this._chart,i),!0}}function N4(n,e){const t=[],i=Object.keys(e);for(let l=0;l0||!t&&s<0)return l.index}return null}function dd(n,e){const{chart:t,_cachedMeta:i}=n,l=t._stacks||(t._stacks={}),{iScale:s,vScale:o,index:r}=i,a=s.axis,u=o.axis,f=j4(s,o,i),c=e.length;let d;for(let m=0;mt[i].axis===e).shift()}function V4(n,e){return Nl(n,{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:"default",type:"dataset"})}function B4(n,e,t){return Nl(n,{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:t,index:e,mode:"default",type:"data"})}function ys(n,e){const t=n.controller.index,i=n.vScale&&n.vScale.axis;if(i){e=e||n._parsed;for(const l of e){const s=l._stacks;if(!s||s[i]===void 0||s[i][t]===void 0)return;delete s[i][t],s[i]._visualValues!==void 0&&s[i]._visualValues[t]!==void 0&&delete s[i]._visualValues[t]}}}const ma=n=>n==="reset"||n==="none",pd=(n,e)=>e?n:Object.assign({},n),W4=(n,e,t)=>n&&!e.hidden&&e._stacked&&{keys:Cy(t,!0),values:null};class Rs{constructor(e,t){this.chart=e,this._ctx=e.ctx,this.index=t,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=fd(e.vScale,e),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(e){this.index!==e&&ys(this._cachedMeta),this.index=e}linkScales(){const e=this.chart,t=this._cachedMeta,i=this.getDataset(),l=(c,d,m,h)=>c==="x"?d:c==="r"?h:m,s=t.xAxisID=Ct(i.xAxisID,pa(e,"x")),o=t.yAxisID=Ct(i.yAxisID,pa(e,"y")),r=t.rAxisID=Ct(i.rAxisID,pa(e,"r")),a=t.indexAxis,u=t.iAxisID=l(a,s,o,r),f=t.vAxisID=l(a,o,s,r);t.xScale=this.getScaleForId(s),t.yScale=this.getScaleForId(o),t.rScale=this.getScaleForId(r),t.iScale=this.getScaleForId(u),t.vScale=this.getScaleForId(f)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){const t=this._cachedMeta;return e===t.iScale?t.vScale:t.iScale}reset(){this._update("reset")}_destroy(){const e=this._cachedMeta;this._data&&Bc(this._data,this),e._stacked&&ys(e)}_dataCheck(){const e=this.getDataset(),t=e.data||(e.data=[]),i=this._data;if(bt(t)){const l=this._cachedMeta;this._data=H4(t,l)}else if(i!==t){if(i){Bc(i,this);const l=this._cachedMeta;ys(l),l._parsed=[]}t&&Object.isExtensible(t)&&TS(t,this),this._syncList=[],this._data=t}}addElements(){const e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){const t=this._cachedMeta,i=this.getDataset();let l=!1;this._dataCheck();const s=t._stacked;t._stacked=fd(t.vScale,t),t.stack!==i.stack&&(l=!0,ys(t),t.stack=i.stack),this._resyncElements(e),(l||s!==t._stacked)&&dd(this,t._parsed)}configure(){const e=this.chart.config,t=e.datasetScopeKeys(this._type),i=e.getOptionScopes(this.getDataset(),t,!0);this.options=e.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,t){const{_cachedMeta:i,_data:l}=this,{iScale:s,_stacked:o}=i,r=s.axis;let a=e===0&&t===l.length?!0:i._sorted,u=e>0&&i._parsed[e-1],f,c,d;if(this._parsing===!1)i._parsed=l,i._sorted=!0,d=l;else{un(l[e])?d=this.parseArrayData(i,l,e,t):bt(l[e])?d=this.parseObjectData(i,l,e,t):d=this.parsePrimitiveData(i,l,e,t);const m=()=>c[r]===null||u&&c[r]g||c=0;--d)if(!h()){this.updateRangeFromParsed(u,e,m,a);break}}return u}getAllParsedValues(e){const t=this._cachedMeta._parsed,i=[];let l,s,o;for(l=0,s=t.length;l=0&&ethis.getContext(i,l,t),g=u.resolveNamedOptions(d,m,h,c);return g.$shared&&(g.$shared=a,s[o]=Object.freeze(pd(g,a))),g}_resolveAnimations(e,t,i){const l=this.chart,s=this._cachedDataOpts,o=`animation-${t}`,r=s[o];if(r)return r;let a;if(l.options.animation!==!1){const f=this.chart.config,c=f.datasetAnimationScopeKeys(this._type,t),d=f.getOptionScopes(this.getDataset(),c);a=f.createResolver(d,this.getContext(e,i,t))}const u=new $y(l,a&&a.animations);return a&&a._cacheable&&(s[o]=Object.freeze(u)),u}getSharedOptions(e){if(e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,t){return!t||ma(e)||this.chart._animationsDisabled}_getSharedOptions(e,t){const i=this.resolveDataElementOptions(e,t),l=this._sharedOptions,s=this.getSharedOptions(i),o=this.includeOptions(t,s)||s!==l;return this.updateSharedOptions(s,t,i),{sharedOptions:s,includeOptions:o}}updateElement(e,t,i,l){ma(l)?Object.assign(e,i):this._resolveAnimations(t,l).update(e,i)}updateSharedOptions(e,t,i){e&&!ma(t)&&this._resolveAnimations(void 0,t).update(e,i)}_setStyle(e,t,i,l){e.active=l;const s=this.getStyle(t,l);this._resolveAnimations(t,i,l).update(e,{options:!l&&this.getSharedOptions(s)||s})}removeHoverStyle(e,t,i){this._setStyle(e,i,"active",!1)}setHoverStyle(e,t,i){this._setStyle(e,i,"active",!0)}_removeDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!1)}_setDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!0)}_resyncElements(e){const t=this._data,i=this._cachedMeta.data;for(const[r,a,u]of this._syncList)this[r](a,u);this._syncList=[];const l=i.length,s=t.length,o=Math.min(s,l);o&&this.parse(0,o),s>l?this._insertElements(l,s-l,e):s{for(u.length+=t,r=u.length-1;r>=o;r--)u[r]=u[r-t]};for(a(s),r=e;r0&&this.getParsed(t-1);for(let E=0;E=S){L.skip=!0;continue}const I=this.getParsed(E),A=Kt(I[m]),P=L[d]=o.getPixelForValue(I[d],E),R=L[m]=s||A?r.getBasePixel():r.getPixelForValue(a?this.applyStack(r,I,a):I[m],E);L.skip=isNaN(P)||isNaN(R)||A,L.stop=E>0&&Math.abs(I[d]-$[d])>_,g&&(L.parsed=I,L.raw=u.data[E]),c&&(L.options=f||this.resolveDataElementOptions(E,M.active?"active":l)),y||this.updateElement(M,E,L,l),$=I}}getMaxOverflow(){const e=this._cachedMeta,t=e.dataset,i=t.options&&t.options.borderWidth||0,l=e.data||[];if(!l.length)return i;const s=l[0].size(this.resolveDataElementOptions(0)),o=l[l.length-1].size(this.resolveDataElementOptions(l.length-1));return Math.max(i,s,o)/2}draw(){const e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}}ct(sr,"id","line"),ct(sr,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),ct(sr,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}});function vl(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Ju{constructor(e){ct(this,"options");this.options=e||{}}static override(e){Object.assign(Ju.prototype,e)}init(){}formats(){return vl()}parse(){return vl()}format(){return vl()}add(){return vl()}diff(){return vl()}startOf(){return vl()}endOf(){return vl()}}var Oy={_date:Ju};function Y4(n,e,t,i){const{controller:l,data:s,_sorted:o}=n,r=l._cachedMeta.iScale;if(r&&e===r.axis&&e!=="r"&&o&&s.length){const a=r._reversePixels?wS:Cl;if(i){if(l._sharedOptions){const u=s[0],f=typeof u.getRange=="function"&&u.getRange(e);if(f){const c=a(s,e,t-f),d=a(s,e,t+f);return{lo:c.lo,hi:d.hi}}}}else return a(s,e,t)}return{lo:0,hi:s.length-1}}function po(n,e,t,i,l){const s=n.getSortedVisibleDatasetMetas(),o=t[e];for(let r=0,a=s.length;r{a[o]&&a[o](e[t],l)&&(s.push({element:a,datasetIndex:u,index:f}),r=r||a.inRange(e.x,e.y,l))}),i&&!r?[]:s}var G4={evaluateInteractionItems:po,modes:{index(n,e,t,i){const l=ki(e,n),s=t.axis||"x",o=t.includeInvisible||!1,r=t.intersect?ha(n,l,s,i,o):_a(n,l,s,!1,i,o),a=[];return r.length?(n.getSortedVisibleDatasetMetas().forEach(u=>{const f=r[0].index,c=u.data[f];c&&!c.skip&&a.push({element:c,datasetIndex:u.index,index:f})}),a):[]},dataset(n,e,t,i){const l=ki(e,n),s=t.axis||"xy",o=t.includeInvisible||!1;let r=t.intersect?ha(n,l,s,i,o):_a(n,l,s,!1,i,o);if(r.length>0){const a=r[0].datasetIndex,u=n.getDatasetMeta(a).data;r=[];for(let f=0;ft.pos===e)}function hd(n,e){return n.filter(t=>Ey.indexOf(t.pos)===-1&&t.box.axis===e)}function vs(n,e){return n.sort((t,i)=>{const l=e?i:t,s=e?t:i;return l.weight===s.weight?l.index-s.index:l.weight-s.weight})}function X4(n){const e=[];let t,i,l,s,o,r;for(t=0,i=(n||[]).length;tu.box.fullSize),!0),i=vs(ks(e,"left"),!0),l=vs(ks(e,"right")),s=vs(ks(e,"top"),!0),o=vs(ks(e,"bottom")),r=hd(e,"x"),a=hd(e,"y");return{fullSize:t,leftAndTop:i.concat(s),rightAndBottom:l.concat(a).concat(o).concat(r),chartArea:ks(e,"chartArea"),vertical:i.concat(l).concat(a),horizontal:s.concat(o).concat(r)}}function _d(n,e,t,i){return Math.max(n[t],e[t])+Math.max(n[i],e[i])}function My(n,e){n.top=Math.max(n.top,e.top),n.left=Math.max(n.left,e.left),n.bottom=Math.max(n.bottom,e.bottom),n.right=Math.max(n.right,e.right)}function tT(n,e,t,i){const{pos:l,box:s}=t,o=n.maxPadding;if(!bt(l)){t.size&&(n[l]-=t.size);const c=i[t.stack]||{size:0,count:1};c.size=Math.max(c.size,t.horizontal?s.height:s.width),t.size=c.size/c.count,n[l]+=t.size}s.getPadding&&My(o,s.getPadding());const r=Math.max(0,e.outerWidth-_d(o,n,"left","right")),a=Math.max(0,e.outerHeight-_d(o,n,"top","bottom")),u=r!==n.w,f=a!==n.h;return n.w=r,n.h=a,t.horizontal?{same:u,other:f}:{same:f,other:u}}function nT(n){const e=n.maxPadding;function t(i){const l=Math.max(e[i]-n[i],0);return n[i]+=l,l}n.y+=t("top"),n.x+=t("left"),t("right"),t("bottom")}function iT(n,e){const t=e.maxPadding;function i(l){const s={left:0,top:0,right:0,bottom:0};return l.forEach(o=>{s[o]=Math.max(e[o],t[o])}),s}return i(n?["left","right"]:["top","bottom"])}function Os(n,e,t,i){const l=[];let s,o,r,a,u,f;for(s=0,o=n.length,u=0;s{typeof g.beforeLayout=="function"&&g.beforeLayout()});const f=a.reduce((g,_)=>_.box.options&&_.box.options.display===!1?g:g+1,0)||1,c=Object.freeze({outerWidth:e,outerHeight:t,padding:l,availableWidth:s,availableHeight:o,vBoxMaxWidth:s/2/f,hBoxMaxHeight:o/2}),d=Object.assign({},l);My(d,ll(i));const m=Object.assign({maxPadding:d,w:s,h:o,x:l.left,y:l.top},l),h=x4(a.concat(u),c);Os(r.fullSize,m,c,h),Os(a,m,c,h),Os(u,m,c,h)&&Os(a,m,c,h),nT(m),gd(r.leftAndTop,m,c,h),m.x+=m.w,m.y+=m.h,gd(r.rightAndBottom,m,c,h),n.chartArea={left:m.left,top:m.top,right:m.left+m.w,bottom:m.top+m.h,height:m.h,width:m.w},ht(r.chartArea,g=>{const _=g.box;Object.assign(_,n.chartArea),_.update(m.w,m.h,{left:0,top:0,right:0,bottom:0})})}};class Dy{acquireContext(e,t){}releaseContext(e){return!1}addEventListener(e,t,i){}removeEventListener(e,t,i){}getDevicePixelRatio(){return 1}getMaximumSize(e,t,i,l){return t=Math.max(0,t||e.width),i=i||e.height,{width:t,height:Math.max(0,l?Math.floor(t/l):i)}}isAttached(e){return!0}updateConfig(e){}}class lT extends Dy{acquireContext(e){return e&&e.getContext&&e.getContext("2d")||null}updateConfig(e){e.options.animation=!1}}const or="$chartjs",sT={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},bd=n=>n===null||n==="";function oT(n,e){const t=n.style,i=n.getAttribute("height"),l=n.getAttribute("width");if(n[or]={initial:{height:i,width:l,style:{display:t.display,height:t.height,width:t.width}}},t.display=t.display||"block",t.boxSizing=t.boxSizing||"border-box",bd(l)){const s=id(n,"width");s!==void 0&&(n.width=s)}if(bd(i))if(n.style.height==="")n.height=n.width/(e||2);else{const s=id(n,"height");s!==void 0&&(n.height=s)}return n}const Iy=y4?{passive:!0}:!1;function rT(n,e,t){n&&n.addEventListener(e,t,Iy)}function aT(n,e,t){n&&n.canvas&&n.canvas.removeEventListener(e,t,Iy)}function uT(n,e){const t=sT[n.type]||n.type,{x:i,y:l}=ki(n,e);return{type:t,chart:e,native:n,x:i!==void 0?i:null,y:l!==void 0?l:null}}function $r(n,e){for(const t of n)if(t===e||t.contains(e))return!0}function fT(n,e,t){const i=n.canvas,l=new MutationObserver(s=>{let o=!1;for(const r of s)o=o||$r(r.addedNodes,i),o=o&&!$r(r.removedNodes,i);o&&t()});return l.observe(document,{childList:!0,subtree:!0}),l}function cT(n,e,t){const i=n.canvas,l=new MutationObserver(s=>{let o=!1;for(const r of s)o=o||$r(r.removedNodes,i),o=o&&!$r(r.addedNodes,i);o&&t()});return l.observe(document,{childList:!0,subtree:!0}),l}const Xs=new Map;let yd=0;function Ly(){const n=window.devicePixelRatio;n!==yd&&(yd=n,Xs.forEach((e,t)=>{t.currentDevicePixelRatio!==n&&e()}))}function dT(n,e){Xs.size||window.addEventListener("resize",Ly),Xs.set(n,e)}function pT(n){Xs.delete(n),Xs.size||window.removeEventListener("resize",Ly)}function mT(n,e,t){const i=n.canvas,l=i&&Ku(i);if(!l)return;const s=dy((r,a)=>{const u=l.clientWidth;t(r,a),u{const a=r[0],u=a.contentRect.width,f=a.contentRect.height;u===0&&f===0||s(u,f)});return o.observe(l),dT(n,s),o}function ga(n,e,t){t&&t.disconnect(),e==="resize"&&pT(n)}function hT(n,e,t){const i=n.canvas,l=dy(s=>{n.ctx!==null&&t(uT(s,n))},n);return rT(i,e,l),l}class _T extends Dy{acquireContext(e,t){const i=e&&e.getContext&&e.getContext("2d");return i&&i.canvas===e?(oT(e,t),i):null}releaseContext(e){const t=e.canvas;if(!t[or])return!1;const i=t[or].initial;["height","width"].forEach(s=>{const o=i[s];Kt(o)?t.removeAttribute(s):t.setAttribute(s,o)});const l=i.style||{};return Object.keys(l).forEach(s=>{t.style[s]=l[s]}),t.width=t.width,delete t[or],!0}addEventListener(e,t,i){this.removeEventListener(e,t);const l=e.$proxies||(e.$proxies={}),o={attach:fT,detach:cT,resize:mT}[t]||hT;l[t]=o(e,t,i)}removeEventListener(e,t){const i=e.$proxies||(e.$proxies={}),l=i[t];if(!l)return;({attach:ga,detach:ga,resize:ga}[t]||aT)(e,t,l),i[t]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,i,l){return b4(e,t,i,l)}isAttached(e){const t=e&&Ku(e);return!!(t&&t.isConnected)}}function gT(n){return!Yu()||typeof OffscreenCanvas<"u"&&n instanceof OffscreenCanvas?lT:_T}class Al{constructor(){ct(this,"x");ct(this,"y");ct(this,"active",!1);ct(this,"options");ct(this,"$animations")}tooltipPosition(e){const{x:t,y:i}=this.getProps(["x","y"],e);return{x:t,y:i}}hasValue(){return Zs(this.x)&&Zs(this.y)}getProps(e,t){const i=this.$animations;if(!t||!i)return this;const l={};return e.forEach(s=>{l[s]=i[s]&&i[s].active()?i[s]._to:this[s]}),l}}ct(Al,"defaults",{}),ct(Al,"defaultRoutes");function bT(n,e){const t=n.options.ticks,i=yT(n),l=Math.min(t.maxTicksLimit||i,i),s=t.major.enabled?vT(e):[],o=s.length,r=s[0],a=s[o-1],u=[];if(o>l)return wT(e,u,s,o/l),u;const f=kT(s,e,l);if(o>0){let c,d;const m=o>1?Math.round((a-r)/(o-1)):null;for(jo(e,u,f,Kt(m)?0:r-m,r),c=0,d=o-1;cl)return a}return Math.max(l,1)}function vT(n){const e=[];let t,i;for(t=0,i=n.length;tn==="left"?"right":n==="right"?"left":n,kd=(n,e,t)=>e==="top"||e==="left"?n[e]+t:n[e]-t,vd=(n,e)=>Math.min(e||n,n);function wd(n,e){const t=[],i=n.length/e,l=n.length;let s=0;for(;so+r)))return a}function CT(n,e){ht(n,t=>{const i=t.gc,l=i.length/2;let s;if(l>e){for(s=0;si?i:t,i=l&&t>i?t:i,{min:_i(t,_i(i,t)),max:_i(i,_i(t,i))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}getLabelItems(e=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(e))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){dt(this.options.beforeUpdate,[this])}update(e,t,i){const{beginAtZero:l,grace:s,ticks:o}=this.options,r=o.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=t,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=GS(this,s,l),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const a=r=s||i<=1||!this.isHorizontal()){this.labelRotation=l;return}const f=this._getLabelSizes(),c=f.widest.width,d=f.highest.height,m=ri(this.chart.width-c,0,this.maxWidth);r=e.offset?this.maxWidth/i:m/(i-1),c+6>r&&(r=m/(i-(e.offset?.5:1)),a=this.maxHeight-ws(e.grid)-t.padding-Sd(e.title,this.chart.options.font),u=Math.sqrt(c*c+d*d),o=bS(Math.min(Math.asin(ri((f.highest.height+6)/r,-1,1)),Math.asin(ri(a/u,-1,1))-Math.asin(ri(d/u,-1,1)))),o=Math.max(l,Math.min(s,o))),this.labelRotation=o}afterCalculateLabelRotation(){dt(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){dt(this.options.beforeFit,[this])}fit(){const e={width:0,height:0},{chart:t,options:{ticks:i,title:l,grid:s}}=this,o=this._isVisible(),r=this.isHorizontal();if(o){const a=Sd(l,t.options.font);if(r?(e.width=this.maxWidth,e.height=ws(s)+a):(e.height=this.maxHeight,e.width=ws(s)+a),i.display&&this.ticks.length){const{first:u,last:f,widest:c,highest:d}=this._getLabelSizes(),m=i.padding*2,h=$l(this.labelRotation),g=Math.cos(h),_=Math.sin(h);if(r){const y=i.mirror?0:_*c.width+g*d.height;e.height=Math.min(this.maxHeight,e.height+y+m)}else{const y=i.mirror?0:g*c.width+_*d.height;e.width=Math.min(this.maxWidth,e.width+y+m)}this._calculatePadding(u,f,_,g)}}this._handleMargins(),r?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,t,i,l){const{ticks:{align:s,padding:o},position:r}=this.options,a=this.labelRotation!==0,u=r!=="top"&&this.axis==="x";if(this.isHorizontal()){const f=this.getPixelForTick(0)-this.left,c=this.right-this.getPixelForTick(this.ticks.length-1);let d=0,m=0;a?u?(d=l*e.width,m=i*t.height):(d=i*e.height,m=l*t.width):s==="start"?m=t.width:s==="end"?d=e.width:s!=="inner"&&(d=e.width/2,m=t.width/2),this.paddingLeft=Math.max((d-f+o)*this.width/(this.width-f),0),this.paddingRight=Math.max((m-c+o)*this.width/(this.width-c),0)}else{let f=t.height/2,c=e.height/2;s==="start"?(f=0,c=e.height):s==="end"&&(f=t.height,c=0),this.paddingTop=f+o,this.paddingBottom=c+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){dt(this.options.afterFit,[this])}isHorizontal(){const{axis:e,position:t}=this.options;return t==="top"||t==="bottom"||e==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let t,i;for(t=0,i=e.length;t({width:o[A]||0,height:r[A]||0});return{first:I(0),last:I(t-1),widest:I(M),highest:I(L),widths:o,heights:r}}getLabelForValue(e){return e}getPixelForValue(e,t){return NaN}getValueForPixel(e){}getPixelForTick(e){const t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);const t=this._startPixel+e*this._length;return vS(this._alignToPixels?kl(this.chart,t,0):t)}getDecimalForPixel(e){const t=(e-this._startPixel)/this._length;return this._reversePixels?1-t:t}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:e,max:t}=this;return e<0&&t<0?t:e>0&&t>0?e:0}getContext(e){const t=this.ticks||[];if(e>=0&&er*l?r/i:a/l:a*l0}_computeGridLineItems(e){const t=this.axis,i=this.chart,l=this.options,{grid:s,position:o,border:r}=l,a=s.offset,u=this.isHorizontal(),c=this.ticks.length+(a?1:0),d=ws(s),m=[],h=r.setContext(this.getContext()),g=h.display?h.width:0,_=g/2,y=function(K){return kl(i,K,g)};let S,T,$,E,M,L,I,A,P,R,N,U;if(o==="top")S=y(this.bottom),L=this.bottom-d,A=S-_,R=y(e.top)+_,U=e.bottom;else if(o==="bottom")S=y(this.top),R=e.top,U=y(e.bottom)-_,L=S+_,A=this.top+d;else if(o==="left")S=y(this.right),M=this.right-d,I=S-_,P=y(e.left)+_,N=e.right;else if(o==="right")S=y(this.left),P=e.left,N=y(e.right)-_,M=S+_,I=this.left+d;else if(t==="x"){if(o==="center")S=y((e.top+e.bottom)/2+.5);else if(bt(o)){const K=Object.keys(o)[0],J=o[K];S=y(this.chart.scales[K].getPixelForValue(J))}R=e.top,U=e.bottom,L=S+_,A=L+d}else if(t==="y"){if(o==="center")S=y((e.left+e.right)/2);else if(bt(o)){const K=Object.keys(o)[0],J=o[K];S=y(this.chart.scales[K].getPixelForValue(J))}M=S-_,I=M-d,P=e.left,N=e.right}const j=Ct(l.ticks.maxTicksLimit,c),V=Math.max(1,Math.ceil(c/j));for(T=0;T0&&(et-=We/2);break}Se={left:et,top:st,width:We+ke.width,height:Ce+ke.height,color:V.backdropColor}}_.push({label:$,font:A,textOffset:N,options:{rotation:g,color:J,strokeColor:ee,strokeWidth:X,textAlign:oe,textBaseline:U,translation:[E,M],backdrop:Se}})}return _}_getXAxisLabelAlignment(){const{position:e,ticks:t}=this.options;if(-$l(this.labelRotation))return e==="top"?"left":"right";let l="center";return t.align==="start"?l="left":t.align==="end"?l="right":t.align==="inner"&&(l="inner"),l}_getYAxisLabelAlignment(e){const{position:t,ticks:{crossAlign:i,mirror:l,padding:s}}=this.options,o=this._getLabelSizes(),r=e+s,a=o.widest.width;let u,f;return t==="left"?l?(f=this.right+s,i==="near"?u="left":i==="center"?(u="center",f+=a/2):(u="right",f+=a)):(f=this.right-r,i==="near"?u="right":i==="center"?(u="center",f-=a/2):(u="left",f=this.left)):t==="right"?l?(f=this.left+s,i==="near"?u="right":i==="center"?(u="center",f-=a/2):(u="left",f-=a)):(f=this.left+r,i==="near"?u="left":i==="center"?(u="center",f+=a/2):(u="right",f=this.right)):u="right",{textAlign:u,x:f}}_computeLabelArea(){if(this.options.ticks.mirror)return;const e=this.chart,t=this.options.position;if(t==="left"||t==="right")return{top:0,left:this.left,bottom:e.height,right:this.right};if(t==="top"||t==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:e.width}}drawBackground(){const{ctx:e,options:{backgroundColor:t},left:i,top:l,width:s,height:o}=this;t&&(e.save(),e.fillStyle=t,e.fillRect(i,l,s,o),e.restore())}getLineWidthForValue(e){const t=this.options.grid;if(!this._isVisible()||!t.display)return 0;const l=this.ticks.findIndex(s=>s.value===e);return l>=0?t.setContext(this.getContext(l)).lineWidth:0}drawGrid(e){const t=this.options.grid,i=this.ctx,l=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e));let s,o;const r=(a,u,f)=>{!f.width||!f.color||(i.save(),i.lineWidth=f.width,i.strokeStyle=f.color,i.setLineDash(f.borderDash||[]),i.lineDashOffset=f.borderDashOffset,i.beginPath(),i.moveTo(a.x,a.y),i.lineTo(u.x,u.y),i.stroke(),i.restore())};if(t.display)for(s=0,o=l.length;s{this.draw(s)}}]:[{z:i,draw:s=>{this.drawBackground(),this.drawGrid(s),this.drawTitle()}},{z:l,draw:()=>{this.drawBorder()}},{z:t,draw:s=>{this.drawLabels(s)}}]}getMatchingVisibleMetas(e){const t=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",l=[];let s,o;for(s=0,o=t.length;s{const i=t.split("."),l=i.pop(),s=[n].concat(i).join("."),o=e[t].split("."),r=o.pop(),a=o.join(".");nn.route(s,l,a,r)})}function AT(n){return"id"in n&&"defaults"in n}class PT{constructor(){this.controllers=new zo(Rs,"datasets",!0),this.elements=new zo(Al,"elements"),this.plugins=new zo(Object,"plugins"),this.scales=new zo(mo,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each("register",e)}remove(...e){this._each("unregister",e)}addControllers(...e){this._each("register",e,this.controllers)}addElements(...e){this._each("register",e,this.elements)}addPlugins(...e){this._each("register",e,this.plugins)}addScales(...e){this._each("register",e,this.scales)}getController(e){return this._get(e,this.controllers,"controller")}getElement(e){return this._get(e,this.elements,"element")}getPlugin(e){return this._get(e,this.plugins,"plugin")}getScale(e){return this._get(e,this.scales,"scale")}removeControllers(...e){this._each("unregister",e,this.controllers)}removeElements(...e){this._each("unregister",e,this.elements)}removePlugins(...e){this._each("unregister",e,this.plugins)}removeScales(...e){this._each("unregister",e,this.scales)}_each(e,t,i){[...t].forEach(l=>{const s=i||this._getRegistryForType(l);i||s.isForType(l)||s===this.plugins&&l.id?this._exec(e,s,l):ht(l,o=>{const r=i||this._getRegistryForType(o);this._exec(e,r,o)})})}_exec(e,t,i){const l=qu(e);dt(i["before"+l],[],i),t[e](i),dt(i["after"+l],[],i)}_getRegistryForType(e){for(let t=0;ts.filter(r=>!o.some(a=>r.plugin.id===a.plugin.id));this._notify(l(t,i),e,"stop"),this._notify(l(i,t),e,"start")}}function RT(n){const e={},t=[],i=Object.keys(bi.plugins.items);for(let s=0;s1&&Td(n[0].toLowerCase());if(i)return i}throw new Error(`Cannot determine type of '${n}' axis. Please provide 'axis' or 'position' option.`)}function $d(n,e,t){if(t[e+"AxisID"]===n)return{axis:e}}function VT(n,e){if(e.data&&e.data.datasets){const t=e.data.datasets.filter(i=>i.xAxisID===n||i.yAxisID===n);if(t.length)return $d(n,"x",t[0])||$d(n,"y",t[0])}return{}}function BT(n,e){const t=Ll[n.type]||{scales:{}},i=e.scales||{},l=Xa(n.type,e),s=Object.create(null);return Object.keys(i).forEach(o=>{const r=i[o];if(!bt(r))return console.error(`Invalid scale configuration for scale: ${o}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${o}`);const a=Qa(o,r,VT(o,n),nn.scales[r.type]),u=zT(a,l),f=t.scales||{};s[o]=Ls(Object.create(null),[{axis:a},r,f[a],f[u]])}),n.data.datasets.forEach(o=>{const r=o.type||n.type,a=o.indexAxis||Xa(r,e),f=(Ll[r]||{}).scales||{};Object.keys(f).forEach(c=>{const d=jT(c,a),m=o[d+"AxisID"]||d;s[m]=s[m]||Object.create(null),Ls(s[m],[{axis:d},i[m],f[c]])})}),Object.keys(s).forEach(o=>{const r=s[o];Ls(r,[nn.scales[r.type],nn.scale])}),s}function Ay(n){const e=n.options||(n.options={});e.plugins=Ct(e.plugins,{}),e.scales=BT(n,e)}function Py(n){return n=n||{},n.datasets=n.datasets||[],n.labels=n.labels||[],n}function WT(n){return n=n||{},n.data=Py(n.data),Ay(n),n}const Cd=new Map,Ny=new Set;function Uo(n,e){let t=Cd.get(n);return t||(t=e(),Cd.set(n,t),Ny.add(t)),t}const Ss=(n,e,t)=>{const i=vr(e,t);i!==void 0&&n.add(i)};class YT{constructor(e){this._config=WT(e),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(e){this._config.type=e}get data(){return this._config.data}set data(e){this._config.data=Py(e)}get options(){return this._config.options}set options(e){this._config.options=e}get plugins(){return this._config.plugins}update(){const e=this._config;this.clearCache(),Ay(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Uo(e,()=>[[`datasets.${e}`,""]])}datasetAnimationScopeKeys(e,t){return Uo(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,""]])}datasetElementScopeKeys(e,t){return Uo(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,""]])}pluginScopeKeys(e){const t=e.id,i=this.type;return Uo(`${i}-plugin-${t}`,()=>[[`plugins.${t}`,...e.additionalOptionScopes||[]]])}_cachedScopes(e,t){const i=this._scopeCache;let l=i.get(e);return(!l||t)&&(l=new Map,i.set(e,l)),l}getOptionScopes(e,t,i){const{options:l,type:s}=this,o=this._cachedScopes(e,i),r=o.get(t);if(r)return r;const a=new Set;t.forEach(f=>{e&&(a.add(e),f.forEach(c=>Ss(a,e,c))),f.forEach(c=>Ss(a,l,c)),f.forEach(c=>Ss(a,Ll[s]||{},c)),f.forEach(c=>Ss(a,nn,c)),f.forEach(c=>Ss(a,Za,c))});const u=Array.from(a);return u.length===0&&u.push(Object.create(null)),Ny.has(t)&&o.set(t,u),u}chartOptionScopes(){const{options:e,type:t}=this;return[e,Ll[t]||{},nn.datasets[t]||{},{type:t},nn,Za]}resolveNamedOptions(e,t,i,l=[""]){const s={$shared:!0},{resolver:o,subPrefixes:r}=Od(this._resolverCache,e,l);let a=o;if(JT(o,t)){s.$shared=!1,i=nl(i)?i():i;const u=this.createResolver(e,i,r);a=is(o,i,u)}for(const u of t)s[u]=a[u];return s}createResolver(e,t,i=[""],l){const{resolver:s}=Od(this._resolverCache,e,i);return bt(t)?is(s,t,void 0,l):s}}function Od(n,e,t){let i=n.get(e);i||(i=new Map,n.set(e,i));const l=t.join();let s=i.get(l);return s||(s={resolver:Vu(e,t),subPrefixes:t.filter(r=>!r.toLowerCase().includes("hover"))},i.set(l,s)),s}const KT=n=>bt(n)&&Object.getOwnPropertyNames(n).some(e=>nl(n[e]));function JT(n,e){const{isScriptable:t,isIndexable:i}=gy(n);for(const l of e){const s=t(l),o=i(l),r=(o||s)&&n[l];if(s&&(nl(r)||KT(r))||o&&un(r))return!0}return!1}var ZT="4.4.4";const GT=["top","bottom","left","right","chartArea"];function Ed(n,e){return n==="top"||n==="bottom"||GT.indexOf(n)===-1&&e==="x"}function Md(n,e){return function(t,i){return t[n]===i[n]?t[e]-i[e]:t[n]-i[n]}}function Dd(n){const e=n.chart,t=e.options.animation;e.notifyPlugins("afterRender"),dt(t&&t.onComplete,[n],e)}function XT(n){const e=n.chart,t=e.options.animation;dt(t&&t.onProgress,[n],e)}function Ry(n){return Yu()&&typeof n=="string"?n=document.getElementById(n):n&&n.length&&(n=n[0]),n&&n.canvas&&(n=n.canvas),n}const rr={},Id=n=>{const e=Ry(n);return Object.values(rr).filter(t=>t.canvas===e).pop()};function QT(n,e,t){const i=Object.keys(n);for(const l of i){const s=+l;if(s>=e){const o=n[l];delete n[l],(t>0||s>e)&&(n[s+t]=o)}}}function xT(n,e,t,i){return!t||n.type==="mouseout"?null:i?e:n}function Vo(n,e,t){return n.options.clip?n[t]:e[t]}function e$(n,e){const{xScale:t,yScale:i}=n;return t&&i?{left:Vo(t,e,"left"),right:Vo(t,e,"right"),top:Vo(i,e,"top"),bottom:Vo(i,e,"bottom")}:e}class vi{static register(...e){bi.add(...e),Ld()}static unregister(...e){bi.remove(...e),Ld()}constructor(e,t){const i=this.config=new YT(t),l=Ry(e),s=Id(l);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas with ID '"+s.canvas.id+"' can be reused.");const o=i.createResolver(i.chartOptionScopes(),this.getContext());this.platform=new(i.platform||gT(l)),this.platform.updateConfig(i);const r=this.platform.acquireContext(l,o.aspectRatio),a=r&&r.canvas,u=a&&a.height,f=a&&a.width;if(this.id=oS(),this.ctx=r,this.canvas=a,this.width=f,this.height=u,this._options=o,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new NT,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=CS(c=>this.update(c),o.resizeDelay||0),this._dataChanges=[],rr[this.id]=this,!r||!a){console.error("Failed to create chart: can't acquire context from the given item");return}Li.listen(this,"complete",Dd),Li.listen(this,"progress",XT),this._initialize(),this.attached&&this.update()}get aspectRatio(){const{options:{aspectRatio:e,maintainAspectRatio:t},width:i,height:l,_aspectRatio:s}=this;return Kt(e)?t&&s?s:l?i/l:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return bi}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():nd(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Xc(this.canvas,this.ctx),this}stop(){return Li.stop(this),this}resize(e,t){Li.running(this)?this._resizeBeforeDraw={width:e,height:t}:this._resize(e,t)}_resize(e,t){const i=this.options,l=this.canvas,s=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(l,e,t,s),r=i.devicePixelRatio||this.platform.getDevicePixelRatio(),a=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,nd(this,r,!0)&&(this.notifyPlugins("resize",{size:o}),dt(i.onResize,[this,o],this),this.attached&&this._doResize(a)&&this.render())}ensureScalesHaveIDs(){const t=this.options.scales||{};ht(t,(i,l)=>{i.id=l})}buildOrUpdateScales(){const e=this.options,t=e.scales,i=this.scales,l=Object.keys(i).reduce((o,r)=>(o[r]=!1,o),{});let s=[];t&&(s=s.concat(Object.keys(t).map(o=>{const r=t[o],a=Qa(o,r),u=a==="r",f=a==="x";return{options:r,dposition:u?"chartArea":f?"bottom":"left",dtype:u?"radialLinear":f?"category":"linear"}}))),ht(s,o=>{const r=o.options,a=r.id,u=Qa(a,r),f=Ct(r.type,o.dtype);(r.position===void 0||Ed(r.position,u)!==Ed(o.dposition))&&(r.position=o.dposition),l[a]=!0;let c=null;if(a in i&&i[a].type===f)c=i[a];else{const d=bi.getScale(f);c=new d({id:a,type:f,ctx:this.ctx,chart:this}),i[c.id]=c}c.init(r,e)}),ht(l,(o,r)=>{o||delete i[r]}),ht(i,o=>{Ho.configure(this,o,o.options),Ho.addBox(this,o)})}_updateMetasets(){const e=this._metasets,t=this.data.datasets.length,i=e.length;if(e.sort((l,s)=>l.index-s.index),i>t){for(let l=t;lt.length&&delete this._stacks,e.forEach((i,l)=>{t.filter(s=>s===i._dataset).length===0&&this._destroyDatasetMeta(l)})}buildOrUpdateControllers(){const e=[],t=this.data.datasets;let i,l;for(this._removeUnreferencedMetasets(),i=0,l=t.length;i{this.getDatasetMeta(t).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(e){const t=this.config;t.update();const i=this._options=t.createResolver(t.chartOptionScopes(),this.getContext()),l=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:e,cancelable:!0})===!1)return;const s=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let u=0,f=this.data.datasets.length;u{u.reset()}),this._updateDatasets(e),this.notifyPlugins("afterUpdate",{mode:e}),this._layers.sort(Md("z","_idx"));const{_active:r,_lastEvent:a}=this;a?this._eventHandler(a,!0):r.length&&this._updateHoverStyles(r,r,!0),this.render()}_updateScales(){ht(this.scales,e=>{Ho.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const e=this.options,t=new Set(Object.keys(this._listeners)),i=new Set(e.events);(!jc(t,i)||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:e}=this,t=this._getUniformDataChanges()||[];for(const{method:i,start:l,count:s}of t){const o=i==="_removeElements"?-s:s;QT(e,l,o)}}_getUniformDataChanges(){const e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];const t=this.data.datasets.length,i=s=>new Set(e.filter(o=>o[0]===s).map((o,r)=>r+","+o.splice(1).join(","))),l=i(0);for(let s=1;ss.split(",")).map(s=>({method:s[1],start:+s[2],count:+s[3]}))}_updateLayout(e){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;Ho.update(this,this.width,this.height,e);const t=this.chartArea,i=t.width<=0||t.height<=0;this._layers=[],ht(this.boxes,l=>{i&&l.position==="chartArea"||(l.configure&&l.configure(),this._layers.push(...l._layers()))},this),this._layers.forEach((l,s)=>{l._idx=s}),this.notifyPlugins("afterLayout")}_updateDatasets(e){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:e,cancelable:!0})!==!1){for(let t=0,i=this.data.datasets.length;t=0;--t)this._drawDataset(e[t]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(e){const t=this.ctx,i=e._clip,l=!i.disabled,s=e$(e,this.chartArea),o={meta:e,index:e.index,cancelable:!0};this.notifyPlugins("beforeDatasetDraw",o)!==!1&&(l&&zu(t,{left:i.left===!1?0:s.left-i.left,right:i.right===!1?this.width:s.right+i.right,top:i.top===!1?0:s.top-i.top,bottom:i.bottom===!1?this.height:s.bottom+i.bottom}),e.controller.draw(),l&&Uu(t),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(e){return Gs(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,i,l){const s=G4.modes[t];return typeof s=="function"?s(this,e,i,l):[]}getDatasetMeta(e){const t=this.data.datasets[e],i=this._metasets;let l=i.filter(s=>s&&s._dataset===t).pop();return l||(l={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:t&&t.order||0,index:e,_dataset:t,_parsed:[],_sorted:!1},i.push(l)),l}getContext(){return this.$context||(this.$context=Nl(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){const t=this.data.datasets[e];if(!t)return!1;const i=this.getDatasetMeta(e);return typeof i.hidden=="boolean"?!i.hidden:!t.hidden}setDatasetVisibility(e,t){const i=this.getDatasetMeta(e);i.hidden=!t}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,t,i){const l=i?"show":"hide",s=this.getDatasetMeta(e),o=s.controller._resolveAnimations(void 0,l);wr(t)?(s.data[t].hidden=!i,this.update()):(this.setDatasetVisibility(e,i),o.update(s,{visible:i}),this.update(r=>r.datasetIndex===e?l:void 0))}hide(e,t){this._updateVisibility(e,t,!1)}show(e,t){this._updateVisibility(e,t,!0)}_destroyDatasetMeta(e){const t=this._metasets[e];t&&t.controller&&t.controller._destroy(),delete this._metasets[e]}_stop(){let e,t;for(this.stop(),Li.remove(this),e=0,t=this.data.datasets.length;e{t.addEventListener(this,s,o),e[s]=o},l=(s,o,r)=>{s.offsetX=o,s.offsetY=r,this._eventHandler(s)};ht(this.options.events,s=>i(s,l))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const e=this._responsiveListeners,t=this.platform,i=(a,u)=>{t.addEventListener(this,a,u),e[a]=u},l=(a,u)=>{e[a]&&(t.removeEventListener(this,a,u),delete e[a])},s=(a,u)=>{this.canvas&&this.resize(a,u)};let o;const r=()=>{l("attach",r),this.attached=!0,this.resize(),i("resize",s),i("detach",o)};o=()=>{this.attached=!1,l("resize",s),this._stop(),this._resize(0,0),i("attach",r)},t.isAttached(this.canvas)?r():o()}unbindEvents(){ht(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},ht(this._responsiveListeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,t,i){const l=i?"set":"remove";let s,o,r,a;for(t==="dataset"&&(s=this.getDatasetMeta(e[0].datasetIndex),s.controller["_"+l+"DatasetHoverStyle"]()),r=0,a=e.length;r{const r=this.getDatasetMeta(s);if(!r)throw new Error("No dataset found at index "+s);return{datasetIndex:s,element:r.data[o],index:o}});!yr(i,t)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,t))}notifyPlugins(e,t,i){return this._plugins.notify(this,e,t,i)}isPluginEnabled(e){return this._plugins._cache.filter(t=>t.plugin.id===e).length===1}_updateHoverStyles(e,t,i){const l=this.options.hover,s=(a,u)=>a.filter(f=>!u.some(c=>f.datasetIndex===c.datasetIndex&&f.index===c.index)),o=s(t,e),r=i?e:s(e,t);o.length&&this.updateHoverStyle(o,l.mode,!1),r.length&&l.mode&&this.updateHoverStyle(r,l.mode,!0)}_eventHandler(e,t){const i={event:e,replay:t,cancelable:!0,inChartArea:this.isPointInArea(e)},l=o=>(o.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins("beforeEvent",i,l)===!1)return;const s=this._handleEvent(e,t,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,l),(s||i.changed)&&this.render(),this}_handleEvent(e,t,i){const{_active:l=[],options:s}=this,o=t,r=this._getActiveElements(e,l,i,o),a=dS(e),u=xT(e,this._lastEvent,i,a);i&&(this._lastEvent=null,dt(s.onHover,[e,r,this],this),a&&dt(s.onClick,[e,r,this],this));const f=!yr(r,l);return(f||t)&&(this._active=r,this._updateHoverStyles(r,l,t)),this._lastEvent=u,f}_getActiveElements(e,t,i,l){if(e.type==="mouseout")return[];if(!i)return t;const s=this.options.hover;return this.getElementsAtEventForMode(e,s.mode,s,l)}}ct(vi,"defaults",nn),ct(vi,"instances",rr),ct(vi,"overrides",Ll),ct(vi,"registry",bi),ct(vi,"version",ZT),ct(vi,"getChart",Id);function Ld(){return ht(vi.instances,n=>n._plugins.invalidate())}function Fy(n,e,t=e){n.lineCap=Ct(t.borderCapStyle,e.borderCapStyle),n.setLineDash(Ct(t.borderDash,e.borderDash)),n.lineDashOffset=Ct(t.borderDashOffset,e.borderDashOffset),n.lineJoin=Ct(t.borderJoinStyle,e.borderJoinStyle),n.lineWidth=Ct(t.borderWidth,e.borderWidth),n.strokeStyle=Ct(t.borderColor,e.borderColor)}function t$(n,e,t){n.lineTo(t.x,t.y)}function n$(n){return n.stepped?jS:n.tension||n.cubicInterpolationMode==="monotone"?zS:t$}function qy(n,e,t={}){const i=n.length,{start:l=0,end:s=i-1}=t,{start:o,end:r}=e,a=Math.max(l,o),u=Math.min(s,r),f=lr&&s>r;return{count:i,start:a,loop:e.loop,ilen:u(o+(u?r-$:$))%s,T=()=>{g!==_&&(n.lineTo(f,_),n.lineTo(f,g),n.lineTo(f,y))};for(a&&(m=l[S(0)],n.moveTo(m.x,m.y)),d=0;d<=r;++d){if(m=l[S(d)],m.skip)continue;const $=m.x,E=m.y,M=$|0;M===h?(E_&&(_=E),f=(c*f+$)/++c):(T(),n.lineTo($,E),h=M,c=0,g=_=E),y=E}T()}function xa(n){const e=n.options,t=e.borderDash&&e.borderDash.length;return!n._decimated&&!n._loop&&!e.tension&&e.cubicInterpolationMode!=="monotone"&&!e.stepped&&!t?l$:i$}function s$(n){return n.stepped?k4:n.tension||n.cubicInterpolationMode==="monotone"?v4:wl}function o$(n,e,t,i){let l=e._path;l||(l=e._path=new Path2D,e.path(l,t,i)&&l.closePath()),Fy(n,e.options),n.stroke(l)}function r$(n,e,t,i){const{segments:l,options:s}=e,o=xa(e);for(const r of l)Fy(n,s,r.style),n.beginPath(),o(n,e,r,{start:t,end:t+i-1})&&n.closePath(),n.stroke()}const a$=typeof Path2D=="function";function u$(n,e,t,i){a$&&!e.options.segment?o$(n,e,t,i):r$(n,e,t,i)}class Gi extends Al{constructor(e){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,e&&Object.assign(this,e)}updateControlPoints(e,t){const i=this.options;if((i.tension||i.cubicInterpolationMode==="monotone")&&!i.stepped&&!this._pointsUpdated){const l=i.spanGaps?this._loop:this._fullLoop;d4(this._points,i,e,l,t),this._pointsUpdated=!0}}set points(e){this._points=e,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=M4(this,this.options.segment))}first(){const e=this.segments,t=this.points;return e.length&&t[e[0].start]}last(){const e=this.segments,t=this.points,i=e.length;return i&&t[e[i-1].end]}interpolate(e,t){const i=this.options,l=e[t],s=this.points,o=Ty(this,{property:t,start:l,end:l});if(!o.length)return;const r=[],a=s$(i);let u,f;for(u=0,f=o.length;ue!=="borderDash"&&e!=="fill"});function Ad(n,e,t,i){const l=n.options,{[t]:s}=n.getProps([t],i);return Math.abs(e-s){r=Zu(o,r,l);const a=l[o],u=l[r];i!==null?(s.push({x:a.x,y:i}),s.push({x:u.x,y:i})):t!==null&&(s.push({x:t,y:a.y}),s.push({x:t,y:u.y}))}),s}function Zu(n,e,t){for(;e>n;e--){const i=t[e];if(!isNaN(i.x)&&!isNaN(i.y))break}return e}function Pd(n,e,t,i){return n&&e?i(n[t],e[t]):n?n[t]:e?e[t]:0}function Hy(n,e){let t=[],i=!1;return un(n)?(i=!0,t=n):t=c$(n,e),t.length?new Gi({points:t,options:{tension:0},_loop:i,_fullLoop:i}):null}function Nd(n){return n&&n.fill!==!1}function d$(n,e,t){let l=n[e].fill;const s=[e];let o;if(!t)return l;for(;l!==!1&&s.indexOf(l)===-1;){if(!wn(l))return l;if(o=n[l],!o)return!1;if(o.visible)return l;s.push(l),l=o.fill}return!1}function p$(n,e,t){const i=g$(n);if(bt(i))return isNaN(i.value)?!1:i;let l=parseFloat(i);return wn(l)&&Math.floor(l)===l?m$(i[0],e,l,t):["origin","start","end","stack","shape"].indexOf(i)>=0&&i}function m$(n,e,t,i){return(n==="-"||n==="+")&&(t=e+t),t===e||t<0||t>=i?!1:t}function h$(n,e){let t=null;return n==="start"?t=e.bottom:n==="end"?t=e.top:bt(n)?t=e.getPixelForValue(n.value):e.getBasePixel&&(t=e.getBasePixel()),t}function _$(n,e,t){let i;return n==="start"?i=t:n==="end"?i=e.options.reverse?e.min:e.max:bt(n)?i=n.value:i=e.getBaseValue(),i}function g$(n){const e=n.options,t=e.fill;let i=Ct(t&&t.target,t);return i===void 0&&(i=!!e.backgroundColor),i===!1||i===null?!1:i===!0?"origin":i}function b$(n){const{scale:e,index:t,line:i}=n,l=[],s=i.segments,o=i.points,r=y$(e,t);r.push(Hy({x:null,y:e.bottom},i));for(let a=0;a=0;--o){const r=l[o].$filler;r&&(r.line.updateControlPoints(s,r.axis),i&&r.fill&&ba(n.ctx,r,s))}},beforeDatasetsDraw(n,e,t){if(t.drawTime!=="beforeDatasetsDraw")return;const i=n.getSortedVisibleDatasetMetas();for(let l=i.length-1;l>=0;--l){const s=i[l].$filler;Nd(s)&&ba(n.ctx,s,n.chartArea)}},beforeDatasetDraw(n,e,t){const i=e.meta.$filler;!Nd(i)||t.drawTime!=="beforeDatasetDraw"||ba(n.ctx,i,n.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const Es={average(n){if(!n.length)return!1;let e,t,i=new Set,l=0,s=0;for(e=0,t=n.length;er+a)/i.size,y:l/s}},nearest(n,e){if(!n.length)return!1;let t=e.x,i=e.y,l=Number.POSITIVE_INFINITY,s,o,r;for(s=0,o=n.length;s-1?n.split(` +`):n}function D$(n,e){const{element:t,datasetIndex:i,index:l}=e,s=n.getDatasetMeta(i).controller,{label:o,value:r}=s.getLabelAndValue(l);return{chart:n,label:o,parsed:s.getParsed(l),raw:n.data.datasets[i].data[l],formattedValue:r,dataset:s.getDataset(),dataIndex:l,datasetIndex:i,element:t}}function Hd(n,e){const t=n.chart.ctx,{body:i,footer:l,title:s}=n,{boxWidth:o,boxHeight:r}=e,a=Si(e.bodyFont),u=Si(e.titleFont),f=Si(e.footerFont),c=s.length,d=l.length,m=i.length,h=ll(e.padding);let g=h.height,_=0,y=i.reduce(($,E)=>$+E.before.length+E.lines.length+E.after.length,0);if(y+=n.beforeBody.length+n.afterBody.length,c&&(g+=c*u.lineHeight+(c-1)*e.titleSpacing+e.titleMarginBottom),y){const $=e.displayColors?Math.max(r,a.lineHeight):a.lineHeight;g+=m*$+(y-m)*a.lineHeight+(y-1)*e.bodySpacing}d&&(g+=e.footerMarginTop+d*f.lineHeight+(d-1)*e.footerSpacing);let S=0;const T=function($){_=Math.max(_,t.measureText($).width+S)};return t.save(),t.font=u.string,ht(n.title,T),t.font=a.string,ht(n.beforeBody.concat(n.afterBody),T),S=e.displayColors?o+2+e.boxPadding:0,ht(i,$=>{ht($.before,T),ht($.lines,T),ht($.after,T)}),S=0,t.font=f.string,ht(n.footer,T),t.restore(),_+=h.width,{width:_,height:g}}function I$(n,e){const{y:t,height:i}=e;return tn.height-i/2?"bottom":"center"}function L$(n,e,t,i){const{x:l,width:s}=i,o=t.caretSize+t.caretPadding;if(n==="left"&&l+s+o>e.width||n==="right"&&l-s-o<0)return!0}function A$(n,e,t,i){const{x:l,width:s}=t,{width:o,chartArea:{left:r,right:a}}=n;let u="center";return i==="center"?u=l<=(r+a)/2?"left":"right":l<=s/2?u="left":l>=o-s/2&&(u="right"),L$(u,n,e,t)&&(u="center"),u}function jd(n,e,t){const i=t.yAlign||e.yAlign||I$(n,t);return{xAlign:t.xAlign||e.xAlign||A$(n,e,t,i),yAlign:i}}function P$(n,e){let{x:t,width:i}=n;return e==="right"?t-=i:e==="center"&&(t-=i/2),t}function N$(n,e,t){let{y:i,height:l}=n;return e==="top"?i+=t:e==="bottom"?i-=l+t:i-=l/2,i}function zd(n,e,t,i){const{caretSize:l,caretPadding:s,cornerRadius:o}=n,{xAlign:r,yAlign:a}=t,u=l+s,{topLeft:f,topRight:c,bottomLeft:d,bottomRight:m}=lr(o);let h=P$(e,r);const g=N$(e,a,u);return a==="center"?r==="left"?h+=u:r==="right"&&(h-=u):r==="left"?h-=Math.max(f,d)+l:r==="right"&&(h+=Math.max(c,m)+l),{x:ri(h,0,i.width-e.width),y:ri(g,0,i.height-e.height)}}function Bo(n,e,t){const i=ll(t.padding);return e==="center"?n.x+n.width/2:e==="right"?n.x+n.width-i.right:n.x+i.left}function Ud(n){return gi([],Ai(n))}function R$(n,e,t){return Nl(n,{tooltip:e,tooltipItems:t,type:"tooltip"})}function Vd(n,e){const t=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return t?n.override(t):n}const zy={beforeTitle:Ii,title(n){if(n.length>0){const e=n[0],t=e.chart.data.labels,i=t?t.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(i>0&&e.dataIndex"u"?zy[e].call(t,i):l}class tu extends Al{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const t=this.chart,i=this.options.setContext(this.getContext()),l=i.enabled&&t.options.animation&&i.animations,s=new $y(this.chart,l);return l._cacheable&&(this._cachedAnimations=Object.freeze(s)),s}getContext(){return this.$context||(this.$context=R$(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){const{callbacks:i}=t,l=Dn(i,"beforeTitle",this,e),s=Dn(i,"title",this,e),o=Dn(i,"afterTitle",this,e);let r=[];return r=gi(r,Ai(l)),r=gi(r,Ai(s)),r=gi(r,Ai(o)),r}getBeforeBody(e,t){return Ud(Dn(t.callbacks,"beforeBody",this,e))}getBody(e,t){const{callbacks:i}=t,l=[];return ht(e,s=>{const o={before:[],lines:[],after:[]},r=Vd(i,s);gi(o.before,Ai(Dn(r,"beforeLabel",this,s))),gi(o.lines,Dn(r,"label",this,s)),gi(o.after,Ai(Dn(r,"afterLabel",this,s))),l.push(o)}),l}getAfterBody(e,t){return Ud(Dn(t.callbacks,"afterBody",this,e))}getFooter(e,t){const{callbacks:i}=t,l=Dn(i,"beforeFooter",this,e),s=Dn(i,"footer",this,e),o=Dn(i,"afterFooter",this,e);let r=[];return r=gi(r,Ai(l)),r=gi(r,Ai(s)),r=gi(r,Ai(o)),r}_createItems(e){const t=this._active,i=this.chart.data,l=[],s=[],o=[];let r=[],a,u;for(a=0,u=t.length;ae.filter(f,c,d,i))),e.itemSort&&(r=r.sort((f,c)=>e.itemSort(f,c,i))),ht(r,f=>{const c=Vd(e.callbacks,f);l.push(Dn(c,"labelColor",this,f)),s.push(Dn(c,"labelPointStyle",this,f)),o.push(Dn(c,"labelTextColor",this,f))}),this.labelColors=l,this.labelPointStyles=s,this.labelTextColors=o,this.dataPoints=r,r}update(e,t){const i=this.options.setContext(this.getContext()),l=this._active;let s,o=[];if(!l.length)this.opacity!==0&&(s={opacity:0});else{const r=Es[i.position].call(this,l,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const a=this._size=Hd(this,i),u=Object.assign({},r,a),f=jd(this.chart,i,u),c=zd(i,u,f,this.chart);this.xAlign=f.xAlign,this.yAlign=f.yAlign,s={opacity:1,x:c.x,y:c.y,width:a.width,height:a.height,caretX:r.x,caretY:r.y}}this._tooltipItems=o,this.$context=void 0,s&&this._resolveAnimations().update(this,s),e&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:t})}drawCaret(e,t,i,l){const s=this.getCaretPosition(e,i,l);t.lineTo(s.x1,s.y1),t.lineTo(s.x2,s.y2),t.lineTo(s.x3,s.y3)}getCaretPosition(e,t,i){const{xAlign:l,yAlign:s}=this,{caretSize:o,cornerRadius:r}=i,{topLeft:a,topRight:u,bottomLeft:f,bottomRight:c}=lr(r),{x:d,y:m}=e,{width:h,height:g}=t;let _,y,S,T,$,E;return s==="center"?($=m+g/2,l==="left"?(_=d,y=_-o,T=$+o,E=$-o):(_=d+h,y=_+o,T=$-o,E=$+o),S=_):(l==="left"?y=d+Math.max(a,f)+o:l==="right"?y=d+h-Math.max(u,c)-o:y=this.caretX,s==="top"?(T=m,$=T-o,_=y-o,S=y+o):(T=m+g,$=T+o,_=y+o,S=y-o),E=T),{x1:_,x2:y,x3:S,y1:T,y2:$,y3:E}}drawTitle(e,t,i){const l=this.title,s=l.length;let o,r,a;if(s){const u=da(i.rtl,this.x,this.width);for(e.x=Bo(this,i.titleAlign,i),t.textAlign=u.textAlign(i.titleAlign),t.textBaseline="middle",o=Si(i.titleFont),r=i.titleSpacing,t.fillStyle=i.titleColor,t.font=o.string,a=0;aS!==0)?(e.beginPath(),e.fillStyle=s.multiKeyBackground,xc(e,{x:g,y:h,w:u,h:a,radius:y}),e.fill(),e.stroke(),e.fillStyle=o.backgroundColor,e.beginPath(),xc(e,{x:_,y:h+1,w:u-2,h:a-2,radius:y}),e.fill()):(e.fillStyle=s.multiKeyBackground,e.fillRect(g,h,u,a),e.strokeRect(g,h,u,a),e.fillStyle=o.backgroundColor,e.fillRect(_,h+1,u-2,a-2))}e.fillStyle=this.labelTextColors[i]}drawBody(e,t,i){const{body:l}=this,{bodySpacing:s,bodyAlign:o,displayColors:r,boxHeight:a,boxWidth:u,boxPadding:f}=i,c=Si(i.bodyFont);let d=c.lineHeight,m=0;const h=da(i.rtl,this.x,this.width),g=function(I){t.fillText(I,h.x(e.x+m),e.y+d/2),e.y+=d+s},_=h.textAlign(o);let y,S,T,$,E,M,L;for(t.textAlign=o,t.textBaseline="middle",t.font=c.string,e.x=Bo(this,_,i),t.fillStyle=i.bodyColor,ht(this.beforeBody,g),m=r&&_!=="right"?o==="center"?u/2+f:u+2+f:0,$=0,M=l.length;$0&&t.stroke()}_updateAnimationTarget(e){const t=this.chart,i=this.$animations,l=i&&i.x,s=i&&i.y;if(l||s){const o=Es[e.position].call(this,this._active,this._eventPosition);if(!o)return;const r=this._size=Hd(this,e),a=Object.assign({},o,this._size),u=jd(t,e,a),f=zd(e,a,u,t);(l._to!==f.x||s._to!==f.y)&&(this.xAlign=u.xAlign,this.yAlign=u.yAlign,this.width=r.width,this.height=r.height,this.caretX=o.x,this.caretY=o.y,this._resolveAnimations().update(this,f))}}_willRender(){return!!this.opacity}draw(e){const t=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(t);const l={width:this.width,height:this.height},s={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ll(t.padding),r=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;t.enabled&&r&&(e.save(),e.globalAlpha=i,this.drawBackground(s,e,l,t),T4(e,t.textDirection),s.y+=o.top,this.drawTitle(s,e,t),this.drawBody(s,e,t),this.drawFooter(s,e,t),$4(e,t.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,t){const i=this._active,l=e.map(({datasetIndex:r,index:a})=>{const u=this.chart.getDatasetMeta(r);if(!u)throw new Error("Cannot find a dataset at index "+r);return{datasetIndex:r,element:u.data[a],index:a}}),s=!yr(i,l),o=this._positionChanged(l,t);(s||o)&&(this._active=l,this._eventPosition=t,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,t,i=!0){if(t&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const l=this.options,s=this._active||[],o=this._getActiveElements(e,s,t,i),r=this._positionChanged(o,e),a=t||!yr(o,s)||r;return a&&(this._active=o,(l.enabled||l.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,t))),a}_getActiveElements(e,t,i,l){const s=this.options;if(e.type==="mouseout")return[];if(!l)return t.filter(r=>this.chart.data.datasets[r.datasetIndex]&&this.chart.getDatasetMeta(r.datasetIndex).controller.getParsed(r.index)!==void 0);const o=this.chart.getElementsAtEventForMode(e,s.mode,s,i);return s.reverse&&o.reverse(),o}_positionChanged(e,t){const{caretX:i,caretY:l,options:s}=this,o=Es[s.position].call(this,e,t);return o!==!1&&(i!==o.x||l!==o.y)}}ct(tu,"positioners",Es);var F$={id:"tooltip",_element:tu,positioners:Es,afterInit(n,e,t){t&&(n.tooltip=new tu({chart:n,options:t}))},beforeUpdate(n,e,t){n.tooltip&&n.tooltip.initialize(t)},reset(n,e,t){n.tooltip&&n.tooltip.initialize(t)},afterDraw(n){const e=n.tooltip;if(e&&e._willRender()){const t={tooltip:e};if(n.notifyPlugins("beforeTooltipDraw",{...t,cancelable:!0})===!1)return;e.draw(n.ctx),n.notifyPlugins("afterTooltipDraw",t)}},afterEvent(n,e){if(n.tooltip){const t=e.replay;n.tooltip.handleEvent(e.event,t,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(n,e)=>e.bodyFont.size,boxWidth:(n,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:zy},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:n=>n!=="filter"&&n!=="itemSort"&&n!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};function q$(n,e){const t=[],{bounds:l,step:s,min:o,max:r,precision:a,count:u,maxTicks:f,maxDigits:c,includeBounds:d}=n,m=s||1,h=f-1,{min:g,max:_}=e,y=!Kt(o),S=!Kt(r),T=!Kt(u),$=(_-g)/(c+1);let E=Uc((_-g)/h/m)*m,M,L,I,A;if(E<1e-14&&!y&&!S)return[{value:g},{value:_}];A=Math.ceil(_/E)-Math.floor(g/E),A>h&&(E=Uc(A*E/h/m)*m),Kt(a)||(M=Math.pow(10,a),E=Math.ceil(E*M)/M),l==="ticks"?(L=Math.floor(g/E)*E,I=Math.ceil(_/E)*E):(L=g,I=_),y&&S&&s&&_S((r-o)/s,E/1e3)?(A=Math.round(Math.min((r-o)/E,f)),E=(r-o)/A,L=o,I=r):T?(L=y?o:L,I=S?r:I,A=u-1,E=(I-L)/A):(A=(I-L)/E,As(A,Math.round(A),E/1e3)?A=Math.round(A):A=Math.ceil(A));const P=Math.max(Vc(E),Vc(L));M=Math.pow(10,Kt(a)?P:a),L=Math.round(L*M)/M,I=Math.round(I*M)/M;let R=0;for(y&&(d&&L!==o?(t.push({value:o}),Lr)break;t.push({value:N})}return S&&d&&I!==r?t.length&&As(t[t.length-1].value,r,Bd(r,$,n))?t[t.length-1].value=r:t.push({value:r}):(!S||I===r)&&t.push({value:I}),t}function Bd(n,e,{horizontal:t,minRotation:i}){const l=$l(i),s=(t?Math.sin(l):Math.cos(l))||.001,o=.75*e*(""+n).length;return Math.min(e/s,o)}class H$ extends mo{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,t){return Kt(e)||(typeof e=="number"||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){const{beginAtZero:e}=this.options,{minDefined:t,maxDefined:i}=this.getUserBounds();let{min:l,max:s}=this;const o=a=>l=t?l:a,r=a=>s=i?s:a;if(e){const a=il(l),u=il(s);a<0&&u<0?r(0):a>0&&u>0&&o(0)}if(l===s){let a=s===0?1:Math.abs(s*.05);r(s+a),e||o(l-a)}this.min=l,this.max=s}getTickLimit(){const e=this.options.ticks;let{maxTicksLimit:t,stepSize:i}=e,l;return i?(l=Math.ceil(this.max/i)-Math.floor(this.min/i)+1,l>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${i} would result generating up to ${l} ticks. Limiting to 1000.`),l=1e3)):(l=this.computeTickLimit(),t=t||11),t&&(l=Math.min(t,l)),l}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const e=this.options,t=e.ticks;let i=this.getTickLimit();i=Math.max(2,i);const l={maxTicks:i,bounds:e.bounds,min:e.min,max:e.max,precision:t.precision,step:t.stepSize,count:t.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:t.minRotation||0,includeBounds:t.includeBounds!==!1},s=this._range||this,o=q$(l,s);return e.bounds==="ticks"&&gS(o,this,"value"),e.reverse?(o.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),o}configure(){const e=this.ticks;let t=this.min,i=this.max;if(super.configure(),this.options.offset&&e.length){const l=(i-t)/Math.max(e.length-1,1)/2;t-=l,i+=l}this._startValue=t,this._endValue=i,this._valueRange=i-t}getLabelForValue(e){return py(e,this.chart.options.locale,this.options.ticks.format)}}class nu extends H${determineDataLimits(){const{min:e,max:t}=this.getMinMax(!0);this.min=wn(e)?e:0,this.max=wn(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){const e=this.isHorizontal(),t=e?this.width:this.height,i=$l(this.options.ticks.minRotation),l=(e?Math.sin(i):Math.cos(i))||.001,s=this._resolveTickFontOptions(0);return Math.ceil(t/Math.min(40,s.lineHeight/l))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}}ct(nu,"id","linear"),ct(nu,"defaults",{ticks:{callback:hy.formatters.numeric}});const zr={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Pn=Object.keys(zr);function Wd(n,e){return n-e}function Yd(n,e){if(Kt(e))return null;const t=n._adapter,{parser:i,round:l,isoWeekday:s}=n._parseOpts;let o=e;return typeof i=="function"&&(o=i(o)),wn(o)||(o=typeof i=="string"?t.parse(o,i):t.parse(o)),o===null?null:(l&&(o=l==="week"&&(Zs(s)||s===!0)?t.startOf(o,"isoWeek",s):t.startOf(o,l)),+o)}function Kd(n,e,t,i){const l=Pn.length;for(let s=Pn.indexOf(n);s=Pn.indexOf(t);s--){const o=Pn[s];if(zr[o].common&&n._adapter.diff(l,i,o)>=e-1)return o}return Pn[t?Pn.indexOf(t):0]}function z$(n){for(let e=Pn.indexOf(n)+1,t=Pn.length;e=e?t[i]:t[l];n[s]=!0}}function U$(n,e,t,i){const l=n._adapter,s=+l.startOf(e[0].value,i),o=e[e.length-1].value;let r,a;for(r=s;r<=o;r=+l.add(r,1,i))a=t[r],a>=0&&(e[a].major=!0);return e}function Zd(n,e,t){const i=[],l={},s=e.length;let o,r;for(o=0;o+e.value))}initOffsets(e=[]){let t=0,i=0,l,s;this.options.offset&&e.length&&(l=this.getDecimalForValue(e[0]),e.length===1?t=1-l:t=(this.getDecimalForValue(e[1])-l)/2,s=this.getDecimalForValue(e[e.length-1]),e.length===1?i=s:i=(s-this.getDecimalForValue(e[e.length-2]))/2);const o=e.length<3?.5:.25;t=ri(t,0,o),i=ri(i,0,o),this._offsets={start:t,end:i,factor:1/(t+1+i)}}_generate(){const e=this._adapter,t=this.min,i=this.max,l=this.options,s=l.time,o=s.unit||Kd(s.minUnit,t,i,this._getLabelCapacity(t)),r=Ct(l.ticks.stepSize,1),a=o==="week"?s.isoWeekday:!1,u=Zs(a)||a===!0,f={};let c=t,d,m;if(u&&(c=+e.startOf(c,"isoWeek",a)),c=+e.startOf(c,u?"day":o),e.diff(i,t,o)>1e5*r)throw new Error(t+" and "+i+" are too far apart with stepSize of "+r+" "+o);const h=l.ticks.source==="data"&&this.getDataTimestamps();for(d=c,m=0;d+g)}getLabelForValue(e){const t=this._adapter,i=this.options.time;return i.tooltipFormat?t.format(e,i.tooltipFormat):t.format(e,i.displayFormats.datetime)}format(e,t){const l=this.options.time.displayFormats,s=this._unit,o=t||l[s];return this._adapter.format(e,o)}_tickFormatFunction(e,t,i,l){const s=this.options,o=s.ticks.callback;if(o)return dt(o,[e,t,i],this);const r=s.time.displayFormats,a=this._unit,u=this._majorUnit,f=a&&r[a],c=u&&r[u],d=i[t],m=u&&c&&d&&d.major;return this._adapter.format(e,l||(m?c:f))}generateTickLabels(e){let t,i,l;for(t=0,i=e.length;t0?r:1}getDataTimestamps(){let e=this._cache.data||[],t,i;if(e.length)return e;const l=this.getMatchingVisibleMetas();if(this._normalized&&l.length)return this._cache.data=l[0].controller.getAllParsedValues(this);for(t=0,i=l.length;t=n[i].pos&&e<=n[l].pos&&({lo:i,hi:l}=Cl(n,"pos",e)),{pos:s,time:r}=n[i],{pos:o,time:a}=n[l]):(e>=n[i].time&&e<=n[l].time&&({lo:i,hi:l}=Cl(n,"time",e)),{time:s,pos:r}=n[i],{time:o,pos:a}=n[l]);const u=o-s;return u?r+(a-r)*(e-s)/u:r}class Gd extends Qs{constructor(e){super(e),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const e=this._getTimestampsForTable(),t=this._table=this.buildLookupTable(e);this._minPos=Wo(t,this.min),this._tableRange=Wo(t,this.max)-this._minPos,super.initOffsets(e)}buildLookupTable(e){const{min:t,max:i}=this,l=[],s=[];let o,r,a,u,f;for(o=0,r=e.length;o=t&&u<=i&&l.push(u);if(l.length<2)return[{time:t,pos:0},{time:i,pos:1}];for(o=0,r=l.length;ol-s)}_getTimestampsForTable(){let e=this._cache.all||[];if(e.length)return e;const t=this.getDataTimestamps(),i=this.getLabelTimestamps();return t.length&&i.length?e=this.normalize(t.concat(i)):e=t.length?t:i,e=this._cache.all=e,e}getDecimalForValue(e){return(Wo(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){const t=this._offsets,i=this.getDecimalForPixel(e)/t.factor-t.end;return Wo(this._table,i*this._tableRange+this._minPos,!0)}}ct(Gd,"id","timeseries"),ct(Gd,"defaults",Qs.defaults);/*! + * chartjs-adapter-luxon v1.3.1 + * https://www.chartjs.org + * (c) 2023 chartjs-adapter-luxon Contributors + * Released under the MIT license + */const V$={datetime:Xe.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:Xe.TIME_WITH_SECONDS,minute:Xe.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};Oy._date.override({_id:"luxon",_create:function(n){return Xe.fromMillis(n,this.options)},init(n){this.options.locale||(this.options.locale=n.locale)},formats:function(){return V$},parse:function(n,e){const t=this.options,i=typeof n;return n===null||i==="undefined"?null:(i==="number"?n=this._create(n):i==="string"?typeof e=="string"?n=Xe.fromFormat(n,e,t):n=Xe.fromISO(n,t):n instanceof Date?n=Xe.fromJSDate(n,t):i==="object"&&!(n instanceof Xe)&&(n=Xe.fromObject(n,t)),n.isValid?n.valueOf():null)},format:function(n,e){const t=this._create(n);return typeof e=="string"?t.toFormat(e):t.toLocaleString(e)},add:function(n,e,t){const i={};return i[t]=e,this._create(n).plus(i).valueOf()},diff:function(n,e,t){return this._create(n).diff(this._create(e)).as(t).valueOf()},startOf:function(n,e,t){if(e==="isoWeek"){t=Math.trunc(Math.min(Math.max(0,t),6));const i=this._create(n);return i.minus({days:(i.weekday-t+7)%7}).startOf("day").valueOf()}return e?this._create(n).startOf(e).valueOf():n},endOf:function(n,e){return this._create(n).endOf(e).valueOf()}});function B$(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}var Uy={exports:{}};/*! Hammer.JS - v2.0.7 - 2016-04-22 + * http://hammerjs.github.io/ + * + * Copyright (c) 2016 Jorik Tangelder; + * Licensed under the MIT license */(function(n){(function(e,t,i,l){var s=["","webkit","Moz","MS","ms","o"],o=t.createElement("div"),r="function",a=Math.round,u=Math.abs,f=Date.now;function c(W,G,ne){return setTimeout(T(W,ne),G)}function d(W,G,ne){return Array.isArray(W)?(m(W,ne[G],ne),!0):!1}function m(W,G,ne){var de;if(W)if(W.forEach)W.forEach(G,ne);else if(W.length!==l)for(de=0;de\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",mt=e.console&&(e.console.warn||e.console.log);return mt&&mt.call(e.console,de,Ye),W.apply(this,arguments)}}var g;typeof Object.assign!="function"?g=function(G){if(G===l||G===null)throw new TypeError("Cannot convert undefined or null to object");for(var ne=Object(G),de=1;de-1}function P(W){return W.trim().split(/\s+/g)}function R(W,G,ne){if(W.indexOf&&!ne)return W.indexOf(G);for(var de=0;deTn[G]}),de}function j(W,G){for(var ne,de,Ie=G[0].toUpperCase()+G.slice(1),Ye=0;Ye1&&!ne.firstMultiple?ne.firstMultiple=mn(G):Ie===1&&(ne.firstMultiple=!1);var Ye=ne.firstInput,mt=ne.firstMultiple,an=mt?mt.center:Ye.center,dn=G.center=hn(de);G.timeStamp=f(),G.deltaTime=G.timeStamp-Ye.timeStamp,G.angle=rn(an,dn),G.distance=gt(an,dn),Dt(ne,G),G.offsetDirection=Ci(G.deltaX,G.deltaY);var Tn=pi(G.deltaTime,G.deltaX,G.deltaY);G.overallVelocityX=Tn.x,G.overallVelocityY=Tn.y,G.overallVelocity=u(Tn.x)>u(Tn.y)?Tn.x:Tn.y,G.scale=mt?rl(mt.pointers,de):1,G.rotation=mt?sn(mt.pointers,de):0,G.maxPointers=ne.prevInput?G.pointers.length>ne.prevInput.maxPointers?G.pointers.length:ne.prevInput.maxPointers:G.pointers.length,Gt(ne,G);var hi=W.element;I(G.srcEvent.target,hi)&&(hi=G.srcEvent.target),G.target=hi}function Dt(W,G){var ne=G.center,de=W.offsetDelta||{},Ie=W.prevDelta||{},Ye=W.prevInput||{};(G.eventType===Be||Ye.eventType===Je)&&(Ie=W.prevDelta={x:Ye.deltaX||0,y:Ye.deltaY||0},de=W.offsetDelta={x:ne.x,y:ne.y}),G.deltaX=Ie.x+(ne.x-de.x),G.deltaY=Ie.y+(ne.y-de.y)}function Gt(W,G){var ne=W.lastInterval||G,de=G.timeStamp-ne.timeStamp,Ie,Ye,mt,an;if(G.eventType!=at&&(de>et||ne.velocity===l)){var dn=G.deltaX-ne.deltaX,Tn=G.deltaY-ne.deltaY,hi=pi(de,dn,Tn);Ye=hi.x,mt=hi.y,Ie=u(hi.x)>u(hi.y)?hi.x:hi.y,an=Ci(dn,Tn),W.lastInterval=G}else Ie=ne.velocity,Ye=ne.velocityX,mt=ne.velocityY,an=ne.direction;G.velocity=Ie,G.velocityX=Ye,G.velocityY=mt,G.direction=an}function mn(W){for(var G=[],ne=0;ne=u(G)?W<0?Te:Ze:G<0?ot:Le}function gt(W,G,ne){ne||(ne=ut);var de=G[ne[0]]-W[ne[0]],Ie=G[ne[1]]-W[ne[1]];return Math.sqrt(de*de+Ie*Ie)}function rn(W,G,ne){ne||(ne=ut);var de=G[ne[0]]-W[ne[0]],Ie=G[ne[1]]-W[ne[1]];return Math.atan2(Ie,de)*180/Math.PI}function sn(W,G){return rn(G[1],G[0],Ne)+rn(W[1],W[0],Ne)}function rl(W,G){return gt(G[0],G[1],Ne)/gt(W[0],W[1],Ne)}var It={mousedown:Be,mousemove:rt,mouseup:Je},al="mousedown",ul="mousemove mouseup";function Hi(){this.evEl=al,this.evWin=ul,this.pressed=!1,xe.apply(this,arguments)}S(Hi,xe,{handler:function(G){var ne=It[G.type];ne&Be&&G.button===0&&(this.pressed=!0),ne&rt&&G.which!==1&&(ne=Je),this.pressed&&(ne&Je&&(this.pressed=!1),this.callback(this.manager,ne,{pointers:[G],changedPointers:[G],pointerType:We,srcEvent:G}))}});var ji={pointerdown:Be,pointermove:rt,pointerup:Je,pointercancel:at,pointerout:at},fl={2:ke,3:Ce,4:We,5:st},Mn="pointerdown",Rl="pointermove pointerup pointercancel";e.MSPointerEvent&&!e.PointerEvent&&(Mn="MSPointerDown",Rl="MSPointerMove MSPointerUp MSPointerCancel");function cl(){this.evEl=Mn,this.evWin=Rl,xe.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}S(cl,xe,{handler:function(G){var ne=this.store,de=!1,Ie=G.type.toLowerCase().replace("ms",""),Ye=ji[Ie],mt=fl[G.pointerType]||G.pointerType,an=mt==ke,dn=R(ne,G.pointerId,"pointerId");Ye&Be&&(G.button===0||an)?dn<0&&(ne.push(G),dn=ne.length-1):Ye&(Je|at)&&(de=!0),!(dn<0)&&(ne[dn]=G,this.callback(this.manager,Ye,{pointers:ne,changedPointers:[G],pointerType:mt,srcEvent:G}),de&&ne.splice(dn,1))}});var Z={touchstart:Be,touchmove:rt,touchend:Je,touchcancel:at},Q="touchstart",se="touchstart touchmove touchend touchcancel";function he(){this.evTarget=Q,this.evWin=se,this.started=!1,xe.apply(this,arguments)}S(he,xe,{handler:function(G){var ne=Z[G.type];if(ne===Be&&(this.started=!0),!!this.started){var de=qe.call(this,G,ne);ne&(Je|at)&&de[0].length-de[1].length===0&&(this.started=!1),this.callback(this.manager,ne,{pointers:de[0],changedPointers:de[1],pointerType:ke,srcEvent:G})}}});function qe(W,G){var ne=N(W.touches),de=N(W.changedTouches);return G&(Je|at)&&(ne=U(ne.concat(de),"identifier")),[ne,de]}var le={touchstart:Be,touchmove:rt,touchend:Je,touchcancel:at},Ee="touchstart touchmove touchend touchcancel";function Re(){this.evTarget=Ee,this.targetIds={},xe.apply(this,arguments)}S(Re,xe,{handler:function(G){var ne=le[G.type],de=Ke.call(this,G,ne);de&&this.callback(this.manager,ne,{pointers:de[0],changedPointers:de[1],pointerType:ke,srcEvent:G})}});function Ke(W,G){var ne=N(W.touches),de=this.targetIds;if(G&(Be|rt)&&ne.length===1)return de[ne[0].identifier]=!0,[ne,ne];var Ie,Ye,mt=N(W.changedTouches),an=[],dn=this.target;if(Ye=ne.filter(function(Tn){return I(Tn.target,dn)}),G===Be)for(Ie=0;Ie-1&&de.splice(Ye,1)};setTimeout(Ie,Ae)}}function dl(W){for(var G=W.srcEvent.clientX,ne=W.srcEvent.clientY,de=0;de-1&&this.requireFail.splice(G,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(W){return!!this.simultaneous[W.id]},emit:function(W){var G=this,ne=this.state;function de(Ie){G.manager.emit(Ie,W)}ne=zi&&de(G.options.event+af(ne))},tryEmit:function(W){if(this.canEmit())return this.emit(W);this.state=mi},canEmit:function(){for(var W=0;WG.threshold&&Ie&G.direction},attrTest:function(W){return ni.prototype.attrTest.call(this,W)&&(this.state&Bn||!(this.state&Bn)&&this.directionTest(W))},emit:function(W){this.pX=W.deltaX,this.pY=W.deltaY;var G=uf(W.direction);G&&(W.additionalEvent=this.options.event+G),this._super.emit.call(this,W)}});function Br(){ni.apply(this,arguments)}S(Br,ni,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[Ei]},attrTest:function(W){return this._super.attrTest.call(this,W)&&(Math.abs(W.scale-1)>this.options.threshold||this.state&Bn)},emit:function(W){if(W.scale!==1){var G=W.scale<1?"in":"out";W.additionalEvent=this.options.event+G}this._super.emit.call(this,W)}});function Wr(){Di.apply(this,arguments),this._timer=null,this._input=null}S(Wr,Di,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[bo]},process:function(W){var G=this.options,ne=W.pointers.length===G.pointers,de=W.distanceG.time;if(this._input=W,!de||!ne||W.eventType&(Je|at)&&!Ie)this.reset();else if(W.eventType&Be)this.reset(),this._timer=c(function(){this.state=Mi,this.tryEmit()},G.time,this);else if(W.eventType&Je)return Mi;return mi},reset:function(){clearTimeout(this._timer)},emit:function(W){this.state===Mi&&(W&&W.eventType&Je?this.manager.emit(this.options.event+"up",W):(this._input.timeStamp=f(),this.manager.emit(this.options.event,this._input)))}});function Yr(){ni.apply(this,arguments)}S(Yr,ni,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[Ei]},attrTest:function(W){return this._super.attrTest.call(this,W)&&(Math.abs(W.rotation)>this.options.threshold||this.state&Bn)}});function Kr(){ni.apply(this,arguments)}S(Kr,ni,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Ve|we,pointers:1},getTouchAction:function(){return vo.prototype.getTouchAction.call(this)},attrTest:function(W){var G=this.options.direction,ne;return G&(Ve|we)?ne=W.overallVelocity:G&Ve?ne=W.overallVelocityX:G&we&&(ne=W.overallVelocityY),this._super.attrTest.call(this,W)&&G&W.offsetDirection&&W.distance>this.options.threshold&&W.maxPointers==this.options.pointers&&u(ne)>this.options.velocity&&W.eventType&Je},emit:function(W){var G=uf(W.offsetDirection);G&&this.manager.emit(this.options.event+G,W),this.manager.emit(this.options.event,W)}});function wo(){Di.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}S(wo,Di,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ms]},process:function(W){var G=this.options,ne=W.pointers.length===G.pointers,de=W.distancen&&n.enabled&&n.modifierKey,Vy=(n,e)=>n&&e[n+"Key"],Gu=(n,e)=>n&&!e[n+"Key"];function sl(n,e,t){return n===void 0?!0:typeof n=="string"?n.indexOf(e)!==-1:typeof n=="function"?n({chart:t}).indexOf(e)!==-1:!1}function ya(n,e){return typeof n=="function"&&(n=n({chart:e})),typeof n=="string"?{x:n.indexOf("x")!==-1,y:n.indexOf("y")!==-1}:{x:!1,y:!1}}function Y$(n,e){let t;return function(){return clearTimeout(t),t=setTimeout(n,e),e}}function K$({x:n,y:e},t){const i=t.scales,l=Object.keys(i);for(let s=0;s=o.top&&e<=o.bottom&&n>=o.left&&n<=o.right)return o}return null}function By(n,e,t){const{mode:i="xy",scaleMode:l,overScaleMode:s}=n||{},o=K$(e,t),r=ya(i,t),a=ya(l,t);if(s){const f=ya(s,t);for(const c of["x","y"])f[c]&&(a[c]=r[c],r[c]=!1)}if(o&&a[o.axis])return[o];const u=[];return ht(t.scales,function(f){r[f.axis]&&u.push(f)}),u}const iu=new WeakMap;function Jt(n){let e=iu.get(n);return e||(e={originalScaleLimits:{},updatedScaleLimits:{},handlers:{},panDelta:{}},iu.set(n,e)),e}function J$(n){iu.delete(n)}function Wy(n,e,t){const i=n.max-n.min,l=i*(e-1),s=n.isHorizontal()?t.x:t.y,o=Math.max(0,Math.min(1,(n.getValueForPixel(s)-n.min)/i||0)),r=1-o;return{min:l*o,max:l*r}}function Xd(n,e,t,i,l){let s=t[i];if(s==="original"){const o=n.originalScaleLimits[e.id][i];s=Ct(o.options,o.scale)}return Ct(s,l)}function Z$(n,e,t){const i=n.getValueForPixel(e),l=n.getValueForPixel(t);return{min:Math.min(i,l),max:Math.max(i,l)}}function fs(n,{min:e,max:t},i,l=!1){const s=Jt(n.chart),{id:o,axis:r,options:a}=n,u=i&&(i[o]||i[r])||{},{minRange:f=0}=u,c=Xd(s,n,u,"min",-1/0),d=Xd(s,n,u,"max",1/0),m=l?Math.max(t-e,f):n.max-n.min,h=(m-t+e)/2;return e-=h,t+=h,ed&&(t=d,e=Math.max(d-m,c)),a.min=e,a.max=t,s.updatedScaleLimits[n.id]={min:e,max:t},n.parse(e)!==n.min||n.parse(t)!==n.max}function G$(n,e,t,i){const l=Wy(n,e,t),s={min:n.min+l.min,max:n.max-l.max};return fs(n,s,i,!0)}function X$(n,e,t,i){fs(n,Z$(n,e,t),i,!0)}const Qd=n=>n===0||isNaN(n)?0:n<0?Math.min(Math.round(n),-1):Math.max(Math.round(n),1);function Q$(n){const t=n.getLabels().length-1;n.min>0&&(n.min-=1),n.maxa&&(s=Math.max(0,s-u),o=r===1?s:s+r,f=s===0),fs(n,{min:s,max:o},t)||f}const nC={second:500,minute:30*1e3,hour:30*60*1e3,day:12*60*60*1e3,week:3.5*24*60*60*1e3,month:15*24*60*60*1e3,quarter:60*24*60*60*1e3,year:182*24*60*60*1e3};function Yy(n,e,t,i=!1){const{min:l,max:s,options:o}=n,r=o.time&&o.time.round,a=nC[r]||0,u=n.getValueForPixel(n.getPixelForValue(l+a)-e),f=n.getValueForPixel(n.getPixelForValue(s+a)-e),{min:c=-1/0,max:d=1/0}=i&&t&&t[n.axis]||{};return isNaN(u)||isNaN(f)||ud?!0:fs(n,{min:u,max:f},t,i)}function xd(n,e,t){return Yy(n,e,t,!0)}const lu={category:x$,default:G$},su={default:X$},ou={category:tC,default:Yy,logarithmic:xd,timeseries:xd};function iC(n,e,t){const{id:i,options:{min:l,max:s}}=n;if(!e[i]||!t[i])return!0;const o=t[i];return o.min!==l||o.max!==s}function ep(n,e){ht(n,(t,i)=>{e[i]||delete n[i]})}function cs(n,e){const{scales:t}=n,{originalScaleLimits:i,updatedScaleLimits:l}=e;return ht(t,function(s){iC(s,i,l)&&(i[s.id]={min:{scale:s.min,options:s.options.min},max:{scale:s.max,options:s.options.max}})}),ep(i,t),ep(l,t),i}function tp(n,e,t,i){const l=lu[n.type]||lu.default;dt(l,[n,e,t,i])}function np(n,e,t,i,l){const s=su[n.type]||su.default;dt(s,[n,e,t,i,l])}function lC(n){const e=n.chartArea;return{x:(e.left+e.right)/2,y:(e.top+e.bottom)/2}}function Xu(n,e,t="none"){const{x:i=1,y:l=1,focalPoint:s=lC(n)}=typeof e=="number"?{x:e,y:e}:e,o=Jt(n),{options:{limits:r,zoom:a}}=o;cs(n,o);const u=i!==1,f=l!==1,c=By(a,s,n);ht(c||n.scales,function(d){d.isHorizontal()&&u?tp(d,i,s,r):!d.isHorizontal()&&f&&tp(d,l,s,r)}),n.update(t),dt(a.onZoom,[{chart:n}])}function Ky(n,e,t,i="none"){const l=Jt(n),{options:{limits:s,zoom:o}}=l,{mode:r="xy"}=o;cs(n,l);const a=sl(r,"x",n),u=sl(r,"y",n);ht(n.scales,function(f){f.isHorizontal()&&a?np(f,e.x,t.x,s):!f.isHorizontal()&&u&&np(f,e.y,t.y,s)}),n.update(i),dt(o.onZoom,[{chart:n}])}function sC(n,e,t,i="none"){cs(n,Jt(n));const l=n.scales[e];fs(l,t,void 0,!0),n.update(i)}function oC(n,e="default"){const t=Jt(n),i=cs(n,t);ht(n.scales,function(l){const s=l.options;i[l.id]?(s.min=i[l.id].min.options,s.max=i[l.id].max.options):(delete s.min,delete s.max)}),n.update(e),dt(t.options.zoom.onZoomComplete,[{chart:n}])}function rC(n,e){const t=n.originalScaleLimits[e];if(!t)return;const{min:i,max:l}=t;return Ct(l.options,l.scale)-Ct(i.options,i.scale)}function aC(n){const e=Jt(n);let t=1,i=1;return ht(n.scales,function(l){const s=rC(e,l.id);if(s){const o=Math.round(s/(l.max-l.min)*100)/100;t=Math.min(t,o),i=Math.max(i,o)}}),t<1?t:i}function ip(n,e,t,i){const{panDelta:l}=i,s=l[n.id]||0;il(s)===il(e)&&(e+=s);const o=ou[n.type]||ou.default;dt(o,[n,e,t])?l[n.id]=0:l[n.id]=e}function Jy(n,e,t,i="none"){const{x:l=0,y:s=0}=typeof e=="number"?{x:e,y:e}:e,o=Jt(n),{options:{pan:r,limits:a}}=o,{onPan:u}=r||{};cs(n,o);const f=l!==0,c=s!==0;ht(t||n.scales,function(d){d.isHorizontal()&&f?ip(d,l,a,o):!d.isHorizontal()&&c&&ip(d,s,a,o)}),n.update(i),dt(u,[{chart:n}])}function Zy(n){const e=Jt(n);cs(n,e);const t={};for(const i of Object.keys(n.scales)){const{min:l,max:s}=e.originalScaleLimits[i]||{min:{},max:{}};t[i]={min:l.scale,max:s.scale}}return t}function uC(n){const e=Zy(n);for(const t of Object.keys(n.scales)){const{min:i,max:l}=e[t];if(i!==void 0&&n.scales[t].min!==i||l!==void 0&&n.scales[t].max!==l)return!0}return!1}function Ln(n,e){const{handlers:t}=Jt(n),i=t[e];i&&i.target&&(i.target.removeEventListener(e,i),delete t[e])}function qs(n,e,t,i){const{handlers:l,options:s}=Jt(n),o=l[t];o&&o.target===e||(Ln(n,t),l[t]=r=>i(n,r,s),l[t].target=e,e.addEventListener(t,l[t]))}function fC(n,e){const t=Jt(n);t.dragStart&&(t.dragging=!0,t.dragEnd=e,n.update("none"))}function cC(n,e){const t=Jt(n);!t.dragStart||e.key!=="Escape"||(Ln(n,"keydown"),t.dragging=!1,t.dragStart=t.dragEnd=null,n.update("none"))}function Gy(n,e,t){const{onZoomStart:i,onZoomRejected:l}=t;if(i){const s=ki(e,n);if(dt(i,[{chart:n,event:e,point:s}])===!1)return dt(l,[{chart:n,event:e}]),!1}}function dC(n,e){const t=Jt(n),{pan:i,zoom:l={}}=t.options;if(e.button!==0||Vy(xs(i),e)||Gu(xs(l.drag),e))return dt(l.onZoomRejected,[{chart:n,event:e}]);Gy(n,e,l)!==!1&&(t.dragStart=e,qs(n,n.canvas,"mousemove",fC),qs(n,window.document,"keydown",cC))}function Xy(n,e,t,i){const l=sl(e,"x",n),s=sl(e,"y",n);let{top:o,left:r,right:a,bottom:u,width:f,height:c}=n.chartArea;const d=ki(t,n),m=ki(i,n);l&&(r=Math.min(d.x,m.x),a=Math.max(d.x,m.x)),s&&(o=Math.min(d.y,m.y),u=Math.max(d.y,m.y));const h=a-r,g=u-o;return{left:r,top:o,right:a,bottom:u,width:h,height:g,zoomX:l&&h?1+(f-h)/f:1,zoomY:s&&g?1+(c-g)/c:1}}function pC(n,e){const t=Jt(n);if(!t.dragStart)return;Ln(n,"mousemove");const{mode:i,onZoomComplete:l,drag:{threshold:s=0}}=t.options.zoom,o=Xy(n,i,t.dragStart,e),r=sl(i,"x",n)?o.width:0,a=sl(i,"y",n)?o.height:0,u=Math.sqrt(r*r+a*a);if(t.dragStart=t.dragEnd=null,u<=s){t.dragging=!1,n.update("none");return}Ky(n,{x:o.left,y:o.top},{x:o.right,y:o.bottom},"zoom"),setTimeout(()=>t.dragging=!1,500),dt(l,[{chart:n}])}function mC(n,e,t){if(Gu(xs(t.wheel),e)){dt(t.onZoomRejected,[{chart:n,event:e}]);return}if(Gy(n,e,t)!==!1&&(e.cancelable&&e.preventDefault(),e.deltaY!==void 0))return!0}function hC(n,e){const{handlers:{onZoomComplete:t},options:{zoom:i}}=Jt(n);if(!mC(n,e,i))return;const l=e.target.getBoundingClientRect(),s=1+(e.deltaY>=0?-i.wheel.speed:i.wheel.speed),o={x:s,y:s,focalPoint:{x:e.clientX-l.left,y:e.clientY-l.top}};Xu(n,o),t&&t()}function _C(n,e,t,i){t&&(Jt(n).handlers[e]=Y$(()=>dt(t,[{chart:n}]),i))}function gC(n,e){const t=n.canvas,{wheel:i,drag:l,onZoomComplete:s}=e.zoom;i.enabled?(qs(n,t,"wheel",hC),_C(n,"onZoomComplete",s,250)):Ln(n,"wheel"),l.enabled?(qs(n,t,"mousedown",dC),qs(n,t.ownerDocument,"mouseup",pC)):(Ln(n,"mousedown"),Ln(n,"mousemove"),Ln(n,"mouseup"),Ln(n,"keydown"))}function bC(n){Ln(n,"mousedown"),Ln(n,"mousemove"),Ln(n,"mouseup"),Ln(n,"wheel"),Ln(n,"click"),Ln(n,"keydown")}function yC(n,e){return function(t,i){const{pan:l,zoom:s={}}=e.options;if(!l||!l.enabled)return!1;const o=i&&i.srcEvent;return o&&!e.panning&&i.pointerType==="mouse"&&(Gu(xs(l),o)||Vy(xs(s.drag),o))?(dt(l.onPanRejected,[{chart:n,event:i}]),!1):!0}}function kC(n,e){const t=Math.abs(n.clientX-e.clientX),i=Math.abs(n.clientY-e.clientY),l=t/i;let s,o;return l>.3&&l<1.7?s=o=!0:t>i?s=!0:o=!0,{x:s,y:o}}function Qy(n,e,t){if(e.scale){const{center:i,pointers:l}=t,s=1/e.scale*t.scale,o=t.target.getBoundingClientRect(),r=kC(l[0],l[1]),a=e.options.zoom.mode,u={x:r.x&&sl(a,"x",n)?s:1,y:r.y&&sl(a,"y",n)?s:1,focalPoint:{x:i.x-o.left,y:i.y-o.top}};Xu(n,u),e.scale=t.scale}}function vC(n,e){e.options.zoom.pinch.enabled&&(e.scale=1)}function wC(n,e,t){e.scale&&(Qy(n,e,t),e.scale=null,dt(e.options.zoom.onZoomComplete,[{chart:n}]))}function xy(n,e,t){const i=e.delta;i&&(e.panning=!0,Jy(n,{x:t.deltaX-i.x,y:t.deltaY-i.y},e.panScales),e.delta={x:t.deltaX,y:t.deltaY})}function SC(n,e,t){const{enabled:i,onPanStart:l,onPanRejected:s}=e.options.pan;if(!i)return;const o=t.target.getBoundingClientRect(),r={x:t.center.x-o.left,y:t.center.y-o.top};if(dt(l,[{chart:n,event:t,point:r}])===!1)return dt(s,[{chart:n,event:t}]);e.panScales=By(e.options.pan,r,n),e.delta={x:0,y:0},clearTimeout(e.panEndTimeout),xy(n,e,t)}function TC(n,e){e.delta=null,e.panning&&(e.panEndTimeout=setTimeout(()=>e.panning=!1,500),dt(e.options.pan.onPanComplete,[{chart:n}]))}const ru=new WeakMap;function $C(n,e){const t=Jt(n),i=n.canvas,{pan:l,zoom:s}=e,o=new Fs.Manager(i);s&&s.pinch.enabled&&(o.add(new Fs.Pinch),o.on("pinchstart",()=>vC(n,t)),o.on("pinch",r=>Qy(n,t,r)),o.on("pinchend",r=>wC(n,t,r))),l&&l.enabled&&(o.add(new Fs.Pan({threshold:l.threshold,enable:yC(n,t)})),o.on("panstart",r=>SC(n,t,r)),o.on("panmove",r=>xy(n,t,r)),o.on("panend",()=>TC(n,t))),ru.set(n,o)}function CC(n){const e=ru.get(n);e&&(e.remove("pinchstart"),e.remove("pinch"),e.remove("pinchend"),e.remove("panstart"),e.remove("pan"),e.remove("panend"),e.destroy(),ru.delete(n))}var OC="2.0.1";function Yo(n,e,t){const i=t.zoom.drag,{dragStart:l,dragEnd:s}=Jt(n);if(i.drawTime!==e||!s)return;const{left:o,top:r,width:a,height:u}=Xy(n,t.zoom.mode,l,s),f=n.ctx;f.save(),f.beginPath(),f.fillStyle=i.backgroundColor||"rgba(225,225,225,0.3)",f.fillRect(o,r,a,u),i.borderWidth>0&&(f.lineWidth=i.borderWidth,f.strokeStyle=i.borderColor||"rgba(225,225,225)",f.strokeRect(o,r,a,u)),f.restore()}var EC={id:"zoom",version:OC,defaults:{pan:{enabled:!1,mode:"xy",threshold:10,modifierKey:null},zoom:{wheel:{enabled:!1,speed:.1,modifierKey:null},drag:{enabled:!1,drawTime:"beforeDatasetsDraw",modifierKey:null},pinch:{enabled:!1},mode:"xy"}},start:function(n,e,t){const i=Jt(n);i.options=t,Object.prototype.hasOwnProperty.call(t.zoom,"enabled")&&console.warn("The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`."),(Object.prototype.hasOwnProperty.call(t.zoom,"overScaleMode")||Object.prototype.hasOwnProperty.call(t.pan,"overScaleMode"))&&console.warn("The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired)."),Fs&&$C(n,t),n.pan=(l,s,o)=>Jy(n,l,s,o),n.zoom=(l,s)=>Xu(n,l,s),n.zoomRect=(l,s,o)=>Ky(n,l,s,o),n.zoomScale=(l,s,o)=>sC(n,l,s,o),n.resetZoom=l=>oC(n,l),n.getZoomLevel=()=>aC(n),n.getInitialScaleBounds=()=>Zy(n),n.isZoomedOrPanned=()=>uC(n)},beforeEvent(n){const e=Jt(n);if(e.panning||e.dragging)return!1},beforeUpdate:function(n,e,t){const i=Jt(n);i.options=t,gC(n,t)},beforeDatasetsDraw(n,e,t){Yo(n,"beforeDatasetsDraw",t)},afterDatasetsDraw(n,e,t){Yo(n,"afterDatasetsDraw",t)},beforeDraw(n,e,t){Yo(n,"beforeDraw",t)},afterDraw(n,e,t){Yo(n,"afterDraw",t)},stop:function(n){bC(n),Fs&&CC(n),J$(n)},panFunctions:ou,zoomFunctions:lu,zoomRectFunctions:su};function lp(n){let e,t,i;return{c(){e=b("div"),p(e,"class","chart-loader loader svelte-kfnurg")},m(l,s){v(l,e,s),i=!0},i(l){i||(l&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150},!0)),t.run(1))}),i=!0)},o(l){l&&(t||(t=ze(e,Mt,{duration:150},!1)),t.run(0)),i=!1},d(l){l&&k(e),l&&t&&t.end()}}}function sp(n){let e,t,i;return{c(){e=b("button"),e.textContent="Reset zoom",p(e,"type","button"),p(e,"class","btn btn-secondary btn-sm btn-chart-zoom svelte-kfnurg")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[4]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function MC(n){let e,t,i,l,s,o=n[1]==1?"log":"logs",r,a,u,f,c,d,m,h=n[2]&&lp(),g=n[3]&&sp(n);return{c(){e=b("div"),t=b("div"),i=Y("Found "),l=Y(n[1]),s=C(),r=Y(o),a=C(),h&&h.c(),u=C(),f=b("canvas"),c=C(),g&&g.c(),p(t,"class","total-logs entrance-right svelte-kfnurg"),x(t,"hidden",n[2]),p(f,"class","chart-canvas svelte-kfnurg"),p(e,"class","chart-wrapper svelte-kfnurg"),x(e,"loading",n[2])},m(_,y){v(_,e,y),w(e,t),w(t,i),w(t,l),w(t,s),w(t,r),w(e,a),h&&h.m(e,null),w(e,u),w(e,f),n[11](f),w(e,c),g&&g.m(e,null),d||(m=B(f,"dblclick",n[4]),d=!0)},p(_,[y]){y&2&&ue(l,_[1]),y&2&&o!==(o=_[1]==1?"log":"logs")&&ue(r,o),y&4&&x(t,"hidden",_[2]),_[2]?h?y&4&&O(h,1):(h=lp(),h.c(),O(h,1),h.m(e,u)):h&&(re(),D(h,1,1,()=>{h=null}),ae()),_[3]?g?g.p(_,y):(g=sp(_),g.c(),g.m(e,null)):g&&(g.d(1),g=null),y&4&&x(e,"loading",_[2])},i(_){O(h)},o(_){D(h)},d(_){_&&k(e),h&&h.d(),n[11](null),g&&g.d(),d=!1,m()}}}function DC(n,e,t){let{filter:i=""}=e,{zoom:l={}}=e,{presets:s=""}=e,o,r,a=[],u=0,f=!1,c=!1;async function d(){t(2,f=!0);const _=[s,z.normalizeLogsFilter(i)].filter(Boolean).join("&&");return me.logs.getStats({filter:_}).then(y=>{m(),y=z.toArray(y);for(let S of y)a.push({x:new Date(S.date),y:S.total}),t(1,u+=S.total)}).catch(y=>{y!=null&&y.isAbort||(m(),console.warn(y),me.error(y,!_||(y==null?void 0:y.status)!=400))}).finally(()=>{t(2,f=!1)})}function m(){t(10,a=[]),t(1,u=0)}function h(){r==null||r.resetZoom()}Yt(()=>(vi.register(Gi,ar,sr,nu,Qs,M$,F$),vi.register(EC),t(9,r=new vi(o,{type:"line",data:{datasets:[{label:"Total requests",data:a,borderColor:"#e34562",pointBackgroundColor:"#e34562",backgroundColor:"rgb(239,69,101,0.05)",borderWidth:2,pointRadius:1,pointBorderWidth:0,fill:!0}]},options:{resizeDelay:250,maintainAspectRatio:!1,animation:!1,interaction:{intersect:!1,mode:"index"},scales:{y:{beginAtZero:!0,grid:{color:"#edf0f3"},border:{color:"#e4e9ec"},ticks:{precision:0,maxTicksLimit:4,autoSkip:!0,color:"#666f75"}},x:{type:"time",time:{unit:"hour",tooltipFormat:"DD h a"},grid:{color:_=>{var y;return(y=_.tick)!=null&&y.major?"#edf0f3":""}},color:"#e4e9ec",ticks:{maxTicksLimit:15,autoSkip:!0,maxRotation:0,major:{enabled:!0},color:_=>{var y;return(y=_.tick)!=null&&y.major?"#16161a":"#666f75"}}}},plugins:{legend:{display:!1},zoom:{enabled:!0,zoom:{mode:"x",pinch:{enabled:!0},drag:{enabled:!0,backgroundColor:"rgba(255, 99, 132, 0.2)",borderWidth:0,threshold:10},limits:{x:{minRange:1e8},y:{minRange:1e8}},onZoomComplete:({chart:_})=>{t(3,c=_.isZoomedOrPanned()),c?(t(5,l.min=z.formatToUTCDate(_.scales.x.min,"yyyy-MM-dd HH")+":00:00.000Z",l),t(5,l.max=z.formatToUTCDate(_.scales.x.max,"yyyy-MM-dd HH")+":59:59.999Z",l)):(l.min||l.max)&&t(5,l={})}}}}}})),()=>r==null?void 0:r.destroy()));function g(_){ie[_?"unshift":"push"](()=>{o=_,t(0,o)})}return n.$$set=_=>{"filter"in _&&t(6,i=_.filter),"zoom"in _&&t(5,l=_.zoom),"presets"in _&&t(7,s=_.presets)},n.$$.update=()=>{n.$$.dirty&192&&(typeof i<"u"||typeof s<"u")&&d(),n.$$.dirty&1536&&typeof a<"u"&&r&&(t(9,r.data.datasets[0].data=a,r),r.update())},[o,u,f,c,h,l,i,s,d,r,a,g]}class IC extends ye{constructor(e){super(),be(this,e,DC,MC,_e,{filter:6,zoom:5,presets:7,load:8})}get load(){return this.$$.ctx[8]}}function LC(n){let e,t,i;return{c(){e=b("div"),t=b("code"),p(t,"class","svelte-s3jkbp"),p(e,"class",i="code-wrapper prism-light "+n[0]+" svelte-s3jkbp")},m(l,s){v(l,e,s),w(e,t),t.innerHTML=n[1]},p(l,[s]){s&2&&(t.innerHTML=l[1]),s&1&&i!==(i="code-wrapper prism-light "+l[0]+" svelte-s3jkbp")&&p(e,"class",i)},i:te,o:te,d(l){l&&k(e)}}}function AC(n,e,t){let{content:i=""}=e,{language:l="javascript"}=e,{class:s=""}=e,o="";function r(a){return a=typeof a=="string"?a:"",a=Prism.plugins.NormalizeWhitespace.normalize(a,{"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.highlight(a,Prism.languages[l]||Prism.languages.javascript,l)}return n.$$set=a=>{"content"in a&&t(2,i=a.content),"language"in a&&t(3,l=a.language),"class"in a&&t(0,s=a.class)},n.$$.update=()=>{n.$$.dirty&4&&typeof Prism<"u"&&i&&t(1,o=r(i))},[s,o,i,l]}class Qu extends ye{constructor(e){super(),be(this,e,AC,LC,_e,{content:2,language:3,class:0})}}function PC(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"tabindex","-1"),p(e,"role","button"),p(e,"class",t=n[3]?n[2]:n[1]),p(e,"aria-label","Copy to clipboard")},m(o,r){v(o,e,r),l||(s=[Me(i=He.call(null,e,n[3]?void 0:n[0])),B(e,"click",On(n[4]))],l=!0)},p(o,[r]){r&14&&t!==(t=o[3]?o[2]:o[1])&&p(e,"class",t),i&&Rt(i.update)&&r&9&&i.update.call(null,o[3]?void 0:o[0])},i:te,o:te,d(o){o&&k(e),l=!1,De(s)}}}function NC(n,e,t){let{value:i=""}=e,{tooltip:l="Copy"}=e,{idleClasses:s="ri-file-copy-line txt-sm link-hint"}=e,{successClasses:o="ri-check-line txt-sm txt-success"}=e,{successDuration:r=500}=e,a;function u(){i&&(z.copyToClipboard(i),clearTimeout(a),t(3,a=setTimeout(()=>{clearTimeout(a),t(3,a=null)},r)))}return Yt(()=>()=>{a&&clearTimeout(a)}),n.$$set=f=>{"value"in f&&t(5,i=f.value),"tooltip"in f&&t(0,l=f.tooltip),"idleClasses"in f&&t(1,s=f.idleClasses),"successClasses"in f&&t(2,o=f.successClasses),"successDuration"in f&&t(6,r=f.successDuration)},[l,s,o,a,u,i,r]}class ai extends ye{constructor(e){super(),be(this,e,NC,PC,_e,{value:5,tooltip:0,idleClasses:1,successClasses:2,successDuration:6})}}function op(n,e,t){const i=n.slice();i[16]=e[t];const l=i[1].data[i[16]];i[17]=l;const s=z.isEmpty(i[17]);i[18]=s;const o=!i[18]&&i[17]!==null&&typeof i[17]=="object";return i[19]=o,i}function RC(n){let e,t,i,l,s,o,r,a=n[1].id+"",u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P,R,N,U,j,V,K,J;d=new ai({props:{value:n[1].id}}),S=new iy({props:{level:n[1].level}}),E=new ai({props:{value:n[1].level}}),R=new ny({props:{date:n[1].created}}),j=new ai({props:{value:n[1].created}});let ee=!n[4]&&rp(n),X=pe(n[5](n[1].data)),oe=[];for(let ke=0;keD(oe[ke],1,1,()=>{oe[ke]=null});return{c(){e=b("table"),t=b("tbody"),i=b("tr"),l=b("td"),l.textContent="id",s=C(),o=b("td"),r=b("span"),u=Y(a),f=C(),c=b("div"),H(d.$$.fragment),m=C(),h=b("tr"),g=b("td"),g.textContent="level",_=C(),y=b("td"),H(S.$$.fragment),T=C(),$=b("div"),H(E.$$.fragment),M=C(),L=b("tr"),I=b("td"),I.textContent="created",A=C(),P=b("td"),H(R.$$.fragment),N=C(),U=b("div"),H(j.$$.fragment),V=C(),ee&&ee.c(),K=C();for(let ke=0;ke{ee=null}),ae()):ee?(ee.p(ke,Ce),Ce&16&&O(ee,1)):(ee=rp(ke),ee.c(),O(ee,1),ee.m(t,K)),Ce&50){X=pe(ke[5](ke[1].data));let Je;for(Je=0;Je',p(e,"class","block txt-center")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function rp(n){let e,t,i,l,s,o,r;const a=[HC,qC],u=[];function f(c,d){return c[1].message?0:1}return s=f(n),o=u[s]=a[s](n),{c(){e=b("tr"),t=b("td"),t.textContent="message",i=C(),l=b("td"),o.c(),p(t,"class","min-width txt-hint txt-bold svelte-1c23bpt"),p(l,"class","svelte-1c23bpt"),p(e,"class","svelte-1c23bpt")},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),u[s].m(l,null),r=!0},p(c,d){let m=s;s=f(c),s===m?u[s].p(c,d):(re(),D(u[m],1,1,()=>{u[m]=null}),ae(),o=u[s],o?o.p(c,d):(o=u[s]=a[s](c),o.c()),O(o,1),o.m(l,null))},i(c){r||(O(o),r=!0)},o(c){D(o),r=!1},d(c){c&&k(e),u[s].d()}}}function qC(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function HC(n){let e,t=n[1].message+"",i,l,s,o,r;return o=new ai({props:{value:n[1].message}}),{c(){e=b("span"),i=Y(t),l=C(),s=b("div"),H(o.$$.fragment),p(e,"class","txt"),p(s,"class","copy-icon-wrapper svelte-1c23bpt")},m(a,u){v(a,e,u),w(e,i),v(a,l,u),v(a,s,u),F(o,s,null),r=!0},p(a,u){(!r||u&2)&&t!==(t=a[1].message+"")&&ue(i,t);const f={};u&2&&(f.value=a[1].message),o.$set(f)},i(a){r||(O(o.$$.fragment,a),r=!0)},o(a){D(o.$$.fragment,a),r=!1},d(a){a&&(k(e),k(l),k(s)),q(o)}}}function jC(n){let e,t=n[17]+"",i,l=n[4]&&n[16]=="execTime"?"ms":"",s;return{c(){e=b("span"),i=Y(t),s=Y(l),p(e,"class","txt")},m(o,r){v(o,e,r),w(e,i),w(e,s)},p(o,r){r&2&&t!==(t=o[17]+"")&&ue(i,t),r&18&&l!==(l=o[4]&&o[16]=="execTime"?"ms":"")&&ue(s,l)},i:te,o:te,d(o){o&&k(e)}}}function zC(n){let e,t;return e=new Qu({props:{content:n[17],language:"html"}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.content=i[17]),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function UC(n){let e,t=n[17]+"",i;return{c(){e=b("span"),i=Y(t),p(e,"class","label label-danger log-error-label svelte-1c23bpt")},m(l,s){v(l,e,s),w(e,i)},p(l,s){s&2&&t!==(t=l[17]+"")&&ue(i,t)},i:te,o:te,d(l){l&&k(e)}}}function VC(n){let e,t;return e=new Qu({props:{content:JSON.stringify(n[17],null,2)}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.content=JSON.stringify(i[17],null,2)),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function BC(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function ap(n){let e,t,i;return t=new ai({props:{value:n[17]}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","copy-icon-wrapper svelte-1c23bpt")},m(l,s){v(l,e,s),F(t,e,null),i=!0},p(l,s){const o={};s&2&&(o.value=l[17]),t.$set(o)},i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function up(n){let e,t,i,l=n[16]+"",s,o,r,a,u,f,c,d;const m=[BC,VC,UC,zC,jC],h=[];function g(y,S){return y[18]?0:y[19]?1:y[16]=="error"?2:y[16]=="details"?3:4}a=g(n),u=h[a]=m[a](n);let _=!n[18]&&ap(n);return{c(){e=b("tr"),t=b("td"),i=Y("data."),s=Y(l),o=C(),r=b("td"),u.c(),f=C(),_&&_.c(),c=C(),p(t,"class","min-width txt-hint txt-bold svelte-1c23bpt"),x(t,"v-align-top",n[19]),p(r,"class","svelte-1c23bpt"),p(e,"class","svelte-1c23bpt")},m(y,S){v(y,e,S),w(e,t),w(t,i),w(t,s),w(e,o),w(e,r),h[a].m(r,null),w(r,f),_&&_.m(r,null),w(e,c),d=!0},p(y,S){(!d||S&2)&&l!==(l=y[16]+"")&&ue(s,l),(!d||S&34)&&x(t,"v-align-top",y[19]);let T=a;a=g(y),a===T?h[a].p(y,S):(re(),D(h[T],1,1,()=>{h[T]=null}),ae(),u=h[a],u?u.p(y,S):(u=h[a]=m[a](y),u.c()),O(u,1),u.m(r,f)),y[18]?_&&(re(),D(_,1,1,()=>{_=null}),ae()):_?(_.p(y,S),S&2&&O(_,1)):(_=ap(y),_.c(),O(_,1),_.m(r,null))},i(y){d||(O(u),O(_),d=!0)},o(y){D(u),D(_),d=!1},d(y){y&&k(e),h[a].d(),_&&_.d()}}}function WC(n){let e,t,i,l;const s=[FC,RC],o=[];function r(a,u){var f;return a[3]?0:(f=a[1])!=null&&f.id?1:-1}return~(e=r(n))&&(t=o[e]=s[e](n)),{c(){t&&t.c(),i=ge()},m(a,u){~e&&o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?~e&&o[e].p(a,u):(t&&(re(),D(o[f],1,1,()=>{o[f]=null}),ae()),~e?(t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i)):t=null)},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),~e&&o[e].d(a)}}}function YC(n){let e;return{c(){e=b("h4"),e.textContent="Request log"},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function KC(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),e.innerHTML='Close',t=C(),i=b("button"),l=b("i"),s=C(),o=b("span"),o.textContent="Download as JSON",p(e,"type","button"),p(e,"class","btn btn-transparent"),p(l,"class","ri-download-line"),p(o,"class","txt"),p(i,"type","button"),p(i,"class","btn btn-primary"),i.disabled=n[3]},m(u,f){v(u,e,f),v(u,t,f),v(u,i,f),w(i,l),w(i,s),w(i,o),r||(a=[B(e,"click",n[9]),B(i,"click",n[10])],r=!0)},p(u,f){f&8&&(i.disabled=u[3])},d(u){u&&(k(e),k(t),k(i)),r=!1,De(a)}}}function JC(n){let e,t,i={class:"overlay-panel-lg log-panel",$$slots:{footer:[KC],header:[YC],default:[WC]},$$scope:{ctx:n}};return e=new ln({props:i}),n[11](e),e.$on("hide",n[7]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&4194330&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[11](null),q(e,l)}}}const fp="log_view";function ZC(n,e,t){let i;const l=_t();let s,o={},r=!1;function a(T){return f(T).then($=>{t(1,o=$),h()}),s==null?void 0:s.show()}function u(){return me.cancelRequest(fp),s==null?void 0:s.hide()}async function f(T){if(T&&typeof T!="string")return t(3,r=!1),T;t(3,r=!0);let $={};try{$=await me.logs.getOne(T,{requestKey:fp})}catch(E){E.isAbort||(u(),console.warn("resolveModel:",E),$i(`Unable to load log with id "${T}"`))}return t(3,r=!1),$}const c=["execTime","type","auth","authId","status","method","url","referer","remoteIP","userIP","userAgent","error","details"];function d(T){if(!T)return[];let $=[];for(let M of c)typeof T[M]<"u"&&$.push(M);const E=Object.keys(T);for(let M of E)$.includes(M)||$.push(M);return $}function m(){z.downloadJson(o,"log_"+o.created.replaceAll(/[-:\. ]/gi,"")+".json")}function h(){l("show",o)}function g(){l("hide",o),t(1,o={})}const _=()=>u(),y=()=>m();function S(T){ie[T?"unshift":"push"](()=>{s=T,t(2,s)})}return n.$$.update=()=>{var T;n.$$.dirty&2&&t(4,i=((T=o.data)==null?void 0:T.type)=="request")},[u,o,s,r,i,d,m,g,a,_,y,S]}class GC extends ye{constructor(e){super(),be(this,e,ZC,JC,_e,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function XC(n,e,t){const i=n.slice();return i[1]=e[t],i}function QC(n){let e;return{c(){e=b("code"),e.textContent=`${n[1].level}:${n[1].label}`,p(e,"class","txt-xs")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function xC(n){let e,t,i,l=pe(q0),s=[];for(let o=0;o{"class"in l&&t(0,i=l.class)},[i]}class ek extends ye{constructor(e){super(),be(this,e,e5,xC,_e,{class:0})}}function t5(n){let e,t,i,l,s,o,r,a,u,f,c;return t=new fe({props:{class:"form-field required",name:"logs.maxDays",$$slots:{default:[i5,({uniqueId:d})=>({23:d}),({uniqueId:d})=>d?8388608:0]},$$scope:{ctx:n}}}),l=new fe({props:{class:"form-field",name:"logs.minLevel",$$slots:{default:[l5,({uniqueId:d})=>({23:d}),({uniqueId:d})=>d?8388608:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field form-field-toggle",name:"logs.logIP",$$slots:{default:[s5,({uniqueId:d})=>({23:d}),({uniqueId:d})=>d?8388608:0]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field form-field-toggle",name:"logs.logAuthId",$$slots:{default:[o5,({uniqueId:d})=>({23:d}),({uniqueId:d})=>d?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),H(t.$$.fragment),i=C(),H(l.$$.fragment),s=C(),H(o.$$.fragment),r=C(),H(a.$$.fragment),p(e,"id",n[6]),p(e,"class","grid"),p(e,"autocomplete","off")},m(d,m){v(d,e,m),F(t,e,null),w(e,i),F(l,e,null),w(e,s),F(o,e,null),w(e,r),F(a,e,null),u=!0,f||(c=B(e,"submit",tt(n[7])),f=!0)},p(d,m){const h={};m&25165826&&(h.$$scope={dirty:m,ctx:d}),t.$set(h);const g={};m&25165826&&(g.$$scope={dirty:m,ctx:d}),l.$set(g);const _={};m&25165826&&(_.$$scope={dirty:m,ctx:d}),o.$set(_);const y={};m&25165826&&(y.$$scope={dirty:m,ctx:d}),a.$set(y)},i(d){u||(O(t.$$.fragment,d),O(l.$$.fragment,d),O(o.$$.fragment,d),O(a.$$.fragment,d),u=!0)},o(d){D(t.$$.fragment,d),D(l.$$.fragment,d),D(o.$$.fragment,d),D(a.$$.fragment,d),u=!1},d(d){d&&k(e),q(t),q(l),q(o),q(a),f=!1,c()}}}function n5(n){let e;return{c(){e=b("div"),e.innerHTML='
',p(e,"class","block txt-center")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function i5(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Max days retention"),l=C(),s=b("input"),r=C(),a=b("div"),a.innerHTML="Set to 0 to disable logs persistence.",p(e,"for",i=n[23]),p(s,"type","number"),p(s,"id",o=n[23]),s.required=!0,p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[1].logs.maxDays),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[11]),u=!0)},p(c,d){d&8388608&&i!==(i=c[23])&&p(e,"for",i),d&8388608&&o!==(o=c[23])&&p(s,"id",o),d&2&&St(s.value)!==c[1].logs.maxDays&&ce(s,c[1].logs.maxDays)},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function l5(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;return f=new ek({}),{c(){e=b("label"),t=Y("Min log level"),l=C(),s=b("input"),o=C(),r=b("div"),a=b("p"),a.textContent="Logs with level below the minimum will be ignored.",u=C(),H(f.$$.fragment),p(e,"for",i=n[23]),p(s,"type","number"),s.required=!0,p(s,"min","-100"),p(s,"max","100"),p(r,"class","help-block")},m(h,g){v(h,e,g),w(e,t),v(h,l,g),v(h,s,g),ce(s,n[1].logs.minLevel),v(h,o,g),v(h,r,g),w(r,a),w(r,u),F(f,r,null),c=!0,d||(m=B(s,"input",n[12]),d=!0)},p(h,g){(!c||g&8388608&&i!==(i=h[23]))&&p(e,"for",i),g&2&&St(s.value)!==h[1].logs.minLevel&&ce(s,h[1].logs.minLevel)},i(h){c||(O(f.$$.fragment,h),c=!0)},o(h){D(f.$$.fragment,h),c=!1},d(h){h&&(k(e),k(l),k(s),k(o),k(r)),q(f),d=!1,m()}}}function s5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable IP logging"),p(e,"type","checkbox"),p(e,"id",t=n[23]),p(l,"for",o=n[23])},m(u,f){v(u,e,f),e.checked=n[1].logs.logIP,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[13]),r=!0)},p(u,f){f&8388608&&t!==(t=u[23])&&p(e,"id",t),f&2&&(e.checked=u[1].logs.logIP),f&8388608&&o!==(o=u[23])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function o5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable Auth Id logging"),p(e,"type","checkbox"),p(e,"id",t=n[23]),p(l,"for",o=n[23])},m(u,f){v(u,e,f),e.checked=n[1].logs.logAuthId,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[14]),r=!0)},p(u,f){f&8388608&&t!==(t=u[23])&&p(e,"id",t),f&2&&(e.checked=u[1].logs.logAuthId),f&8388608&&o!==(o=u[23])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function r5(n){let e,t,i,l;const s=[n5,t5],o=[];function r(a,u){return a[4]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function a5(n){let e;return{c(){e=b("h4"),e.textContent="Logs settings"},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function u5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=C(),l=b("button"),s=b("span"),s.textContent="Save changes",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[3],x(l,"btn-loading",n[3])},m(u,f){v(u,e,f),w(e,t),v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"click",n[0]),r=!0)},p(u,f){f&8&&(e.disabled=u[3]),f&40&&o!==(o=!u[5]||u[3])&&(l.disabled=o),f&8&&x(l,"btn-loading",u[3])},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function f5(n){let e,t,i={popup:!0,class:"superuser-panel",beforeHide:n[15],$$slots:{footer:[u5],header:[a5],default:[r5]},$$scope:{ctx:n}};return e=new ln({props:i}),n[16](e),e.$on("hide",n[17]),e.$on("show",n[18]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&8&&(o.beforeHide=l[15]),s&16777274&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[16](null),q(e,l)}}}function c5(n,e,t){let i,l;const s=_t(),o="logs_settings_"+z.randomString(3);let r,a=!1,u=!1,f={},c={};function d(){return h(),g(),r==null?void 0:r.show()}function m(){return r==null?void 0:r.hide()}function h(){Wt(),t(9,f={}),t(1,c=JSON.parse(JSON.stringify(f||{})))}async function g(){t(4,u=!0);try{const P=await me.settings.getAll()||{};y(P)}catch(P){me.error(P)}t(4,u=!1)}async function _(){if(l){t(3,a=!0);try{const P=await me.settings.update(z.filterRedactedProps(c));y(P),t(3,a=!1),m(),tn("Successfully saved logs settings."),s("save",P)}catch(P){t(3,a=!1),me.error(P)}}}function y(P={}){t(1,c={logs:(P==null?void 0:P.logs)||{}}),t(9,f=JSON.parse(JSON.stringify(c)))}function S(){c.logs.maxDays=St(this.value),t(1,c)}function T(){c.logs.minLevel=St(this.value),t(1,c)}function $(){c.logs.logIP=this.checked,t(1,c)}function E(){c.logs.logAuthId=this.checked,t(1,c)}const M=()=>!a;function L(P){ie[P?"unshift":"push"](()=>{r=P,t(2,r)})}function I(P){Pe.call(this,n,P)}function A(P){Pe.call(this,n,P)}return n.$$.update=()=>{n.$$.dirty&512&&t(10,i=JSON.stringify(f)),n.$$.dirty&1026&&t(5,l=i!=JSON.stringify(c))},[m,c,r,a,u,l,o,_,d,f,i,S,T,$,E,M,L,I,A]}class d5 extends ye{constructor(e){super(),be(this,e,c5,f5,_e,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function p5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Include requests by superusers"),p(e,"type","checkbox"),p(e,"id",t=n[25]),p(l,"for",o=n[25])},m(u,f){v(u,e,f),e.checked=n[2],v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[12]),r=!0)},p(u,f){f&33554432&&t!==(t=u[25])&&p(e,"id",t),f&4&&(e.checked=u[2]),f&33554432&&o!==(o=u[25])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function cp(n){let e,t,i;function l(o){n[14](o)}let s={filter:n[1],presets:n[6]};return n[5]!==void 0&&(s.zoom=n[5]),e=new IC({props:s}),ie.push(()=>ve(e,"zoom",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&2&&(a.filter=o[1]),r&64&&(a.presets=o[6]),!t&&r&32&&(t=!0,a.zoom=o[5],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function dp(n){let e,t,i,l;function s(a){n[15](a)}function o(a){n[16](a)}let r={presets:n[6]};return n[1]!==void 0&&(r.filter=n[1]),n[5]!==void 0&&(r.zoom=n[5]),e=new F3({props:r}),ie.push(()=>ve(e,"filter",s)),ie.push(()=>ve(e,"zoom",o)),e.$on("select",n[17]),{c(){H(e.$$.fragment)},m(a,u){F(e,a,u),l=!0},p(a,u){const f={};u&64&&(f.presets=a[6]),!t&&u&2&&(t=!0,f.filter=a[1],$e(()=>t=!1)),!i&&u&32&&(i=!0,f.zoom=a[5],$e(()=>i=!1)),e.$set(f)},i(a){l||(O(e.$$.fragment,a),l=!0)},o(a){D(e.$$.fragment,a),l=!1},d(a){q(e,a)}}}function m5(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$=n[4],E,M=n[4],L,I,A,P;u=new Au({}),u.$on("refresh",n[11]),h=new fe({props:{class:"form-field form-field-toggle m-0",$$slots:{default:[p5,({uniqueId:U})=>({25:U}),({uniqueId:U})=>U?33554432:0]},$$scope:{ctx:n}}}),_=new Hr({props:{value:n[1],placeholder:"Search term or filter like `level > 0 && data.auth = 'guest'`",extraAutocompleteKeys:["level","message","data."]}}),_.$on("submit",n[13]),S=new ek({props:{class:"block txt-sm txt-hint m-t-xs m-b-base"}});let R=cp(n),N=dp(n);return{c(){e=b("div"),t=b("header"),i=b("nav"),l=b("div"),s=Y(n[7]),o=C(),r=b("button"),r.innerHTML='',a=C(),H(u.$$.fragment),f=C(),c=b("div"),d=C(),m=b("div"),H(h.$$.fragment),g=C(),H(_.$$.fragment),y=C(),H(S.$$.fragment),T=C(),R.c(),E=C(),N.c(),L=ge(),p(l,"class","breadcrumb-item"),p(i,"class","breadcrumbs"),p(r,"type","button"),p(r,"aria-label","Logs settings"),p(r,"class","btn btn-transparent btn-circle"),p(c,"class","flex-fill"),p(m,"class","inline-flex"),p(t,"class","page-header"),p(e,"class","page-header-wrapper m-b-0")},m(U,j){v(U,e,j),w(e,t),w(t,i),w(i,l),w(l,s),w(t,o),w(t,r),w(t,a),F(u,t,null),w(t,f),w(t,c),w(t,d),w(t,m),F(h,m,null),w(e,g),F(_,e,null),w(e,y),F(S,e,null),w(e,T),R.m(e,null),v(U,E,j),N.m(U,j),v(U,L,j),I=!0,A||(P=[Me(He.call(null,r,{text:"Logs settings",position:"right"})),B(r,"click",n[10])],A=!0)},p(U,j){(!I||j&128)&&ue(s,U[7]);const V={};j&100663300&&(V.$$scope={dirty:j,ctx:U}),h.$set(V);const K={};j&2&&(K.value=U[1]),_.$set(K),j&16&&_e($,$=U[4])?(re(),D(R,1,1,te),ae(),R=cp(U),R.c(),O(R,1),R.m(e,null)):R.p(U,j),j&16&&_e(M,M=U[4])?(re(),D(N,1,1,te),ae(),N=dp(U),N.c(),O(N,1),N.m(L.parentNode,L)):N.p(U,j)},i(U){I||(O(u.$$.fragment,U),O(h.$$.fragment,U),O(_.$$.fragment,U),O(S.$$.fragment,U),O(R),O(N),I=!0)},o(U){D(u.$$.fragment,U),D(h.$$.fragment,U),D(_.$$.fragment,U),D(S.$$.fragment,U),D(R),D(N),I=!1},d(U){U&&(k(e),k(E),k(L)),q(u),q(h),q(_),q(S),R.d(U),N.d(U),A=!1,De(P)}}}function h5(n){let e,t,i,l,s,o;e=new di({props:{$$slots:{default:[m5]},$$scope:{ctx:n}}});let r={};i=new GC({props:r}),n[18](i),i.$on("show",n[19]),i.$on("hide",n[20]);let a={};return s=new d5({props:a}),n[21](s),s.$on("save",n[8]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),l=C(),H(s.$$.fragment)},m(u,f){F(e,u,f),v(u,t,f),F(i,u,f),v(u,l,f),F(s,u,f),o=!0},p(u,[f]){const c={};f&67109119&&(c.$$scope={dirty:f,ctx:u}),e.$set(c);const d={};i.$set(d);const m={};s.$set(m)},i(u){o||(O(e.$$.fragment,u),O(i.$$.fragment,u),O(s.$$.fragment,u),o=!0)},o(u){D(e.$$.fragment,u),D(i.$$.fragment,u),D(s.$$.fragment,u),o=!1},d(u){u&&(k(t),k(l)),q(e,u),n[18](null),q(i,u),n[21](null),q(s,u)}}}const Ko="logId",pp="superuserRequests",mp="superuserLogRequests";function _5(n,e,t){var N;let i,l,s;Qe(n,Lu,U=>t(22,l=U)),Qe(n,cn,U=>t(7,s=U)),Nn(cn,s="Logs",s);const o=new URLSearchParams(l);let r,a,u=1,f=o.get("filter")||"",c={},d=(o.get(pp)||((N=window.localStorage)==null?void 0:N.getItem(mp)))<<0,m=d;function h(){t(4,u++,u)}function g(U={}){let j={};j.filter=f||null,j[pp]=d<<0||null,z.replaceHashQueryParams(Object.assign(j,U))}const _=()=>a==null?void 0:a.show(),y=()=>h();function S(){d=this.checked,t(2,d)}const T=U=>t(1,f=U.detail);function $(U){c=U,t(5,c)}function E(U){f=U,t(1,f)}function M(U){c=U,t(5,c)}const L=U=>r==null?void 0:r.show(U==null?void 0:U.detail);function I(U){ie[U?"unshift":"push"](()=>{r=U,t(0,r)})}const A=U=>{var V;let j={};j[Ko]=((V=U.detail)==null?void 0:V.id)||null,z.replaceHashQueryParams(j)},P=()=>{let U={};U[Ko]=null,z.replaceHashQueryParams(U)};function R(U){ie[U?"unshift":"push"](()=>{a=U,t(3,a)})}return n.$$.update=()=>{var U;n.$$.dirty&1&&o.get(Ko)&&r&&r.show(o.get(Ko)),n.$$.dirty&4&&t(6,i=d?"":'data.auth!="_superusers"'),n.$$.dirty&516&&m!=d&&(t(9,m=d),(U=window.localStorage)==null||U.setItem(mp,d<<0),g()),n.$$.dirty&2&&typeof f<"u"&&g()},[r,f,d,a,u,c,i,s,h,m,_,y,S,T,$,E,M,L,I,A,P,R]}class g5 extends ye{constructor(e){super(),be(this,e,_5,h5,_e,{})}}function hp(n,e,t){const i=n.slice();return i[14]=e[t][0],i[15]=e[t][1],i}function _p(n){n[18]=n[19].default}function gp(n,e,t){const i=n.slice();return i[14]=e[t][0],i[15]=e[t][1],i[21]=t,i}function bp(n){let e;return{c(){e=b("hr"),p(e,"class","m-t-sm m-b-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function yp(n,e){let t,i=e[21]===Object.keys(e[6]).length,l,s,o=e[15].label+"",r,a,u,f,c=i&&bp();function d(){return e[9](e[14])}return{key:n,first:null,c(){t=ge(),c&&c.c(),l=C(),s=b("button"),r=Y(o),a=C(),p(s,"type","button"),p(s,"class","sidebar-item"),x(s,"active",e[5]===e[14]),this.first=t},m(m,h){v(m,t,h),c&&c.m(m,h),v(m,l,h),v(m,s,h),w(s,r),w(s,a),u||(f=B(s,"click",d),u=!0)},p(m,h){e=m,h&8&&(i=e[21]===Object.keys(e[6]).length),i?c||(c=bp(),c.c(),c.m(l.parentNode,l)):c&&(c.d(1),c=null),h&8&&o!==(o=e[15].label+"")&&ue(r,o),h&40&&x(s,"active",e[5]===e[14])},d(m){m&&(k(t),k(l),k(s)),c&&c.d(m),u=!1,f()}}}function kp(n){let e,t,i,l={ctx:n,current:null,token:null,hasCatch:!1,pending:k5,then:y5,catch:b5,value:19,blocks:[,,,]};return mf(t=n[15].component,l),{c(){e=ge(),l.block.c()},m(s,o){v(s,e,o),l.block.m(s,l.anchor=o),l.mount=()=>e.parentNode,l.anchor=e,i=!0},p(s,o){n=s,l.ctx=n,o&8&&t!==(t=n[15].component)&&mf(t,l)||Lk(l,n,o)},i(s){i||(O(l.block),i=!0)},o(s){for(let o=0;o<3;o+=1){const r=l.blocks[o];D(r)}i=!1},d(s){s&&k(e),l.block.d(s),l.token=null,l=null}}}function b5(n){return{c:te,m:te,p:te,i:te,o:te,d:te}}function y5(n){_p(n);let e,t,i;return e=new n[18]({props:{collection:n[2]}}),{c(){H(e.$$.fragment),t=C()},m(l,s){F(e,l,s),v(l,t,s),i=!0},p(l,s){_p(l);const o={};s&4&&(o.collection=l[2]),e.$set(o)},i(l){i||(O(e.$$.fragment,l),i=!0)},o(l){D(e.$$.fragment,l),i=!1},d(l){l&&k(t),q(e,l)}}}function k5(n){return{c:te,m:te,p:te,i:te,o:te,d:te}}function vp(n,e){let t,i,l,s=e[5]===e[14]&&kp(e);return{key:n,first:null,c(){t=ge(),s&&s.c(),i=ge(),this.first=t},m(o,r){v(o,t,r),s&&s.m(o,r),v(o,i,r),l=!0},p(o,r){e=o,e[5]===e[14]?s?(s.p(e,r),r&40&&O(s,1)):(s=kp(e),s.c(),O(s,1),s.m(i.parentNode,i)):s&&(re(),D(s,1,1,()=>{s=null}),ae())},i(o){l||(O(s),l=!0)},o(o){D(s),l=!1},d(o){o&&(k(t),k(i)),s&&s.d(o)}}}function v5(n){let e,t,i,l=[],s=new Map,o,r,a=[],u=new Map,f,c=pe(Object.entries(n[3]));const d=g=>g[14];for(let g=0;gg[14];for(let g=0;gClose',p(e,"type","button"),p(e,"class","btn btn-transparent")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[8]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function S5(n){let e,t,i={class:"docs-panel",$$slots:{footer:[w5],default:[v5]},$$scope:{ctx:n}};return e=new ln({props:i}),n[10](e),e.$on("hide",n[11]),e.$on("show",n[12]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&4194348&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[10](null),q(e,l)}}}function T5(n,e,t){const i={list:{label:"List/Search",component:Ot(()=>import("./ListApiDocs-C0epAOOQ.js"),__vite__mapDeps([2,3,4]),import.meta.url)},view:{label:"View",component:Ot(()=>import("./ViewApiDocs-B6MdbOQi.js"),__vite__mapDeps([5,3]),import.meta.url)},create:{label:"Create",component:Ot(()=>import("./CreateApiDocs-Cvocn8eg.js"),__vite__mapDeps([6,3]),import.meta.url)},update:{label:"Update",component:Ot(()=>import("./UpdateApiDocs-BIFiuRUJ.js"),__vite__mapDeps([7,3]),import.meta.url)},delete:{label:"Delete",component:Ot(()=>import("./DeleteApiDocs-CPOP5CUw.js"),[],import.meta.url)},realtime:{label:"Realtime",component:Ot(()=>import("./RealtimeApiDocs-BCmrhp7T.js"),[],import.meta.url)}},l={"list-auth-methods":{label:"List auth methods",component:Ot(()=>import("./AuthMethodsDocs-DkjR8bbt.js"),__vite__mapDeps([8,3]),import.meta.url)},refresh:{label:"Auth refresh",component:Ot(()=>import("./AuthRefreshDocs-DVyzazkj.js"),__vite__mapDeps([9,3]),import.meta.url)},"auth-with-password":{label:"Auth with password",component:Ot(()=>import("./AuthWithPasswordDocs-CTYk9AZ_.js"),__vite__mapDeps([10,3]),import.meta.url)},"auth-with-oauth2":{label:"Auth with OAuth2",component:Ot(()=>import("./AuthWithOAuth2Docs-C0rwhR6l.js"),__vite__mapDeps([11,3]),import.meta.url)},"auth-with-otp":{label:"Auth with OTP",component:Ot(()=>import("./AuthWithOtpDocs-CB9fgmn9.js"),[],import.meta.url)},verification:{label:"Verification",component:Ot(()=>import("./VerificationDocs-BIWtoqhd.js"),[],import.meta.url)},"password-reset":{label:"Password reset",component:Ot(()=>import("./PasswordResetDocs-BWqelyHu.js"),[],import.meta.url)},"email-change":{label:"Email change",component:Ot(()=>import("./EmailChangeDocs-VdDPaZK5.js"),[],import.meta.url)}};let s,o={},r,a=[];a.length&&(r=Object.keys(a)[0]);function u(y){return t(2,o=y),c(Object.keys(a)[0]),s==null?void 0:s.show()}function f(){return s==null?void 0:s.hide()}function c(y){t(5,r=y)}const d=()=>f(),m=y=>c(y);function h(y){ie[y?"unshift":"push"](()=>{s=y,t(4,s)})}function g(y){Pe.call(this,n,y)}function _(y){Pe.call(this,n,y)}return n.$$.update=()=>{n.$$.dirty&12&&(o.type==="auth"?(t(3,a=Object.assign({},i,l)),o.passwordAuth.enabled||delete a["auth-with-password"],o.oauth2.enabled||delete a["auth-with-oauth2"],o.otp.enabled||delete a["auth-with-otp"]):o.type==="view"?(t(3,a=Object.assign({},i)),delete a.create,delete a.update,delete a.delete,delete a.realtime):t(3,a=Object.assign({},i)))},[f,c,o,a,s,r,i,u,d,m,h,g,_]}class $5 extends ye{constructor(e){super(),be(this,e,T5,S5,_e,{show:7,hide:0,changeTab:1})}get show(){return this.$$.ctx[7]}get hide(){return this.$$.ctx[0]}get changeTab(){return this.$$.ctx[1]}}const C5=n=>({active:n&1}),wp=n=>({active:n[0]});function Sp(n){let e,t,i;const l=n[15].default,s=Lt(l,n,n[14],null);return{c(){e=b("div"),s&&s.c(),p(e,"class","accordion-content")},m(o,r){v(o,e,r),s&&s.m(e,null),i=!0},p(o,r){s&&s.p&&(!i||r&16384)&&Pt(s,l,o,o[14],i?At(l,o[14],r,null):Nt(o[14]),null)},i(o){i||(O(s,o),o&&nt(()=>{i&&(t||(t=ze(e,vt,{delay:10,duration:150},!0)),t.run(1))}),i=!0)},o(o){D(s,o),o&&(t||(t=ze(e,vt,{delay:10,duration:150},!1)),t.run(0)),i=!1},d(o){o&&k(e),s&&s.d(o),o&&t&&t.end()}}}function O5(n){let e,t,i,l,s,o,r;const a=n[15].header,u=Lt(a,n,n[14],wp);let f=n[0]&&Sp(n);return{c(){e=b("div"),t=b("button"),u&&u.c(),i=C(),f&&f.c(),p(t,"type","button"),p(t,"class","accordion-header"),p(t,"draggable",n[2]),p(t,"aria-expanded",n[0]),x(t,"interactive",n[3]),p(e,"class",l="accordion "+(n[7]?"drag-over":"")+" "+n[1]),x(e,"active",n[0])},m(c,d){v(c,e,d),w(e,t),u&&u.m(t,null),w(e,i),f&&f.m(e,null),n[22](e),s=!0,o||(r=[B(t,"click",tt(n[17])),B(t,"drop",tt(n[18])),B(t,"dragstart",n[19]),B(t,"dragenter",n[20]),B(t,"dragleave",n[21]),B(t,"dragover",tt(n[16]))],o=!0)},p(c,[d]){u&&u.p&&(!s||d&16385)&&Pt(u,a,c,c[14],s?At(a,c[14],d,C5):Nt(c[14]),wp),(!s||d&4)&&p(t,"draggable",c[2]),(!s||d&1)&&p(t,"aria-expanded",c[0]),(!s||d&8)&&x(t,"interactive",c[3]),c[0]?f?(f.p(c,d),d&1&&O(f,1)):(f=Sp(c),f.c(),O(f,1),f.m(e,null)):f&&(re(),D(f,1,1,()=>{f=null}),ae()),(!s||d&130&&l!==(l="accordion "+(c[7]?"drag-over":"")+" "+c[1]))&&p(e,"class",l),(!s||d&131)&&x(e,"active",c[0])},i(c){s||(O(u,c),O(f),s=!0)},o(c){D(u,c),D(f),s=!1},d(c){c&&k(e),u&&u.d(c),f&&f.d(),n[22](null),o=!1,De(r)}}}function E5(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=_t();let o,r,{class:a=""}=e,{draggable:u=!1}=e,{active:f=!1}=e,{interactive:c=!0}=e,{single:d=!1}=e,m=!1;function h(){return!!f}function g(){S(),t(0,f=!0),s("expand")}function _(){t(0,f=!1),clearTimeout(r),s("collapse")}function y(){s("toggle"),f?_():g()}function S(){if(d&&o.closest(".accordions")){const P=o.closest(".accordions").querySelectorAll(".accordion.active .accordion-header.interactive");for(const R of P)R.click()}}Yt(()=>()=>clearTimeout(r));function T(P){Pe.call(this,n,P)}const $=()=>c&&y(),E=P=>{u&&(t(7,m=!1),S(),s("drop",P))},M=P=>u&&s("dragstart",P),L=P=>{u&&(t(7,m=!0),s("dragenter",P))},I=P=>{u&&(t(7,m=!1),s("dragleave",P))};function A(P){ie[P?"unshift":"push"](()=>{o=P,t(6,o)})}return n.$$set=P=>{"class"in P&&t(1,a=P.class),"draggable"in P&&t(2,u=P.draggable),"active"in P&&t(0,f=P.active),"interactive"in P&&t(3,c=P.interactive),"single"in P&&t(9,d=P.single),"$$scope"in P&&t(14,l=P.$$scope)},n.$$.update=()=>{n.$$.dirty&8257&&f&&(clearTimeout(r),t(13,r=setTimeout(()=>{o!=null&&o.scrollIntoViewIfNeeded?o.scrollIntoViewIfNeeded():o!=null&&o.scrollIntoView&&o.scrollIntoView({behavior:"smooth",block:"nearest"})},200)))},[f,a,u,c,y,S,o,m,s,d,h,g,_,r,l,i,T,$,E,M,L,I,A]}class qi extends ye{constructor(e){super(),be(this,e,E5,O5,_e,{class:1,draggable:2,active:0,interactive:3,single:9,isExpanded:10,expand:11,collapse:12,toggle:4,collapseSiblings:5})}get isExpanded(){return this.$$.ctx[10]}get expand(){return this.$$.ctx[11]}get collapse(){return this.$$.ctx[12]}get toggle(){return this.$$.ctx[4]}get collapseSiblings(){return this.$$.ctx[5]}}function Tp(n,e,t){const i=n.slice();return i[25]=e[t],i}function $p(n,e,t){const i=n.slice();return i[25]=e[t],i}function Cp(n){let e,t,i=pe(n[3]),l=[];for(let s=0;s0&&Cp(n);return{c(){e=b("label"),t=Y("Subject"),l=C(),s=b("input"),r=C(),c&&c.c(),a=ge(),p(e,"for",i=n[24]),p(s,"type","text"),p(s,"id",o=n[24]),p(s,"spellcheck","false"),s.required=!0},m(m,h){v(m,e,h),w(e,t),v(m,l,h),v(m,s,h),ce(s,n[0].subject),v(m,r,h),c&&c.m(m,h),v(m,a,h),u||(f=B(s,"input",n[14]),u=!0)},p(m,h){var g;h&16777216&&i!==(i=m[24])&&p(e,"for",i),h&16777216&&o!==(o=m[24])&&p(s,"id",o),h&1&&s.value!==m[0].subject&&ce(s,m[0].subject),((g=m[3])==null?void 0:g.length)>0?c?c.p(m,h):(c=Cp(m),c.c(),c.m(a.parentNode,a)):c&&(c.d(1),c=null)},d(m){m&&(k(e),k(l),k(s),k(r),k(a)),c&&c.d(m),u=!1,f()}}}function D5(n){let e,t,i,l;return{c(){e=b("textarea"),p(e,"id",t=n[24]),p(e,"class","txt-mono"),p(e,"spellcheck","false"),p(e,"rows","14"),e.required=!0},m(s,o){v(s,e,o),ce(e,n[0].body),i||(l=B(e,"input",n[17]),i=!0)},p(s,o){o&16777216&&t!==(t=s[24])&&p(e,"id",t),o&1&&ce(e,s[0].body)},i:te,o:te,d(s){s&&k(e),i=!1,l()}}}function I5(n){let e,t,i,l;function s(a){n[16](a)}var o=n[5];function r(a,u){let f={id:a[24],language:"html"};return a[0].body!==void 0&&(f.value=a[0].body),{props:f}}return o&&(e=jt(o,r(n)),ie.push(()=>ve(e,"value",s))),{c(){e&&H(e.$$.fragment),i=ge()},m(a,u){e&&F(e,a,u),v(a,i,u),l=!0},p(a,u){if(u&32&&o!==(o=a[5])){if(e){re();const f=e;D(f.$$.fragment,1,0,()=>{q(f,1)}),ae()}o?(e=jt(o,r(a)),ie.push(()=>ve(e,"value",s)),H(e.$$.fragment),O(e.$$.fragment,1),F(e,i.parentNode,i)):e=null}else if(o){const f={};u&16777216&&(f.id=a[24]),!t&&u&1&&(t=!0,f.value=a[0].body,$e(()=>t=!1)),e.$set(f)}},i(a){l||(e&&O(e.$$.fragment,a),l=!0)},o(a){e&&D(e.$$.fragment,a),l=!1},d(a){a&&k(i),e&&q(e,a)}}}function Ep(n){let e,t,i=pe(n[3]),l=[];for(let s=0;s0&&Ep(n);return{c(){e=b("label"),t=Y("Body (HTML)"),l=C(),o.c(),r=C(),m&&m.c(),a=ge(),p(e,"for",i=n[24])},m(g,_){v(g,e,_),w(e,t),v(g,l,_),c[s].m(g,_),v(g,r,_),m&&m.m(g,_),v(g,a,_),u=!0},p(g,_){var S;(!u||_&16777216&&i!==(i=g[24]))&&p(e,"for",i);let y=s;s=d(g),s===y?c[s].p(g,_):(re(),D(c[y],1,1,()=>{c[y]=null}),ae(),o=c[s],o?o.p(g,_):(o=c[s]=f[s](g),o.c()),O(o,1),o.m(r.parentNode,r)),((S=g[3])==null?void 0:S.length)>0?m?m.p(g,_):(m=Ep(g),m.c(),m.m(a.parentNode,a)):m&&(m.d(1),m=null)},i(g){u||(O(o),u=!0)},o(g){D(o),u=!1},d(g){g&&(k(e),k(l),k(r),k(a)),c[s].d(g),m&&m.d(g)}}}function A5(n){let e,t,i,l;return e=new fe({props:{class:"form-field required",name:n[1]+".subject",$$slots:{default:[M5,({uniqueId:s})=>({24:s}),({uniqueId:s})=>s?16777216:0]},$$scope:{ctx:n}}}),i=new fe({props:{class:"form-field m-0 required",name:n[1]+".body",$$slots:{default:[L5,({uniqueId:s})=>({24:s}),({uniqueId:s})=>s?16777216:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o&2&&(r.name=s[1]+".subject"),o&1090519049&&(r.$$scope={dirty:o,ctx:s}),e.$set(r);const a={};o&2&&(a.name=s[1]+".body"),o&1090519145&&(a.$$scope={dirty:o,ctx:s}),i.$set(a)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function Dp(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function P5(n){let e,t,i,l,s,o,r,a,u,f=n[7]&&Dp();return{c(){e=b("div"),t=b("i"),i=C(),l=b("span"),s=Y(n[2]),o=C(),r=b("div"),a=C(),f&&f.c(),u=ge(),p(t,"class","ri-draft-line"),p(l,"class","txt"),p(e,"class","inline-flex"),p(r,"class","flex-fill")},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),w(l,s),v(c,o,d),v(c,r,d),v(c,a,d),f&&f.m(c,d),v(c,u,d)},p(c,d){d&4&&ue(s,c[2]),c[7]?f?d&128&&O(f,1):(f=Dp(),f.c(),O(f,1),f.m(u.parentNode,u)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(o),k(r),k(a),k(u)),f&&f.d(c)}}}function N5(n){let e,t;const i=[n[9]];let l={$$slots:{header:[P5],default:[A5]},$$scope:{ctx:n}};for(let s=0;st(13,o=N));let{key:r}=e,{title:a}=e,{config:u={}}=e,{placeholders:f=[]}=e,c,d=Ip,m=!1;function h(){c==null||c.expand()}function g(){c==null||c.collapse()}function _(){c==null||c.collapseSiblings()}async function y(){d||m||(t(6,m=!0),t(5,d=(await Ot(async()=>{const{default:N}=await import("./CodeEditor-CPgcqnd5.js");return{default:N}},__vite__mapDeps([12,1]),import.meta.url)).default),Ip=d,t(6,m=!1))}function S(N){N=N.replace("*",""),z.copyToClipboard(N),Ys(`Copied ${N} to clipboard`,2e3)}y();function T(){u.subject=this.value,t(0,u)}const $=N=>S("{"+N+"}");function E(N){n.$$.not_equal(u.body,N)&&(u.body=N,t(0,u))}function M(){u.body=this.value,t(0,u)}const L=N=>S("{"+N+"}");function I(N){ie[N?"unshift":"push"](()=>{c=N,t(4,c)})}function A(N){Pe.call(this,n,N)}function P(N){Pe.call(this,n,N)}function R(N){Pe.call(this,n,N)}return n.$$set=N=>{e=je(je({},e),Ut(N)),t(9,s=lt(e,l)),"key"in N&&t(1,r=N.key),"title"in N&&t(2,a=N.title),"config"in N&&t(0,u=N.config),"placeholders"in N&&t(3,f=N.placeholders)},n.$$.update=()=>{n.$$.dirty&8194&&t(7,i=!z.isEmpty(z.getNestedVal(o,r))),n.$$.dirty&3&&(u.enabled||fi(r))},[u,r,a,f,c,d,m,i,S,s,h,g,_,o,T,$,E,M,L,I,A,P,R]}class F5 extends ye{constructor(e){super(),be(this,e,R5,N5,_e,{key:1,title:2,config:0,placeholders:3,expand:10,collapse:11,collapseSiblings:12})}get expand(){return this.$$.ctx[10]}get collapse(){return this.$$.ctx[11]}get collapseSiblings(){return this.$$.ctx[12]}}function q5(n){let e,t,i,l,s,o,r,a,u,f,c,d;return{c(){e=b("label"),t=Y(n[3]),i=Y(" duration (in seconds)"),s=C(),o=b("input"),a=C(),u=b("div"),f=b("span"),f.textContent="Invalidate all previously issued tokens",p(e,"for",l=n[6]),p(o,"type","number"),p(o,"id",r=n[6]),o.required=!0,p(o,"placeholder","No change"),p(f,"class","link-primary"),x(f,"txt-success",!!n[1]),p(u,"class","help-block")},m(m,h){v(m,e,h),w(e,t),w(e,i),v(m,s,h),v(m,o,h),ce(o,n[0]),v(m,a,h),v(m,u,h),w(u,f),c||(d=[B(o,"input",n[4]),B(f,"click",n[5])],c=!0)},p(m,h){h&8&&ue(t,m[3]),h&64&&l!==(l=m[6])&&p(e,"for",l),h&64&&r!==(r=m[6])&&p(o,"id",r),h&1&&St(o.value)!==m[0]&&ce(o,m[0]),h&2&&x(f,"txt-success",!!m[1])},d(m){m&&(k(e),k(s),k(o),k(a),k(u)),c=!1,De(d)}}}function H5(n){let e,t;return e=new fe({props:{class:"form-field required",name:n[2]+".duration",$$slots:{default:[q5,({uniqueId:i})=>({6:i}),({uniqueId:i})=>i?64:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&4&&(s.name=i[2]+".duration"),l&203&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function j5(n,e,t){let{key:i}=e,{label:l}=e,{duration:s}=e,{secret:o}=e;function r(){s=St(this.value),t(0,s)}const a=()=>{o?t(1,o=void 0):t(1,o=z.randomSecret(50))};return n.$$set=u=>{"key"in u&&t(2,i=u.key),"label"in u&&t(3,l=u.label),"duration"in u&&t(0,s=u.duration),"secret"in u&&t(1,o=u.secret)},[s,o,i,l,r,a]}class z5 extends ye{constructor(e){super(),be(this,e,j5,H5,_e,{key:2,label:3,duration:0,secret:1})}}function Lp(n,e,t){const i=n.slice();return i[8]=e[t],i[9]=e,i[10]=t,i}function Ap(n,e){let t,i,l,s,o,r;function a(c){e[5](c,e[8])}function u(c){e[6](c,e[8])}let f={key:e[8].key,label:e[8].label};return e[0][e[8].key].duration!==void 0&&(f.duration=e[0][e[8].key].duration),e[0][e[8].key].secret!==void 0&&(f.secret=e[0][e[8].key].secret),i=new z5({props:f}),ie.push(()=>ve(i,"duration",a)),ie.push(()=>ve(i,"secret",u)),{key:n,first:null,c(){t=b("div"),H(i.$$.fragment),o=C(),p(t,"class","col-sm-6"),this.first=t},m(c,d){v(c,t,d),F(i,t,null),w(t,o),r=!0},p(c,d){e=c;const m={};d&2&&(m.key=e[8].key),d&2&&(m.label=e[8].label),!l&&d&3&&(l=!0,m.duration=e[0][e[8].key].duration,$e(()=>l=!1)),!s&&d&3&&(s=!0,m.secret=e[0][e[8].key].secret,$e(()=>s=!1)),i.$set(m)},i(c){r||(O(i.$$.fragment,c),r=!0)},o(c){D(i.$$.fragment,c),r=!1},d(c){c&&k(t),q(i)}}}function U5(n){let e,t=[],i=new Map,l,s=pe(n[1]);const o=r=>r[8].key;for(let r=0;r{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function V5(n){let e,t,i,l,s,o=n[2]&&Pp();return{c(){e=b("div"),e.innerHTML=' Tokens options (invalidate, duration)',t=C(),i=b("div"),l=C(),o&&o.c(),s=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(r,a){v(r,e,a),v(r,t,a),v(r,i,a),v(r,l,a),o&&o.m(r,a),v(r,s,a)},p(r,a){r[2]?o?a&4&&O(o,1):(o=Pp(),o.c(),O(o,1),o.m(s.parentNode,s)):o&&(re(),D(o,1,1,()=>{o=null}),ae())},d(r){r&&(k(e),k(t),k(i),k(l),k(s)),o&&o.d(r)}}}function B5(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[V5],default:[U5]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2055&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function W5(n,e,t){let i,l,s;Qe(n,Sn,c=>t(4,s=c));let{collection:o}=e,r=[];function a(c){if(z.isEmpty(c))return!1;for(let d of r)if(c[d.key])return!0;return!1}function u(c,d){n.$$.not_equal(o[d.key].duration,c)&&(o[d.key].duration=c,t(0,o))}function f(c,d){n.$$.not_equal(o[d.key].secret,c)&&(o[d.key].secret=c,t(0,o))}return n.$$set=c=>{"collection"in c&&t(0,o=c.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(3,i=(o==null?void 0:o.system)&&(o==null?void 0:o.name)==="_superusers"),n.$$.dirty&8&&t(1,r=i?[{key:"authToken",label:"Auth"},{key:"passwordResetToken",label:"Password reset"},{key:"fileToken",label:"Protected file access"}]:[{key:"authToken",label:"Auth"},{key:"verificationToken",label:"Email verification"},{key:"passwordResetToken",label:"Password reset"},{key:"emailChangeToken",label:"Email change"},{key:"fileToken",label:"Protected file access"}]),n.$$.dirty&16&&t(2,l=a(s))},[o,r,l,i,s,u,f]}class Y5 extends ye{constructor(e){super(),be(this,e,W5,B5,_e,{collection:0})}}const K5=n=>({isSuperuserOnly:n&2048}),Np=n=>({isSuperuserOnly:n[11]}),J5=n=>({isSuperuserOnly:n&2048}),Rp=n=>({isSuperuserOnly:n[11]}),Z5=n=>({isSuperuserOnly:n&2048}),Fp=n=>({isSuperuserOnly:n[11]});function G5(n){let e,t;return e=new fe({props:{class:"form-field rule-field "+(n[4]?"requied":"")+" "+(n[11]?"disabled":""),name:n[3],$$slots:{default:[Q5,({uniqueId:i})=>({21:i}),({uniqueId:i})=>i?2097152:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&2064&&(s.class="form-field rule-field "+(i[4]?"requied":"")+" "+(i[11]?"disabled":"")),l&8&&(s.name=i[3]),l&2362855&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function X5(n){let e;return{c(){e=b("div"),e.innerHTML='',p(e,"class","txt-center")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function qp(n){let e,t,i,l,s,o;return{c(){e=b("button"),t=b("i"),i=C(),l=b("span"),l.textContent="Set Superusers only",p(t,"class","ri-lock-line"),p(t,"aria-hidden","true"),p(l,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-sm btn-transparent btn-hint lock-toggle svelte-dnx4io"),p(e,"aria-hidden",n[10]),e.disabled=n[10]},m(r,a){v(r,e,a),w(e,t),w(e,i),w(e,l),s||(o=B(e,"click",n[13]),s=!0)},p(r,a){a&1024&&p(e,"aria-hidden",r[10]),a&1024&&(e.disabled=r[10])},d(r){r&&k(e),s=!1,o()}}}function Hp(n){let e,t,i,l,s,o,r,a=!n[10]&&jp();return{c(){e=b("button"),a&&a.c(),t=C(),i=b("div"),i.innerHTML='',p(i,"class","icon svelte-dnx4io"),p(i,"aria-hidden","true"),p(e,"type","button"),p(e,"class","unlock-overlay svelte-dnx4io"),e.disabled=n[10],p(e,"aria-hidden",n[10])},m(u,f){v(u,e,f),a&&a.m(e,null),w(e,t),w(e,i),s=!0,o||(r=B(e,"click",n[12]),o=!0)},p(u,f){u[10]?a&&(a.d(1),a=null):a||(a=jp(),a.c(),a.m(e,t)),(!s||f&1024)&&(e.disabled=u[10]),(!s||f&1024)&&p(e,"aria-hidden",u[10])},i(u){s||(u&&nt(()=>{s&&(l||(l=ze(e,Mt,{duration:150,start:.98},!0)),l.run(1))}),s=!0)},o(u){u&&(l||(l=ze(e,Mt,{duration:150,start:.98},!1)),l.run(0)),s=!1},d(u){u&&k(e),a&&a.d(),u&&l&&l.end(),o=!1,r()}}}function jp(n){let e;return{c(){e=b("small"),e.textContent="Unlock and set custom rule",p(e,"class","txt svelte-dnx4io")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function Q5(n){let e,t,i,l,s,o,r=n[11]?"- Superusers only":"",a,u,f,c,d,m,h,g,_,y,S,T,$,E;const M=n[15].beforeLabel,L=Lt(M,n,n[18],Fp),I=n[15].afterLabel,A=Lt(I,n,n[18],Rp);let P=n[5]&&!n[11]&&qp(n);function R(J){n[17](J)}var N=n[8];function U(J,ee){let X={id:J[21],baseCollection:J[1],disabled:J[10],placeholder:J[11]?"":J[6]};return J[0]!==void 0&&(X.value=J[0]),{props:X}}N&&(m=jt(N,U(n)),n[16](m),ie.push(()=>ve(m,"value",R)));let j=n[5]&&n[11]&&Hp(n);const V=n[15].default,K=Lt(V,n,n[18],Np);return{c(){e=b("div"),t=b("label"),L&&L.c(),i=C(),l=b("span"),s=Y(n[2]),o=C(),a=Y(r),u=C(),A&&A.c(),f=C(),P&&P.c(),d=C(),m&&H(m.$$.fragment),g=C(),j&&j.c(),y=C(),S=b("div"),K&&K.c(),p(l,"class","txt"),x(l,"txt-hint",n[11]),p(t,"for",c=n[21]),p(e,"class","input-wrapper svelte-dnx4io"),p(S,"class","help-block")},m(J,ee){v(J,e,ee),w(e,t),L&&L.m(t,null),w(t,i),w(t,l),w(l,s),w(l,o),w(l,a),w(t,u),A&&A.m(t,null),w(t,f),P&&P.m(t,null),w(e,d),m&&F(m,e,null),w(e,g),j&&j.m(e,null),v(J,y,ee),v(J,S,ee),K&&K.m(S,null),T=!0,$||(E=Me(_=He.call(null,e,n[1].system?{text:"System collection rule cannot be changed.",position:"top"}:void 0)),$=!0)},p(J,ee){if(L&&L.p&&(!T||ee&264192)&&Pt(L,M,J,J[18],T?At(M,J[18],ee,Z5):Nt(J[18]),Fp),(!T||ee&4)&&ue(s,J[2]),(!T||ee&2048)&&r!==(r=J[11]?"- Superusers only":"")&&ue(a,r),(!T||ee&2048)&&x(l,"txt-hint",J[11]),A&&A.p&&(!T||ee&264192)&&Pt(A,I,J,J[18],T?At(I,J[18],ee,J5):Nt(J[18]),Rp),J[5]&&!J[11]?P?P.p(J,ee):(P=qp(J),P.c(),P.m(t,null)):P&&(P.d(1),P=null),(!T||ee&2097152&&c!==(c=J[21]))&&p(t,"for",c),ee&256&&N!==(N=J[8])){if(m){re();const X=m;D(X.$$.fragment,1,0,()=>{q(X,1)}),ae()}N?(m=jt(N,U(J)),J[16](m),ie.push(()=>ve(m,"value",R)),H(m.$$.fragment),O(m.$$.fragment,1),F(m,e,g)):m=null}else if(N){const X={};ee&2097152&&(X.id=J[21]),ee&2&&(X.baseCollection=J[1]),ee&1024&&(X.disabled=J[10]),ee&2112&&(X.placeholder=J[11]?"":J[6]),!h&&ee&1&&(h=!0,X.value=J[0],$e(()=>h=!1)),m.$set(X)}J[5]&&J[11]?j?(j.p(J,ee),ee&2080&&O(j,1)):(j=Hp(J),j.c(),O(j,1),j.m(e,null)):j&&(re(),D(j,1,1,()=>{j=null}),ae()),_&&Rt(_.update)&&ee&2&&_.update.call(null,J[1].system?{text:"System collection rule cannot be changed.",position:"top"}:void 0),K&&K.p&&(!T||ee&264192)&&Pt(K,V,J,J[18],T?At(V,J[18],ee,K5):Nt(J[18]),Np)},i(J){T||(O(L,J),O(A,J),m&&O(m.$$.fragment,J),O(j),O(K,J),T=!0)},o(J){D(L,J),D(A,J),m&&D(m.$$.fragment,J),D(j),D(K,J),T=!1},d(J){J&&(k(e),k(y),k(S)),L&&L.d(J),A&&A.d(J),P&&P.d(),n[16](null),m&&q(m),j&&j.d(),K&&K.d(J),$=!1,E()}}}function x5(n){let e,t,i,l;const s=[X5,G5],o=[];function r(a,u){return a[9]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,[u]){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}let zp;function e6(n,e,t){let i,l,{$$slots:s={},$$scope:o}=e,{collection:r=null}=e,{rule:a=null}=e,{label:u="Rule"}=e,{formKey:f="rule"}=e,{required:c=!1}=e,{disabled:d=!1}=e,{superuserToggle:m=!0}=e,{placeholder:h="Leave empty to grant everyone access..."}=e,g=null,_=null,y=zp,S=!1;T();async function T(){y||S||(t(9,S=!0),t(8,y=(await Ot(async()=>{const{default:I}=await import("./FilterAutocompleteInput-DvxlPb20.js");return{default:I}},__vite__mapDeps([0,1]),import.meta.url)).default),zp=y,t(9,S=!1))}async function $(){t(0,a=_||""),await fn(),g==null||g.focus()}async function E(){_=a,t(0,a=null)}function M(I){ie[I?"unshift":"push"](()=>{g=I,t(7,g)})}function L(I){a=I,t(0,a)}return n.$$set=I=>{"collection"in I&&t(1,r=I.collection),"rule"in I&&t(0,a=I.rule),"label"in I&&t(2,u=I.label),"formKey"in I&&t(3,f=I.formKey),"required"in I&&t(4,c=I.required),"disabled"in I&&t(14,d=I.disabled),"superuserToggle"in I&&t(5,m=I.superuserToggle),"placeholder"in I&&t(6,h=I.placeholder),"$$scope"in I&&t(18,o=I.$$scope)},n.$$.update=()=>{n.$$.dirty&33&&t(11,i=m&&a===null),n.$$.dirty&16386&&t(10,l=d||r.system)},[a,r,u,f,c,m,h,g,y,S,l,i,$,E,d,s,M,L,o]}class tl extends ye{constructor(e){super(),be(this,e,e6,x5,_e,{collection:1,rule:0,label:2,formKey:3,required:4,disabled:14,superuserToggle:5,placeholder:6})}}function t6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Enable",p(e,"type","checkbox"),p(e,"id",t=n[5]),p(s,"class","txt"),p(l,"for",o=n[5])},m(u,f){v(u,e,f),e.checked=n[0].mfa.enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[3]),r=!0)},p(u,f){f&32&&t!==(t=u[5])&&p(e,"id",t),f&1&&(e.checked=u[0].mfa.enabled),f&32&&o!==(o=u[5])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function n6(n){let e,t,i,l,s;return{c(){e=b("p"),e.textContent="This optional rule could be used to enable/disable MFA per account basis.",t=C(),i=b("p"),i.innerHTML=`For example, to require MFA only for accounts with non-empty email you can set it to + email != ''.`,l=C(),s=b("p"),s.textContent="Leave the rule empty to require MFA for everyone."},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),v(o,l,r),v(o,s,r)},p:te,d(o){o&&(k(e),k(t),k(i),k(l),k(s))}}}function i6(n){let e,t,i,l,s,o,r,a,u;l=new fe({props:{class:"form-field form-field-toggle",name:"mfa.enabled",$$slots:{default:[t6,({uniqueId:d})=>({5:d}),({uniqueId:d})=>d?32:0]},$$scope:{ctx:n}}});function f(d){n[4](d)}let c={label:"MFA rule",formKey:"mfa.rule",superuserToggle:!1,disabled:!n[0].mfa.enabled,placeholder:"Leave empty to require MFA for everyone",collection:n[0],$$slots:{default:[n6]},$$scope:{ctx:n}};return n[0].mfa.rule!==void 0&&(c.rule=n[0].mfa.rule),r=new tl({props:c}),ie.push(()=>ve(r,"rule",f)),{c(){e=b("div"),e.innerHTML=`

This feature is experimental and may change in the future.

`,t=C(),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),p(e,"class","content m-b-sm"),p(o,"class","content"),x(o,"fade",!n[0].mfa.enabled),p(i,"class","grid")},m(d,m){v(d,e,m),v(d,t,m),v(d,i,m),F(l,i,null),w(i,s),w(i,o),F(r,o,null),u=!0},p(d,m){const h={};m&97&&(h.$$scope={dirty:m,ctx:d}),l.$set(h);const g={};m&1&&(g.disabled=!d[0].mfa.enabled),m&1&&(g.collection=d[0]),m&64&&(g.$$scope={dirty:m,ctx:d}),!a&&m&1&&(a=!0,g.rule=d[0].mfa.rule,$e(()=>a=!1)),r.$set(g),(!u||m&1)&&x(o,"fade",!d[0].mfa.enabled)},i(d){u||(O(l.$$.fragment,d),O(r.$$.fragment,d),u=!0)},o(d){D(l.$$.fragment,d),D(r.$$.fragment,d),u=!1},d(d){d&&(k(e),k(t),k(i)),q(l),q(r)}}}function l6(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function s6(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function Up(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function o6(n){let e,t,i,l,s,o;function r(c,d){return c[0].mfa.enabled?s6:l6}let a=r(n),u=a(n),f=n[1]&&Up();return{c(){e=b("div"),e.innerHTML=' Multi-factor authentication (MFA)',t=C(),i=b("div"),l=C(),u.c(),s=C(),f&&f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),u.m(c,d),v(c,s,d),f&&f.m(c,d),v(c,o,d)},p(c,d){a!==(a=r(c))&&(u.d(1),u=a(c),u&&(u.c(),u.m(s.parentNode,s))),c[1]?f?d&2&&O(f,1):(f=Up(),f.c(),O(f,1),f.m(o.parentNode,o)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),u.d(c),f&&f.d(c)}}}function r6(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[o6],default:[i6]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&67&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function a6(n,e,t){let i,l;Qe(n,Sn,a=>t(2,l=a));let{collection:s}=e;function o(){s.mfa.enabled=this.checked,t(0,s)}function r(a){n.$$.not_equal(s.mfa.rule,a)&&(s.mfa.rule=a,t(0,s))}return n.$$set=a=>{"collection"in a&&t(0,s=a.collection)},n.$$.update=()=>{n.$$.dirty&4&&t(1,i=!z.isEmpty(l==null?void 0:l.mfa))},[s,i,l,o,r]}class u6 extends ye{constructor(e){super(),be(this,e,a6,r6,_e,{collection:0})}}const f6=n=>({}),Vp=n=>({});function Bp(n,e,t){const i=n.slice();return i[50]=e[t],i}const c6=n=>({}),Wp=n=>({});function Yp(n,e,t){const i=n.slice();return i[50]=e[t],i[54]=t,i}function Kp(n){let e,t,i;return{c(){e=b("div"),t=Y(n[2]),i=C(),p(e,"class","block txt-placeholder"),x(e,"link-hint",!n[5]&&!n[6])},m(l,s){v(l,e,s),w(e,t),w(e,i)},p(l,s){s[0]&4&&ue(t,l[2]),s[0]&96&&x(e,"link-hint",!l[5]&&!l[6])},d(l){l&&k(e)}}}function d6(n){let e,t=n[50]+"",i;return{c(){e=b("span"),i=Y(t),p(e,"class","txt")},m(l,s){v(l,e,s),w(e,i)},p(l,s){s[0]&1&&t!==(t=l[50]+"")&&ue(i,t)},i:te,o:te,d(l){l&&k(e)}}}function p6(n){let e,t,i;const l=[{item:n[50]},n[11]];var s=n[10];function o(r,a){let u={};for(let f=0;f{q(u,1)}),ae()}s?(e=jt(s,o(r,a)),H(e.$$.fragment),O(e.$$.fragment,1),F(e,t.parentNode,t)):e=null}else if(s){const u=a[0]&2049?kt(l,[a[0]&1&&{item:r[50]},a[0]&2048&&Ft(r[11])]):{};e.$set(u)}},i(r){i||(e&&O(e.$$.fragment,r),i=!0)},o(r){e&&D(e.$$.fragment,r),i=!1},d(r){r&&k(t),e&&q(e,r)}}}function Jp(n){let e,t,i;function l(){return n[37](n[50])}return{c(){e=b("span"),e.innerHTML='',p(e,"class","clear")},m(s,o){v(s,e,o),t||(i=[Me(He.call(null,e,"Clear")),B(e,"click",On(tt(l)))],t=!0)},p(s,o){n=s},d(s){s&&k(e),t=!1,De(i)}}}function Zp(n){let e,t,i,l,s,o;const r=[p6,d6],a=[];function u(c,d){return c[10]?0:1}t=u(n),i=a[t]=r[t](n);let f=(n[4]||n[8])&&Jp(n);return{c(){e=b("div"),i.c(),l=C(),f&&f.c(),s=C(),p(e,"class","option")},m(c,d){v(c,e,d),a[t].m(e,null),w(e,l),f&&f.m(e,null),w(e,s),o=!0},p(c,d){let m=t;t=u(c),t===m?a[t].p(c,d):(re(),D(a[m],1,1,()=>{a[m]=null}),ae(),i=a[t],i?i.p(c,d):(i=a[t]=r[t](c),i.c()),O(i,1),i.m(e,l)),c[4]||c[8]?f?f.p(c,d):(f=Jp(c),f.c(),f.m(e,s)):f&&(f.d(1),f=null)},i(c){o||(O(i),o=!0)},o(c){D(i),o=!1},d(c){c&&k(e),a[t].d(),f&&f.d()}}}function Gp(n){let e,t,i={class:"dropdown dropdown-block options-dropdown dropdown-left "+(n[7]?"dropdown-upside":""),trigger:n[20],$$slots:{default:[_6]},$$scope:{ctx:n}};return e=new Hn({props:i}),n[42](e),e.$on("show",n[26]),e.$on("hide",n[43]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,s){const o={};s[0]&128&&(o.class="dropdown dropdown-block options-dropdown dropdown-left "+(l[7]?"dropdown-upside":"")),s[0]&1048576&&(o.trigger=l[20]),s[0]&6451722|s[1]&16384&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[42](null),q(e,l)}}}function Xp(n){let e,t,i,l,s,o,r,a,u=n[17].length&&Qp(n);return{c(){e=b("div"),t=b("label"),i=b("div"),i.innerHTML='',l=C(),s=b("input"),o=C(),u&&u.c(),p(i,"class","addon p-r-0"),s.autofocus=!0,p(s,"type","text"),p(s,"placeholder",n[3]),p(t,"class","input-group"),p(e,"class","form-field form-field-sm options-search")},m(f,c){v(f,e,c),w(e,t),w(t,i),w(t,l),w(t,s),ce(s,n[17]),w(t,o),u&&u.m(t,null),s.focus(),r||(a=B(s,"input",n[39]),r=!0)},p(f,c){c[0]&8&&p(s,"placeholder",f[3]),c[0]&131072&&s.value!==f[17]&&ce(s,f[17]),f[17].length?u?u.p(f,c):(u=Qp(f),u.c(),u.m(t,null)):u&&(u.d(1),u=null)},d(f){f&&k(e),u&&u.d(),r=!1,a()}}}function Qp(n){let e,t,i,l;return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","btn btn-sm btn-circle btn-transparent clear"),p(e,"class","addon suffix p-r-5")},m(s,o){v(s,e,o),w(e,t),i||(l=B(t,"click",On(tt(n[23]))),i=!0)},p:te,d(s){s&&k(e),i=!1,l()}}}function xp(n){let e,t=n[1]&&em(n);return{c(){t&&t.c(),e=ge()},m(i,l){t&&t.m(i,l),v(i,e,l)},p(i,l){i[1]?t?t.p(i,l):(t=em(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){i&&k(e),t&&t.d(i)}}}function em(n){let e,t;return{c(){e=b("div"),t=Y(n[1]),p(e,"class","txt-missing")},m(i,l){v(i,e,l),w(e,t)},p(i,l){l[0]&2&&ue(t,i[1])},d(i){i&&k(e)}}}function m6(n){let e=n[50]+"",t;return{c(){t=Y(e)},m(i,l){v(i,t,l)},p(i,l){l[0]&4194304&&e!==(e=i[50]+"")&&ue(t,e)},i:te,o:te,d(i){i&&k(t)}}}function h6(n){let e,t,i;const l=[{item:n[50]},n[13]];var s=n[12];function o(r,a){let u={};for(let f=0;f{q(u,1)}),ae()}s?(e=jt(s,o(r,a)),H(e.$$.fragment),O(e.$$.fragment,1),F(e,t.parentNode,t)):e=null}else if(s){const u=a[0]&4202496?kt(l,[a[0]&4194304&&{item:r[50]},a[0]&8192&&Ft(r[13])]):{};e.$set(u)}},i(r){i||(e&&O(e.$$.fragment,r),i=!0)},o(r){e&&D(e.$$.fragment,r),i=!1},d(r){r&&k(t),e&&q(e,r)}}}function tm(n){let e,t,i,l,s,o,r;const a=[h6,m6],u=[];function f(m,h){return m[12]?0:1}t=f(n),i=u[t]=a[t](n);function c(...m){return n[40](n[50],...m)}function d(...m){return n[41](n[50],...m)}return{c(){e=b("div"),i.c(),l=C(),p(e,"tabindex","0"),p(e,"class","dropdown-item option"),p(e,"role","menuitem"),x(e,"closable",n[9]),x(e,"selected",n[21](n[50]))},m(m,h){v(m,e,h),u[t].m(e,null),w(e,l),s=!0,o||(r=[B(e,"click",c),B(e,"keydown",d)],o=!0)},p(m,h){n=m;let g=t;t=f(n),t===g?u[t].p(n,h):(re(),D(u[g],1,1,()=>{u[g]=null}),ae(),i=u[t],i?i.p(n,h):(i=u[t]=a[t](n),i.c()),O(i,1),i.m(e,l)),(!s||h[0]&512)&&x(e,"closable",n[9]),(!s||h[0]&6291456)&&x(e,"selected",n[21](n[50]))},i(m){s||(O(i),s=!0)},o(m){D(i),s=!1},d(m){m&&k(e),u[t].d(),o=!1,De(r)}}}function _6(n){let e,t,i,l,s,o=n[14]&&Xp(n);const r=n[36].beforeOptions,a=Lt(r,n,n[45],Wp);let u=pe(n[22]),f=[];for(let g=0;gD(f[g],1,1,()=>{f[g]=null});let d=null;u.length||(d=xp(n));const m=n[36].afterOptions,h=Lt(m,n,n[45],Vp);return{c(){o&&o.c(),e=C(),a&&a.c(),t=C(),i=b("div");for(let g=0;gD(a[d],1,1,()=>{a[d]=null});let f=null;r.length||(f=Kp(n));let c=!n[5]&&!n[6]&&Gp(n);return{c(){e=b("div"),t=b("div");for(let d=0;d{c=null}),ae()),(!o||m[0]&32768&&s!==(s="select "+d[15]))&&p(e,"class",s),(!o||m[0]&32896)&&x(e,"upside",d[7]),(!o||m[0]&32784)&&x(e,"multiple",d[4]),(!o||m[0]&32800)&&x(e,"disabled",d[5]),(!o||m[0]&32832)&&x(e,"readonly",d[6])},i(d){if(!o){for(let m=0;md?[]:void 0}=e,{selected:y=_()}=e,{toggle:S=d}=e,{closable:T=!0}=e,{labelComponent:$=void 0}=e,{labelComponentProps:E={}}=e,{optionComponent:M=void 0}=e,{optionComponentProps:L={}}=e,{searchable:I=!1}=e,{searchFunc:A=void 0}=e;const P=_t();let{class:R=""}=e,N,U="",j,V;function K(we){if(z.isEmpty(y))return;let Oe=z.toArray(y);z.inArray(Oe,we)&&(z.removeByValue(Oe,we),t(0,y=d?Oe:(Oe==null?void 0:Oe[0])||_())),P("change",{selected:y}),j==null||j.dispatchEvent(new CustomEvent("change",{detail:y,bubbles:!0}))}function J(we){if(d){let Oe=z.toArray(y);z.inArray(Oe,we)||t(0,y=[...Oe,we])}else t(0,y=we);P("change",{selected:y}),j==null||j.dispatchEvent(new CustomEvent("change",{detail:y,bubbles:!0}))}function ee(we){return l(we)?K(we):J(we)}function X(){t(0,y=_()),P("change",{selected:y}),j==null||j.dispatchEvent(new CustomEvent("change",{detail:y,bubbles:!0}))}function oe(){N!=null&&N.show&&(N==null||N.show())}function Se(){N!=null&&N.hide&&(N==null||N.hide())}function ke(){if(z.isEmpty(y)||z.isEmpty(c))return;let we=z.toArray(y),Oe=[];for(const ut of we)z.inArray(c,ut)||Oe.push(ut);if(Oe.length){for(const ut of Oe)z.removeByValue(we,ut);t(0,y=d?we:we[0])}}function Ce(){t(17,U="")}function We(we,Oe){we=we||[];const ut=A||b6;return we.filter(Ne=>ut(Ne,Oe))||[]}function st(we,Oe){we.preventDefault(),S&&d?ee(Oe):J(Oe)}function et(we,Oe){(we.code==="Enter"||we.code==="Space")&&(st(we,Oe),T&&Se())}function Be(){Ce(),setTimeout(()=>{const we=j==null?void 0:j.querySelector(".dropdown-item.option.selected");we&&(we.focus(),we.scrollIntoView({block:"nearest"}))},0)}function rt(we){we.stopPropagation(),!h&&!m&&(N==null||N.toggle())}Yt(()=>{const we=document.querySelectorAll(`label[for="${r}"]`);for(const Oe of we)Oe.addEventListener("click",rt);return()=>{for(const Oe of we)Oe.removeEventListener("click",rt)}});const Je=we=>K(we);function at(we){ie[we?"unshift":"push"](()=>{V=we,t(20,V)})}function Ht(){U=this.value,t(17,U)}const Te=(we,Oe)=>st(Oe,we),Ze=(we,Oe)=>et(Oe,we);function ot(we){ie[we?"unshift":"push"](()=>{N=we,t(18,N)})}function Le(we){Pe.call(this,n,we)}function Ve(we){ie[we?"unshift":"push"](()=>{j=we,t(19,j)})}return n.$$set=we=>{"id"in we&&t(27,r=we.id),"noOptionsText"in we&&t(1,a=we.noOptionsText),"selectPlaceholder"in we&&t(2,u=we.selectPlaceholder),"searchPlaceholder"in we&&t(3,f=we.searchPlaceholder),"items"in we&&t(28,c=we.items),"multiple"in we&&t(4,d=we.multiple),"disabled"in we&&t(5,m=we.disabled),"readonly"in we&&t(6,h=we.readonly),"upside"in we&&t(7,g=we.upside),"zeroFunc"in we&&t(29,_=we.zeroFunc),"selected"in we&&t(0,y=we.selected),"toggle"in we&&t(8,S=we.toggle),"closable"in we&&t(9,T=we.closable),"labelComponent"in we&&t(10,$=we.labelComponent),"labelComponentProps"in we&&t(11,E=we.labelComponentProps),"optionComponent"in we&&t(12,M=we.optionComponent),"optionComponentProps"in we&&t(13,L=we.optionComponentProps),"searchable"in we&&t(14,I=we.searchable),"searchFunc"in we&&t(30,A=we.searchFunc),"class"in we&&t(15,R=we.class),"$$scope"in we&&t(45,o=we.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&268435456&&c&&(ke(),Ce()),n.$$.dirty[0]&268566528&&t(22,i=We(c,U)),n.$$.dirty[0]&1&&t(21,l=function(we){const Oe=z.toArray(y);return z.inArray(Oe,we)})},[y,a,u,f,d,m,h,g,S,T,$,E,M,L,I,R,K,U,N,j,V,l,i,Ce,st,et,Be,r,c,_,A,J,ee,X,oe,Se,s,Je,at,Ht,Te,Ze,ot,Le,Ve,o]}class ds extends ye{constructor(e){super(),be(this,e,y6,g6,_e,{id:27,noOptionsText:1,selectPlaceholder:2,searchPlaceholder:3,items:28,multiple:4,disabled:5,readonly:6,upside:7,zeroFunc:29,selected:0,toggle:8,closable:9,labelComponent:10,labelComponentProps:11,optionComponent:12,optionComponentProps:13,searchable:14,searchFunc:30,class:15,deselectItem:16,selectItem:31,toggleItem:32,reset:33,showDropdown:34,hideDropdown:35},null,[-1,-1])}get deselectItem(){return this.$$.ctx[16]}get selectItem(){return this.$$.ctx[31]}get toggleItem(){return this.$$.ctx[32]}get reset(){return this.$$.ctx[33]}get showDropdown(){return this.$$.ctx[34]}get hideDropdown(){return this.$$.ctx[35]}}function k6(n){let e,t,i,l=[{type:"password"},{autocomplete:"new-password"},n[4]],s={};for(let o=0;o',i=C(),l=b("input"),p(t,"type","button"),p(t,"class","btn btn-transparent btn-circle"),p(e,"class","form-field-addon"),Xn(l,a)},m(u,f){v(u,e,f),w(e,t),v(u,i,f),v(u,l,f),l.autofocus&&l.focus(),s||(o=[Me(He.call(null,t,{position:"left",text:"Set new value"})),B(t,"click",tt(n[3]))],s=!0)},p(u,f){Xn(l,a=kt(r,[{readOnly:!0},{type:"text"},{placeholder:"******"},f&16&&u[4]]))},d(u){u&&(k(e),k(i),k(l)),s=!1,De(o)}}}function w6(n){let e;function t(s,o){return s[0]?v6:k6}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:te,o:te,d(s){s&&k(e),l.d(s)}}}function S6(n,e,t){const i=["mask","value"];let l=lt(e,i),{mask:s=!1}=e,{value:o=""}=e,r;async function a(){t(1,o=""),t(0,s=!1),await fn(),r==null||r.focus()}function u(c){ie[c?"unshift":"push"](()=>{r=c,t(2,r)})}function f(){o=this.value,t(1,o)}return n.$$set=c=>{e=je(je({},e),Ut(c)),t(4,l=lt(e,i)),"mask"in c&&t(0,s=c.mask),"value"in c&&t(1,o=c.value)},[s,o,r,a,l,u,f]}class xu extends ye{constructor(e){super(),be(this,e,S6,w6,_e,{mask:0,value:1})}}function T6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Client ID"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23])},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[1].clientId),r||(a=B(s,"input",n[14]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&2&&s.value!==u[1].clientId&&ce(s,u[1].clientId)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function $6(n){let e,t,i,l,s,o,r,a;function u(d){n[15](d)}function f(d){n[16](d)}let c={id:n[23]};return n[5]!==void 0&&(c.mask=n[5]),n[1].clientSecret!==void 0&&(c.value=n[1].clientSecret),s=new xu({props:c}),ie.push(()=>ve(s,"mask",u)),ie.push(()=>ve(s,"value",f)),{c(){e=b("label"),t=Y("Client secret"),l=C(),H(s.$$.fragment),p(e,"for",i=n[23])},m(d,m){v(d,e,m),w(e,t),v(d,l,m),F(s,d,m),a=!0},p(d,m){(!a||m&8388608&&i!==(i=d[23]))&&p(e,"for",i);const h={};m&8388608&&(h.id=d[23]),!o&&m&32&&(o=!0,h.mask=d[5],$e(()=>o=!1)),!r&&m&2&&(r=!0,h.value=d[1].clientSecret,$e(()=>r=!1)),s.$set(h)},i(d){a||(O(s.$$.fragment,d),a=!0)},o(d){D(s.$$.fragment,d),a=!1},d(d){d&&(k(e),k(l)),q(s,d)}}}function nm(n){let e,t,i,l;const s=[{key:n[6]},n[3].optionsComponentProps||{}];function o(u){n[17](u)}var r=n[3].optionsComponent;function a(u,f){let c={};for(let d=0;dve(t,"config",o))),{c(){e=b("div"),t&&H(t.$$.fragment),p(e,"class","col-lg-12")},m(u,f){v(u,e,f),t&&F(t,e,null),l=!0},p(u,f){if(f&8&&r!==(r=u[3].optionsComponent)){if(t){re();const c=t;D(c.$$.fragment,1,0,()=>{q(c,1)}),ae()}r?(t=jt(r,a(u,f)),ie.push(()=>ve(t,"config",o)),H(t.$$.fragment),O(t.$$.fragment,1),F(t,e,null)):t=null}else if(r){const c=f&72?kt(s,[f&64&&{key:u[6]},f&8&&Ft(u[3].optionsComponentProps||{})]):{};!i&&f&2&&(i=!0,c.config=u[1],$e(()=>i=!1)),t.$set(c)}},i(u){l||(t&&O(t.$$.fragment,u),l=!0)},o(u){t&&D(t.$$.fragment,u),l=!1},d(u){u&&k(e),t&&q(t)}}}function C6(n){let e,t,i,l,s,o,r,a;t=new fe({props:{class:"form-field required",name:n[6]+".clientId",$$slots:{default:[T6,({uniqueId:f})=>({23:f}),({uniqueId:f})=>f?8388608:0]},$$scope:{ctx:n}}}),l=new fe({props:{class:"form-field required",name:n[6]+".clientSecret",$$slots:{default:[$6,({uniqueId:f})=>({23:f}),({uniqueId:f})=>f?8388608:0]},$$scope:{ctx:n}}});let u=n[3].optionsComponent&&nm(n);return{c(){e=b("form"),H(t.$$.fragment),i=C(),H(l.$$.fragment),s=C(),u&&u.c(),p(e,"id",n[8]),p(e,"autocomplete","off")},m(f,c){v(f,e,c),F(t,e,null),w(e,i),F(l,e,null),w(e,s),u&&u.m(e,null),o=!0,r||(a=B(e,"submit",tt(n[18])),r=!0)},p(f,c){const d={};c&64&&(d.name=f[6]+".clientId"),c&25165826&&(d.$$scope={dirty:c,ctx:f}),t.$set(d);const m={};c&64&&(m.name=f[6]+".clientSecret"),c&25165858&&(m.$$scope={dirty:c,ctx:f}),l.$set(m),f[3].optionsComponent?u?(u.p(f,c),c&8&&O(u,1)):(u=nm(f),u.c(),O(u,1),u.m(e,null)):u&&(re(),D(u,1,1,()=>{u=null}),ae())},i(f){o||(O(t.$$.fragment,f),O(l.$$.fragment,f),O(u),o=!0)},o(f){D(t.$$.fragment,f),D(l.$$.fragment,f),D(u),o=!1},d(f){f&&k(e),q(t),q(l),u&&u.d(),r=!1,a()}}}function O6(n){let e;return{c(){e=b("i"),p(e,"class","ri-puzzle-line txt-sm txt-hint")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function E6(n){let e,t,i;return{c(){e=b("img"),vn(e.src,t="./images/oauth2/"+n[3].logo)||p(e,"src",t),p(e,"alt",i=n[3].title+" logo")},m(l,s){v(l,e,s)},p(l,s){s&8&&!vn(e.src,t="./images/oauth2/"+l[3].logo)&&p(e,"src",t),s&8&&i!==(i=l[3].title+" logo")&&p(e,"alt",i)},d(l){l&&k(e)}}}function M6(n){let e,t,i,l=n[3].title+"",s,o,r,a,u=n[3].key+"",f,c;function d(g,_){return g[3].logo?E6:O6}let m=d(n),h=m(n);return{c(){e=b("figure"),h.c(),t=C(),i=b("h4"),s=Y(l),o=C(),r=b("small"),a=Y("("),f=Y(u),c=Y(")"),p(e,"class","provider-logo"),p(r,"class","txt-hint"),p(i,"class","center txt-break")},m(g,_){v(g,e,_),h.m(e,null),v(g,t,_),v(g,i,_),w(i,s),w(i,o),w(i,r),w(r,a),w(r,f),w(r,c)},p(g,_){m===(m=d(g))&&h?h.p(g,_):(h.d(1),h=m(g),h&&(h.c(),h.m(e,null))),_&8&&l!==(l=g[3].title+"")&&ue(s,l),_&8&&u!==(u=g[3].key+"")&&ue(f,u)},d(g){g&&(k(e),k(t),k(i)),h.d()}}}function im(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='',t=C(),i=b("div"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-circle btn-hint btn-sm"),p(e,"aria-label","Remove provider"),p(i,"class","flex-fill")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),l||(s=[Me(He.call(null,e,{text:"Remove provider",position:"right"})),B(e,"click",n[10])],l=!0)},p:te,d(o){o&&(k(e),k(t),k(i)),l=!1,De(s)}}}function D6(n){let e,t,i,l,s,o,r,a,u=!n[4]&&im(n);return{c(){u&&u.c(),e=C(),t=b("button"),t.textContent="Cancel",i=C(),l=b("button"),s=b("span"),s.textContent="Set provider config",p(t,"type","button"),p(t,"class","btn btn-transparent"),p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[8]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[7]},m(f,c){u&&u.m(f,c),v(f,e,c),v(f,t,c),v(f,i,c),v(f,l,c),w(l,s),r||(a=B(t,"click",n[0]),r=!0)},p(f,c){f[4]?u&&(u.d(1),u=null):u?u.p(f,c):(u=im(f),u.c(),u.m(e.parentNode,e)),c&128&&o!==(o=!f[7])&&(l.disabled=o)},d(f){f&&(k(e),k(t),k(i),k(l)),u&&u.d(f),r=!1,a()}}}function I6(n){let e,t,i={btnClose:!1,$$slots:{footer:[D6],header:[M6],default:[C6]},$$scope:{ctx:n}};return e=new ln({props:i}),n[19](e),e.$on("show",n[20]),e.$on("hide",n[21]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&16777466&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[19](null),q(e,l)}}}function L6(n,e,t){let i,l;const s=_t(),o="provider_popup_"+z.randomString(5);let r,a={},u={},f=!1,c="",d=!1,m=0;function h(P,R,N){t(13,m=N||0),t(4,f=z.isEmpty(R)),t(3,a=Object.assign({},P)),t(1,u=Object.assign({},R)),t(5,d=!!u.clientId),t(12,c=JSON.stringify(u)),r==null||r.show()}function g(){fi(l),r==null||r.hide()}async function _(){s("submit",{uiOptions:a,config:u}),g()}async function y(){pn(`Do you really want to remove the "${a.title}" OAuth2 provider from the collection?`,()=>{s("remove",{uiOptions:a}),g()})}function S(){u.clientId=this.value,t(1,u)}function T(P){d=P,t(5,d)}function $(P){n.$$.not_equal(u.clientSecret,P)&&(u.clientSecret=P,t(1,u))}function E(P){u=P,t(1,u)}const M=()=>_();function L(P){ie[P?"unshift":"push"](()=>{r=P,t(2,r)})}function I(P){Pe.call(this,n,P)}function A(P){Pe.call(this,n,P)}return n.$$.update=()=>{n.$$.dirty&4098&&t(7,i=JSON.stringify(u)!=c),n.$$.dirty&8192&&t(6,l="oauth2.providers."+m)},[g,u,r,a,f,d,l,i,o,_,y,h,c,m,S,T,$,E,M,L,I,A]}class A6 extends ye{constructor(e){super(),be(this,e,L6,I6,_e,{show:11,hide:0})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[0]}}function P6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Client ID"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[2]),r||(a=B(s,"input",n[12]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&4&&s.value!==u[2]&&ce(s,u[2])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function N6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Team ID"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[3]),r||(a=B(s,"input",n[13]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&8&&s.value!==u[3]&&ce(s,u[3])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function R6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Key ID"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[4]),r||(a=B(s,"input",n[14]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&16&&s.value!==u[4]&&ce(s,u[4])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function F6(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=b("span"),t.textContent="Duration (in seconds)",i=C(),l=b("i"),o=C(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(r,"type","text"),p(r,"id",a=n[23]),p(r,"max",ur),r.required=!0},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),v(c,o,d),v(c,r,d),ce(r,n[6]),u||(f=[Me(He.call(null,l,{text:`Max ${ur} seconds (~${ur/(60*60*24*30)<<0} months).`,position:"top"})),B(r,"input",n[15])],u=!0)},p(c,d){d&8388608&&s!==(s=c[23])&&p(e,"for",s),d&8388608&&a!==(a=c[23])&&p(r,"id",a),d&64&&r.value!==c[6]&&ce(r,c[6])},d(c){c&&(k(e),k(o),k(r)),u=!1,De(f)}}}function q6(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Private key"),l=C(),s=b("textarea"),r=C(),a=b("div"),a.textContent="The key is not stored on the server and it is used only for generating the signed JWT.",p(e,"for",i=n[23]),p(s,"id",o=n[23]),s.required=!0,p(s,"rows","8"),p(s,"placeholder",`-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY-----`),p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[5]),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[16]),u=!0)},p(c,d){d&8388608&&i!==(i=c[23])&&p(e,"for",i),d&8388608&&o!==(o=c[23])&&p(s,"id",o),d&32&&ce(s,c[5])},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function H6(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S;return l=new fe({props:{class:"form-field required",name:"clientId",$$slots:{default:[P6,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field required",name:"teamId",$$slots:{default:[N6,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),f=new fe({props:{class:"form-field required",name:"keyId",$$slots:{default:[R6,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),m=new fe({props:{class:"form-field required",name:"duration",$$slots:{default:[F6,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),g=new fe({props:{class:"form-field required",name:"privateKey",$$slots:{default:[q6,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("div"),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),a=C(),u=b("div"),H(f.$$.fragment),c=C(),d=b("div"),H(m.$$.fragment),h=C(),H(g.$$.fragment),p(i,"class","col-lg-6"),p(o,"class","col-lg-6"),p(u,"class","col-lg-6"),p(d,"class","col-lg-6"),p(t,"class","grid"),p(e,"id",n[9]),p(e,"autocomplete","off")},m(T,$){v(T,e,$),w(e,t),w(t,i),F(l,i,null),w(t,s),w(t,o),F(r,o,null),w(t,a),w(t,u),F(f,u,null),w(t,c),w(t,d),F(m,d,null),w(t,h),F(g,t,null),_=!0,y||(S=B(e,"submit",tt(n[17])),y=!0)},p(T,$){const E={};$&25165828&&(E.$$scope={dirty:$,ctx:T}),l.$set(E);const M={};$&25165832&&(M.$$scope={dirty:$,ctx:T}),r.$set(M);const L={};$&25165840&&(L.$$scope={dirty:$,ctx:T}),f.$set(L);const I={};$&25165888&&(I.$$scope={dirty:$,ctx:T}),m.$set(I);const A={};$&25165856&&(A.$$scope={dirty:$,ctx:T}),g.$set(A)},i(T){_||(O(l.$$.fragment,T),O(r.$$.fragment,T),O(f.$$.fragment,T),O(m.$$.fragment,T),O(g.$$.fragment,T),_=!0)},o(T){D(l.$$.fragment,T),D(r.$$.fragment,T),D(f.$$.fragment,T),D(m.$$.fragment,T),D(g.$$.fragment,T),_=!1},d(T){T&&k(e),q(l),q(r),q(f),q(m),q(g),y=!1,S()}}}function j6(n){let e;return{c(){e=b("h4"),e.textContent="Generate Apple client secret",p(e,"class","center txt-break")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function z6(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("button"),t=Y("Close"),i=C(),l=b("button"),s=b("i"),o=C(),r=b("span"),r.textContent="Generate and set secret",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[7],p(s,"class","ri-key-line"),p(r,"class","txt"),p(l,"type","submit"),p(l,"form",n[9]),p(l,"class","btn btn-expanded"),l.disabled=a=!n[8]||n[7],x(l,"btn-loading",n[7])},m(c,d){v(c,e,d),w(e,t),v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=B(e,"click",n[0]),u=!0)},p(c,d){d&128&&(e.disabled=c[7]),d&384&&a!==(a=!c[8]||c[7])&&(l.disabled=a),d&128&&x(l,"btn-loading",c[7])},d(c){c&&(k(e),k(i),k(l)),u=!1,f()}}}function U6(n){let e,t,i={overlayClose:!n[7],escClose:!n[7],beforeHide:n[18],popup:!0,$$slots:{footer:[z6],header:[j6],default:[H6]},$$scope:{ctx:n}};return e=new ln({props:i}),n[19](e),e.$on("show",n[20]),e.$on("hide",n[21]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&128&&(o.overlayClose=!l[7]),s&128&&(o.escClose=!l[7]),s&128&&(o.beforeHide=l[18]),s&16777724&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[19](null),q(e,l)}}}const ur=15777e3;function V6(n,e,t){let i;const l=_t(),s="apple_secret_"+z.randomString(5);let o,r,a,u,f,c,d=!1;function m(P={}){t(2,r=P.clientId||""),t(3,a=P.teamId||""),t(4,u=P.keyId||""),t(5,f=P.privateKey||""),t(6,c=P.duration||ur),Wt({}),o==null||o.show()}function h(){return o==null?void 0:o.hide()}async function g(){t(7,d=!0);try{const P=await me.settings.generateAppleClientSecret(r,a,u,f.trim(),c);t(7,d=!1),tn("Successfully generated client secret."),l("submit",P),o==null||o.hide()}catch(P){me.error(P)}t(7,d=!1)}function _(){r=this.value,t(2,r)}function y(){a=this.value,t(3,a)}function S(){u=this.value,t(4,u)}function T(){c=this.value,t(6,c)}function $(){f=this.value,t(5,f)}const E=()=>g(),M=()=>!d;function L(P){ie[P?"unshift":"push"](()=>{o=P,t(1,o)})}function I(P){Pe.call(this,n,P)}function A(P){Pe.call(this,n,P)}return t(8,i=!0),[h,o,r,a,u,f,c,d,i,s,g,m,_,y,S,T,$,E,M,L,I,A]}class B6 extends ye{constructor(e){super(),be(this,e,V6,U6,_e,{show:11,hide:0})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[0]}}function W6(n){let e,t,i,l,s,o,r,a,u,f,c={};return r=new B6({props:c}),n[4](r),r.$on("submit",n[5]),{c(){e=b("button"),t=b("i"),i=C(),l=b("span"),l.textContent="Generate secret",o=C(),H(r.$$.fragment),p(t,"class","ri-key-line"),p(l,"class","txt"),p(e,"type","button"),p(e,"class",s="btn btn-sm btn-secondary btn-provider-"+n[1])},m(d,m){v(d,e,m),w(e,t),w(e,i),w(e,l),v(d,o,m),F(r,d,m),a=!0,u||(f=B(e,"click",n[3]),u=!0)},p(d,[m]){(!a||m&2&&s!==(s="btn btn-sm btn-secondary btn-provider-"+d[1]))&&p(e,"class",s);const h={};r.$set(h)},i(d){a||(O(r.$$.fragment,d),a=!0)},o(d){D(r.$$.fragment,d),a=!1},d(d){d&&(k(e),k(o)),n[4](null),q(r,d),u=!1,f()}}}function Y6(n,e,t){let{key:i=""}=e,{config:l={}}=e,s;const o=()=>s==null?void 0:s.show({clientId:l.clientId});function r(u){ie[u?"unshift":"push"](()=>{s=u,t(2,s)})}const a=u=>{var f;t(0,l.clientSecret=((f=u.detail)==null?void 0:f.secret)||"",l)};return n.$$set=u=>{"key"in u&&t(1,i=u.key),"config"in u&&t(0,l=u.config)},[l,i,s,o,r,a]}class K6 extends ye{constructor(e){super(),be(this,e,Y6,W6,_e,{key:1,config:0})}}function J6(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Auth URL"),l=C(),s=b("input"),r=C(),a=b("div"),a.textContent="Ex. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize",p(e,"for",i=n[4]),p(s,"type","url"),p(s,"id",o=n[4]),s.required=!0,p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[0].authURL),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[2]),u=!0)},p(c,d){d&16&&i!==(i=c[4])&&p(e,"for",i),d&16&&o!==(o=c[4])&&p(s,"id",o),d&1&&s.value!==c[0].authURL&&ce(s,c[0].authURL)},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function Z6(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Token URL"),l=C(),s=b("input"),r=C(),a=b("div"),a.textContent="Ex. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token",p(e,"for",i=n[4]),p(s,"type","url"),p(s,"id",o=n[4]),s.required=!0,p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[0].tokenURL),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[3]),u=!0)},p(c,d){d&16&&i!==(i=c[4])&&p(e,"for",i),d&16&&o!==(o=c[4])&&p(s,"id",o),d&1&&s.value!==c[0].tokenURL&&ce(s,c[0].tokenURL)},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function G6(n){let e,t,i,l,s,o;return i=new fe({props:{class:"form-field required",name:n[1]+".authURL",$$slots:{default:[J6,({uniqueId:r})=>({4:r}),({uniqueId:r})=>r?16:0]},$$scope:{ctx:n}}}),s=new fe({props:{class:"form-field required",name:n[1]+".tokenURL",$$slots:{default:[Z6,({uniqueId:r})=>({4:r}),({uniqueId:r})=>r?16:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.textContent="Azure AD endpoints",t=C(),H(i.$$.fragment),l=C(),H(s.$$.fragment),p(e,"class","section-title")},m(r,a){v(r,e,a),v(r,t,a),F(i,r,a),v(r,l,a),F(s,r,a),o=!0},p(r,[a]){const u={};a&2&&(u.name=r[1]+".authURL"),a&49&&(u.$$scope={dirty:a,ctx:r}),i.$set(u);const f={};a&2&&(f.name=r[1]+".tokenURL"),a&49&&(f.$$scope={dirty:a,ctx:r}),s.$set(f)},i(r){o||(O(i.$$.fragment,r),O(s.$$.fragment,r),o=!0)},o(r){D(i.$$.fragment,r),D(s.$$.fragment,r),o=!1},d(r){r&&(k(e),k(t),k(l)),q(i,r),q(s,r)}}}function X6(n,e,t){let{key:i=""}=e,{config:l={}}=e;function s(){l.authURL=this.value,t(0,l)}function o(){l.tokenURL=this.value,t(0,l)}return n.$$set=r=>{"key"in r&&t(1,i=r.key),"config"in r&&t(0,l=r.config)},[l,i,s,o]}class Q6 extends ye{constructor(e){super(),be(this,e,X6,G6,_e,{key:1,config:0})}}function x6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Display name"),l=C(),s=b("input"),p(e,"for",i=n[7]),p(s,"type","text"),p(s,"id",o=n[7]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].displayName),r||(a=B(s,"input",n[2]),r=!0)},p(u,f){f&128&&i!==(i=u[7])&&p(e,"for",i),f&128&&o!==(o=u[7])&&p(s,"id",o),f&1&&s.value!==u[0].displayName&&ce(s,u[0].displayName)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function e8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Auth URL"),l=C(),s=b("input"),p(e,"for",i=n[7]),p(s,"type","url"),p(s,"id",o=n[7]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].authURL),r||(a=B(s,"input",n[3]),r=!0)},p(u,f){f&128&&i!==(i=u[7])&&p(e,"for",i),f&128&&o!==(o=u[7])&&p(s,"id",o),f&1&&s.value!==u[0].authURL&&ce(s,u[0].authURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function t8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Token URL"),l=C(),s=b("input"),p(e,"for",i=n[7]),p(s,"type","url"),p(s,"id",o=n[7]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].tokenURL),r||(a=B(s,"input",n[4]),r=!0)},p(u,f){f&128&&i!==(i=u[7])&&p(e,"for",i),f&128&&o!==(o=u[7])&&p(s,"id",o),f&1&&s.value!==u[0].tokenURL&&ce(s,u[0].tokenURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function n8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("User info URL"),l=C(),s=b("input"),p(e,"for",i=n[7]),p(s,"type","url"),p(s,"id",o=n[7]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].userInfoURL),r||(a=B(s,"input",n[5]),r=!0)},p(u,f){f&128&&i!==(i=u[7])&&p(e,"for",i),f&128&&o!==(o=u[7])&&p(s,"id",o),f&1&&s.value!==u[0].userInfoURL&&ce(s,u[0].userInfoURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function i8(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Support PKCE",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[7]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[7])},m(c,d){v(c,e,d),e.checked=n[0].pkce,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[6]),Me(He.call(null,r,{text:"Usually it should be safe to be always enabled as most providers will just ignore the extra query parameters if they don't support PKCE.",position:"right"}))],u=!0)},p(c,d){d&128&&t!==(t=c[7])&&p(e,"id",t),d&1&&(e.checked=c[0].pkce),d&128&&a!==(a=c[7])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function l8(n){let e,t,i,l,s,o,r,a,u,f,c,d;return e=new fe({props:{class:"form-field required",name:n[1]+".displayName",$$slots:{default:[x6,({uniqueId:m})=>({7:m}),({uniqueId:m})=>m?128:0]},$$scope:{ctx:n}}}),s=new fe({props:{class:"form-field required",name:n[1]+".authURL",$$slots:{default:[e8,({uniqueId:m})=>({7:m}),({uniqueId:m})=>m?128:0]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field required",name:n[1]+".tokenURL",$$slots:{default:[t8,({uniqueId:m})=>({7:m}),({uniqueId:m})=>m?128:0]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field required",name:n[1]+".userInfoURL",$$slots:{default:[n8,({uniqueId:m})=>({7:m}),({uniqueId:m})=>m?128:0]},$$scope:{ctx:n}}}),c=new fe({props:{class:"form-field",name:n[1]+".pkce",$$slots:{default:[i8,({uniqueId:m})=>({7:m}),({uniqueId:m})=>m?128:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),i=b("div"),i.textContent="Endpoints",l=C(),H(s.$$.fragment),o=C(),H(r.$$.fragment),a=C(),H(u.$$.fragment),f=C(),H(c.$$.fragment),p(i,"class","section-title")},m(m,h){F(e,m,h),v(m,t,h),v(m,i,h),v(m,l,h),F(s,m,h),v(m,o,h),F(r,m,h),v(m,a,h),F(u,m,h),v(m,f,h),F(c,m,h),d=!0},p(m,[h]){const g={};h&2&&(g.name=m[1]+".displayName"),h&385&&(g.$$scope={dirty:h,ctx:m}),e.$set(g);const _={};h&2&&(_.name=m[1]+".authURL"),h&385&&(_.$$scope={dirty:h,ctx:m}),s.$set(_);const y={};h&2&&(y.name=m[1]+".tokenURL"),h&385&&(y.$$scope={dirty:h,ctx:m}),r.$set(y);const S={};h&2&&(S.name=m[1]+".userInfoURL"),h&385&&(S.$$scope={dirty:h,ctx:m}),u.$set(S);const T={};h&2&&(T.name=m[1]+".pkce"),h&385&&(T.$$scope={dirty:h,ctx:m}),c.$set(T)},i(m){d||(O(e.$$.fragment,m),O(s.$$.fragment,m),O(r.$$.fragment,m),O(u.$$.fragment,m),O(c.$$.fragment,m),d=!0)},o(m){D(e.$$.fragment,m),D(s.$$.fragment,m),D(r.$$.fragment,m),D(u.$$.fragment,m),D(c.$$.fragment,m),d=!1},d(m){m&&(k(t),k(i),k(l),k(o),k(a),k(f)),q(e,m),q(s,m),q(r,m),q(u,m),q(c,m)}}}function s8(n,e,t){let{key:i=""}=e,{config:l={}}=e;z.isEmpty(l.pkce)&&(l.pkce=!0),l.displayName||(l.displayName="OIDC");function s(){l.displayName=this.value,t(0,l)}function o(){l.authURL=this.value,t(0,l)}function r(){l.tokenURL=this.value,t(0,l)}function a(){l.userInfoURL=this.value,t(0,l)}function u(){l.pkce=this.checked,t(0,l)}return n.$$set=f=>{"key"in f&&t(1,i=f.key),"config"in f&&t(0,l=f.config)},[l,i,s,o,r,a,u]}class ka extends ye{constructor(e){super(),be(this,e,s8,l8,_e,{key:1,config:0})}}function o8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Auth URL"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].authURL),r||(a=B(s,"input",n[5]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&8&&(s.required=u[3]),f&1&&s.value!==u[0].authURL&&ce(s,u[0].authURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function r8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Token URL"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].tokenURL),r||(a=B(s,"input",n[6]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&8&&(s.required=u[3]),f&1&&s.value!==u[0].tokenURL&&ce(s,u[0].tokenURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function a8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("User info URL"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].userInfoURL),r||(a=B(s,"input",n[7]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&8&&(s.required=u[3]),f&1&&s.value!==u[0].userInfoURL&&ce(s,u[0].userInfoURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function u8(n){let e,t,i,l,s,o,r,a,u;return l=new fe({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".authURL",$$slots:{default:[o8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".tokenURL",$$slots:{default:[r8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".userInfoURL",$$slots:{default:[a8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=Y(n[2]),i=C(),H(l.$$.fragment),s=C(),H(o.$$.fragment),r=C(),H(a.$$.fragment),p(e,"class","section-title")},m(f,c){v(f,e,c),w(e,t),v(f,i,c),F(l,f,c),v(f,s,c),F(o,f,c),v(f,r,c),F(a,f,c),u=!0},p(f,[c]){(!u||c&4)&&ue(t,f[2]);const d={};c&8&&(d.class="form-field "+(f[3]?"required":"")),c&2&&(d.name=f[1]+".authURL"),c&777&&(d.$$scope={dirty:c,ctx:f}),l.$set(d);const m={};c&8&&(m.class="form-field "+(f[3]?"required":"")),c&2&&(m.name=f[1]+".tokenURL"),c&777&&(m.$$scope={dirty:c,ctx:f}),o.$set(m);const h={};c&8&&(h.class="form-field "+(f[3]?"required":"")),c&2&&(h.name=f[1]+".userInfoURL"),c&777&&(h.$$scope={dirty:c,ctx:f}),a.$set(h)},i(f){u||(O(l.$$.fragment,f),O(o.$$.fragment,f),O(a.$$.fragment,f),u=!0)},o(f){D(l.$$.fragment,f),D(o.$$.fragment,f),D(a.$$.fragment,f),u=!1},d(f){f&&(k(e),k(i),k(s),k(r)),q(l,f),q(o,f),q(a,f)}}}function f8(n,e,t){let i,{key:l=""}=e,{config:s={}}=e,{required:o=!1}=e,{title:r="Provider endpoints"}=e;function a(){s.authURL=this.value,t(0,s)}function u(){s.tokenURL=this.value,t(0,s)}function f(){s.userInfoURL=this.value,t(0,s)}return n.$$set=c=>{"key"in c&&t(1,l=c.key),"config"in c&&t(0,s=c.config),"required"in c&&t(4,o=c.required),"title"in c&&t(2,r=c.title)},n.$$.update=()=>{n.$$.dirty&17&&t(3,i=o&&(s==null?void 0:s.enabled))},[s,l,r,i,o,a,u,f]}class va extends ye{constructor(e){super(),be(this,e,f8,u8,_e,{key:1,config:0,required:4,title:2})}}const ef=[{key:"apple",title:"Apple",logo:"apple.svg",optionsComponent:K6},{key:"google",title:"Google",logo:"google.svg"},{key:"microsoft",title:"Microsoft",logo:"microsoft.svg",optionsComponent:Q6},{key:"yandex",title:"Yandex",logo:"yandex.svg"},{key:"facebook",title:"Facebook",logo:"facebook.svg"},{key:"instagram",title:"Instagram",logo:"instagram.svg"},{key:"github",title:"GitHub",logo:"github.svg"},{key:"gitlab",title:"GitLab",logo:"gitlab.svg",optionsComponent:va,optionsComponentProps:{title:"Self-hosted endpoints (optional)"}},{key:"bitbucket",title:"Bitbucket",logo:"bitbucket.svg"},{key:"gitee",title:"Gitee",logo:"gitee.svg"},{key:"gitea",title:"Gitea",logo:"gitea.svg",optionsComponent:va,optionsComponentProps:{title:"Self-hosted endpoints (optional)"}},{key:"discord",title:"Discord",logo:"discord.svg"},{key:"twitter",title:"Twitter",logo:"twitter.svg"},{key:"kakao",title:"Kakao",logo:"kakao.svg"},{key:"vk",title:"VK",logo:"vk.svg"},{key:"spotify",title:"Spotify",logo:"spotify.svg"},{key:"twitch",title:"Twitch",logo:"twitch.svg"},{key:"patreon",title:"Patreon (v2)",logo:"patreon.svg"},{key:"strava",title:"Strava",logo:"strava.svg"},{key:"livechat",title:"LiveChat",logo:"livechat.svg"},{key:"mailcow",title:"mailcow",logo:"mailcow.svg",optionsComponent:va,optionsComponentProps:{required:!0}},{key:"planningcenter",title:"Planning Center",logo:"planningcenter.svg"},{key:"oidc",title:"OpenID Connect",logo:"oidc.svg",optionsComponent:ka},{key:"oidc2",title:"(2) OpenID Connect",logo:"oidc.svg",optionsComponent:ka},{key:"oidc3",title:"(3) OpenID Connect",logo:"oidc.svg",optionsComponent:ka}];function lm(n,e,t){const i=n.slice();return i[16]=e[t],i}function sm(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Clear',p(e,"type","button"),p(e,"class","btn btn-transparent btn-sm btn-hint p-l-xs p-r-xs m-l-10")},m(o,r){v(o,e,r),i=!0,l||(s=B(e,"click",n[9]),l=!0)},p:te,i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Fn,{duration:150,x:5},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Fn,{duration:150,x:5},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function c8(n){let e,t,i,l,s,o,r,a,u,f,c=n[1]!=""&&sm(n);return{c(){e=b("label"),t=b("i"),l=C(),s=b("input"),r=C(),c&&c.c(),a=ge(),p(t,"class","ri-search-line"),p(e,"for",i=n[19]),p(e,"class","m-l-10 txt-xl"),p(s,"id",o=n[19]),p(s,"type","text"),p(s,"placeholder","Search provider")},m(d,m){v(d,e,m),w(e,t),v(d,l,m),v(d,s,m),ce(s,n[1]),v(d,r,m),c&&c.m(d,m),v(d,a,m),u||(f=B(s,"input",n[8]),u=!0)},p(d,m){m&524288&&i!==(i=d[19])&&p(e,"for",i),m&524288&&o!==(o=d[19])&&p(s,"id",o),m&2&&s.value!==d[1]&&ce(s,d[1]),d[1]!=""?c?(c.p(d,m),m&2&&O(c,1)):(c=sm(d),c.c(),O(c,1),c.m(a.parentNode,a)):c&&(re(),D(c,1,1,()=>{c=null}),ae())},d(d){d&&(k(e),k(l),k(s),k(r),k(a)),c&&c.d(d),u=!1,f()}}}function om(n){let e,t,i,l,s=n[1]!=""&&rm(n);return{c(){e=b("div"),t=b("span"),t.textContent="No providers to select.",i=C(),s&&s.c(),l=C(),p(t,"class","txt-hint"),p(e,"class","flex inline-flex")},m(o,r){v(o,e,r),w(e,t),w(e,i),s&&s.m(e,null),w(e,l)},p(o,r){o[1]!=""?s?s.p(o,r):(s=rm(o),s.c(),s.m(e,l)):s&&(s.d(1),s=null)},d(o){o&&k(e),s&&s.d()}}}function rm(n){let e,t,i;return{c(){e=b("button"),e.textContent="Clear filter",p(e,"type","button"),p(e,"class","btn btn-sm btn-secondary")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[5]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function am(n){let e,t,i;return{c(){e=b("img"),vn(e.src,t="./images/oauth2/"+n[16].logo)||p(e,"src",t),p(e,"alt",i=n[16].title+" logo")},m(l,s){v(l,e,s)},p(l,s){s&8&&!vn(e.src,t="./images/oauth2/"+l[16].logo)&&p(e,"src",t),s&8&&i!==(i=l[16].title+" logo")&&p(e,"alt",i)},d(l){l&&k(e)}}}function um(n,e){let t,i,l,s,o,r,a=e[16].title+"",u,f,c,d=e[16].key+"",m,h,g,_,y=e[16].logo&&am(e);function S(){return e[10](e[16])}return{key:n,first:null,c(){t=b("div"),i=b("button"),l=b("figure"),y&&y.c(),s=C(),o=b("div"),r=b("div"),u=Y(a),f=C(),c=b("em"),m=Y(d),h=C(),p(l,"class","provider-logo"),p(r,"class","title"),p(c,"class","txt-hint txt-sm m-r-auto"),p(o,"class","content"),p(i,"type","button"),p(i,"class","provider-card handle"),p(t,"class","col-lg-6"),this.first=t},m(T,$){v(T,t,$),w(t,i),w(i,l),y&&y.m(l,null),w(i,s),w(i,o),w(o,r),w(r,u),w(o,f),w(o,c),w(c,m),w(t,h),g||(_=B(i,"click",S),g=!0)},p(T,$){e=T,e[16].logo?y?y.p(e,$):(y=am(e),y.c(),y.m(l,null)):y&&(y.d(1),y=null),$&8&&a!==(a=e[16].title+"")&&ue(u,a),$&8&&d!==(d=e[16].key+"")&&ue(m,d)},d(T){T&&k(t),y&&y.d(),g=!1,_()}}}function d8(n){let e,t,i,l=[],s=new Map,o;e=new fe({props:{class:"searchbar m-b-sm",$$slots:{default:[c8,({uniqueId:f})=>({19:f}),({uniqueId:f})=>f?524288:0]},$$scope:{ctx:n}}});let r=pe(n[3]);const a=f=>f[16].key;for(let f=0;f!l.includes($.key)&&(T==""||$.key.toLowerCase().includes(T)||$.title.toLowerCase().includes(T)))}function d(){t(1,o="")}function m(){o=this.value,t(1,o)}const h=()=>t(1,o=""),g=T=>f(T);function _(T){ie[T?"unshift":"push"](()=>{s=T,t(2,s)})}function y(T){Pe.call(this,n,T)}function S(T){Pe.call(this,n,T)}return n.$$set=T=>{"disabled"in T&&t(6,l=T.disabled)},n.$$.update=()=>{n.$$.dirty&66&&(o!==-1||l!==-1)&&t(3,r=c())},[u,o,s,r,f,d,l,a,m,h,g,_,y,S]}class g8 extends ye{constructor(e){super(),be(this,e,_8,h8,_e,{disabled:6,show:7,hide:0})}get show(){return this.$$.ctx[7]}get hide(){return this.$$.ctx[0]}}function fm(n,e,t){const i=n.slice();i[28]=e[t],i[31]=t;const l=i[9](i[28].name);return i[29]=l,i}function b8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[27]),p(l,"for",o=n[27])},m(u,f){v(u,e,f),e.checked=n[0].oauth2.enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[10]),r=!0)},p(u,f){f[0]&134217728&&t!==(t=u[27])&&p(e,"id",t),f[0]&1&&(e.checked=u[0].oauth2.enabled),f[0]&134217728&&o!==(o=u[27])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function y8(n){let e;return{c(){e=b("i"),p(e,"class","ri-puzzle-line txt-sm txt-hint")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function k8(n){let e,t,i;return{c(){e=b("img"),vn(e.src,t="./images/oauth2/"+n[29].logo)||p(e,"src",t),p(e,"alt",i=n[29].title+" logo")},m(l,s){v(l,e,s)},p(l,s){s[0]&1&&!vn(e.src,t="./images/oauth2/"+l[29].logo)&&p(e,"src",t),s[0]&1&&i!==(i=l[29].title+" logo")&&p(e,"alt",i)},d(l){l&&k(e)}}}function cm(n){let e,t,i;function l(){return n[11](n[29],n[28],n[31])}return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-circle btn-hint btn-transparent"),p(e,"aria-label","Provider settings")},m(s,o){v(s,e,o),t||(i=[Me(He.call(null,e,{text:"Edit config",position:"left"})),B(e,"click",l)],t=!0)},p(s,o){n=s},d(s){s&&k(e),t=!1,De(i)}}}function dm(n,e){var T;let t,i,l,s,o,r,a=(e[28].displayName||((T=e[29])==null?void 0:T.title)||"Custom")+"",u,f,c,d=e[28].name+"",m,h;function g($,E){var M;return(M=$[29])!=null&&M.logo?k8:y8}let _=g(e),y=_(e),S=e[29]&&cm(e);return{key:n,first:null,c(){var $,E,M;t=b("div"),i=b("div"),l=b("figure"),y.c(),s=C(),o=b("div"),r=b("div"),u=Y(a),f=C(),c=b("em"),m=Y(d),h=C(),S&&S.c(),p(l,"class","provider-logo"),p(r,"class","title"),p(c,"class","txt-hint txt-sm m-r-auto"),p(o,"class","content"),p(i,"class","provider-card"),x(i,"error",!z.isEmpty((M=(E=($=e[1])==null?void 0:$.oauth2)==null?void 0:E.providers)==null?void 0:M[e[31]])),p(t,"class","col-lg-6"),this.first=t},m($,E){v($,t,E),w(t,i),w(i,l),y.m(l,null),w(i,s),w(i,o),w(o,r),w(r,u),w(o,f),w(o,c),w(c,m),w(i,h),S&&S.m(i,null)},p($,E){var M,L,I,A;e=$,_===(_=g(e))&&y?y.p(e,E):(y.d(1),y=_(e),y&&(y.c(),y.m(l,null))),E[0]&1&&a!==(a=(e[28].displayName||((M=e[29])==null?void 0:M.title)||"Custom")+"")&&ue(u,a),E[0]&1&&d!==(d=e[28].name+"")&&ue(m,d),e[29]?S?S.p(e,E):(S=cm(e),S.c(),S.m(i,null)):S&&(S.d(1),S=null),E[0]&3&&x(i,"error",!z.isEmpty((A=(I=(L=e[1])==null?void 0:L.oauth2)==null?void 0:I.providers)==null?void 0:A[e[31]]))},d($){$&&k(t),y.d(),S&&S.d()}}}function v8(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function w8(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function pm(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g;return l=new fe({props:{class:"form-field form-field-toggle",name:"oauth2.mappedFields.name",$$slots:{default:[S8,({uniqueId:_})=>({27:_}),({uniqueId:_})=>[_?134217728:0]]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field form-field-toggle",name:"oauth2.mappedFields.avatarURL",$$slots:{default:[T8,({uniqueId:_})=>({27:_}),({uniqueId:_})=>[_?134217728:0]]},$$scope:{ctx:n}}}),f=new fe({props:{class:"form-field form-field-toggle",name:"oauth2.mappedFields.id",$$slots:{default:[$8,({uniqueId:_})=>({27:_}),({uniqueId:_})=>[_?134217728:0]]},$$scope:{ctx:n}}}),m=new fe({props:{class:"form-field form-field-toggle",name:"oauth2.mappedFields.username",$$slots:{default:[C8,({uniqueId:_})=>({27:_}),({uniqueId:_})=>[_?134217728:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),a=C(),u=b("div"),H(f.$$.fragment),c=C(),d=b("div"),H(m.$$.fragment),p(i,"class","col-sm-6"),p(o,"class","col-sm-6"),p(u,"class","col-sm-6"),p(d,"class","col-sm-6"),p(t,"class","grid grid-sm p-t-xs"),p(e,"class","block")},m(_,y){v(_,e,y),w(e,t),w(t,i),F(l,i,null),w(t,s),w(t,o),F(r,o,null),w(t,a),w(t,u),F(f,u,null),w(t,c),w(t,d),F(m,d,null),g=!0},p(_,y){const S={};y[0]&134217761|y[1]&2&&(S.$$scope={dirty:y,ctx:_}),l.$set(S);const T={};y[0]&134217793|y[1]&2&&(T.$$scope={dirty:y,ctx:_}),r.$set(T);const $={};y[0]&134217761|y[1]&2&&($.$$scope={dirty:y,ctx:_}),f.$set($);const E={};y[0]&134217761|y[1]&2&&(E.$$scope={dirty:y,ctx:_}),m.$set(E)},i(_){g||(O(l.$$.fragment,_),O(r.$$.fragment,_),O(f.$$.fragment,_),O(m.$$.fragment,_),_&&nt(()=>{g&&(h||(h=ze(e,vt,{duration:150},!0)),h.run(1))}),g=!0)},o(_){D(l.$$.fragment,_),D(r.$$.fragment,_),D(f.$$.fragment,_),D(m.$$.fragment,_),_&&(h||(h=ze(e,vt,{duration:150},!1)),h.run(0)),g=!1},d(_){_&&k(e),q(l),q(r),q(f),q(m),_&&h&&h.end()}}}function S8(n){let e,t,i,l,s,o,r;function a(f){n[14](f)}let u={id:n[27],items:n[5],toggle:!0,zeroFunc:L8,selectPlaceholder:"Select field"};return n[0].oauth2.mappedFields.name!==void 0&&(u.selected=n[0].oauth2.mappedFields.name),s=new ds({props:u}),ie.push(()=>ve(s,"selected",a)),{c(){e=b("label"),t=Y("OAuth2 full name"),l=C(),H(s.$$.fragment),p(e,"for",i=n[27])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[0]&134217728&&i!==(i=f[27]))&&p(e,"for",i);const d={};c[0]&134217728&&(d.id=f[27]),c[0]&32&&(d.items=f[5]),!o&&c[0]&1&&(o=!0,d.selected=f[0].oauth2.mappedFields.name,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function T8(n){let e,t,i,l,s,o,r;function a(f){n[15](f)}let u={id:n[27],items:n[6],toggle:!0,zeroFunc:A8,selectPlaceholder:"Select field"};return n[0].oauth2.mappedFields.avatarURL!==void 0&&(u.selected=n[0].oauth2.mappedFields.avatarURL),s=new ds({props:u}),ie.push(()=>ve(s,"selected",a)),{c(){e=b("label"),t=Y("OAuth2 avatar"),l=C(),H(s.$$.fragment),p(e,"for",i=n[27])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[0]&134217728&&i!==(i=f[27]))&&p(e,"for",i);const d={};c[0]&134217728&&(d.id=f[27]),c[0]&64&&(d.items=f[6]),!o&&c[0]&1&&(o=!0,d.selected=f[0].oauth2.mappedFields.avatarURL,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function $8(n){let e,t,i,l,s,o,r;function a(f){n[16](f)}let u={id:n[27],items:n[5],toggle:!0,zeroFunc:P8,selectPlaceholder:"Select field"};return n[0].oauth2.mappedFields.id!==void 0&&(u.selected=n[0].oauth2.mappedFields.id),s=new ds({props:u}),ie.push(()=>ve(s,"selected",a)),{c(){e=b("label"),t=Y("OAuth2 id"),l=C(),H(s.$$.fragment),p(e,"for",i=n[27])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[0]&134217728&&i!==(i=f[27]))&&p(e,"for",i);const d={};c[0]&134217728&&(d.id=f[27]),c[0]&32&&(d.items=f[5]),!o&&c[0]&1&&(o=!0,d.selected=f[0].oauth2.mappedFields.id,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function C8(n){let e,t,i,l,s,o,r;function a(f){n[17](f)}let u={id:n[27],items:n[5],toggle:!0,zeroFunc:N8,selectPlaceholder:"Select field"};return n[0].oauth2.mappedFields.username!==void 0&&(u.selected=n[0].oauth2.mappedFields.username),s=new ds({props:u}),ie.push(()=>ve(s,"selected",a)),{c(){e=b("label"),t=Y("OAuth2 username"),l=C(),H(s.$$.fragment),p(e,"for",i=n[27])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[0]&134217728&&i!==(i=f[27]))&&p(e,"for",i);const d={};c[0]&134217728&&(d.id=f[27]),c[0]&32&&(d.items=f[5]),!o&&c[0]&1&&(o=!0,d.selected=f[0].oauth2.mappedFields.username,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function O8(n){let e,t,i,l=[],s=new Map,o,r,a,u,f,c,d,m=n[0].name+"",h,g,_,y,S,T,$,E,M;e=new fe({props:{class:"form-field form-field-toggle",name:"oauth2.enabled",$$slots:{default:[b8,({uniqueId:U})=>({27:U}),({uniqueId:U})=>[U?134217728:0]]},$$scope:{ctx:n}}});let L=pe(n[0].oauth2.providers);const I=U=>U[28].name;for(let U=0;U Add provider',u=C(),f=b("button"),c=b("strong"),d=Y("Optional "),h=Y(m),g=Y(" create fields map"),_=C(),R.c(),S=C(),N&&N.c(),T=ge(),p(a,"class","btn btn-block btn-lg btn-secondary txt-base"),p(r,"class","col-lg-6"),p(i,"class","grid grid-sm"),p(c,"class","txt"),p(f,"type","button"),p(f,"class",y="m-t-25 btn btn-sm "+(n[4]?"btn-secondary":"btn-hint btn-transparent"))},m(U,j){F(e,U,j),v(U,t,j),v(U,i,j);for(let V=0;V{N=null}),ae())},i(U){$||(O(e.$$.fragment,U),O(N),$=!0)},o(U){D(e.$$.fragment,U),D(N),$=!1},d(U){U&&(k(t),k(i),k(u),k(f),k(S),k(T)),q(e,U);for(let j=0;j0),p(r,"class","label label-success")},m(a,u){v(a,e,u),w(e,t),w(e,i),w(e,s),v(a,o,u),v(a,r,u)},p(a,u){u[0]&128&&ue(t,a[7]),u[0]&128&&l!==(l=a[7]==1?"provider":"providers")&&ue(s,l),u[0]&128&&x(e,"label-warning",!a[7]),u[0]&128&&x(e,"label-info",a[7]>0)},d(a){a&&(k(e),k(o),k(r))}}}function mm(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function D8(n){let e,t,i,l,s,o;function r(c,d){return c[0].oauth2.enabled?M8:E8}let a=r(n),u=a(n),f=n[8]&&mm();return{c(){e=b("div"),e.innerHTML=' OAuth2',t=C(),i=b("div"),l=C(),u.c(),s=C(),f&&f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),u.m(c,d),v(c,s,d),f&&f.m(c,d),v(c,o,d)},p(c,d){a===(a=r(c))&&u?u.p(c,d):(u.d(1),u=a(c),u&&(u.c(),u.m(s.parentNode,s))),c[8]?f?d[0]&256&&O(f,1):(f=mm(),f.c(),O(f,1),f.m(o.parentNode,o)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),u.d(c),f&&f.d(c)}}}function I8(n){var u,f;let e,t,i,l,s,o;e=new qi({props:{single:!0,$$slots:{header:[D8],default:[O8]},$$scope:{ctx:n}}});let r={disabled:((f=(u=n[0].oauth2)==null?void 0:u.providers)==null?void 0:f.map(hm))||[]};i=new g8({props:r}),n[18](i),i.$on("select",n[19]);let a={};return s=new A6({props:a}),n[20](s),s.$on("remove",n[21]),s.$on("submit",n[22]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),l=C(),H(s.$$.fragment)},m(c,d){F(e,c,d),v(c,t,d),F(i,c,d),v(c,l,d),F(s,c,d),o=!0},p(c,d){var _,y;const m={};d[0]&511|d[1]&2&&(m.$$scope={dirty:d,ctx:c}),e.$set(m);const h={};d[0]&1&&(h.disabled=((y=(_=c[0].oauth2)==null?void 0:_.providers)==null?void 0:y.map(hm))||[]),i.$set(h);const g={};s.$set(g)},i(c){o||(O(e.$$.fragment,c),O(i.$$.fragment,c),O(s.$$.fragment,c),o=!0)},o(c){D(e.$$.fragment,c),D(i.$$.fragment,c),D(s.$$.fragment,c),o=!1},d(c){c&&(k(t),k(l)),q(e,c),n[18](null),q(i,c),n[20](null),q(s,c)}}}const L8=()=>"",A8=()=>"",P8=()=>"",N8=()=>"",hm=n=>n.name;function R8(n,e,t){let i,l,s;Qe(n,Sn,j=>t(1,s=j));let{collection:o}=e;const r=["id","email","emailVisibility","verified","tokenKey","password"],a=["text","editor","url","email","json"],u=a.concat("file");let f,c,d=!1,m=[],h=[];function g(j=[]){var V,K;t(5,m=((V=j==null?void 0:j.filter(J=>a.includes(J.type)&&!r.includes(J.name)))==null?void 0:V.map(J=>J.name))||[]),t(6,h=((K=j==null?void 0:j.filter(J=>u.includes(J.type)&&!r.includes(J.name)))==null?void 0:K.map(J=>J.name))||[])}function _(j){for(let V of ef)if(V.key==j)return V;return null}function y(){o.oauth2.enabled=this.checked,t(0,o)}const S=(j,V,K)=>{c==null||c.show(j,V,K)},T=()=>f==null?void 0:f.show(),$=()=>t(4,d=!d);function E(j){n.$$.not_equal(o.oauth2.mappedFields.name,j)&&(o.oauth2.mappedFields.name=j,t(0,o))}function M(j){n.$$.not_equal(o.oauth2.mappedFields.avatarURL,j)&&(o.oauth2.mappedFields.avatarURL=j,t(0,o))}function L(j){n.$$.not_equal(o.oauth2.mappedFields.id,j)&&(o.oauth2.mappedFields.id=j,t(0,o))}function I(j){n.$$.not_equal(o.oauth2.mappedFields.username,j)&&(o.oauth2.mappedFields.username=j,t(0,o))}function A(j){ie[j?"unshift":"push"](()=>{f=j,t(2,f)})}const P=j=>{var V,K;c.show(j.detail,{},((K=(V=o.oauth2)==null?void 0:V.providers)==null?void 0:K.length)||0)};function R(j){ie[j?"unshift":"push"](()=>{c=j,t(3,c)})}const N=j=>{const V=j.detail.uiOptions;z.removeByKey(o.oauth2.providers,"name",V.key),t(0,o)},U=j=>{const V=j.detail.uiOptions,K=j.detail.config;t(0,o.oauth2.providers=o.oauth2.providers||[],o),z.pushOrReplaceByKey(o.oauth2.providers,Object.assign({name:V.key},K),"name"),t(0,o)};return n.$$set=j=>{"collection"in j&&t(0,o=j.collection)},n.$$.update=()=>{var j,V;n.$$.dirty[0]&1&&z.isEmpty(o.oauth2)&&t(0,o.oauth2={enabled:!1,mappedFields:{},providers:[]},o),n.$$.dirty[0]&1&&g(o.fields),n.$$.dirty[0]&2&&t(8,i=!z.isEmpty(s==null?void 0:s.oauth2)),n.$$.dirty[0]&1&&t(7,l=((V=(j=o.oauth2)==null?void 0:j.providers)==null?void 0:V.length)||0)},[o,s,f,c,d,m,h,l,i,_,y,S,T,$,E,M,L,I,A,P,R,N,U]}class F8 extends ye{constructor(e){super(),be(this,e,R8,I8,_e,{collection:0},null,[-1,-1])}}function _m(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-information-line link-hint")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,{text:"Superusers can have OTP only as part of Two-factor authentication.",position:"right"})),t=!0)},d(l){l&&k(e),t=!1,i()}}}function q8(n){let e,t,i,l,s,o,r,a,u,f,c=n[2]&&_m();return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable"),r=C(),c&&c.c(),a=ge(),p(e,"type","checkbox"),p(e,"id",t=n[8]),p(l,"for",o=n[8])},m(d,m){v(d,e,m),e.checked=n[0].otp.enabled,v(d,i,m),v(d,l,m),w(l,s),v(d,r,m),c&&c.m(d,m),v(d,a,m),u||(f=[B(e,"change",n[4]),B(e,"change",n[5])],u=!0)},p(d,m){m&256&&t!==(t=d[8])&&p(e,"id",t),m&1&&(e.checked=d[0].otp.enabled),m&256&&o!==(o=d[8])&&p(l,"for",o),d[2]?c||(c=_m(),c.c(),c.m(a.parentNode,a)):c&&(c.d(1),c=null)},d(d){d&&(k(e),k(i),k(l),k(r),k(a)),c&&c.d(d),u=!1,De(f)}}}function H8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Duration (in seconds)"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","number"),p(s,"min","0"),p(s,"step","1"),p(s,"id",o=n[8]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].otp.duration),r||(a=B(s,"input",n[6]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&1&&St(s.value)!==u[0].otp.duration&&ce(s,u[0].otp.duration)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function j8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Generated password length"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","text"),p(s,"id",o=n[8]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].otp.length),r||(a=B(s,"input",n[7]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&1&&s.value!==u[0].otp.length&&ce(s,u[0].otp.length)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function z8(n){let e,t,i,l,s,o,r,a,u;return e=new fe({props:{class:"form-field form-field-toggle",name:"otp.enabled",$$slots:{default:[q8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),s=new fe({props:{class:"form-field form-field-toggle required",name:"otp.duration",$$slots:{default:[H8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field form-field-toggle required",name:"otp.length",$$slots:{default:[j8,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),i=b("div"),l=b("div"),H(s.$$.fragment),o=C(),r=b("div"),H(a.$$.fragment),p(l,"class","col-sm-6"),p(r,"class","col-sm-6"),p(i,"class","grid grid-sm")},m(f,c){F(e,f,c),v(f,t,c),v(f,i,c),w(i,l),F(s,l,null),w(i,o),w(i,r),F(a,r,null),u=!0},p(f,c){const d={};c&773&&(d.$$scope={dirty:c,ctx:f}),e.$set(d);const m={};c&769&&(m.$$scope={dirty:c,ctx:f}),s.$set(m);const h={};c&769&&(h.$$scope={dirty:c,ctx:f}),a.$set(h)},i(f){u||(O(e.$$.fragment,f),O(s.$$.fragment,f),O(a.$$.fragment,f),u=!0)},o(f){D(e.$$.fragment,f),D(s.$$.fragment,f),D(a.$$.fragment,f),u=!1},d(f){f&&(k(t),k(i)),q(e,f),q(s),q(a)}}}function U8(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function V8(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function gm(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function B8(n){let e,t,i,l,s,o;function r(c,d){return c[0].otp.enabled?V8:U8}let a=r(n),u=a(n),f=n[1]&&gm();return{c(){e=b("div"),e.innerHTML=' One-time password (OTP)',t=C(),i=b("div"),l=C(),u.c(),s=C(),f&&f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),u.m(c,d),v(c,s,d),f&&f.m(c,d),v(c,o,d)},p(c,d){a!==(a=r(c))&&(u.d(1),u=a(c),u&&(u.c(),u.m(s.parentNode,s))),c[1]?f?d&2&&O(f,1):(f=gm(),f.c(),O(f,1),f.m(o.parentNode,o)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),u.d(c),f&&f.d(c)}}}function W8(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[B8],default:[z8]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&519&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function Y8(n,e,t){let i,l,s;Qe(n,Sn,c=>t(3,s=c));let{collection:o}=e;function r(){o.otp.enabled=this.checked,t(0,o)}const a=c=>{i&&t(0,o.mfa.enabled=c.target.checked,o)};function u(){o.otp.duration=St(this.value),t(0,o)}function f(){o.otp.length=this.value,t(0,o)}return n.$$set=c=>{"collection"in c&&t(0,o=c.collection)},n.$$.update=()=>{n.$$.dirty&1&&z.isEmpty(o.otp)&&t(0,o.otp={enabled:!0,duration:300,length:8},o),n.$$.dirty&1&&t(2,i=(o==null?void 0:o.system)&&(o==null?void 0:o.name)==="_superusers"),n.$$.dirty&8&&t(1,l=!z.isEmpty(s==null?void 0:s.otp))},[o,l,i,s,r,a,u,f]}class K8 extends ye{constructor(e){super(),be(this,e,Y8,W8,_e,{collection:0})}}function bm(n){let e,t;return{c(){e=b("i"),p(e,"class",t="icon "+n[0].icon)},m(i,l){v(i,e,l)},p(i,l){l&1&&t!==(t="icon "+i[0].icon)&&p(e,"class",t)},d(i){i&&k(e)}}}function J8(n){let e,t,i=(n[0].label||n[0].name||n[0].title||n[0].id||n[0].value)+"",l,s=n[0].icon&&bm(n);return{c(){s&&s.c(),e=C(),t=b("span"),l=Y(i),p(t,"class","txt")},m(o,r){s&&s.m(o,r),v(o,e,r),v(o,t,r),w(t,l)},p(o,[r]){o[0].icon?s?s.p(o,r):(s=bm(o),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null),r&1&&i!==(i=(o[0].label||o[0].name||o[0].title||o[0].id||o[0].value)+"")&&ue(l,i)},i:te,o:te,d(o){o&&(k(e),k(t)),s&&s.d(o)}}}function Z8(n,e,t){let{item:i={}}=e;return n.$$set=l=>{"item"in l&&t(0,i=l.item)},[i]}class ym extends ye{constructor(e){super(),be(this,e,Z8,J8,_e,{item:0})}}const G8=n=>({}),km=n=>({});function X8(n){let e;const t=n[8].afterOptions,i=Lt(t,n,n[12],km);return{c(){i&&i.c()},m(l,s){i&&i.m(l,s),e=!0},p(l,s){i&&i.p&&(!e||s&4096)&&Pt(i,t,l,l[12],e?At(t,l[12],s,G8):Nt(l[12]),km)},i(l){e||(O(i,l),e=!0)},o(l){D(i,l),e=!1},d(l){i&&i.d(l)}}}function Q8(n){let e,t,i;const l=[{items:n[1]},{multiple:n[2]},{labelComponent:n[3]},{optionComponent:n[4]},n[5]];function s(r){n[9](r)}let o={$$slots:{afterOptions:[X8]},$$scope:{ctx:n}};for(let r=0;rve(e,"selected",s)),e.$on("show",n[10]),e.$on("hide",n[11]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&62?kt(l,[a&2&&{items:r[1]},a&4&&{multiple:r[2]},a&8&&{labelComponent:r[3]},a&16&&{optionComponent:r[4]},a&32&&Ft(r[5])]):{};a&4096&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.selected=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function x8(n,e,t){const i=["items","multiple","selected","labelComponent","optionComponent","selectionKey","keyOfSelected"];let l=lt(e,i),{$$slots:s={},$$scope:o}=e,{items:r=[]}=e,{multiple:a=!1}=e,{selected:u=a?[]:void 0}=e,{labelComponent:f=ym}=e,{optionComponent:c=ym}=e,{selectionKey:d="value"}=e,{keyOfSelected:m=a?[]:void 0}=e;function h(T){T=z.toArray(T,!0);let $=[];for(let E of T){const M=z.findByKey(r,d,E);M&&$.push(M)}T.length&&!$.length||t(0,u=a?$:$[0])}async function g(T){let $=z.toArray(T,!0).map(E=>E[d]);r.length&&t(6,m=a?$:$[0])}function _(T){u=T,t(0,u)}function y(T){Pe.call(this,n,T)}function S(T){Pe.call(this,n,T)}return n.$$set=T=>{e=je(je({},e),Ut(T)),t(5,l=lt(e,i)),"items"in T&&t(1,r=T.items),"multiple"in T&&t(2,a=T.multiple),"selected"in T&&t(0,u=T.selected),"labelComponent"in T&&t(3,f=T.labelComponent),"optionComponent"in T&&t(4,c=T.optionComponent),"selectionKey"in T&&t(7,d=T.selectionKey),"keyOfSelected"in T&&t(6,m=T.keyOfSelected),"$$scope"in T&&t(12,o=T.$$scope)},n.$$.update=()=>{n.$$.dirty&66&&r&&h(m),n.$$.dirty&1&&g(u)},[u,r,a,f,c,l,m,d,s,_,y,S,o]}class xn extends ye{constructor(e){super(),be(this,e,x8,Q8,_e,{items:1,multiple:2,selected:0,labelComponent:3,optionComponent:4,selectionKey:7,keyOfSelected:6})}}function vm(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-information-line link-hint")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,{text:"Superusers are required to have password auth enabled.",position:"right"})),t=!0)},d(l){l&&k(e),t=!1,i()}}}function eO(n){let e,t,i,l,s,o,r,a,u,f,c=n[3]&&vm();return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable"),r=C(),c&&c.c(),a=ge(),p(e,"type","checkbox"),p(e,"id",t=n[8]),e.disabled=n[3],p(l,"for",o=n[8])},m(d,m){v(d,e,m),e.checked=n[0].passwordAuth.enabled,v(d,i,m),v(d,l,m),w(l,s),v(d,r,m),c&&c.m(d,m),v(d,a,m),u||(f=B(e,"change",n[5]),u=!0)},p(d,m){m&256&&t!==(t=d[8])&&p(e,"id",t),m&8&&(e.disabled=d[3]),m&1&&(e.checked=d[0].passwordAuth.enabled),m&256&&o!==(o=d[8])&&p(l,"for",o),d[3]?c||(c=vm(),c.c(),c.m(a.parentNode,a)):c&&(c.d(1),c=null)},d(d){d&&(k(e),k(i),k(l),k(r),k(a)),c&&c.d(d),u=!1,f()}}}function tO(n){let e,t,i,l,s,o,r;function a(f){n[6](f)}let u={items:n[1],multiple:!0};return n[0].passwordAuth.identityFields!==void 0&&(u.keyOfSelected=n[0].passwordAuth.identityFields),s=new xn({props:u}),ie.push(()=>ve(s,"keyOfSelected",a)),{c(){e=b("label"),t=b("span"),t.textContent="Unique identity fields",l=C(),H(s.$$.fragment),p(t,"class","txt"),p(e,"for",i=n[8])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c&256&&i!==(i=f[8]))&&p(e,"for",i);const d={};c&2&&(d.items=f[1]),!o&&c&1&&(o=!0,d.keyOfSelected=f[0].passwordAuth.identityFields,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function nO(n){let e,t,i,l;return e=new fe({props:{class:"form-field form-field-toggle",name:"passwordAuth.enabled",$$slots:{default:[eO,({uniqueId:s})=>({8:s}),({uniqueId:s})=>s?256:0]},$$scope:{ctx:n}}}),i=new fe({props:{class:"form-field required m-0",name:"passwordAuth.identityFields",$$slots:{default:[tO,({uniqueId:s})=>({8:s}),({uniqueId:s})=>s?256:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o&777&&(r.$$scope={dirty:o,ctx:s}),e.$set(r);const a={};o&771&&(a.$$scope={dirty:o,ctx:s}),i.$set(a)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function iO(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function lO(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function wm(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function sO(n){let e,t,i,l,s,o;function r(c,d){return c[0].passwordAuth.enabled?lO:iO}let a=r(n),u=a(n),f=n[2]&&wm();return{c(){e=b("div"),e.innerHTML=' Identity/Password',t=C(),i=b("div"),l=C(),u.c(),s=C(),f&&f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),u.m(c,d),v(c,s,d),f&&f.m(c,d),v(c,o,d)},p(c,d){a!==(a=r(c))&&(u.d(1),u=a(c),u&&(u.c(),u.m(s.parentNode,s))),c[2]?f?d&4&&O(f,1):(f=wm(),f.c(),O(f,1),f.m(o.parentNode,o)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),u.d(c),f&&f.d(c)}}}function oO(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[sO],default:[nO]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&527&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function rO(n,e,t){let i,l,s;Qe(n,Sn,c=>t(4,s=c));let{collection:o}=e,r=[];function a(c){const d=[{value:"email"}],m=(c==null?void 0:c.fields)||[],h=(c==null?void 0:c.indexes)||[];for(let g of h){const _=z.parseIndex(g);if(!_.unique||_.columns.length!=1||_.columns[0].name=="email")continue;const y=m.find(S=>!S.hidden&&S.name.toLowerCase()==_.columns[0].name.toLowerCase());y&&d.push({value:y.name})}return d}Yt(()=>{t(1,r=a(o))});function u(){o.passwordAuth.enabled=this.checked,t(0,o)}function f(c){n.$$.not_equal(o.passwordAuth.identityFields,c)&&(o.passwordAuth.identityFields=c,t(0,o))}return n.$$set=c=>{"collection"in c&&t(0,o=c.collection)},n.$$.update=()=>{n.$$.dirty&1&&z.isEmpty(o.passwordAuth)&&t(0,o.passwordAuth={enabled:!0,identityFields:["email"]},o),n.$$.dirty&1&&t(3,i=(o==null?void 0:o.system)&&(o==null?void 0:o.name)==="_superusers"),n.$$.dirty&16&&t(2,l=!z.isEmpty(s==null?void 0:s.passwordAuth))},[o,r,l,i,s,u,f]}class aO extends ye{constructor(e){super(),be(this,e,rO,oO,_e,{collection:0})}}function Sm(n,e,t){const i=n.slice();return i[22]=e[t],i}function Tm(n,e){let t,i,l,s,o,r=e[22].label+"",a,u,f,c,d,m;return c=vk(e[11][0]),{key:n,first:null,c(){t=b("div"),i=b("input"),s=C(),o=b("label"),a=Y(r),f=C(),p(i,"type","radio"),p(i,"name","template"),p(i,"id",l=e[21]+e[22].value),i.__value=e[22].value,ce(i,i.__value),p(o,"for",u=e[21]+e[22].value),p(t,"class","form-field-block"),c.p(i),this.first=t},m(h,g){v(h,t,g),w(t,i),i.checked=i.__value===e[2],w(t,s),w(t,o),w(o,a),w(t,f),d||(m=B(i,"change",e[10]),d=!0)},p(h,g){e=h,g&2097152&&l!==(l=e[21]+e[22].value)&&p(i,"id",l),g&4&&(i.checked=i.__value===e[2]),g&2097152&&u!==(u=e[21]+e[22].value)&&p(o,"for",u)},d(h){h&&k(t),c.r(),d=!1,m()}}}function uO(n){let e=[],t=new Map,i,l=pe(n[7]);const s=o=>o[22].value;for(let o=0;o({21:a}),({uniqueId:a})=>a?2097152:0]},$$scope:{ctx:n}}}),l=new fe({props:{class:"form-field required m-0",name:"email",$$slots:{default:[fO,({uniqueId:a})=>({21:a}),({uniqueId:a})=>a?2097152:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),H(t.$$.fragment),i=C(),H(l.$$.fragment),p(e,"id",n[6]),p(e,"autocomplete","off")},m(a,u){v(a,e,u),F(t,e,null),w(e,i),F(l,e,null),s=!0,o||(r=B(e,"submit",tt(n[13])),o=!0)},p(a,u){const f={};u&35651588&&(f.$$scope={dirty:u,ctx:a}),t.$set(f);const c={};u&35651586&&(c.$$scope={dirty:u,ctx:a}),l.$set(c)},i(a){s||(O(t.$$.fragment,a),O(l.$$.fragment,a),s=!0)},o(a){D(t.$$.fragment,a),D(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(t),q(l),o=!1,r()}}}function dO(n){let e;return{c(){e=b("h4"),e.textContent="Send test email",p(e,"class","center txt-break")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function pO(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("button"),t=Y("Close"),i=C(),l=b("button"),s=b("i"),o=C(),r=b("span"),r.textContent="Send",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[4],p(s,"class","ri-mail-send-line"),p(r,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=a=!n[5]||n[4],x(l,"btn-loading",n[4])},m(c,d){v(c,e,d),w(e,t),v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=B(e,"click",n[0]),u=!0)},p(c,d){d&16&&(e.disabled=c[4]),d&48&&a!==(a=!c[5]||c[4])&&(l.disabled=a),d&16&&x(l,"btn-loading",c[4])},d(c){c&&(k(e),k(i),k(l)),u=!1,f()}}}function mO(n){let e,t,i={class:"overlay-panel-sm email-test-popup",overlayClose:!n[4],escClose:!n[4],beforeHide:n[14],popup:!0,$$slots:{footer:[pO],header:[dO],default:[cO]},$$scope:{ctx:n}};return e=new ln({props:i}),n[15](e),e.$on("show",n[16]),e.$on("hide",n[17]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.overlayClose=!l[4]),s&16&&(o.escClose=!l[4]),s&16&&(o.beforeHide=l[14]),s&33554486&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[15](null),q(e,l)}}}const wa="last_email_test",$m="email_test_request";function hO(n,e,t){let i;const l=_t(),s="email_test_"+z.randomString(5),o=[{label:"Verification",value:"verification"},{label:"Password reset",value:"password-reset"},{label:"Confirm email change",value:"email-change"},{label:"OTP",value:"otp"},{label:"Login alert",value:"login-alert"}];let r,a="",u=localStorage.getItem(wa),f=o[0].value,c=!1,d=null;function m(I="",A="",P=""){a=I||"_superusers",t(1,u=A||localStorage.getItem(wa)),t(2,f=P||o[0].value),Wt({}),r==null||r.show()}function h(){return clearTimeout(d),r==null?void 0:r.hide()}async function g(){if(!(!i||c)){t(4,c=!0),localStorage==null||localStorage.setItem(wa,u),clearTimeout(d),d=setTimeout(()=>{me.cancelRequest($m),$i("Test email send timeout.")},3e4);try{await me.settings.testEmail(a,u,f,{$cancelKey:$m}),tn("Successfully sent test email."),l("submit"),t(4,c=!1),await fn(),h()}catch(I){t(4,c=!1),me.error(I)}clearTimeout(d)}}const _=[[]];function y(){f=this.__value,t(2,f)}function S(){u=this.value,t(1,u)}const T=()=>g(),$=()=>!c;function E(I){ie[I?"unshift":"push"](()=>{r=I,t(3,r)})}function M(I){Pe.call(this,n,I)}function L(I){Pe.call(this,n,I)}return n.$$.update=()=>{n.$$.dirty&6&&t(5,i=!!u&&!!f)},[h,u,f,r,c,i,s,o,g,m,y,_,S,T,$,E,M,L]}class tk extends ye{constructor(e){super(),be(this,e,hO,mO,_e,{show:9,hide:0})}get show(){return this.$$.ctx[9]}get hide(){return this.$$.ctx[0]}}function Cm(n,e,t){const i=n.slice();return i[18]=e[t],i[19]=e,i[20]=t,i}function _O(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Send email alert for new logins"),p(e,"type","checkbox"),p(e,"id",t=n[21]),p(l,"for",o=n[21])},m(u,f){v(u,e,f),e.checked=n[0].authAlert.enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[9]),r=!0)},p(u,f){f&2097152&&t!==(t=u[21])&&p(e,"id",t),f&1&&(e.checked=u[0].authAlert.enabled),f&2097152&&o!==(o=u[21])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function Om(n){let e,t,i;function l(o){n[12](o)}let s={};return n[0]!==void 0&&(s.collection=n[0]),e=new F8({props:s}),ie.push(()=>ve(e,"collection",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};!t&&r&1&&(t=!0,a.collection=o[0],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function Em(n,e){var a;let t,i,l,s;function o(u){e[15](u,e[18])}let r={single:!0,key:e[18].key,title:e[18].label,placeholders:(a=e[18])==null?void 0:a.placeholders};return e[18].config!==void 0&&(r.config=e[18].config),i=new F5({props:r}),ie.push(()=>ve(i,"config",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(u,f){v(u,t,f),F(i,u,f),s=!0},p(u,f){var d;e=u;const c={};f&4&&(c.key=e[18].key),f&4&&(c.title=e[18].label),f&4&&(c.placeholders=(d=e[18])==null?void 0:d.placeholders),!l&&f&4&&(l=!0,c.config=e[18].config,$e(()=>l=!1)),i.$set(c)},i(u){s||(O(i.$$.fragment,u),s=!0)},o(u){D(i.$$.fragment,u),s=!1},d(u){u&&k(t),q(i,u)}}}function gO(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P=[],R=new Map,N,U,j,V,K,J,ee,X,oe,Se,ke;o=new fe({props:{class:"form-field form-field-sm form-field-toggle m-0",name:"authAlert.enabled",inlineError:!0,$$slots:{default:[_O,({uniqueId:Le})=>({21:Le}),({uniqueId:Le})=>Le?2097152:0]},$$scope:{ctx:n}}});function Ce(Le){n[10](Le)}let We={};n[0]!==void 0&&(We.collection=n[0]),u=new K8({props:We}),ie.push(()=>ve(u,"collection",Ce));function st(Le){n[11](Le)}let et={};n[0]!==void 0&&(et.collection=n[0]),d=new aO({props:et}),ie.push(()=>ve(d,"collection",st));let Be=!n[1]&&Om(n);function rt(Le){n[13](Le)}let Je={};n[0]!==void 0&&(Je.collection=n[0]),_=new u6({props:Je}),ie.push(()=>ve(_,"collection",rt));let at=pe(n[2]);const Ht=Le=>Le[18].key;for(let Le=0;Leve(K,"collection",Te));let ot={};return X=new tk({props:ot}),n[17](X),{c(){e=b("h4"),t=b("div"),i=b("span"),i.textContent="Auth methods",l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),H(u.$$.fragment),c=C(),H(d.$$.fragment),h=C(),Be&&Be.c(),g=C(),H(_.$$.fragment),S=C(),T=b("h4"),$=b("span"),$.textContent="Mail templates",E=C(),M=b("button"),M.textContent="Send test email",L=C(),I=b("div"),A=b("div");for(let Le=0;Lef=!1)),u.$set(Oe);const ut={};!m&&Ve&1&&(m=!0,ut.collection=Le[0],$e(()=>m=!1)),d.$set(ut),Le[1]?Be&&(re(),D(Be,1,1,()=>{Be=null}),ae()):Be?(Be.p(Le,Ve),Ve&2&&O(Be,1)):(Be=Om(Le),Be.c(),O(Be,1),Be.m(a,g));const Ne={};!y&&Ve&1&&(y=!0,Ne.collection=Le[0],$e(()=>y=!1)),_.$set(Ne),Ve&4&&(at=pe(Le[2]),re(),P=yt(P,Ve,Ht,1,Le,at,R,A,zt,Em,null,Cm),ae());const xe={};!J&&Ve&1&&(J=!0,xe.collection=Le[0],$e(()=>J=!1)),K.$set(xe);const qt={};X.$set(qt)},i(Le){if(!oe){O(o.$$.fragment,Le),O(u.$$.fragment,Le),O(d.$$.fragment,Le),O(Be),O(_.$$.fragment,Le);for(let Ve=0;Vec==null?void 0:c.show(u.id);function S(E,M){n.$$.not_equal(M.config,E)&&(M.config=E,t(2,f),t(1,i),t(7,l),t(5,r),t(4,a),t(8,s),t(6,o),t(0,u))}function T(E){u=E,t(0,u)}function $(E){ie[E?"unshift":"push"](()=>{c=E,t(3,c)})}return n.$$set=E=>{"collection"in E&&t(0,u=E.collection)},n.$$.update=()=>{var E,M;n.$$.dirty&1&&typeof((E=u.otp)==null?void 0:E.emailTemplate)>"u"&&(t(0,u.otp=u.otp||{},u),t(0,u.otp.emailTemplate={},u)),n.$$.dirty&1&&typeof((M=u.authAlert)==null?void 0:M.emailTemplate)>"u"&&(t(0,u.authAlert=u.authAlert||{},u),t(0,u.authAlert.emailTemplate={},u)),n.$$.dirty&1&&t(1,i=u.system&&u.name==="_superusers"),n.$$.dirty&1&&t(7,l={key:"resetPasswordTemplate",label:"Default Password reset email template",placeholders:["APP_NAME","APP_URL","RECORD:*","TOKEN"],config:u.resetPasswordTemplate}),n.$$.dirty&1&&t(8,s={key:"verificationTemplate",label:"Default Verification email template",placeholders:["APP_NAME","APP_URL","RECORD:*","TOKEN"],config:u.verificationTemplate}),n.$$.dirty&1&&t(6,o={key:"confirmEmailChangeTemplate",label:"Default Confirm email change email template",placeholders:["APP_NAME","APP_URL","RECORD:*","TOKEN"],config:u.confirmEmailChangeTemplate}),n.$$.dirty&1&&t(5,r={key:"otp.emailTemplate",label:"Default OTP email template",placeholders:["APP_NAME","APP_URL","RECORD:*","OTP","OTP_ID"],config:u.otp.emailTemplate}),n.$$.dirty&1&&t(4,a={key:"authAlert.emailTemplate",label:"Default Login alert email template",placeholders:["APP_NAME","APP_URL","RECORD:*"],config:u.authAlert.emailTemplate}),n.$$.dirty&498&&t(2,f=i?[l,r,a]:[s,l,o,r,a])},[u,i,f,c,a,r,o,l,s,d,m,h,g,_,y,S,T,$]}class yO extends ye{constructor(e){super(),be(this,e,bO,gO,_e,{collection:0})}}const kO=n=>({dragging:n&4,dragover:n&8}),Mm=n=>({dragging:n[2],dragover:n[3]});function vO(n){let e,t,i,l,s;const o=n[10].default,r=Lt(o,n,n[9],Mm);return{c(){e=b("div"),r&&r.c(),p(e,"draggable",t=!n[1]),p(e,"class","draggable svelte-19c69j7"),x(e,"dragging",n[2]),x(e,"dragover",n[3])},m(a,u){v(a,e,u),r&&r.m(e,null),i=!0,l||(s=[B(e,"dragover",tt(n[11])),B(e,"dragleave",tt(n[12])),B(e,"dragend",n[13]),B(e,"dragstart",n[14]),B(e,"drop",n[15])],l=!0)},p(a,[u]){r&&r.p&&(!i||u&524)&&Pt(r,o,a,a[9],i?At(o,a[9],u,kO):Nt(a[9]),Mm),(!i||u&2&&t!==(t=!a[1]))&&p(e,"draggable",t),(!i||u&4)&&x(e,"dragging",a[2]),(!i||u&8)&&x(e,"dragover",a[3])},i(a){i||(O(r,a),i=!0)},o(a){D(r,a),i=!1},d(a){a&&k(e),r&&r.d(a),l=!1,De(s)}}}function wO(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=_t();let{index:o}=e,{list:r=[]}=e,{group:a="default"}=e,{disabled:u=!1}=e,{dragHandleClass:f=""}=e,c=!1,d=!1;function m($,E){if(!(!$||u)){if(f&&!$.target.classList.contains(f)){t(3,d=!1),t(2,c=!1),$.preventDefault();return}t(2,c=!0),$.dataTransfer.effectAllowed="move",$.dataTransfer.dropEffect="move",$.dataTransfer.setData("text/plain",JSON.stringify({index:E,group:a})),s("drag",$)}}function h($,E){if(t(3,d=!1),t(2,c=!1),!$||u)return;$.dataTransfer.dropEffect="move";let M={};try{M=JSON.parse($.dataTransfer.getData("text/plain"))}catch{}if(M.group!=a)return;const L=M.index<<0;L{t(3,d=!0)},_=()=>{t(3,d=!1)},y=()=>{t(3,d=!1),t(2,c=!1)},S=$=>m($,o),T=$=>h($,o);return n.$$set=$=>{"index"in $&&t(0,o=$.index),"list"in $&&t(6,r=$.list),"group"in $&&t(7,a=$.group),"disabled"in $&&t(1,u=$.disabled),"dragHandleClass"in $&&t(8,f=$.dragHandleClass),"$$scope"in $&&t(9,l=$.$$scope)},[o,u,c,d,m,h,r,a,f,l,i,g,_,y,S,T]}class ho extends ye{constructor(e){super(),be(this,e,wO,vO,_e,{index:0,list:6,group:7,disabled:1,dragHandleClass:8})}}function Dm(n,e,t){const i=n.slice();return i[27]=e[t],i}function SO(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("input"),l=C(),s=b("label"),o=Y("Unique"),p(e,"type","checkbox"),p(e,"id",t=n[30]),e.checked=i=n[3].unique,p(s,"for",r=n[30])},m(f,c){v(f,e,c),v(f,l,c),v(f,s,c),w(s,o),a||(u=B(e,"change",n[19]),a=!0)},p(f,c){c[0]&1073741824&&t!==(t=f[30])&&p(e,"id",t),c[0]&8&&i!==(i=f[3].unique)&&(e.checked=i),c[0]&1073741824&&r!==(r=f[30])&&p(s,"for",r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function TO(n){let e,t,i,l;function s(a){n[20](a)}var o=n[7];function r(a,u){var c;let f={id:a[30],placeholder:`eg. CREATE INDEX idx_test on ${(c=a[0])==null?void 0:c.name} (created)`,language:"sql-create-index",minHeight:"85"};return a[2]!==void 0&&(f.value=a[2]),{props:f}}return o&&(e=jt(o,r(n)),ie.push(()=>ve(e,"value",s))),{c(){e&&H(e.$$.fragment),i=ge()},m(a,u){e&&F(e,a,u),v(a,i,u),l=!0},p(a,u){var f;if(u[0]&128&&o!==(o=a[7])){if(e){re();const c=e;D(c.$$.fragment,1,0,()=>{q(c,1)}),ae()}o?(e=jt(o,r(a)),ie.push(()=>ve(e,"value",s)),H(e.$$.fragment),O(e.$$.fragment,1),F(e,i.parentNode,i)):e=null}else if(o){const c={};u[0]&1073741824&&(c.id=a[30]),u[0]&1&&(c.placeholder=`eg. CREATE INDEX idx_test on ${(f=a[0])==null?void 0:f.name} (created)`),!t&&u[0]&4&&(t=!0,c.value=a[2],$e(()=>t=!1)),e.$set(c)}},i(a){l||(e&&O(e.$$.fragment,a),l=!0)},o(a){e&&D(e.$$.fragment,a),l=!1},d(a){a&&k(i),e&&q(e,a)}}}function $O(n){let e;return{c(){e=b("textarea"),e.disabled=!0,p(e,"rows","7"),p(e,"placeholder","Loading...")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function CO(n){let e,t,i,l;const s=[$O,TO],o=[];function r(a,u){return a[8]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function Im(n){let e,t,i,l=pe(n[10]),s=[];for(let o=0;o({30:a}),({uniqueId:a})=>[a?1073741824:0]]},$$scope:{ctx:n}}}),i=new fe({props:{class:"form-field required m-b-sm",name:`indexes.${n[6]||""}`,$$slots:{default:[CO,({uniqueId:a})=>({30:a}),({uniqueId:a})=>[a?1073741824:0]]},$$scope:{ctx:n}}});let r=n[10].length>0&&Im(n);return{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),l=C(),r&&r.c(),s=ge()},m(a,u){F(e,a,u),v(a,t,u),F(i,a,u),v(a,l,u),r&&r.m(a,u),v(a,s,u),o=!0},p(a,u){const f={};u[0]&1073741837|u[1]&1&&(f.$$scope={dirty:u,ctx:a}),e.$set(f);const c={};u[0]&64&&(c.name=`indexes.${a[6]||""}`),u[0]&1073742213|u[1]&1&&(c.$$scope={dirty:u,ctx:a}),i.$set(c),a[10].length>0?r?r.p(a,u):(r=Im(a),r.c(),r.m(s.parentNode,s)):r&&(r.d(1),r=null)},i(a){o||(O(e.$$.fragment,a),O(i.$$.fragment,a),o=!0)},o(a){D(e.$$.fragment,a),D(i.$$.fragment,a),o=!1},d(a){a&&(k(t),k(l),k(s)),q(e,a),q(i,a),r&&r.d(a)}}}function EO(n){let e,t=n[5]?"Update":"Create",i,l;return{c(){e=b("h4"),i=Y(t),l=Y(" index")},m(s,o){v(s,e,o),w(e,i),w(e,l)},p(s,o){o[0]&32&&t!==(t=s[5]?"Update":"Create")&&ue(i,t)},d(s){s&&k(e)}}}function Am(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-hint btn-transparent m-r-auto")},m(l,s){v(l,e,s),t||(i=[Me(He.call(null,e,{text:"Delete",position:"top"})),B(e,"click",n[16])],t=!0)},p:te,d(l){l&&k(e),t=!1,De(i)}}}function MO(n){let e,t,i,l,s,o,r=n[5]!=""&&Am(n);return{c(){r&&r.c(),e=C(),t=b("button"),t.innerHTML='Cancel',i=C(),l=b("button"),l.innerHTML='Set index',p(t,"type","button"),p(t,"class","btn btn-transparent"),p(l,"type","button"),p(l,"class","btn"),x(l,"btn-disabled",n[9].length<=0)},m(a,u){r&&r.m(a,u),v(a,e,u),v(a,t,u),v(a,i,u),v(a,l,u),s||(o=[B(t,"click",n[17]),B(l,"click",n[18])],s=!0)},p(a,u){a[5]!=""?r?r.p(a,u):(r=Am(a),r.c(),r.m(e.parentNode,e)):r&&(r.d(1),r=null),u[0]&512&&x(l,"btn-disabled",a[9].length<=0)},d(a){a&&(k(e),k(t),k(i),k(l)),r&&r.d(a),s=!1,De(o)}}}function DO(n){let e,t;const i=[{popup:!0},n[14]];let l={$$slots:{footer:[MO],header:[EO],default:[OO]},$$scope:{ctx:n}};for(let s=0;see.name==V);J?z.removeByValue(K.columns,J):z.pushUnique(K.columns,{name:V}),t(2,d=z.buildIndex(K))}Yt(async()=>{t(8,g=!0);try{t(7,h=(await Ot(async()=>{const{default:V}=await import("./CodeEditor-CPgcqnd5.js");return{default:V}},__vite__mapDeps([12,1]),import.meta.url)).default)}catch(V){console.warn(V)}t(8,g=!1)});const M=()=>T(),L=()=>y(),I=()=>$(),A=V=>{t(3,l.unique=V.target.checked,l),t(3,l.tableName=l.tableName||(u==null?void 0:u.name),l),t(2,d=z.buildIndex(l))};function P(V){d=V,t(2,d)}const R=V=>E(V);function N(V){ie[V?"unshift":"push"](()=>{f=V,t(4,f)})}function U(V){Pe.call(this,n,V)}function j(V){Pe.call(this,n,V)}return n.$$set=V=>{e=je(je({},e),Ut(V)),t(14,r=lt(e,o)),"collection"in V&&t(0,u=V.collection)},n.$$.update=()=>{var V,K,J;n.$$.dirty[0]&1&&t(10,i=((K=(V=u==null?void 0:u.fields)==null?void 0:V.filter(ee=>!ee.toDelete&&ee.name!="id"))==null?void 0:K.map(ee=>ee.name))||[]),n.$$.dirty[0]&4&&t(3,l=z.parseIndex(d)),n.$$.dirty[0]&8&&t(9,s=((J=l.columns)==null?void 0:J.map(ee=>ee.name))||[])},[u,y,d,l,f,c,m,h,g,s,i,T,$,E,r,_,M,L,I,A,P,R,N,U,j]}class LO extends ye{constructor(e){super(),be(this,e,IO,DO,_e,{collection:0,show:15,hide:1},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[1]}}function Pm(n,e,t){const i=n.slice();i[10]=e[t],i[13]=t;const l=z.parseIndex(i[10]);return i[11]=l,i}function Nm(n){let e;return{c(){e=b("strong"),e.textContent="Unique:"},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function Rm(n){var d;let e,t,i,l=((d=n[11].columns)==null?void 0:d.map(Fm).join(", "))+"",s,o,r,a,u,f=n[11].unique&&Nm();function c(){return n[4](n[10],n[13])}return{c(){var m,h;e=b("button"),f&&f.c(),t=C(),i=b("span"),s=Y(l),p(i,"class","txt"),p(e,"type","button"),p(e,"class",o="label link-primary "+((h=(m=n[2].indexes)==null?void 0:m[n[13]])!=null&&h.message?"label-danger":"")+" svelte-167lbwu")},m(m,h){var g,_;v(m,e,h),f&&f.m(e,null),w(e,t),w(e,i),w(i,s),a||(u=[Me(r=He.call(null,e,((_=(g=n[2].indexes)==null?void 0:g[n[13]])==null?void 0:_.message)||"")),B(e,"click",c)],a=!0)},p(m,h){var g,_,y,S,T;n=m,n[11].unique?f||(f=Nm(),f.c(),f.m(e,t)):f&&(f.d(1),f=null),h&1&&l!==(l=((g=n[11].columns)==null?void 0:g.map(Fm).join(", "))+"")&&ue(s,l),h&4&&o!==(o="label link-primary "+((y=(_=n[2].indexes)==null?void 0:_[n[13]])!=null&&y.message?"label-danger":"")+" svelte-167lbwu")&&p(e,"class",o),r&&Rt(r.update)&&h&4&&r.update.call(null,((T=(S=n[2].indexes)==null?void 0:S[n[13]])==null?void 0:T.message)||"")},d(m){m&&k(e),f&&f.d(),a=!1,De(u)}}}function AO(n){var $,E,M;let e,t,i=(((E=($=n[0])==null?void 0:$.indexes)==null?void 0:E.length)||0)+"",l,s,o,r,a,u,f,c,d,m,h,g,_=pe(((M=n[0])==null?void 0:M.indexes)||[]),y=[];for(let L=0;L<_.length;L+=1)y[L]=Rm(Pm(n,_,L));function S(L){n[7](L)}let T={};return n[0]!==void 0&&(T.collection=n[0]),c=new LO({props:T}),n[6](c),ie.push(()=>ve(c,"collection",S)),c.$on("remove",n[8]),c.$on("submit",n[9]),{c(){e=b("div"),t=Y("Unique constraints and indexes ("),l=Y(i),s=Y(")"),o=C(),r=b("div");for(let L=0;L+ New index',f=C(),H(c.$$.fragment),p(e,"class","section-title"),p(u,"type","button"),p(u,"class","btn btn-xs btn-transparent btn-pill btn-outline"),p(r,"class","indexes-list svelte-167lbwu")},m(L,I){v(L,e,I),w(e,t),w(e,l),w(e,s),v(L,o,I),v(L,r,I);for(let A=0;Ad=!1)),c.$set(A)},i(L){m||(O(c.$$.fragment,L),m=!0)},o(L){D(c.$$.fragment,L),m=!1},d(L){L&&(k(e),k(o),k(r),k(f)),pt(y,L),n[6](null),q(c,L),h=!1,g()}}}const Fm=n=>n.name;function PO(n,e,t){let i;Qe(n,Sn,m=>t(2,i=m));let{collection:l}=e,s;function o(m,h){for(let g=0;gs==null?void 0:s.show(m,h),a=()=>s==null?void 0:s.show();function u(m){ie[m?"unshift":"push"](()=>{s=m,t(1,s)})}function f(m){l=m,t(0,l)}const c=m=>{for(let h=0;h{o(m.detail.old,m.detail.new)};return n.$$set=m=>{"collection"in m&&t(0,l=m.collection)},[l,s,i,o,r,a,u,f,c,d]}class NO extends ye{constructor(e){super(),be(this,e,PO,AO,_e,{collection:0})}}function qm(n,e,t){const i=n.slice();return i[5]=e[t],i}function Hm(n){let e,t,i,l,s,o,r;function a(){return n[3](n[5])}return{c(){e=b("button"),t=b("i"),i=C(),l=b("span"),l.textContent=`${n[5].label}`,s=C(),p(t,"class","icon "+n[5].icon+" svelte-1gz9b6p"),p(t,"aria-hidden","true"),p(l,"class","txt"),p(e,"type","button"),p(e,"role","menuitem"),p(e,"class","dropdown-item svelte-1gz9b6p")},m(u,f){v(u,e,f),w(e,t),w(e,i),w(e,l),w(e,s),o||(r=B(e,"click",a),o=!0)},p(u,f){n=u},d(u){u&&k(e),o=!1,r()}}}function RO(n){let e,t=pe(n[1]),i=[];for(let l=0;lo(a.value);return n.$$set=a=>{"class"in a&&t(0,i=a.class)},[i,s,o,r]}class HO extends ye{constructor(e){super(),be(this,e,qO,FO,_e,{class:0})}}const jO=n=>({interactive:n[0]&128,hasErrors:n[0]&64}),jm=n=>({interactive:n[7],hasErrors:n[6]}),zO=n=>({interactive:n[0]&128,hasErrors:n[0]&64}),zm=n=>({interactive:n[7],hasErrors:n[6]}),UO=n=>({interactive:n[0]&128,hasErrors:n[0]&64}),Um=n=>({interactive:n[7],hasErrors:n[6]});function Vm(n){let e;return{c(){e=b("div"),e.innerHTML='',p(e,"class","drag-handle-wrapper"),p(e,"draggable",!0),p(e,"aria-label","Sort")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function Bm(n){let e,t;return{c(){e=b("span"),t=Y(n[5]),p(e,"class","label label-success")},m(i,l){v(i,e,l),w(e,t)},p(i,l){l[0]&32&&ue(t,i[5])},d(i){i&&k(e)}}}function Wm(n){let e;return{c(){e=b("span"),e.textContent="Hidden",p(e,"class","label label-danger")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function VO(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h=n[0].required&&Bm(n),g=n[0].hidden&&Wm();return{c(){e=b("div"),h&&h.c(),t=C(),g&&g.c(),i=C(),l=b("div"),s=b("i"),a=C(),u=b("input"),p(e,"class","field-labels"),p(s,"class",o=z.getFieldTypeIcon(n[0].type)),p(l,"class","form-field-addon prefix field-type-icon"),x(l,"txt-disabled",!n[7]||n[0].system),p(u,"type","text"),u.required=!0,u.disabled=f=!n[7]||n[0].system,p(u,"spellcheck","false"),p(u,"placeholder","Field name"),u.value=c=n[0].name,p(u,"title","System field")},m(_,y){v(_,e,y),h&&h.m(e,null),w(e,t),g&&g.m(e,null),v(_,i,y),v(_,l,y),w(l,s),v(_,a,y),v(_,u,y),n[22](u),d||(m=[Me(r=He.call(null,l,n[0].type+(n[0].system?" (system)":""))),B(l,"click",n[21]),B(u,"input",n[23])],d=!0)},p(_,y){_[0].required?h?h.p(_,y):(h=Bm(_),h.c(),h.m(e,t)):h&&(h.d(1),h=null),_[0].hidden?g||(g=Wm(),g.c(),g.m(e,null)):g&&(g.d(1),g=null),y[0]&1&&o!==(o=z.getFieldTypeIcon(_[0].type))&&p(s,"class",o),r&&Rt(r.update)&&y[0]&1&&r.update.call(null,_[0].type+(_[0].system?" (system)":"")),y[0]&129&&x(l,"txt-disabled",!_[7]||_[0].system),y[0]&129&&f!==(f=!_[7]||_[0].system)&&(u.disabled=f),y[0]&1&&c!==(c=_[0].name)&&u.value!==c&&(u.value=c)},d(_){_&&(k(e),k(i),k(l),k(a),k(u)),h&&h.d(),g&&g.d(),n[22](null),d=!1,De(m)}}}function BO(n){let e;return{c(){e=b("span"),p(e,"class","separator")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function WO(n){let e,t,i,l,s,o;return{c(){e=b("button"),t=b("i"),p(t,"class","ri-settings-3-line"),p(e,"type","button"),p(e,"aria-label",i="Toggle "+n[0].name+" field options"),p(e,"class",l="btn btn-sm btn-circle options-trigger "+(n[4]?"btn-secondary":"btn-transparent")),p(e,"aria-expanded",n[4]),x(e,"btn-hint",!n[4]&&!n[6]),x(e,"btn-danger",n[6])},m(r,a){v(r,e,a),w(e,t),s||(o=B(e,"click",n[17]),s=!0)},p(r,a){a[0]&1&&i!==(i="Toggle "+r[0].name+" field options")&&p(e,"aria-label",i),a[0]&16&&l!==(l="btn btn-sm btn-circle options-trigger "+(r[4]?"btn-secondary":"btn-transparent"))&&p(e,"class",l),a[0]&16&&p(e,"aria-expanded",r[4]),a[0]&80&&x(e,"btn-hint",!r[4]&&!r[6]),a[0]&80&&x(e,"btn-danger",r[6])},d(r){r&&k(e),s=!1,o()}}}function YO(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-success btn-transparent options-trigger"),p(e,"aria-label","Restore")},m(l,s){v(l,e,s),t||(i=[Me(He.call(null,e,"Restore")),B(e,"click",n[14])],t=!0)},p:te,d(l){l&&k(e),t=!1,De(i)}}}function Ym(n){let e,t,i,l,s=!n[0].primaryKey&&n[0].type!="autodate"&&(!n[8]||!n[10].includes(n[0].name)),o,r=!n[0].primaryKey&&(!n[8]||!n[11].includes(n[0].name)),a,u=!n[8]||!n[12].includes(n[0].name),f,c,d,m;const h=n[20].options,g=Lt(h,n,n[28],zm);let _=s&&Km(n),y=r&&Jm(n),S=u&&Zm(n);const T=n[20].optionsFooter,$=Lt(T,n,n[28],jm);let E=!n[0]._toDelete&&!n[0].primaryKey&&Gm(n);return{c(){e=b("div"),t=b("div"),g&&g.c(),i=C(),l=b("div"),_&&_.c(),o=C(),y&&y.c(),a=C(),S&&S.c(),f=C(),$&&$.c(),c=C(),E&&E.c(),p(t,"class","hidden-empty m-b-sm"),p(l,"class","schema-field-options-footer"),p(e,"class","schema-field-options")},m(M,L){v(M,e,L),w(e,t),g&&g.m(t,null),w(e,i),w(e,l),_&&_.m(l,null),w(l,o),y&&y.m(l,null),w(l,a),S&&S.m(l,null),w(l,f),$&&$.m(l,null),w(l,c),E&&E.m(l,null),m=!0},p(M,L){g&&g.p&&(!m||L[0]&268435648)&&Pt(g,h,M,M[28],m?At(h,M[28],L,zO):Nt(M[28]),zm),L[0]&257&&(s=!M[0].primaryKey&&M[0].type!="autodate"&&(!M[8]||!M[10].includes(M[0].name))),s?_?(_.p(M,L),L[0]&257&&O(_,1)):(_=Km(M),_.c(),O(_,1),_.m(l,o)):_&&(re(),D(_,1,1,()=>{_=null}),ae()),L[0]&257&&(r=!M[0].primaryKey&&(!M[8]||!M[11].includes(M[0].name))),r?y?(y.p(M,L),L[0]&257&&O(y,1)):(y=Jm(M),y.c(),O(y,1),y.m(l,a)):y&&(re(),D(y,1,1,()=>{y=null}),ae()),L[0]&257&&(u=!M[8]||!M[12].includes(M[0].name)),u?S?(S.p(M,L),L[0]&257&&O(S,1)):(S=Zm(M),S.c(),O(S,1),S.m(l,f)):S&&(re(),D(S,1,1,()=>{S=null}),ae()),$&&$.p&&(!m||L[0]&268435648)&&Pt($,T,M,M[28],m?At(T,M[28],L,jO):Nt(M[28]),jm),!M[0]._toDelete&&!M[0].primaryKey?E?(E.p(M,L),L[0]&1&&O(E,1)):(E=Gm(M),E.c(),O(E,1),E.m(l,null)):E&&(re(),D(E,1,1,()=>{E=null}),ae())},i(M){m||(O(g,M),O(_),O(y),O(S),O($,M),O(E),M&&nt(()=>{m&&(d||(d=ze(e,vt,{delay:10,duration:150},!0)),d.run(1))}),m=!0)},o(M){D(g,M),D(_),D(y),D(S),D($,M),D(E),M&&(d||(d=ze(e,vt,{delay:10,duration:150},!1)),d.run(0)),m=!1},d(M){M&&k(e),g&&g.d(M),_&&_.d(),y&&y.d(),S&&S.d(),$&&$.d(M),E&&E.d(),M&&d&&d.end()}}}function Km(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle",name:"requried",$$slots:{default:[KO,({uniqueId:i})=>({34:i}),({uniqueId:i})=>[0,i?8:0]]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&268435489|l[1]&8&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function KO(n){let e,t,i,l,s,o,r,a,u,f,c,d;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),o=Y(n[5]),r=C(),a=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[34]),p(s,"class","txt"),p(a,"class","ri-information-line link-hint"),p(l,"for",f=n[34])},m(m,h){v(m,e,h),e.checked=n[0].required,v(m,i,h),v(m,l,h),w(l,s),w(s,o),w(l,r),w(l,a),c||(d=[B(e,"change",n[24]),Me(u=He.call(null,a,{text:`Requires the field value NOT to be ${z.zeroDefaultStr(n[0])}.`}))],c=!0)},p(m,h){h[1]&8&&t!==(t=m[34])&&p(e,"id",t),h[0]&1&&(e.checked=m[0].required),h[0]&32&&ue(o,m[5]),u&&Rt(u.update)&&h[0]&1&&u.update.call(null,{text:`Requires the field value NOT to be ${z.zeroDefaultStr(m[0])}.`}),h[1]&8&&f!==(f=m[34])&&p(l,"for",f)},d(m){m&&(k(e),k(i),k(l)),c=!1,De(d)}}}function Jm(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle",name:"hidden",$$slots:{default:[JO,({uniqueId:i})=>({34:i}),({uniqueId:i})=>[0,i?8:0]]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&268435457|l[1]&8&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function JO(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Hidden",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[34]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[34])},m(c,d){v(c,e,d),e.checked=n[0].hidden,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[25]),B(e,"change",n[26]),Me(He.call(null,r,{text:"Hide from the JSON API response and filters."}))],u=!0)},p(c,d){d[1]&8&&t!==(t=c[34])&&p(e,"id",t),d[0]&1&&(e.checked=c[0].hidden),d[1]&8&&a!==(a=c[34])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function Zm(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle m-0",name:"presentable",$$slots:{default:[ZO,({uniqueId:i})=>({34:i}),({uniqueId:i})=>[0,i?8:0]]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&268435457|l[1]&8&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function ZO(n){let e,t,i,l,s,o,r,a,u,f,c,d;return{c(){e=b("input"),l=C(),s=b("label"),o=b("span"),o.textContent="Presentable",r=C(),a=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[34]),e.disabled=i=n[0].hidden,p(o,"class","txt"),p(a,"class",u="ri-information-line "+(n[0].hidden?"txt-disabled":"link-hint")),p(s,"for",f=n[34])},m(m,h){v(m,e,h),e.checked=n[0].presentable,v(m,l,h),v(m,s,h),w(s,o),w(s,r),w(s,a),c||(d=[B(e,"change",n[27]),Me(He.call(null,a,{text:"Whether the field should be preferred in the Superuser UI relation listings (default to auto)."}))],c=!0)},p(m,h){h[1]&8&&t!==(t=m[34])&&p(e,"id",t),h[0]&1&&i!==(i=m[0].hidden)&&(e.disabled=i),h[0]&1&&(e.checked=m[0].presentable),h[0]&1&&u!==(u="ri-information-line "+(m[0].hidden?"txt-disabled":"link-hint"))&&p(a,"class",u),h[1]&8&&f!==(f=m[34])&&p(s,"for",f)},d(m){m&&(k(e),k(l),k(s)),c=!1,De(d)}}}function Gm(n){let e,t,i,l,s,o,r;return o=new Hn({props:{class:"dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width",$$slots:{default:[GO]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),l=b("i"),s=C(),H(o.$$.fragment),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"title","More field options"),p(i,"class","btn btn-circle btn-sm btn-transparent"),p(t,"class","inline-flex flex-gap-sm flex-nowrap"),p(e,"class","m-l-auto txt-right")},m(a,u){v(a,e,u),w(e,t),w(t,i),w(i,l),w(i,s),F(o,i,null),r=!0},p(a,u){const f={};u[0]&268435457&&(f.$$scope={dirty:u,ctx:a}),o.$set(f)},i(a){r||(O(o.$$.fragment,a),r=!0)},o(a){D(o.$$.fragment,a),r=!1},d(a){a&&k(e),q(o)}}}function Xm(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Remove',p(e,"type","button"),p(e,"class","dropdown-item"),p(e,"role","menuitem")},m(l,s){v(l,e,s),t||(i=B(e,"click",tt(n[13])),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function GO(n){let e,t,i,l,s,o=!n[0].system&&Xm(n);return{c(){e=b("button"),e.innerHTML='Duplicate',t=C(),o&&o.c(),i=ge(),p(e,"type","button"),p(e,"class","dropdown-item"),p(e,"role","menuitem")},m(r,a){v(r,e,a),v(r,t,a),o&&o.m(r,a),v(r,i,a),l||(s=B(e,"click",tt(n[15])),l=!0)},p(r,a){r[0].system?o&&(o.d(1),o=null):o?o.p(r,a):(o=Xm(r),o.c(),o.m(i.parentNode,i))},d(r){r&&(k(e),k(t),k(i)),o&&o.d(r),l=!1,s()}}}function XO(n){let e,t,i,l,s,o,r,a,u,f=n[7]&&n[2]&&Vm();l=new fe({props:{class:"form-field required m-0 "+(n[7]?"":"disabled"),name:"fields."+n[1]+".name",inlineError:!0,$$slots:{default:[VO]},$$scope:{ctx:n}}});const c=n[20].default,d=Lt(c,n,n[28],Um),m=d||BO();function h(S,T){if(S[0]._toDelete)return YO;if(S[7])return WO}let g=h(n),_=g&&g(n),y=n[7]&&n[4]&&Ym(n);return{c(){e=b("div"),t=b("div"),f&&f.c(),i=C(),H(l.$$.fragment),s=C(),m&&m.c(),o=C(),_&&_.c(),r=C(),y&&y.c(),p(t,"class","schema-field-header"),p(e,"class","schema-field"),x(e,"required",n[0].required),x(e,"expanded",n[7]&&n[4]),x(e,"deleted",n[0]._toDelete)},m(S,T){v(S,e,T),w(e,t),f&&f.m(t,null),w(t,i),F(l,t,null),w(t,s),m&&m.m(t,null),w(t,o),_&&_.m(t,null),w(e,r),y&&y.m(e,null),u=!0},p(S,T){S[7]&&S[2]?f||(f=Vm(),f.c(),f.m(t,i)):f&&(f.d(1),f=null);const $={};T[0]&128&&($.class="form-field required m-0 "+(S[7]?"":"disabled")),T[0]&2&&($.name="fields."+S[1]+".name"),T[0]&268435625&&($.$$scope={dirty:T,ctx:S}),l.$set($),d&&d.p&&(!u||T[0]&268435648)&&Pt(d,c,S,S[28],u?At(c,S[28],T,UO):Nt(S[28]),Um),g===(g=h(S))&&_?_.p(S,T):(_&&_.d(1),_=g&&g(S),_&&(_.c(),_.m(t,null))),S[7]&&S[4]?y?(y.p(S,T),T[0]&144&&O(y,1)):(y=Ym(S),y.c(),O(y,1),y.m(e,null)):y&&(re(),D(y,1,1,()=>{y=null}),ae()),(!u||T[0]&1)&&x(e,"required",S[0].required),(!u||T[0]&144)&&x(e,"expanded",S[7]&&S[4]),(!u||T[0]&1)&&x(e,"deleted",S[0]._toDelete)},i(S){u||(O(l.$$.fragment,S),O(m,S),O(y),S&&nt(()=>{u&&(a||(a=ze(e,vt,{duration:150},!0)),a.run(1))}),u=!0)},o(S){D(l.$$.fragment,S),D(m,S),D(y),S&&(a||(a=ze(e,vt,{duration:150},!1)),a.run(0)),u=!1},d(S){S&&k(e),f&&f.d(),q(l),m&&m.d(S),_&&_.d(),y&&y.d(),S&&a&&a.end()}}}let Sa=[];function QO(n,e,t){let i,l,s,o,r;Qe(n,Sn,Se=>t(19,r=Se));let{$$slots:a={},$$scope:u}=e;const f="f_"+z.randomString(8),c=_t(),d={bool:"Nonfalsey",number:"Nonzero"},m=["password","tokenKey","id","autodate"],h=["password","tokenKey","id","email"],g=["password","tokenKey"];let{key:_=""}=e,{field:y=z.initSchemaField()}=e,{draggable:S=!0}=e,{collection:T={}}=e,$,E=!1;function M(){y.id?t(0,y._toDelete=!0,y):(R(),c("remove"))}function L(){t(0,y._toDelete=!1,y),Wt({})}function I(){y._toDelete||(R(),c("duplicate"))}function A(Se){return z.slugify(Se)}function P(){t(4,E=!0),U()}function R(){t(4,E=!1)}function N(){E?R():P()}function U(){for(let Se of Sa)Se.id!=f&&Se.collapse()}Yt(()=>(Sa.push({id:f,collapse:R}),y.onMountSelect&&(t(0,y.onMountSelect=!1,y),$==null||$.select()),()=>{z.removeByKey(Sa,"id",f)}));const j=()=>$==null?void 0:$.focus();function V(Se){ie[Se?"unshift":"push"](()=>{$=Se,t(3,$)})}const K=Se=>{const ke=y.name;t(0,y.name=A(Se.target.value),y),Se.target.value=y.name,c("rename",{oldName:ke,newName:y.name})};function J(){y.required=this.checked,t(0,y)}function ee(){y.hidden=this.checked,t(0,y)}const X=Se=>{Se.target.checked&&t(0,y.presentable=!1,y)};function oe(){y.presentable=this.checked,t(0,y)}return n.$$set=Se=>{"key"in Se&&t(1,_=Se.key),"field"in Se&&t(0,y=Se.field),"draggable"in Se&&t(2,S=Se.draggable),"collection"in Se&&t(18,T=Se.collection),"$$scope"in Se&&t(28,u=Se.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&262144&&t(8,i=(T==null?void 0:T.type)=="auth"),n.$$.dirty[0]&1&&y._toDelete&&y._originalName&&y.name!==y._originalName&&t(0,y.name=y._originalName,y),n.$$.dirty[0]&1&&!y._originalName&&y.name&&t(0,y._originalName=y.name,y),n.$$.dirty[0]&1&&typeof y._toDelete>"u"&&t(0,y._toDelete=!1,y),n.$$.dirty[0]&1&&y.required&&t(0,y.nullable=!1,y),n.$$.dirty[0]&1&&t(7,l=!y._toDelete),n.$$.dirty[0]&524290&&t(6,s=!z.isEmpty(z.getNestedVal(r,`fields.${_}`))),n.$$.dirty[0]&1&&t(5,o=d[y==null?void 0:y.type]||"Nonempty")},[y,_,S,$,E,o,s,l,i,c,m,h,g,M,L,I,A,N,T,r,a,j,V,K,J,ee,X,oe,u]}class ei extends ye{constructor(e){super(),be(this,e,QO,XO,_e,{key:1,field:0,draggable:2,collection:18},null,[-1,-1])}}function xO(n){let e,t,i,l,s,o;function r(u){n[5](u)}let a={id:n[13],items:n[3],disabled:n[0].system,readonly:!n[12]};return n[2]!==void 0&&(a.keyOfSelected=n[2]),t=new xn({props:a}),ie.push(()=>ve(t,"keyOfSelected",r)),{c(){e=b("div"),H(t.$$.fragment)},m(u,f){v(u,e,f),F(t,e,null),l=!0,s||(o=Me(He.call(null,e,{text:"Auto set on:",position:"top"})),s=!0)},p(u,f){const c={};f&8192&&(c.id=u[13]),f&1&&(c.disabled=u[0].system),f&4096&&(c.readonly=!u[12]),!i&&f&4&&(i=!0,c.keyOfSelected=u[2],$e(()=>i=!1)),t.$set(c)},i(u){l||(O(t.$$.fragment,u),l=!0)},o(u){D(t.$$.fragment,u),l=!1},d(u){u&&k(e),q(t),s=!1,o()}}}function eE(n){let e,t,i,l,s,o;return i=new fe({props:{class:"form-field form-field-single-multiple-select form-field-autodate-select "+(n[12]?"":"readonly"),inlineError:!0,$$slots:{default:[xO,({uniqueId:r})=>({13:r}),({uniqueId:r})=>r?8192:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),H(i.$$.fragment),l=C(),s=b("div"),p(e,"class","separator"),p(s,"class","separator")},m(r,a){v(r,e,a),v(r,t,a),F(i,r,a),v(r,l,a),v(r,s,a),o=!0},p(r,a){const u={};a&4096&&(u.class="form-field form-field-single-multiple-select form-field-autodate-select "+(r[12]?"":"readonly")),a&28677&&(u.$$scope={dirty:a,ctx:r}),i.$set(u)},i(r){o||(O(i.$$.fragment,r),o=!0)},o(r){D(i.$$.fragment,r),o=!1},d(r){r&&(k(e),k(t),k(l),k(s)),q(i,r)}}}function tE(n){let e,t,i;const l=[{key:n[1]},n[4]];function s(r){n[6](r)}let o={$$slots:{default:[eE,({interactive:r})=>({12:r}),({interactive:r})=>r?4096:0]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&18?kt(l,[a&2&&{key:r[1]},a&16&&Ft(r[4])]):{};a&20485&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}const Ta=1,$a=2,Ca=3;function nE(n,e,t){const i=["field","key"];let l=lt(e,i);const s=[{label:"Create",value:Ta},{label:"Update",value:$a},{label:"Create/Update",value:Ca}];let{field:o}=e,{key:r=""}=e,a=u();function u(){return o.onCreate&&o.onUpdate?Ca:o.onUpdate?$a:Ta}function f(_){switch(_){case Ta:t(0,o.onCreate=!0,o),t(0,o.onUpdate=!1,o);break;case $a:t(0,o.onCreate=!1,o),t(0,o.onUpdate=!0,o);break;case Ca:t(0,o.onCreate=!0,o),t(0,o.onUpdate=!0,o);break}}function c(_){a=_,t(2,a)}function d(_){o=_,t(0,o)}function m(_){Pe.call(this,n,_)}function h(_){Pe.call(this,n,_)}function g(_){Pe.call(this,n,_)}return n.$$set=_=>{e=je(je({},e),Ut(_)),t(4,l=lt(e,i)),"field"in _&&t(0,o=_.field),"key"in _&&t(1,r=_.key)},n.$$.update=()=>{n.$$.dirty&4&&f(a)},[o,r,a,s,l,c,d,m,h,g]}class iE extends ye{constructor(e){super(),be(this,e,nE,tE,_e,{field:0,key:1})}}function lE(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[3](r)}let o={};for(let r=0;rve(e,"field",s)),e.$on("rename",n[4]),e.$on("remove",n[5]),e.$on("duplicate",n[6]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function sE(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;function r(c){s=c,t(0,s)}function a(c){Pe.call(this,n,c)}function u(c){Pe.call(this,n,c)}function f(c){Pe.call(this,n,c)}return n.$$set=c=>{e=je(je({},e),Ut(c)),t(2,l=lt(e,i)),"field"in c&&t(0,s=c.field),"key"in c&&t(1,o=c.key)},[s,o,l,r,a,u,f]}class oE extends ye{constructor(e){super(),be(this,e,sE,lE,_e,{field:0,key:1})}}var Oa=["onChange","onClose","onDayCreate","onDestroy","onKeyDown","onMonthChange","onOpen","onParseConfig","onReady","onValueUpdate","onYearChange","onPreCalendarPosition"],es={_disable:[],allowInput:!1,allowInvalidPreload:!1,altFormat:"F j, Y",altInput:!1,altInputClass:"form-control input",animate:typeof window=="object"&&window.navigator.userAgent.indexOf("MSIE")===-1,ariaDateFormat:"F j, Y",autoFillDefaultTime:!0,clickOpens:!0,closeOnSelect:!0,conjunction:", ",dateFormat:"Y-m-d",defaultHour:12,defaultMinute:0,defaultSeconds:0,disable:[],disableMobile:!1,enableSeconds:!1,enableTime:!1,errorHandler:function(n){return typeof console<"u"&&console.warn(n)},getWeek:function(n){var e=new Date(n.getTime());e.setHours(0,0,0,0),e.setDate(e.getDate()+3-(e.getDay()+6)%7);var t=new Date(e.getFullYear(),0,4);return 1+Math.round(((e.getTime()-t.getTime())/864e5-3+(t.getDay()+6)%7)/7)},hourIncrement:1,ignoredFocusElements:[],inline:!1,locale:"default",minuteIncrement:5,mode:"single",monthSelectorType:"dropdown",nextArrow:"",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},eo={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(n){var e=n%100;if(e>3&&e<21)return"th";switch(e%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",monthAriaLabel:"Month",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},In=function(n,e){return e===void 0&&(e=2),("000"+n).slice(e*-1)},Kn=function(n){return n===!0?1:0};function Qm(n,e){var t;return function(){var i=this,l=arguments;clearTimeout(t),t=setTimeout(function(){return n.apply(i,l)},e)}}var Ea=function(n){return n instanceof Array?n:[n]};function $n(n,e,t){if(t===!0)return n.classList.add(e);n.classList.remove(e)}function $t(n,e,t){var i=window.document.createElement(n);return e=e||"",t=t||"",i.className=e,t!==void 0&&(i.textContent=t),i}function Jo(n){for(;n.firstChild;)n.removeChild(n.firstChild)}function nk(n,e){if(e(n))return n;if(n.parentNode)return nk(n.parentNode,e)}function Zo(n,e){var t=$t("div","numInputWrapper"),i=$t("input","numInput "+n),l=$t("span","arrowUp"),s=$t("span","arrowDown");if(navigator.userAgent.indexOf("MSIE 9.0")===-1?i.type="number":(i.type="text",i.pattern="\\d*"),e!==void 0)for(var o in e)i.setAttribute(o,e[o]);return t.appendChild(i),t.appendChild(l),t.appendChild(s),t}function jn(n){try{if(typeof n.composedPath=="function"){var e=n.composedPath();return e[0]}return n.target}catch{return n.target}}var Ma=function(){},Cr=function(n,e,t){return t.months[e?"shorthand":"longhand"][n]},rE={D:Ma,F:function(n,e,t){n.setMonth(t.months.longhand.indexOf(e))},G:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},H:function(n,e){n.setHours(parseFloat(e))},J:function(n,e){n.setDate(parseFloat(e))},K:function(n,e,t){n.setHours(n.getHours()%12+12*Kn(new RegExp(t.amPM[1],"i").test(e)))},M:function(n,e,t){n.setMonth(t.months.shorthand.indexOf(e))},S:function(n,e){n.setSeconds(parseFloat(e))},U:function(n,e){return new Date(parseFloat(e)*1e3)},W:function(n,e,t){var i=parseInt(e),l=new Date(n.getFullYear(),0,2+(i-1)*7,0,0,0,0);return l.setDate(l.getDate()-l.getDay()+t.firstDayOfWeek),l},Y:function(n,e){n.setFullYear(parseFloat(e))},Z:function(n,e){return new Date(e)},d:function(n,e){n.setDate(parseFloat(e))},h:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},i:function(n,e){n.setMinutes(parseFloat(e))},j:function(n,e){n.setDate(parseFloat(e))},l:Ma,m:function(n,e){n.setMonth(parseFloat(e)-1)},n:function(n,e){n.setMonth(parseFloat(e)-1)},s:function(n,e){n.setSeconds(parseFloat(e))},u:function(n,e){return new Date(parseFloat(e))},w:Ma,y:function(n,e){n.setFullYear(2e3+parseFloat(e))}},Sl={D:"",F:"",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},Hs={Z:function(n){return n.toISOString()},D:function(n,e,t){return e.weekdays.shorthand[Hs.w(n,e,t)]},F:function(n,e,t){return Cr(Hs.n(n,e,t)-1,!1,e)},G:function(n,e,t){return In(Hs.h(n,e,t))},H:function(n){return In(n.getHours())},J:function(n,e){return e.ordinal!==void 0?n.getDate()+e.ordinal(n.getDate()):n.getDate()},K:function(n,e){return e.amPM[Kn(n.getHours()>11)]},M:function(n,e){return Cr(n.getMonth(),!0,e)},S:function(n){return In(n.getSeconds())},U:function(n){return n.getTime()/1e3},W:function(n,e,t){return t.getWeek(n)},Y:function(n){return In(n.getFullYear(),4)},d:function(n){return In(n.getDate())},h:function(n){return n.getHours()%12?n.getHours()%12:12},i:function(n){return In(n.getMinutes())},j:function(n){return n.getDate()},l:function(n,e){return e.weekdays.longhand[n.getDay()]},m:function(n){return In(n.getMonth()+1)},n:function(n){return n.getMonth()+1},s:function(n){return n.getSeconds()},u:function(n){return n.getTime()},w:function(n){return n.getDay()},y:function(n){return String(n.getFullYear()).substring(2)}},ik=function(n){var e=n.config,t=e===void 0?es:e,i=n.l10n,l=i===void 0?eo:i,s=n.isMobile,o=s===void 0?!1:s;return function(r,a,u){var f=u||l;return t.formatDate!==void 0&&!o?t.formatDate(r,a,f):a.split("").map(function(c,d,m){return Hs[c]&&m[d-1]!=="\\"?Hs[c](r,f,t):c!=="\\"?c:""}).join("")}},au=function(n){var e=n.config,t=e===void 0?es:e,i=n.l10n,l=i===void 0?eo:i;return function(s,o,r,a){if(!(s!==0&&!s)){var u=a||l,f,c=s;if(s instanceof Date)f=new Date(s.getTime());else if(typeof s!="string"&&s.toFixed!==void 0)f=new Date(s);else if(typeof s=="string"){var d=o||(t||es).dateFormat,m=String(s).trim();if(m==="today")f=new Date,r=!0;else if(t&&t.parseDate)f=t.parseDate(s,d);else if(/Z$/.test(m)||/GMT$/.test(m))f=new Date(s);else{for(var h=void 0,g=[],_=0,y=0,S="";_Math.min(e,t)&&n=0?new Date:new Date(t.config.minDate.getTime()),se=Ia(t.config);Q.setHours(se.hours,se.minutes,se.seconds,Q.getMilliseconds()),t.selectedDates=[Q],t.latestSelectedDateObj=Q}Z!==void 0&&Z.type!=="blur"&&cl(Z);var he=t._input.value;c(),Mn(),t._input.value!==he&&t._debouncedChange()}function u(Z,Q){return Z%12+12*Kn(Q===t.l10n.amPM[1])}function f(Z){switch(Z%24){case 0:case 12:return 12;default:return Z%12}}function c(){if(!(t.hourElement===void 0||t.minuteElement===void 0)){var Z=(parseInt(t.hourElement.value.slice(-2),10)||0)%24,Q=(parseInt(t.minuteElement.value,10)||0)%60,se=t.secondElement!==void 0?(parseInt(t.secondElement.value,10)||0)%60:0;t.amPM!==void 0&&(Z=u(Z,t.amPM.textContent));var he=t.config.minTime!==void 0||t.config.minDate&&t.minDateHasTime&&t.latestSelectedDateObj&&zn(t.latestSelectedDateObj,t.config.minDate,!0)===0,qe=t.config.maxTime!==void 0||t.config.maxDate&&t.maxDateHasTime&&t.latestSelectedDateObj&&zn(t.latestSelectedDateObj,t.config.maxDate,!0)===0;if(t.config.maxTime!==void 0&&t.config.minTime!==void 0&&t.config.minTime>t.config.maxTime){var le=Da(t.config.minTime.getHours(),t.config.minTime.getMinutes(),t.config.minTime.getSeconds()),Ee=Da(t.config.maxTime.getHours(),t.config.maxTime.getMinutes(),t.config.maxTime.getSeconds()),Re=Da(Z,Q,se);if(Re>Ee&&Re=12)]),t.secondElement!==void 0&&(t.secondElement.value=In(se)))}function h(Z){var Q=jn(Z),se=parseInt(Q.value)+(Z.delta||0);(se/1e3>1||Z.key==="Enter"&&!/[^\d]/.test(se.toString()))&&Be(se)}function g(Z,Q,se,he){if(Q instanceof Array)return Q.forEach(function(qe){return g(Z,qe,se,he)});if(Z instanceof Array)return Z.forEach(function(qe){return g(qe,Q,se,he)});Z.addEventListener(Q,se,he),t._handlers.push({remove:function(){return Z.removeEventListener(Q,se,he)}})}function _(){It("onChange")}function y(){if(t.config.wrap&&["open","close","toggle","clear"].forEach(function(se){Array.prototype.forEach.call(t.element.querySelectorAll("[data-"+se+"]"),function(he){return g(he,"click",t[se])})}),t.isMobile){sn();return}var Z=Qm(Ze,50);if(t._debouncedChange=Qm(_,cE),t.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&g(t.daysContainer,"mouseover",function(se){t.config.mode==="range"&&Te(jn(se))}),g(t._input,"keydown",Ht),t.calendarContainer!==void 0&&g(t.calendarContainer,"keydown",Ht),!t.config.inline&&!t.config.static&&g(window,"resize",Z),window.ontouchstart!==void 0?g(window.document,"touchstart",et):g(window.document,"mousedown",et),g(window.document,"focus",et,{capture:!0}),t.config.clickOpens===!0&&(g(t._input,"focus",t.open),g(t._input,"click",t.open)),t.daysContainer!==void 0&&(g(t.monthNav,"click",Rl),g(t.monthNav,["keyup","increment"],h),g(t.daysContainer,"click",Fe)),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0){var Q=function(se){return jn(se).select()};g(t.timeContainer,["increment"],a),g(t.timeContainer,"blur",a,{capture:!0}),g(t.timeContainer,"click",T),g([t.hourElement,t.minuteElement],["focus","click"],Q),t.secondElement!==void 0&&g(t.secondElement,"focus",function(){return t.secondElement&&t.secondElement.select()}),t.amPM!==void 0&&g(t.amPM,"click",function(se){a(se)})}t.config.allowInput&&g(t._input,"blur",at)}function S(Z,Q){var se=Z!==void 0?t.parseDate(Z):t.latestSelectedDateObj||(t.config.minDate&&t.config.minDate>t.now?t.config.minDate:t.config.maxDate&&t.config.maxDate1),t.calendarContainer.appendChild(Z);var qe=t.config.appendTo!==void 0&&t.config.appendTo.nodeType!==void 0;if((t.config.inline||t.config.static)&&(t.calendarContainer.classList.add(t.config.inline?"inline":"static"),t.config.inline&&(!qe&&t.element.parentNode?t.element.parentNode.insertBefore(t.calendarContainer,t._input.nextSibling):t.config.appendTo!==void 0&&t.config.appendTo.appendChild(t.calendarContainer)),t.config.static)){var le=$t("div","flatpickr-wrapper");t.element.parentNode&&t.element.parentNode.insertBefore(le,t.element),le.appendChild(t.element),t.altInput&&le.appendChild(t.altInput),le.appendChild(t.calendarContainer)}!t.config.static&&!t.config.inline&&(t.config.appendTo!==void 0?t.config.appendTo:window.document.body).appendChild(t.calendarContainer)}function M(Z,Q,se,he){var qe=rt(Q,!0),le=$t("span",Z,Q.getDate().toString());return le.dateObj=Q,le.$i=he,le.setAttribute("aria-label",t.formatDate(Q,t.config.ariaDateFormat)),Z.indexOf("hidden")===-1&&zn(Q,t.now)===0&&(t.todayDateElem=le,le.classList.add("today"),le.setAttribute("aria-current","date")),qe?(le.tabIndex=-1,ul(Q)&&(le.classList.add("selected"),t.selectedDateElem=le,t.config.mode==="range"&&($n(le,"startRange",t.selectedDates[0]&&zn(Q,t.selectedDates[0],!0)===0),$n(le,"endRange",t.selectedDates[1]&&zn(Q,t.selectedDates[1],!0)===0),Z==="nextMonthDay"&&le.classList.add("inRange")))):le.classList.add("flatpickr-disabled"),t.config.mode==="range"&&Hi(Q)&&!ul(Q)&&le.classList.add("inRange"),t.weekNumbers&&t.config.showMonths===1&&Z!=="prevMonthDay"&&he%7===6&&t.weekNumbers.insertAdjacentHTML("beforeend",""+t.config.getWeek(Q)+""),It("onDayCreate",le),le}function L(Z){Z.focus(),t.config.mode==="range"&&Te(Z)}function I(Z){for(var Q=Z>0?0:t.config.showMonths-1,se=Z>0?t.config.showMonths:-1,he=Q;he!=se;he+=Z)for(var qe=t.daysContainer.children[he],le=Z>0?0:qe.children.length-1,Ee=Z>0?qe.children.length:-1,Re=le;Re!=Ee;Re+=Z){var Ke=qe.children[Re];if(Ke.className.indexOf("hidden")===-1&&rt(Ke.dateObj))return Ke}}function A(Z,Q){for(var se=Z.className.indexOf("Month")===-1?Z.dateObj.getMonth():t.currentMonth,he=Q>0?t.config.showMonths:-1,qe=Q>0?1:-1,le=se-t.currentMonth;le!=he;le+=qe)for(var Ee=t.daysContainer.children[le],Re=se-t.currentMonth===le?Z.$i+Q:Q<0?Ee.children.length-1:0,Ke=Ee.children.length,Ae=Re;Ae>=0&&Ae0?Ke:-1);Ae+=qe){var Ge=Ee.children[Ae];if(Ge.className.indexOf("hidden")===-1&&rt(Ge.dateObj)&&Math.abs(Z.$i-Ae)>=Math.abs(Q))return L(Ge)}t.changeMonth(qe),P(I(qe),0)}function P(Z,Q){var se=s(),he=Je(se||document.body),qe=Z!==void 0?Z:he?se:t.selectedDateElem!==void 0&&Je(t.selectedDateElem)?t.selectedDateElem:t.todayDateElem!==void 0&&Je(t.todayDateElem)?t.todayDateElem:I(Q>0?1:-1);qe===void 0?t._input.focus():he?A(qe,Q):L(qe)}function R(Z,Q){for(var se=(new Date(Z,Q,1).getDay()-t.l10n.firstDayOfWeek+7)%7,he=t.utils.getDaysInMonth((Q-1+12)%12,Z),qe=t.utils.getDaysInMonth(Q,Z),le=window.document.createDocumentFragment(),Ee=t.config.showMonths>1,Re=Ee?"prevMonthDay hidden":"prevMonthDay",Ke=Ee?"nextMonthDay hidden":"nextMonthDay",Ae=he+1-se,Ge=0;Ae<=he;Ae++,Ge++)le.appendChild(M("flatpickr-day "+Re,new Date(Z,Q-1,Ae),Ae,Ge));for(Ae=1;Ae<=qe;Ae++,Ge++)le.appendChild(M("flatpickr-day",new Date(Z,Q,Ae),Ae,Ge));for(var ft=qe+1;ft<=42-se&&(t.config.showMonths===1||Ge%7!==0);ft++,Ge++)le.appendChild(M("flatpickr-day "+Ke,new Date(Z,Q+1,ft%qe),ft,Ge));var Xt=$t("div","dayContainer");return Xt.appendChild(le),Xt}function N(){if(t.daysContainer!==void 0){Jo(t.daysContainer),t.weekNumbers&&Jo(t.weekNumbers);for(var Z=document.createDocumentFragment(),Q=0;Q1||t.config.monthSelectorType!=="dropdown")){var Z=function(he){return t.config.minDate!==void 0&&t.currentYear===t.config.minDate.getFullYear()&&het.config.maxDate.getMonth())};t.monthsDropdownContainer.tabIndex=-1,t.monthsDropdownContainer.innerHTML="";for(var Q=0;Q<12;Q++)if(Z(Q)){var se=$t("option","flatpickr-monthDropdown-month");se.value=new Date(t.currentYear,Q).getMonth().toString(),se.textContent=Cr(Q,t.config.shorthandCurrentMonth,t.l10n),se.tabIndex=-1,t.currentMonth===Q&&(se.selected=!0),t.monthsDropdownContainer.appendChild(se)}}}function j(){var Z=$t("div","flatpickr-month"),Q=window.document.createDocumentFragment(),se;t.config.showMonths>1||t.config.monthSelectorType==="static"?se=$t("span","cur-month"):(t.monthsDropdownContainer=$t("select","flatpickr-monthDropdown-months"),t.monthsDropdownContainer.setAttribute("aria-label",t.l10n.monthAriaLabel),g(t.monthsDropdownContainer,"change",function(Ee){var Re=jn(Ee),Ke=parseInt(Re.value,10);t.changeMonth(Ke-t.currentMonth),It("onMonthChange")}),U(),se=t.monthsDropdownContainer);var he=Zo("cur-year",{tabindex:"-1"}),qe=he.getElementsByTagName("input")[0];qe.setAttribute("aria-label",t.l10n.yearAriaLabel),t.config.minDate&&qe.setAttribute("min",t.config.minDate.getFullYear().toString()),t.config.maxDate&&(qe.setAttribute("max",t.config.maxDate.getFullYear().toString()),qe.disabled=!!t.config.minDate&&t.config.minDate.getFullYear()===t.config.maxDate.getFullYear());var le=$t("div","flatpickr-current-month");return le.appendChild(se),le.appendChild(he),Q.appendChild(le),Z.appendChild(Q),{container:Z,yearElement:qe,monthElement:se}}function V(){Jo(t.monthNav),t.monthNav.appendChild(t.prevMonthNav),t.config.showMonths&&(t.yearElements=[],t.monthElements=[]);for(var Z=t.config.showMonths;Z--;){var Q=j();t.yearElements.push(Q.yearElement),t.monthElements.push(Q.monthElement),t.monthNav.appendChild(Q.container)}t.monthNav.appendChild(t.nextMonthNav)}function K(){return t.monthNav=$t("div","flatpickr-months"),t.yearElements=[],t.monthElements=[],t.prevMonthNav=$t("span","flatpickr-prev-month"),t.prevMonthNav.innerHTML=t.config.prevArrow,t.nextMonthNav=$t("span","flatpickr-next-month"),t.nextMonthNav.innerHTML=t.config.nextArrow,V(),Object.defineProperty(t,"_hidePrevMonthArrow",{get:function(){return t.__hidePrevMonthArrow},set:function(Z){t.__hidePrevMonthArrow!==Z&&($n(t.prevMonthNav,"flatpickr-disabled",Z),t.__hidePrevMonthArrow=Z)}}),Object.defineProperty(t,"_hideNextMonthArrow",{get:function(){return t.__hideNextMonthArrow},set:function(Z){t.__hideNextMonthArrow!==Z&&($n(t.nextMonthNav,"flatpickr-disabled",Z),t.__hideNextMonthArrow=Z)}}),t.currentYearElement=t.yearElements[0],ji(),t.monthNav}function J(){t.calendarContainer.classList.add("hasTime"),t.config.noCalendar&&t.calendarContainer.classList.add("noCalendar");var Z=Ia(t.config);t.timeContainer=$t("div","flatpickr-time"),t.timeContainer.tabIndex=-1;var Q=$t("span","flatpickr-time-separator",":"),se=Zo("flatpickr-hour",{"aria-label":t.l10n.hourAriaLabel});t.hourElement=se.getElementsByTagName("input")[0];var he=Zo("flatpickr-minute",{"aria-label":t.l10n.minuteAriaLabel});if(t.minuteElement=he.getElementsByTagName("input")[0],t.hourElement.tabIndex=t.minuteElement.tabIndex=-1,t.hourElement.value=In(t.latestSelectedDateObj?t.latestSelectedDateObj.getHours():t.config.time_24hr?Z.hours:f(Z.hours)),t.minuteElement.value=In(t.latestSelectedDateObj?t.latestSelectedDateObj.getMinutes():Z.minutes),t.hourElement.setAttribute("step",t.config.hourIncrement.toString()),t.minuteElement.setAttribute("step",t.config.minuteIncrement.toString()),t.hourElement.setAttribute("min",t.config.time_24hr?"0":"1"),t.hourElement.setAttribute("max",t.config.time_24hr?"23":"12"),t.hourElement.setAttribute("maxlength","2"),t.minuteElement.setAttribute("min","0"),t.minuteElement.setAttribute("max","59"),t.minuteElement.setAttribute("maxlength","2"),t.timeContainer.appendChild(se),t.timeContainer.appendChild(Q),t.timeContainer.appendChild(he),t.config.time_24hr&&t.timeContainer.classList.add("time24hr"),t.config.enableSeconds){t.timeContainer.classList.add("hasSeconds");var qe=Zo("flatpickr-second");t.secondElement=qe.getElementsByTagName("input")[0],t.secondElement.value=In(t.latestSelectedDateObj?t.latestSelectedDateObj.getSeconds():Z.seconds),t.secondElement.setAttribute("step",t.minuteElement.getAttribute("step")),t.secondElement.setAttribute("min","0"),t.secondElement.setAttribute("max","59"),t.secondElement.setAttribute("maxlength","2"),t.timeContainer.appendChild($t("span","flatpickr-time-separator",":")),t.timeContainer.appendChild(qe)}return t.config.time_24hr||(t.amPM=$t("span","flatpickr-am-pm",t.l10n.amPM[Kn((t.latestSelectedDateObj?t.hourElement.value:t.config.defaultHour)>11)]),t.amPM.title=t.l10n.toggleTitle,t.amPM.tabIndex=-1,t.timeContainer.appendChild(t.amPM)),t.timeContainer}function ee(){t.weekdayContainer?Jo(t.weekdayContainer):t.weekdayContainer=$t("div","flatpickr-weekdays");for(var Z=t.config.showMonths;Z--;){var Q=$t("div","flatpickr-weekdaycontainer");t.weekdayContainer.appendChild(Q)}return X(),t.weekdayContainer}function X(){if(t.weekdayContainer){var Z=t.l10n.firstDayOfWeek,Q=xm(t.l10n.weekdays.shorthand);Z>0&&Z + `+Q.join("")+` + + `}}function oe(){t.calendarContainer.classList.add("hasWeeks");var Z=$t("div","flatpickr-weekwrapper");Z.appendChild($t("span","flatpickr-weekday",t.l10n.weekAbbreviation));var Q=$t("div","flatpickr-weeks");return Z.appendChild(Q),{weekWrapper:Z,weekNumbers:Q}}function Se(Z,Q){Q===void 0&&(Q=!0);var se=Q?Z:Z-t.currentMonth;se<0&&t._hidePrevMonthArrow===!0||se>0&&t._hideNextMonthArrow===!0||(t.currentMonth+=se,(t.currentMonth<0||t.currentMonth>11)&&(t.currentYear+=t.currentMonth>11?1:-1,t.currentMonth=(t.currentMonth+12)%12,It("onYearChange"),U()),N(),It("onMonthChange"),ji())}function ke(Z,Q){if(Z===void 0&&(Z=!0),Q===void 0&&(Q=!0),t.input.value="",t.altInput!==void 0&&(t.altInput.value=""),t.mobileInput!==void 0&&(t.mobileInput.value=""),t.selectedDates=[],t.latestSelectedDateObj=void 0,Q===!0&&(t.currentYear=t._initialDate.getFullYear(),t.currentMonth=t._initialDate.getMonth()),t.config.enableTime===!0){var se=Ia(t.config),he=se.hours,qe=se.minutes,le=se.seconds;m(he,qe,le)}t.redraw(),Z&&It("onChange")}function Ce(){t.isOpen=!1,t.isMobile||(t.calendarContainer!==void 0&&t.calendarContainer.classList.remove("open"),t._input!==void 0&&t._input.classList.remove("active")),It("onClose")}function We(){t.config!==void 0&&It("onDestroy");for(var Z=t._handlers.length;Z--;)t._handlers[Z].remove();if(t._handlers=[],t.mobileInput)t.mobileInput.parentNode&&t.mobileInput.parentNode.removeChild(t.mobileInput),t.mobileInput=void 0;else if(t.calendarContainer&&t.calendarContainer.parentNode)if(t.config.static&&t.calendarContainer.parentNode){var Q=t.calendarContainer.parentNode;if(Q.lastChild&&Q.removeChild(Q.lastChild),Q.parentNode){for(;Q.firstChild;)Q.parentNode.insertBefore(Q.firstChild,Q);Q.parentNode.removeChild(Q)}}else t.calendarContainer.parentNode.removeChild(t.calendarContainer);t.altInput&&(t.input.type="text",t.altInput.parentNode&&t.altInput.parentNode.removeChild(t.altInput),delete t.altInput),t.input&&(t.input.type=t.input._type,t.input.classList.remove("flatpickr-input"),t.input.removeAttribute("readonly")),["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach(function(se){try{delete t[se]}catch{}})}function st(Z){return t.calendarContainer.contains(Z)}function et(Z){if(t.isOpen&&!t.config.inline){var Q=jn(Z),se=st(Q),he=Q===t.input||Q===t.altInput||t.element.contains(Q)||Z.path&&Z.path.indexOf&&(~Z.path.indexOf(t.input)||~Z.path.indexOf(t.altInput)),qe=!he&&!se&&!st(Z.relatedTarget),le=!t.config.ignoredFocusElements.some(function(Ee){return Ee.contains(Q)});qe&&le&&(t.config.allowInput&&t.setDate(t._input.value,!1,t.config.altInput?t.config.altFormat:t.config.dateFormat),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0&&t.input.value!==""&&t.input.value!==void 0&&a(),t.close(),t.config&&t.config.mode==="range"&&t.selectedDates.length===1&&t.clear(!1))}}function Be(Z){if(!(!Z||t.config.minDate&&Zt.config.maxDate.getFullYear())){var Q=Z,se=t.currentYear!==Q;t.currentYear=Q||t.currentYear,t.config.maxDate&&t.currentYear===t.config.maxDate.getFullYear()?t.currentMonth=Math.min(t.config.maxDate.getMonth(),t.currentMonth):t.config.minDate&&t.currentYear===t.config.minDate.getFullYear()&&(t.currentMonth=Math.max(t.config.minDate.getMonth(),t.currentMonth)),se&&(t.redraw(),It("onYearChange"),U())}}function rt(Z,Q){var se;Q===void 0&&(Q=!0);var he=t.parseDate(Z,void 0,Q);if(t.config.minDate&&he&&zn(he,t.config.minDate,Q!==void 0?Q:!t.minDateHasTime)<0||t.config.maxDate&&he&&zn(he,t.config.maxDate,Q!==void 0?Q:!t.maxDateHasTime)>0)return!1;if(!t.config.enable&&t.config.disable.length===0)return!0;if(he===void 0)return!1;for(var qe=!!t.config.enable,le=(se=t.config.enable)!==null&&se!==void 0?se:t.config.disable,Ee=0,Re=void 0;Ee=Re.from.getTime()&&he.getTime()<=Re.to.getTime())return qe}return!qe}function Je(Z){return t.daysContainer!==void 0?Z.className.indexOf("hidden")===-1&&Z.className.indexOf("flatpickr-disabled")===-1&&t.daysContainer.contains(Z):!1}function at(Z){var Q=Z.target===t._input,se=t._input.value.trimEnd()!==fl();Q&&se&&!(Z.relatedTarget&&st(Z.relatedTarget))&&t.setDate(t._input.value,!0,Z.target===t.altInput?t.config.altFormat:t.config.dateFormat)}function Ht(Z){var Q=jn(Z),se=t.config.wrap?n.contains(Q):Q===t._input,he=t.config.allowInput,qe=t.isOpen&&(!he||!se),le=t.config.inline&&se&&!he;if(Z.keyCode===13&&se){if(he)return t.setDate(t._input.value,!0,Q===t.altInput?t.config.altFormat:t.config.dateFormat),t.close(),Q.blur();t.open()}else if(st(Q)||qe||le){var Ee=!!t.timeContainer&&t.timeContainer.contains(Q);switch(Z.keyCode){case 13:Ee?(Z.preventDefault(),a(),Zt()):Fe(Z);break;case 27:Z.preventDefault(),Zt();break;case 8:case 46:se&&!t.config.allowInput&&(Z.preventDefault(),t.clear());break;case 37:case 39:if(!Ee&&!se){Z.preventDefault();var Re=s();if(t.daysContainer!==void 0&&(he===!1||Re&&Je(Re))){var Ke=Z.keyCode===39?1:-1;Z.ctrlKey?(Z.stopPropagation(),Se(Ke),P(I(1),0)):P(void 0,Ke)}}else t.hourElement&&t.hourElement.focus();break;case 38:case 40:Z.preventDefault();var Ae=Z.keyCode===40?1:-1;t.daysContainer&&Q.$i!==void 0||Q===t.input||Q===t.altInput?Z.ctrlKey?(Z.stopPropagation(),Be(t.currentYear-Ae),P(I(1),0)):Ee||P(void 0,Ae*7):Q===t.currentYearElement?Be(t.currentYear-Ae):t.config.enableTime&&(!Ee&&t.hourElement&&t.hourElement.focus(),a(Z),t._debouncedChange());break;case 9:if(Ee){var Ge=[t.hourElement,t.minuteElement,t.secondElement,t.amPM].concat(t.pluginElements).filter(function(_n){return _n}),ft=Ge.indexOf(Q);if(ft!==-1){var Xt=Ge[ft+(Z.shiftKey?-1:1)];Z.preventDefault(),(Xt||t._input).focus()}}else!t.config.noCalendar&&t.daysContainer&&t.daysContainer.contains(Q)&&Z.shiftKey&&(Z.preventDefault(),t._input.focus());break}}if(t.amPM!==void 0&&Q===t.amPM)switch(Z.key){case t.l10n.amPM[0].charAt(0):case t.l10n.amPM[0].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[0],c(),Mn();break;case t.l10n.amPM[1].charAt(0):case t.l10n.amPM[1].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[1],c(),Mn();break}(se||st(Q))&&It("onKeyDown",Z)}function Te(Z,Q){if(Q===void 0&&(Q="flatpickr-day"),!(t.selectedDates.length!==1||Z&&(!Z.classList.contains(Q)||Z.classList.contains("flatpickr-disabled")))){for(var se=Z?Z.dateObj.getTime():t.days.firstElementChild.dateObj.getTime(),he=t.parseDate(t.selectedDates[0],void 0,!0).getTime(),qe=Math.min(se,t.selectedDates[0].getTime()),le=Math.max(se,t.selectedDates[0].getTime()),Ee=!1,Re=0,Ke=0,Ae=qe;Aeqe&&AeRe)?Re=Ae:Ae>he&&(!Ke||Ae ."+Q));Ge.forEach(function(ft){var Xt=ft.dateObj,_n=Xt.getTime(),dl=Re>0&&_n0&&_n>Ke;if(dl){ft.classList.add("notAllowed"),["inRange","startRange","endRange"].forEach(function(Oi){ft.classList.remove(Oi)});return}else if(Ee&&!dl)return;["startRange","inRange","endRange","notAllowed"].forEach(function(Oi){ft.classList.remove(Oi)}),Z!==void 0&&(Z.classList.add(se<=t.selectedDates[0].getTime()?"startRange":"endRange"),hese&&_n===he&&ft.classList.add("endRange"),_n>=Re&&(Ke===0||_n<=Ke)&&aE(_n,he,se)&&ft.classList.add("inRange"))})}}function Ze(){t.isOpen&&!t.config.static&&!t.config.inline&&ut()}function ot(Z,Q){if(Q===void 0&&(Q=t._positionElement),t.isMobile===!0){if(Z){Z.preventDefault();var se=jn(Z);se&&se.blur()}t.mobileInput!==void 0&&(t.mobileInput.focus(),t.mobileInput.click()),It("onOpen");return}else if(t._input.disabled||t.config.inline)return;var he=t.isOpen;t.isOpen=!0,he||(t.calendarContainer.classList.add("open"),t._input.classList.add("active"),It("onOpen"),ut(Q)),t.config.enableTime===!0&&t.config.noCalendar===!0&&t.config.allowInput===!1&&(Z===void 0||!t.timeContainer.contains(Z.relatedTarget))&&setTimeout(function(){return t.hourElement.select()},50)}function Le(Z){return function(Q){var se=t.config["_"+Z+"Date"]=t.parseDate(Q,t.config.dateFormat),he=t.config["_"+(Z==="min"?"max":"min")+"Date"];se!==void 0&&(t[Z==="min"?"minDateHasTime":"maxDateHasTime"]=se.getHours()>0||se.getMinutes()>0||se.getSeconds()>0),t.selectedDates&&(t.selectedDates=t.selectedDates.filter(function(qe){return rt(qe)}),!t.selectedDates.length&&Z==="min"&&d(se),Mn()),t.daysContainer&&(qt(),se!==void 0?t.currentYearElement[Z]=se.getFullYear().toString():t.currentYearElement.removeAttribute(Z),t.currentYearElement.disabled=!!he&&se!==void 0&&he.getFullYear()===se.getFullYear())}}function Ve(){var Z=["wrap","weekNumbers","allowInput","allowInvalidPreload","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],Q=bn(bn({},JSON.parse(JSON.stringify(n.dataset||{}))),e),se={};t.config.parseDate=Q.parseDate,t.config.formatDate=Q.formatDate,Object.defineProperty(t.config,"enable",{get:function(){return t.config._enable},set:function(Ge){t.config._enable=pi(Ge)}}),Object.defineProperty(t.config,"disable",{get:function(){return t.config._disable},set:function(Ge){t.config._disable=pi(Ge)}});var he=Q.mode==="time";if(!Q.dateFormat&&(Q.enableTime||he)){var qe=en.defaultConfig.dateFormat||es.dateFormat;se.dateFormat=Q.noCalendar||he?"H:i"+(Q.enableSeconds?":S":""):qe+" H:i"+(Q.enableSeconds?":S":"")}if(Q.altInput&&(Q.enableTime||he)&&!Q.altFormat){var le=en.defaultConfig.altFormat||es.altFormat;se.altFormat=Q.noCalendar||he?"h:i"+(Q.enableSeconds?":S K":" K"):le+(" h:i"+(Q.enableSeconds?":S":"")+" K")}Object.defineProperty(t.config,"minDate",{get:function(){return t.config._minDate},set:Le("min")}),Object.defineProperty(t.config,"maxDate",{get:function(){return t.config._maxDate},set:Le("max")});var Ee=function(Ge){return function(ft){t.config[Ge==="min"?"_minTime":"_maxTime"]=t.parseDate(ft,"H:i:S")}};Object.defineProperty(t.config,"minTime",{get:function(){return t.config._minTime},set:Ee("min")}),Object.defineProperty(t.config,"maxTime",{get:function(){return t.config._maxTime},set:Ee("max")}),Q.mode==="time"&&(t.config.noCalendar=!0,t.config.enableTime=!0),Object.assign(t.config,se,Q);for(var Re=0;Re-1?t.config[Ae]=Ea(Ke[Ae]).map(o).concat(t.config[Ae]):typeof Q[Ae]>"u"&&(t.config[Ae]=Ke[Ae])}Q.altInputClass||(t.config.altInputClass=we().className+" "+t.config.altInputClass),It("onParseConfig")}function we(){return t.config.wrap?n.querySelector("[data-input]"):n}function Oe(){typeof t.config.locale!="object"&&typeof en.l10ns[t.config.locale]>"u"&&t.config.errorHandler(new Error("flatpickr: invalid locale "+t.config.locale)),t.l10n=bn(bn({},en.l10ns.default),typeof t.config.locale=="object"?t.config.locale:t.config.locale!=="default"?en.l10ns[t.config.locale]:void 0),Sl.D="("+t.l10n.weekdays.shorthand.join("|")+")",Sl.l="("+t.l10n.weekdays.longhand.join("|")+")",Sl.M="("+t.l10n.months.shorthand.join("|")+")",Sl.F="("+t.l10n.months.longhand.join("|")+")",Sl.K="("+t.l10n.amPM[0]+"|"+t.l10n.amPM[1]+"|"+t.l10n.amPM[0].toLowerCase()+"|"+t.l10n.amPM[1].toLowerCase()+")";var Z=bn(bn({},e),JSON.parse(JSON.stringify(n.dataset||{})));Z.time_24hr===void 0&&en.defaultConfig.time_24hr===void 0&&(t.config.time_24hr=t.l10n.time_24hr),t.formatDate=ik(t),t.parseDate=au({config:t.config,l10n:t.l10n})}function ut(Z){if(typeof t.config.position=="function")return void t.config.position(t,Z);if(t.calendarContainer!==void 0){It("onPreCalendarPosition");var Q=Z||t._positionElement,se=Array.prototype.reduce.call(t.calendarContainer.children,function(hs,Vr){return hs+Vr.offsetHeight},0),he=t.calendarContainer.offsetWidth,qe=t.config.position.split(" "),le=qe[0],Ee=qe.length>1?qe[1]:null,Re=Q.getBoundingClientRect(),Ke=window.innerHeight-Re.bottom,Ae=le==="above"||le!=="below"&&Kese,Ge=window.pageYOffset+Re.top+(Ae?-se-2:Q.offsetHeight+2);if($n(t.calendarContainer,"arrowTop",!Ae),$n(t.calendarContainer,"arrowBottom",Ae),!t.config.inline){var ft=window.pageXOffset+Re.left,Xt=!1,_n=!1;Ee==="center"?(ft-=(he-Re.width)/2,Xt=!0):Ee==="right"&&(ft-=he-Re.width,_n=!0),$n(t.calendarContainer,"arrowLeft",!Xt&&!_n),$n(t.calendarContainer,"arrowCenter",Xt),$n(t.calendarContainer,"arrowRight",_n);var dl=window.document.body.offsetWidth-(window.pageXOffset+Re.right),Oi=ft+he>window.document.body.offsetWidth,go=dl+he>window.document.body.offsetWidth;if($n(t.calendarContainer,"rightMost",Oi),!t.config.static)if(t.calendarContainer.style.top=Ge+"px",!Oi)t.calendarContainer.style.left=ft+"px",t.calendarContainer.style.right="auto";else if(!go)t.calendarContainer.style.left="auto",t.calendarContainer.style.right=dl+"px";else{var Fl=Ne();if(Fl===void 0)return;var bo=window.document.body.offsetWidth,ms=Math.max(0,bo/2-he/2),Ei=".flatpickr-calendar.centerMost:before",pl=".flatpickr-calendar.centerMost:after",ml=Fl.cssRules.length,ql="{left:"+Re.left+"px;right:auto;}";$n(t.calendarContainer,"rightMost",!1),$n(t.calendarContainer,"centerMost",!0),Fl.insertRule(Ei+","+pl+ql,ml),t.calendarContainer.style.left=ms+"px",t.calendarContainer.style.right="auto"}}}}function Ne(){for(var Z=null,Q=0;Qt.currentMonth+t.config.showMonths-1)&&t.config.mode!=="range";if(t.selectedDateElem=he,t.config.mode==="single")t.selectedDates=[qe];else if(t.config.mode==="multiple"){var Ee=ul(qe);Ee?t.selectedDates.splice(parseInt(Ee),1):t.selectedDates.push(qe)}else t.config.mode==="range"&&(t.selectedDates.length===2&&t.clear(!1,!1),t.latestSelectedDateObj=qe,t.selectedDates.push(qe),zn(qe,t.selectedDates[0],!0)!==0&&t.selectedDates.sort(function(Ge,ft){return Ge.getTime()-ft.getTime()}));if(c(),le){var Re=t.currentYear!==qe.getFullYear();t.currentYear=qe.getFullYear(),t.currentMonth=qe.getMonth(),Re&&(It("onYearChange"),U()),It("onMonthChange")}if(ji(),N(),Mn(),!le&&t.config.mode!=="range"&&t.config.showMonths===1?L(he):t.selectedDateElem!==void 0&&t.hourElement===void 0&&t.selectedDateElem&&t.selectedDateElem.focus(),t.hourElement!==void 0&&t.hourElement!==void 0&&t.hourElement.focus(),t.config.closeOnSelect){var Ke=t.config.mode==="single"&&!t.config.enableTime,Ae=t.config.mode==="range"&&t.selectedDates.length===2&&!t.config.enableTime;(Ke||Ae)&&Zt()}_()}}var Dt={locale:[Oe,X],showMonths:[V,r,ee],minDate:[S],maxDate:[S],positionElement:[rn],clickOpens:[function(){t.config.clickOpens===!0?(g(t._input,"focus",t.open),g(t._input,"click",t.open)):(t._input.removeEventListener("focus",t.open),t._input.removeEventListener("click",t.open))}]};function Gt(Z,Q){if(Z!==null&&typeof Z=="object"){Object.assign(t.config,Z);for(var se in Z)Dt[se]!==void 0&&Dt[se].forEach(function(he){return he()})}else t.config[Z]=Q,Dt[Z]!==void 0?Dt[Z].forEach(function(he){return he()}):Oa.indexOf(Z)>-1&&(t.config[Z]=Ea(Q));t.redraw(),Mn(!0)}function mn(Z,Q){var se=[];if(Z instanceof Array)se=Z.map(function(he){return t.parseDate(he,Q)});else if(Z instanceof Date||typeof Z=="number")se=[t.parseDate(Z,Q)];else if(typeof Z=="string")switch(t.config.mode){case"single":case"time":se=[t.parseDate(Z,Q)];break;case"multiple":se=Z.split(t.config.conjunction).map(function(he){return t.parseDate(he,Q)});break;case"range":se=Z.split(t.l10n.rangeSeparator).map(function(he){return t.parseDate(he,Q)});break}else t.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(Z)));t.selectedDates=t.config.allowInvalidPreload?se:se.filter(function(he){return he instanceof Date&&rt(he,!1)}),t.config.mode==="range"&&t.selectedDates.sort(function(he,qe){return he.getTime()-qe.getTime()})}function hn(Z,Q,se){if(Q===void 0&&(Q=!1),se===void 0&&(se=t.config.dateFormat),Z!==0&&!Z||Z instanceof Array&&Z.length===0)return t.clear(Q);mn(Z,se),t.latestSelectedDateObj=t.selectedDates[t.selectedDates.length-1],t.redraw(),S(void 0,Q),d(),t.selectedDates.length===0&&t.clear(!1),Mn(Q),Q&&It("onChange")}function pi(Z){return Z.slice().map(function(Q){return typeof Q=="string"||typeof Q=="number"||Q instanceof Date?t.parseDate(Q,void 0,!0):Q&&typeof Q=="object"&&Q.from&&Q.to?{from:t.parseDate(Q.from,void 0),to:t.parseDate(Q.to,void 0)}:Q}).filter(function(Q){return Q})}function Ci(){t.selectedDates=[],t.now=t.parseDate(t.config.now)||new Date;var Z=t.config.defaultDate||((t.input.nodeName==="INPUT"||t.input.nodeName==="TEXTAREA")&&t.input.placeholder&&t.input.value===t.input.placeholder?null:t.input.value);Z&&mn(Z,t.config.dateFormat),t._initialDate=t.selectedDates.length>0?t.selectedDates[0]:t.config.minDate&&t.config.minDate.getTime()>t.now.getTime()?t.config.minDate:t.config.maxDate&&t.config.maxDate.getTime()0&&(t.latestSelectedDateObj=t.selectedDates[0]),t.config.minTime!==void 0&&(t.config.minTime=t.parseDate(t.config.minTime,"H:i")),t.config.maxTime!==void 0&&(t.config.maxTime=t.parseDate(t.config.maxTime,"H:i")),t.minDateHasTime=!!t.config.minDate&&(t.config.minDate.getHours()>0||t.config.minDate.getMinutes()>0||t.config.minDate.getSeconds()>0),t.maxDateHasTime=!!t.config.maxDate&&(t.config.maxDate.getHours()>0||t.config.maxDate.getMinutes()>0||t.config.maxDate.getSeconds()>0)}function gt(){if(t.input=we(),!t.input){t.config.errorHandler(new Error("Invalid input element specified"));return}t.input._type=t.input.type,t.input.type="text",t.input.classList.add("flatpickr-input"),t._input=t.input,t.config.altInput&&(t.altInput=$t(t.input.nodeName,t.config.altInputClass),t._input=t.altInput,t.altInput.placeholder=t.input.placeholder,t.altInput.disabled=t.input.disabled,t.altInput.required=t.input.required,t.altInput.tabIndex=t.input.tabIndex,t.altInput.type="text",t.input.setAttribute("type","hidden"),!t.config.static&&t.input.parentNode&&t.input.parentNode.insertBefore(t.altInput,t.input.nextSibling)),t.config.allowInput||t._input.setAttribute("readonly","readonly"),rn()}function rn(){t._positionElement=t.config.positionElement||t._input}function sn(){var Z=t.config.enableTime?t.config.noCalendar?"time":"datetime-local":"date";t.mobileInput=$t("input",t.input.className+" flatpickr-mobile"),t.mobileInput.tabIndex=1,t.mobileInput.type=Z,t.mobileInput.disabled=t.input.disabled,t.mobileInput.required=t.input.required,t.mobileInput.placeholder=t.input.placeholder,t.mobileFormatStr=Z==="datetime-local"?"Y-m-d\\TH:i:S":Z==="date"?"Y-m-d":"H:i:S",t.selectedDates.length>0&&(t.mobileInput.defaultValue=t.mobileInput.value=t.formatDate(t.selectedDates[0],t.mobileFormatStr)),t.config.minDate&&(t.mobileInput.min=t.formatDate(t.config.minDate,"Y-m-d")),t.config.maxDate&&(t.mobileInput.max=t.formatDate(t.config.maxDate,"Y-m-d")),t.input.getAttribute("step")&&(t.mobileInput.step=String(t.input.getAttribute("step"))),t.input.type="hidden",t.altInput!==void 0&&(t.altInput.type="hidden");try{t.input.parentNode&&t.input.parentNode.insertBefore(t.mobileInput,t.input.nextSibling)}catch{}g(t.mobileInput,"change",function(Q){t.setDate(jn(Q).value,!1,t.mobileFormatStr),It("onChange"),It("onClose")})}function rl(Z){if(t.isOpen===!0)return t.close();t.open(Z)}function It(Z,Q){if(t.config!==void 0){var se=t.config[Z];if(se!==void 0&&se.length>0)for(var he=0;se[he]&&he=0&&zn(Z,t.selectedDates[1])<=0}function ji(){t.config.noCalendar||t.isMobile||!t.monthNav||(t.yearElements.forEach(function(Z,Q){var se=new Date(t.currentYear,t.currentMonth,1);se.setMonth(t.currentMonth+Q),t.config.showMonths>1||t.config.monthSelectorType==="static"?t.monthElements[Q].textContent=Cr(se.getMonth(),t.config.shorthandCurrentMonth,t.l10n)+" ":t.monthsDropdownContainer.value=se.getMonth().toString(),Z.value=se.getFullYear().toString()}),t._hidePrevMonthArrow=t.config.minDate!==void 0&&(t.currentYear===t.config.minDate.getFullYear()?t.currentMonth<=t.config.minDate.getMonth():t.currentYeart.config.maxDate.getMonth():t.currentYear>t.config.maxDate.getFullYear()))}function fl(Z){var Q=Z||(t.config.altInput?t.config.altFormat:t.config.dateFormat);return t.selectedDates.map(function(se){return t.formatDate(se,Q)}).filter(function(se,he,qe){return t.config.mode!=="range"||t.config.enableTime||qe.indexOf(se)===he}).join(t.config.mode!=="range"?t.config.conjunction:t.l10n.rangeSeparator)}function Mn(Z){Z===void 0&&(Z=!0),t.mobileInput!==void 0&&t.mobileFormatStr&&(t.mobileInput.value=t.latestSelectedDateObj!==void 0?t.formatDate(t.latestSelectedDateObj,t.mobileFormatStr):""),t.input.value=fl(t.config.dateFormat),t.altInput!==void 0&&(t.altInput.value=fl(t.config.altFormat)),Z!==!1&&It("onValueUpdate")}function Rl(Z){var Q=jn(Z),se=t.prevMonthNav.contains(Q),he=t.nextMonthNav.contains(Q);se||he?Se(se?-1:1):t.yearElements.indexOf(Q)>=0?Q.select():Q.classList.contains("arrowUp")?t.changeYear(t.currentYear+1):Q.classList.contains("arrowDown")&&t.changeYear(t.currentYear-1)}function cl(Z){Z.preventDefault();var Q=Z.type==="keydown",se=jn(Z),he=se;t.amPM!==void 0&&se===t.amPM&&(t.amPM.textContent=t.l10n.amPM[Kn(t.amPM.textContent===t.l10n.amPM[0])]);var qe=parseFloat(he.getAttribute("min")),le=parseFloat(he.getAttribute("max")),Ee=parseFloat(he.getAttribute("step")),Re=parseInt(he.value,10),Ke=Z.delta||(Q?Z.which===38?1:-1:0),Ae=Re+Ee*Ke;if(typeof he.value<"u"&&he.value.length===2){var Ge=he===t.hourElement,ft=he===t.minuteElement;Aele&&(Ae=he===t.hourElement?Ae-le-Kn(!t.amPM):qe,ft&&$(void 0,1,t.hourElement)),t.amPM&&Ge&&(Ee===1?Ae+Re===23:Math.abs(Ae-Re)>Ee)&&(t.amPM.textContent=t.l10n.amPM[Kn(t.amPM.textContent===t.l10n.amPM[0])]),he.value=In(Ae)}}return l(),t}function ts(n,e){for(var t=Array.prototype.slice.call(n).filter(function(o){return o instanceof HTMLElement}),i=[],l=0;lt===e[i]))}function _E(n,e,t){const i=["value","formattedValue","element","dateFormat","options","input","flatpickr"];let l=lt(e,i),{$$slots:s={},$$scope:o}=e;const r=new Set(["onChange","onOpen","onClose","onMonthChange","onYearChange","onReady","onValueUpdate","onDayCreate"]);let{value:a=void 0,formattedValue:u="",element:f=void 0,dateFormat:c=void 0}=e,{options:d={}}=e,m=!1,{input:h=void 0,flatpickr:g=void 0}=e;Yt(()=>{const $=f??h,E=y(d);return E.onReady.push((M,L,I)=>{a===void 0&&S(M,L,I),fn().then(()=>{t(8,m=!0)})}),t(3,g=en($,Object.assign(E,f?{wrap:!0}:{}))),()=>{g.destroy()}});const _=_t();function y($={}){$=Object.assign({},$);for(const E of r){const M=(L,I,A)=>{_(hE(E),[L,I,A])};E in $?(Array.isArray($[E])||($[E]=[$[E]]),$[E].push(M)):$[E]=[M]}return $.onChange&&!$.onChange.includes(S)&&$.onChange.push(S),$}function S($,E,M){const L=eh(M,$);!th(a,L)&&(a||L)&&t(2,a=L),t(4,u=E)}function T($){ie[$?"unshift":"push"](()=>{h=$,t(0,h)})}return n.$$set=$=>{e=je(je({},e),Ut($)),t(1,l=lt(e,i)),"value"in $&&t(2,a=$.value),"formattedValue"in $&&t(4,u=$.formattedValue),"element"in $&&t(5,f=$.element),"dateFormat"in $&&t(6,c=$.dateFormat),"options"in $&&t(7,d=$.options),"input"in $&&t(0,h=$.input),"flatpickr"in $&&t(3,g=$.flatpickr),"$$scope"in $&&t(9,o=$.$$scope)},n.$$.update=()=>{if(n.$$.dirty&332&&g&&m&&(th(a,eh(g,g.selectedDates))||g.setDate(a,!0,c)),n.$$.dirty&392&&g&&m)for(const[$,E]of Object.entries(y(d)))g.set($,E)},[h,l,a,g,u,f,c,d,m,o,s,T]}class tf extends ye{constructor(e){super(),be(this,e,_E,mE,_e,{value:2,formattedValue:4,element:5,dateFormat:6,options:7,input:0,flatpickr:3})}}function gE(n){let e,t,i,l,s,o,r,a;function u(d){n[6](d)}function f(d){n[7](d)}let c={id:n[16],options:z.defaultFlatpickrOptions()};return n[2]!==void 0&&(c.value=n[2]),n[0].min!==void 0&&(c.formattedValue=n[0].min),s=new tf({props:c}),ie.push(()=>ve(s,"value",u)),ie.push(()=>ve(s,"formattedValue",f)),s.$on("close",n[8]),{c(){e=b("label"),t=Y("Min date (UTC)"),l=C(),H(s.$$.fragment),p(e,"for",i=n[16])},m(d,m){v(d,e,m),w(e,t),v(d,l,m),F(s,d,m),a=!0},p(d,m){(!a||m&65536&&i!==(i=d[16]))&&p(e,"for",i);const h={};m&65536&&(h.id=d[16]),!o&&m&4&&(o=!0,h.value=d[2],$e(()=>o=!1)),!r&&m&1&&(r=!0,h.formattedValue=d[0].min,$e(()=>r=!1)),s.$set(h)},i(d){a||(O(s.$$.fragment,d),a=!0)},o(d){D(s.$$.fragment,d),a=!1},d(d){d&&(k(e),k(l)),q(s,d)}}}function bE(n){let e,t,i,l,s,o,r,a;function u(d){n[9](d)}function f(d){n[10](d)}let c={id:n[16],options:z.defaultFlatpickrOptions()};return n[3]!==void 0&&(c.value=n[3]),n[0].max!==void 0&&(c.formattedValue=n[0].max),s=new tf({props:c}),ie.push(()=>ve(s,"value",u)),ie.push(()=>ve(s,"formattedValue",f)),s.$on("close",n[11]),{c(){e=b("label"),t=Y("Max date (UTC)"),l=C(),H(s.$$.fragment),p(e,"for",i=n[16])},m(d,m){v(d,e,m),w(e,t),v(d,l,m),F(s,d,m),a=!0},p(d,m){(!a||m&65536&&i!==(i=d[16]))&&p(e,"for",i);const h={};m&65536&&(h.id=d[16]),!o&&m&8&&(o=!0,h.value=d[3],$e(()=>o=!1)),!r&&m&1&&(r=!0,h.formattedValue=d[0].max,$e(()=>r=!1)),s.$set(h)},i(d){a||(O(s.$$.fragment,d),a=!0)},o(d){D(s.$$.fragment,d),a=!1},d(d){d&&(k(e),k(l)),q(s,d)}}}function yE(n){let e,t,i,l,s,o,r;return i=new fe({props:{class:"form-field",name:"fields."+n[1]+".min",$$slots:{default:[gE,({uniqueId:a})=>({16:a}),({uniqueId:a})=>a?65536:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".max",$$slots:{default:[bE,({uniqueId:a})=>({16:a}),({uniqueId:a})=>a?65536:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,u){v(a,e,u),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),r=!0},p(a,u){const f={};u&2&&(f.name="fields."+a[1]+".min"),u&196613&&(f.$$scope={dirty:u,ctx:a}),i.$set(f);const c={};u&2&&(c.name="fields."+a[1]+".max"),u&196617&&(c.$$scope={dirty:u,ctx:a}),o.$set(c)},i(a){r||(O(i.$$.fragment,a),O(o.$$.fragment,a),r=!0)},o(a){D(i.$$.fragment,a),D(o.$$.fragment,a),r=!1},d(a){a&&k(e),q(i),q(o)}}}function kE(n){let e,t,i;const l=[{key:n[1]},n[5]];function s(r){n[12](r)}let o={$$slots:{options:[yE]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[13]),e.$on("remove",n[14]),e.$on("duplicate",n[15]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&34?kt(l,[a&2&&{key:r[1]},a&32&&Ft(r[5])]):{};a&131087&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function vE(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e,r=s==null?void 0:s.min,a=s==null?void 0:s.max;function u($,E){$.detail&&$.detail.length==3&&t(0,s[E]=$.detail[1],s)}function f($){r=$,t(2,r),t(0,s)}function c($){n.$$.not_equal(s.min,$)&&(s.min=$,t(0,s))}const d=$=>u($,"min");function m($){a=$,t(3,a),t(0,s)}function h($){n.$$.not_equal(s.max,$)&&(s.max=$,t(0,s))}const g=$=>u($,"max");function _($){s=$,t(0,s)}function y($){Pe.call(this,n,$)}function S($){Pe.call(this,n,$)}function T($){Pe.call(this,n,$)}return n.$$set=$=>{e=je(je({},e),Ut($)),t(5,l=lt(e,i)),"field"in $&&t(0,s=$.field),"key"in $&&t(1,o=$.key)},n.$$.update=()=>{n.$$.dirty&5&&r!=(s==null?void 0:s.min)&&t(2,r=s==null?void 0:s.min),n.$$.dirty&9&&a!=(s==null?void 0:s.max)&&t(3,a=s==null?void 0:s.max)},[s,o,r,a,u,l,f,c,d,m,h,g,_,y,S,T]}class wE extends ye{constructor(e){super(),be(this,e,vE,kE,_e,{field:0,key:1})}}function SE(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Max size "),i=b("small"),i.textContent="(bytes)",s=C(),o=b("input"),p(e,"for",l=n[9]),p(o,"type","number"),p(o,"id",r=n[9]),p(o,"step","1"),p(o,"min","0"),o.value=a=n[0].maxSize||"",p(o,"placeholder","Default to max ~5MB")},m(c,d){v(c,e,d),w(e,t),w(e,i),v(c,s,d),v(c,o,d),u||(f=B(o,"input",n[3]),u=!0)},p(c,d){d&512&&l!==(l=c[9])&&p(e,"for",l),d&512&&r!==(r=c[9])&&p(o,"id",r),d&1&&a!==(a=c[0].maxSize||"")&&o.value!==a&&(o.value=a)},d(c){c&&(k(e),k(s),k(o)),u=!1,f()}}}function TE(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Strip urls domain",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[9]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[9])},m(c,d){v(c,e,d),e.checked=n[0].convertURLs,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[4]),Me(He.call(null,r,{text:"This could help making the editor content more portable between environments since there will be no local base url to replace."}))],u=!0)},p(c,d){d&512&&t!==(t=c[9])&&p(e,"id",t),d&1&&(e.checked=c[0].convertURLs),d&512&&a!==(a=c[9])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function $E(n){let e,t,i,l;return e=new fe({props:{class:"form-field m-b-sm",name:"fields."+n[1]+".maxSize",$$slots:{default:[SE,({uniqueId:s})=>({9:s}),({uniqueId:s})=>s?512:0]},$$scope:{ctx:n}}}),i=new fe({props:{class:"form-field form-field-toggle",name:"fields."+n[1]+".convertURLs",$$slots:{default:[TE,({uniqueId:s})=>({9:s}),({uniqueId:s})=>s?512:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o&2&&(r.name="fields."+s[1]+".maxSize"),o&1537&&(r.$$scope={dirty:o,ctx:s}),e.$set(r);const a={};o&2&&(a.name="fields."+s[1]+".convertURLs"),o&1537&&(a.$$scope={dirty:o,ctx:s}),i.$set(a)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function CE(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[5](r)}let o={$$slots:{options:[$E]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[6]),e.$on("remove",n[7]),e.$on("duplicate",n[8]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};a&1027&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function OE(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;const r=m=>t(0,s.maxSize=m.target.value<<0,s);function a(){s.convertURLs=this.checked,t(0,s)}function u(m){s=m,t(0,s)}function f(m){Pe.call(this,n,m)}function c(m){Pe.call(this,n,m)}function d(m){Pe.call(this,n,m)}return n.$$set=m=>{e=je(je({},e),Ut(m)),t(2,l=lt(e,i)),"field"in m&&t(0,s=m.field),"key"in m&&t(1,o=m.key)},[s,o,l,r,a,u,f,c,d]}class EE extends ye{constructor(e){super(),be(this,e,OE,CE,_e,{field:0,key:1})}}function ME(n){let e,t,i,l,s=[{type:t=n[5].type||"text"},{value:n[4]},{disabled:n[3]},{readOnly:n[2]},n[5]],o={};for(let r=0;r{t(0,o=z.splitNonEmpty(c.target.value,r))};return n.$$set=c=>{e=je(je({},e),Ut(c)),t(5,s=lt(e,l)),"value"in c&&t(0,o=c.value),"separator"in c&&t(1,r=c.separator),"readonly"in c&&t(2,a=c.readonly),"disabled"in c&&t(3,u=c.disabled)},n.$$.update=()=>{n.$$.dirty&3&&t(4,i=z.joinNonEmpty(o,r+" "))},[o,r,a,u,i,s,f]}class _o extends ye{constructor(e){super(),be(this,e,DE,ME,_e,{value:0,separator:1,readonly:2,disabled:3})}}function IE(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;function h(_){n[3](_)}let g={id:n[9],disabled:!z.isEmpty(n[0].onlyDomains)};return n[0].exceptDomains!==void 0&&(g.value=n[0].exceptDomains),r=new _o({props:g}),ie.push(()=>ve(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Except domains",i=C(),l=b("i"),o=C(),H(r.$$.fragment),u=C(),f=b("div"),f.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[9]),p(f,"class","help-block")},m(_,y){v(_,e,y),w(e,t),w(e,i),w(e,l),v(_,o,y),F(r,_,y),v(_,u,y),v(_,f,y),c=!0,d||(m=Me(He.call(null,l,{text:`List of domains that are NOT allowed. + This field is disabled if "Only domains" is set.`,position:"top"})),d=!0)},p(_,y){(!c||y&512&&s!==(s=_[9]))&&p(e,"for",s);const S={};y&512&&(S.id=_[9]),y&1&&(S.disabled=!z.isEmpty(_[0].onlyDomains)),!a&&y&1&&(a=!0,S.value=_[0].exceptDomains,$e(()=>a=!1)),r.$set(S)},i(_){c||(O(r.$$.fragment,_),c=!0)},o(_){D(r.$$.fragment,_),c=!1},d(_){_&&(k(e),k(o),k(u),k(f)),q(r,_),d=!1,m()}}}function LE(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;function h(_){n[4](_)}let g={id:n[9]+".onlyDomains",disabled:!z.isEmpty(n[0].exceptDomains)};return n[0].onlyDomains!==void 0&&(g.value=n[0].onlyDomains),r=new _o({props:g}),ie.push(()=>ve(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Only domains",i=C(),l=b("i"),o=C(),H(r.$$.fragment),u=C(),f=b("div"),f.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[9]+".onlyDomains"),p(f,"class","help-block")},m(_,y){v(_,e,y),w(e,t),w(e,i),w(e,l),v(_,o,y),F(r,_,y),v(_,u,y),v(_,f,y),c=!0,d||(m=Me(He.call(null,l,{text:`List of domains that are ONLY allowed. + This field is disabled if "Except domains" is set.`,position:"top"})),d=!0)},p(_,y){(!c||y&512&&s!==(s=_[9]+".onlyDomains"))&&p(e,"for",s);const S={};y&512&&(S.id=_[9]+".onlyDomains"),y&1&&(S.disabled=!z.isEmpty(_[0].exceptDomains)),!a&&y&1&&(a=!0,S.value=_[0].onlyDomains,$e(()=>a=!1)),r.$set(S)},i(_){c||(O(r.$$.fragment,_),c=!0)},o(_){D(r.$$.fragment,_),c=!1},d(_){_&&(k(e),k(o),k(u),k(f)),q(r,_),d=!1,m()}}}function AE(n){let e,t,i,l,s,o,r;return i=new fe({props:{class:"form-field",name:"fields."+n[1]+".exceptDomains",$$slots:{default:[IE,({uniqueId:a})=>({9:a}),({uniqueId:a})=>a?512:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".onlyDomains",$$slots:{default:[LE,({uniqueId:a})=>({9:a}),({uniqueId:a})=>a?512:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,u){v(a,e,u),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),r=!0},p(a,u){const f={};u&2&&(f.name="fields."+a[1]+".exceptDomains"),u&1537&&(f.$$scope={dirty:u,ctx:a}),i.$set(f);const c={};u&2&&(c.name="fields."+a[1]+".onlyDomains"),u&1537&&(c.$$scope={dirty:u,ctx:a}),o.$set(c)},i(a){r||(O(i.$$.fragment,a),O(o.$$.fragment,a),r=!0)},o(a){D(i.$$.fragment,a),D(o.$$.fragment,a),r=!1},d(a){a&&k(e),q(i),q(o)}}}function PE(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[5](r)}let o={$$slots:{options:[AE]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[6]),e.$on("remove",n[7]),e.$on("duplicate",n[8]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};a&1027&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function NE(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;function r(m){n.$$.not_equal(s.exceptDomains,m)&&(s.exceptDomains=m,t(0,s))}function a(m){n.$$.not_equal(s.onlyDomains,m)&&(s.onlyDomains=m,t(0,s))}function u(m){s=m,t(0,s)}function f(m){Pe.call(this,n,m)}function c(m){Pe.call(this,n,m)}function d(m){Pe.call(this,n,m)}return n.$$set=m=>{e=je(je({},e),Ut(m)),t(2,l=lt(e,i)),"field"in m&&t(0,s=m.field),"key"in m&&t(1,o=m.key)},[s,o,l,r,a,u,f,c,d]}class lk extends ye{constructor(e){super(),be(this,e,NE,PE,_e,{field:0,key:1})}}function RE(n){let e,t=(n[0].ext||"N/A")+"",i,l,s,o=n[0].mimeType+"",r;return{c(){e=b("span"),i=Y(t),l=C(),s=b("small"),r=Y(o),p(e,"class","txt"),p(s,"class","txt-hint")},m(a,u){v(a,e,u),w(e,i),v(a,l,u),v(a,s,u),w(s,r)},p(a,[u]){u&1&&t!==(t=(a[0].ext||"N/A")+"")&&ue(i,t),u&1&&o!==(o=a[0].mimeType+"")&&ue(r,o)},i:te,o:te,d(a){a&&(k(e),k(l),k(s))}}}function FE(n,e,t){let{item:i={}}=e;return n.$$set=l=>{"item"in l&&t(0,i=l.item)},[i]}class nh extends ye{constructor(e){super(),be(this,e,FE,RE,_e,{item:0})}}const qE=[{ext:".xpm",mimeType:"image/x-xpixmap"},{ext:".7z",mimeType:"application/x-7z-compressed"},{ext:".zip",mimeType:"application/zip"},{ext:".xlsx",mimeType:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},{ext:".docx",mimeType:"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},{ext:".pptx",mimeType:"application/vnd.openxmlformats-officedocument.presentationml.presentation"},{ext:".epub",mimeType:"application/epub+zip"},{ext:".jar",mimeType:"application/jar"},{ext:".odt",mimeType:"application/vnd.oasis.opendocument.text"},{ext:".ott",mimeType:"application/vnd.oasis.opendocument.text-template"},{ext:".ods",mimeType:"application/vnd.oasis.opendocument.spreadsheet"},{ext:".ots",mimeType:"application/vnd.oasis.opendocument.spreadsheet-template"},{ext:".odp",mimeType:"application/vnd.oasis.opendocument.presentation"},{ext:".otp",mimeType:"application/vnd.oasis.opendocument.presentation-template"},{ext:".odg",mimeType:"application/vnd.oasis.opendocument.graphics"},{ext:".otg",mimeType:"application/vnd.oasis.opendocument.graphics-template"},{ext:".odf",mimeType:"application/vnd.oasis.opendocument.formula"},{ext:".odc",mimeType:"application/vnd.oasis.opendocument.chart"},{ext:".sxc",mimeType:"application/vnd.sun.xml.calc"},{ext:".pdf",mimeType:"application/pdf"},{ext:".fdf",mimeType:"application/vnd.fdf"},{ext:"",mimeType:"application/x-ole-storage"},{ext:".msi",mimeType:"application/x-ms-installer"},{ext:".aaf",mimeType:"application/octet-stream"},{ext:".msg",mimeType:"application/vnd.ms-outlook"},{ext:".xls",mimeType:"application/vnd.ms-excel"},{ext:".pub",mimeType:"application/vnd.ms-publisher"},{ext:".ppt",mimeType:"application/vnd.ms-powerpoint"},{ext:".doc",mimeType:"application/msword"},{ext:".ps",mimeType:"application/postscript"},{ext:".psd",mimeType:"image/vnd.adobe.photoshop"},{ext:".p7s",mimeType:"application/pkcs7-signature"},{ext:".ogg",mimeType:"application/ogg"},{ext:".oga",mimeType:"audio/ogg"},{ext:".ogv",mimeType:"video/ogg"},{ext:".png",mimeType:"image/png"},{ext:".png",mimeType:"image/vnd.mozilla.apng"},{ext:".jpg",mimeType:"image/jpeg"},{ext:".jxl",mimeType:"image/jxl"},{ext:".jp2",mimeType:"image/jp2"},{ext:".jpf",mimeType:"image/jpx"},{ext:".jpm",mimeType:"image/jpm"},{ext:".jxs",mimeType:"image/jxs"},{ext:".gif",mimeType:"image/gif"},{ext:".webp",mimeType:"image/webp"},{ext:".exe",mimeType:"application/vnd.microsoft.portable-executable"},{ext:"",mimeType:"application/x-elf"},{ext:"",mimeType:"application/x-object"},{ext:"",mimeType:"application/x-executable"},{ext:".so",mimeType:"application/x-sharedlib"},{ext:"",mimeType:"application/x-coredump"},{ext:".a",mimeType:"application/x-archive"},{ext:".deb",mimeType:"application/vnd.debian.binary-package"},{ext:".tar",mimeType:"application/x-tar"},{ext:".xar",mimeType:"application/x-xar"},{ext:".bz2",mimeType:"application/x-bzip2"},{ext:".fits",mimeType:"application/fits"},{ext:".tiff",mimeType:"image/tiff"},{ext:".bmp",mimeType:"image/bmp"},{ext:".ico",mimeType:"image/x-icon"},{ext:".mp3",mimeType:"audio/mpeg"},{ext:".flac",mimeType:"audio/flac"},{ext:".midi",mimeType:"audio/midi"},{ext:".ape",mimeType:"audio/ape"},{ext:".mpc",mimeType:"audio/musepack"},{ext:".amr",mimeType:"audio/amr"},{ext:".wav",mimeType:"audio/wav"},{ext:".aiff",mimeType:"audio/aiff"},{ext:".au",mimeType:"audio/basic"},{ext:".mpeg",mimeType:"video/mpeg"},{ext:".mov",mimeType:"video/quicktime"},{ext:".mqv",mimeType:"video/quicktime"},{ext:".mp4",mimeType:"video/mp4"},{ext:".webm",mimeType:"video/webm"},{ext:".3gp",mimeType:"video/3gpp"},{ext:".3g2",mimeType:"video/3gpp2"},{ext:".avi",mimeType:"video/x-msvideo"},{ext:".flv",mimeType:"video/x-flv"},{ext:".mkv",mimeType:"video/x-matroska"},{ext:".asf",mimeType:"video/x-ms-asf"},{ext:".aac",mimeType:"audio/aac"},{ext:".voc",mimeType:"audio/x-unknown"},{ext:".mp4",mimeType:"audio/mp4"},{ext:".m4a",mimeType:"audio/x-m4a"},{ext:".m3u",mimeType:"application/vnd.apple.mpegurl"},{ext:".m4v",mimeType:"video/x-m4v"},{ext:".rmvb",mimeType:"application/vnd.rn-realmedia-vbr"},{ext:".gz",mimeType:"application/gzip"},{ext:".class",mimeType:"application/x-java-applet"},{ext:".swf",mimeType:"application/x-shockwave-flash"},{ext:".crx",mimeType:"application/x-chrome-extension"},{ext:".ttf",mimeType:"font/ttf"},{ext:".woff",mimeType:"font/woff"},{ext:".woff2",mimeType:"font/woff2"},{ext:".otf",mimeType:"font/otf"},{ext:".ttc",mimeType:"font/collection"},{ext:".eot",mimeType:"application/vnd.ms-fontobject"},{ext:".wasm",mimeType:"application/wasm"},{ext:".shx",mimeType:"application/vnd.shx"},{ext:".shp",mimeType:"application/vnd.shp"},{ext:".dbf",mimeType:"application/x-dbf"},{ext:".dcm",mimeType:"application/dicom"},{ext:".rar",mimeType:"application/x-rar-compressed"},{ext:".djvu",mimeType:"image/vnd.djvu"},{ext:".mobi",mimeType:"application/x-mobipocket-ebook"},{ext:".lit",mimeType:"application/x-ms-reader"},{ext:".bpg",mimeType:"image/bpg"},{ext:".sqlite",mimeType:"application/vnd.sqlite3"},{ext:".dwg",mimeType:"image/vnd.dwg"},{ext:".nes",mimeType:"application/vnd.nintendo.snes.rom"},{ext:".lnk",mimeType:"application/x-ms-shortcut"},{ext:".macho",mimeType:"application/x-mach-binary"},{ext:".qcp",mimeType:"audio/qcelp"},{ext:".icns",mimeType:"image/x-icns"},{ext:".heic",mimeType:"image/heic"},{ext:".heic",mimeType:"image/heic-sequence"},{ext:".heif",mimeType:"image/heif"},{ext:".heif",mimeType:"image/heif-sequence"},{ext:".hdr",mimeType:"image/vnd.radiance"},{ext:".mrc",mimeType:"application/marc"},{ext:".mdb",mimeType:"application/x-msaccess"},{ext:".accdb",mimeType:"application/x-msaccess"},{ext:".zst",mimeType:"application/zstd"},{ext:".cab",mimeType:"application/vnd.ms-cab-compressed"},{ext:".rpm",mimeType:"application/x-rpm"},{ext:".xz",mimeType:"application/x-xz"},{ext:".lz",mimeType:"application/lzip"},{ext:".torrent",mimeType:"application/x-bittorrent"},{ext:".cpio",mimeType:"application/x-cpio"},{ext:"",mimeType:"application/tzif"},{ext:".xcf",mimeType:"image/x-xcf"},{ext:".pat",mimeType:"image/x-gimp-pat"},{ext:".gbr",mimeType:"image/x-gimp-gbr"},{ext:".glb",mimeType:"model/gltf-binary"},{ext:".avif",mimeType:"image/avif"},{ext:".cab",mimeType:"application/x-installshield"},{ext:".jxr",mimeType:"image/jxr"},{ext:".txt",mimeType:"text/plain"},{ext:".html",mimeType:"text/html"},{ext:".svg",mimeType:"image/svg+xml"},{ext:".xml",mimeType:"text/xml"},{ext:".rss",mimeType:"application/rss+xml"},{ext:".atom",mimeType:"applicatiotom+xml"},{ext:".x3d",mimeType:"model/x3d+xml"},{ext:".kml",mimeType:"application/vnd.google-earth.kml+xml"},{ext:".xlf",mimeType:"application/x-xliff+xml"},{ext:".dae",mimeType:"model/vnd.collada+xml"},{ext:".gml",mimeType:"application/gml+xml"},{ext:".gpx",mimeType:"application/gpx+xml"},{ext:".tcx",mimeType:"application/vnd.garmin.tcx+xml"},{ext:".amf",mimeType:"application/x-amf"},{ext:".3mf",mimeType:"application/vnd.ms-package.3dmanufacturing-3dmodel+xml"},{ext:".xfdf",mimeType:"application/vnd.adobe.xfdf"},{ext:".owl",mimeType:"application/owl+xml"},{ext:".php",mimeType:"text/x-php"},{ext:".js",mimeType:"application/javascript"},{ext:".lua",mimeType:"text/x-lua"},{ext:".pl",mimeType:"text/x-perl"},{ext:".py",mimeType:"text/x-python"},{ext:".json",mimeType:"application/json"},{ext:".geojson",mimeType:"application/geo+json"},{ext:".har",mimeType:"application/json"},{ext:".ndjson",mimeType:"application/x-ndjson"},{ext:".rtf",mimeType:"text/rtf"},{ext:".srt",mimeType:"application/x-subrip"},{ext:".tcl",mimeType:"text/x-tcl"},{ext:".csv",mimeType:"text/csv"},{ext:".tsv",mimeType:"text/tab-separated-values"},{ext:".vcf",mimeType:"text/vcard"},{ext:".ics",mimeType:"text/calendar"},{ext:".warc",mimeType:"application/warc"},{ext:".vtt",mimeType:"text/vtt"},{ext:"",mimeType:"application/octet-stream"}];function HE(n){let e,t,i;function l(o){n[16](o)}let s={id:n[23],items:n[4],readonly:!n[24]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new xn({props:s}),ie.push(()=>ve(e,"keyOfSelected",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&8388608&&(a.id=o[23]),r&16777216&&(a.readonly=!o[24]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function jE(n){let e,t,i,l,s,o;return i=new fe({props:{class:"form-field form-field-single-multiple-select "+(n[24]?"":"readonly"),inlineError:!0,$$slots:{default:[HE,({uniqueId:r})=>({23:r}),({uniqueId:r})=>r?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),H(i.$$.fragment),l=C(),s=b("div"),p(e,"class","separator"),p(s,"class","separator")},m(r,a){v(r,e,a),v(r,t,a),F(i,r,a),v(r,l,a),v(r,s,a),o=!0},p(r,a){const u={};a&16777216&&(u.class="form-field form-field-single-multiple-select "+(r[24]?"":"readonly")),a&58720260&&(u.$$scope={dirty:a,ctx:r}),i.$set(u)},i(r){o||(O(i.$$.fragment,r),o=!0)},o(r){D(i.$$.fragment,r),o=!1},d(r){r&&(k(e),k(t),k(l),k(s)),q(i,r)}}}function zE(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("button"),e.innerHTML='Images (jpg, png, svg, gif, webp)',t=C(),i=b("button"),i.innerHTML='Documents (pdf, doc/docx, xls/xlsx)',l=C(),s=b("button"),s.innerHTML='Videos (mp4, avi, mov, 3gp)',o=C(),r=b("button"),r.innerHTML='Archives (zip, 7zip, rar)',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem"),p(i,"type","button"),p(i,"class","dropdown-item closable"),p(i,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(s,"role","menuitem"),p(r,"type","button"),p(r,"class","dropdown-item closable"),p(r,"role","menuitem")},m(f,c){v(f,e,c),v(f,t,c),v(f,i,c),v(f,l,c),v(f,s,c),v(f,o,c),v(f,r,c),a||(u=[B(e,"click",n[8]),B(i,"click",n[9]),B(s,"click",n[10]),B(r,"click",n[11])],a=!0)},p:te,d(f){f&&(k(e),k(t),k(i),k(l),k(s),k(o),k(r)),a=!1,De(u)}}}function UE(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T;function $(M){n[7](M)}let E={id:n[23],multiple:!0,searchable:!0,closable:!1,selectionKey:"mimeType",selectPlaceholder:"No restriction",items:n[3],labelComponent:nh,optionComponent:nh};return n[0].mimeTypes!==void 0&&(E.keyOfSelected=n[0].mimeTypes),r=new xn({props:E}),ie.push(()=>ve(r,"keyOfSelected",$)),_=new Hn({props:{class:"dropdown dropdown-sm dropdown-nowrap dropdown-left",$$slots:{default:[zE]},$$scope:{ctx:n}}}),{c(){e=b("label"),t=b("span"),t.textContent="Allowed mime types",i=C(),l=b("i"),o=C(),H(r.$$.fragment),u=C(),f=b("div"),c=b("div"),d=b("span"),d.textContent="Choose presets",m=C(),h=b("i"),g=C(),H(_.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(d,"class","txt link-primary"),p(h,"class","ri-arrow-drop-down-fill"),p(h,"aria-hidden","true"),p(c,"tabindex","0"),p(c,"role","button"),p(c,"class","inline-flex flex-gap-0"),p(f,"class","help-block")},m(M,L){v(M,e,L),w(e,t),w(e,i),w(e,l),v(M,o,L),F(r,M,L),v(M,u,L),v(M,f,L),w(f,c),w(c,d),w(c,m),w(c,h),w(c,g),F(_,c,null),y=!0,S||(T=Me(He.call(null,l,{text:`Allow files ONLY with the listed mime types. + Leave empty for no restriction.`,position:"top"})),S=!0)},p(M,L){(!y||L&8388608&&s!==(s=M[23]))&&p(e,"for",s);const I={};L&8388608&&(I.id=M[23]),L&8&&(I.items=M[3]),!a&&L&1&&(a=!0,I.keyOfSelected=M[0].mimeTypes,$e(()=>a=!1)),r.$set(I);const A={};L&33554433&&(A.$$scope={dirty:L,ctx:M}),_.$set(A)},i(M){y||(O(r.$$.fragment,M),O(_.$$.fragment,M),y=!0)},o(M){D(r.$$.fragment,M),D(_.$$.fragment,M),y=!1},d(M){M&&(k(e),k(o),k(u),k(f)),q(r,M),q(_),S=!1,T()}}}function VE(n){let e;return{c(){e=b("ul"),e.innerHTML=`
  • WxH + (e.g. 100x50) - crop to WxH viewbox (from center)
  • WxHt + (e.g. 100x50t) - crop to WxH viewbox (from top)
  • WxHb + (e.g. 100x50b) - crop to WxH viewbox (from bottom)
  • WxHf + (e.g. 100x50f) - fit inside a WxH viewbox (without cropping)
  • 0xH + (e.g. 0x50) - resize to H height preserving the aspect ratio
  • Wx0 + (e.g. 100x0) - resize to W width preserving the aspect ratio
  • `,p(e,"class","m-0")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function BE(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E;function M(I){n[12](I)}let L={id:n[23],placeholder:"e.g. 50x50, 480x720"};return n[0].thumbs!==void 0&&(L.value=n[0].thumbs),r=new _o({props:L}),ie.push(()=>ve(r,"value",M)),S=new Hn({props:{class:"dropdown dropdown-sm dropdown-center dropdown-nowrap p-r-10",$$slots:{default:[VE]},$$scope:{ctx:n}}}),{c(){e=b("label"),t=b("span"),t.textContent="Thumb sizes",i=C(),l=b("i"),o=C(),H(r.$$.fragment),u=C(),f=b("div"),c=b("span"),c.textContent="Use comma as separator.",d=C(),m=b("button"),h=b("span"),h.textContent="Supported formats",g=C(),_=b("i"),y=C(),H(S.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(c,"class","txt"),p(h,"class","txt link-primary"),p(_,"class","ri-arrow-drop-down-fill"),p(_,"aria-hidden","true"),p(m,"type","button"),p(m,"class","inline-flex flex-gap-0"),p(f,"class","help-block")},m(I,A){v(I,e,A),w(e,t),w(e,i),w(e,l),v(I,o,A),F(r,I,A),v(I,u,A),v(I,f,A),w(f,c),w(f,d),w(f,m),w(m,h),w(m,g),w(m,_),w(m,y),F(S,m,null),T=!0,$||(E=Me(He.call(null,l,{text:"List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.",position:"top"})),$=!0)},p(I,A){(!T||A&8388608&&s!==(s=I[23]))&&p(e,"for",s);const P={};A&8388608&&(P.id=I[23]),!a&&A&1&&(a=!0,P.value=I[0].thumbs,$e(()=>a=!1)),r.$set(P);const R={};A&33554432&&(R.$$scope={dirty:A,ctx:I}),S.$set(R)},i(I){T||(O(r.$$.fragment,I),O(S.$$.fragment,I),T=!0)},o(I){D(r.$$.fragment,I),D(S.$$.fragment,I),T=!1},d(I){I&&(k(e),k(o),k(u),k(f)),q(r,I),q(S),$=!1,E()}}}function WE(n){let e,t,i,l,s,o,r,a,u,f,c;return{c(){e=b("label"),t=Y("Max file size"),l=C(),s=b("input"),a=C(),u=b("div"),u.textContent="Must be in bytes.",p(e,"for",i=n[23]),p(s,"type","number"),p(s,"id",o=n[23]),p(s,"step","1"),p(s,"min","0"),s.value=r=n[0].maxSize||"",p(s,"placeholder","Default to max ~5MB"),p(u,"class","help-block")},m(d,m){v(d,e,m),w(e,t),v(d,l,m),v(d,s,m),v(d,a,m),v(d,u,m),f||(c=B(s,"input",n[13]),f=!0)},p(d,m){m&8388608&&i!==(i=d[23])&&p(e,"for",i),m&8388608&&o!==(o=d[23])&&p(s,"id",o),m&1&&r!==(r=d[0].maxSize||"")&&s.value!==r&&(s.value=r)},d(d){d&&(k(e),k(l),k(s),k(a),k(u)),f=!1,c()}}}function ih(n){let e,t,i;return t=new fe({props:{class:"form-field",name:"fields."+n[1]+".maxSelect",$$slots:{default:[YE,({uniqueId:l})=>({23:l}),({uniqueId:l})=>l?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","col-sm-3")},m(l,s){v(l,e,s),F(t,e,null),i=!0},p(l,s){const o={};s&2&&(o.name="fields."+l[1]+".maxSelect"),s&41943041&&(o.$$scope={dirty:s,ctx:l}),t.$set(o)},i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function YE(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Max select"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"id",o=n[23]),p(s,"type","number"),p(s,"step","1"),p(s,"min","2"),s.required=!0,p(s,"placeholder","Default to single")},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].maxSelect),r||(a=B(s,"input",n[14]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&St(s.value)!==u[0].maxSelect&&ce(s,u[0].maxSelect)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function KE(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Protected",r=C(),a=b("small"),a.innerHTML=`it will require View API rule permissions and file token to be accessible + (Learn more)`,p(e,"type","checkbox"),p(e,"id",t=n[23]),p(s,"class","txt"),p(l,"for",o=n[23]),p(a,"class","txt-hint")},m(c,d){v(c,e,d),e.checked=n[0].protected,v(c,i,d),v(c,l,d),w(l,s),v(c,r,d),v(c,a,d),u||(f=B(e,"change",n[15]),u=!0)},p(c,d){d&8388608&&t!==(t=c[23])&&p(e,"id",t),d&1&&(e.checked=c[0].protected),d&8388608&&o!==(o=c[23])&&p(l,"for",o)},d(c){c&&(k(e),k(i),k(l),k(r),k(a)),u=!1,f()}}}function JE(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g;i=new fe({props:{class:"form-field",name:"fields."+n[1]+".mimeTypes",$$slots:{default:[UE,({uniqueId:y})=>({23:y}),({uniqueId:y})=>y?8388608:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".thumbs",$$slots:{default:[BE,({uniqueId:y})=>({23:y}),({uniqueId:y})=>y?8388608:0]},$$scope:{ctx:n}}}),f=new fe({props:{class:"form-field",name:"fields."+n[1]+".maxSize",$$slots:{default:[WE,({uniqueId:y})=>({23:y}),({uniqueId:y})=>y?8388608:0]},$$scope:{ctx:n}}});let _=!n[2]&&ih(n);return h=new fe({props:{class:"form-field form-field-toggle",name:"fields."+n[1]+".protected",$$slots:{default:[KE,({uniqueId:y})=>({23:y}),({uniqueId:y})=>y?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),a=C(),u=b("div"),H(f.$$.fragment),d=C(),_&&_.c(),m=C(),H(h.$$.fragment),p(t,"class","col-sm-12"),p(s,"class",r=n[2]?"col-sm-8":"col-sm-6"),p(u,"class",c=n[2]?"col-sm-4":"col-sm-3"),p(e,"class","grid grid-sm")},m(y,S){v(y,e,S),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,a),w(e,u),F(f,u,null),w(e,d),_&&_.m(e,null),w(e,m),F(h,e,null),g=!0},p(y,S){const T={};S&2&&(T.name="fields."+y[1]+".mimeTypes"),S&41943049&&(T.$$scope={dirty:S,ctx:y}),i.$set(T);const $={};S&2&&($.name="fields."+y[1]+".thumbs"),S&41943041&&($.$$scope={dirty:S,ctx:y}),o.$set($),(!g||S&4&&r!==(r=y[2]?"col-sm-8":"col-sm-6"))&&p(s,"class",r);const E={};S&2&&(E.name="fields."+y[1]+".maxSize"),S&41943041&&(E.$$scope={dirty:S,ctx:y}),f.$set(E),(!g||S&4&&c!==(c=y[2]?"col-sm-4":"col-sm-3"))&&p(u,"class",c),y[2]?_&&(re(),D(_,1,1,()=>{_=null}),ae()):_?(_.p(y,S),S&4&&O(_,1)):(_=ih(y),_.c(),O(_,1),_.m(e,m));const M={};S&2&&(M.name="fields."+y[1]+".protected"),S&41943041&&(M.$$scope={dirty:S,ctx:y}),h.$set(M)},i(y){g||(O(i.$$.fragment,y),O(o.$$.fragment,y),O(f.$$.fragment,y),O(_),O(h.$$.fragment,y),g=!0)},o(y){D(i.$$.fragment,y),D(o.$$.fragment,y),D(f.$$.fragment,y),D(_),D(h.$$.fragment,y),g=!1},d(y){y&&k(e),q(i),q(o),q(f),_&&_.d(),q(h)}}}function ZE(n){let e,t,i;const l=[{key:n[1]},n[5]];function s(r){n[17](r)}let o={$$slots:{options:[JE],default:[jE,({interactive:r})=>({24:r}),({interactive:r})=>r?16777216:0]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[18]),e.$on("remove",n[19]),e.$on("duplicate",n[20]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&34?kt(l,[a&2&&{key:r[1]},a&32&&Ft(r[5])]):{};a&50331663&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function GE(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;const r=[{label:"Single",value:!0},{label:"Multiple",value:!1}];let a=qE.slice(),u=s.maxSelect<=1,f=u;function c(){t(0,s.maxSelect=1,s),t(0,s.thumbs=[],s),t(0,s.mimeTypes=[],s),t(2,u=!0),t(6,f=u)}function d(){if(z.isEmpty(s.mimeTypes))return;const R=[];for(const N of s.mimeTypes)a.find(U=>U.mimeType===N)||R.push({mimeType:N});R.length&&t(3,a=a.concat(R))}function m(R){n.$$.not_equal(s.mimeTypes,R)&&(s.mimeTypes=R,t(0,s),t(6,f),t(2,u))}const h=()=>{t(0,s.mimeTypes=["image/jpeg","image/png","image/svg+xml","image/gif","image/webp"],s)},g=()=>{t(0,s.mimeTypes=["application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],s)},_=()=>{t(0,s.mimeTypes=["video/mp4","video/x-ms-wmv","video/quicktime","video/3gpp"],s)},y=()=>{t(0,s.mimeTypes=["application/zip","application/x-7z-compressed","application/x-rar-compressed"],s)};function S(R){n.$$.not_equal(s.thumbs,R)&&(s.thumbs=R,t(0,s),t(6,f),t(2,u))}const T=R=>t(0,s.maxSize=R.target.value<<0,s);function $(){s.maxSelect=St(this.value),t(0,s),t(6,f),t(2,u)}function E(){s.protected=this.checked,t(0,s),t(6,f),t(2,u)}function M(R){u=R,t(2,u)}function L(R){s=R,t(0,s),t(6,f),t(2,u)}function I(R){Pe.call(this,n,R)}function A(R){Pe.call(this,n,R)}function P(R){Pe.call(this,n,R)}return n.$$set=R=>{e=je(je({},e),Ut(R)),t(5,l=lt(e,i)),"field"in R&&t(0,s=R.field),"key"in R&&t(1,o=R.key)},n.$$.update=()=>{n.$$.dirty&68&&f!=u&&(t(6,f=u),u?t(0,s.maxSelect=1,s):t(0,s.maxSelect=99,s)),n.$$.dirty&1&&(typeof s.maxSelect>"u"?c():d())},[s,o,u,a,r,l,f,m,h,g,_,y,S,T,$,E,M,L,I,A,P]}class XE extends ye{constructor(e){super(),be(this,e,GE,ZE,_e,{field:0,key:1})}}function QE(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Max size "),i=b("small"),i.textContent="(bytes)",s=C(),o=b("input"),p(e,"for",l=n[10]),p(o,"type","number"),p(o,"id",r=n[10]),p(o,"step","1"),p(o,"min","0"),o.value=a=n[0].maxSize||"",p(o,"placeholder","Default to max ~5MB")},m(c,d){v(c,e,d),w(e,t),w(e,i),v(c,s,d),v(c,o,d),u||(f=B(o,"input",n[4]),u=!0)},p(c,d){d&1024&&l!==(l=c[10])&&p(e,"for",l),d&1024&&r!==(r=c[10])&&p(o,"id",r),d&1&&a!==(a=c[0].maxSize||"")&&o.value!==a&&(o.value=a)},d(c){c&&(k(e),k(s),k(o)),u=!1,f()}}}function xE(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function eM(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function lh(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L='"{"a":1,"b":2}"',I,A,P,R,N,U,j,V,K,J,ee,X,oe;return{c(){e=b("div"),t=b("div"),i=b("div"),l=Y("In order to support seamlessly both "),s=b("code"),s.textContent="application/json",o=Y(` and + `),r=b("code"),r.textContent="multipart/form-data",a=Y(` + requests, the following normalization rules are applied if the `),u=b("code"),u.textContent="json",f=Y(` field + is a + `),c=b("strong"),c.textContent="plain string",d=Y(`: + `),m=b("ul"),h=b("li"),h.innerHTML=""true" is converted to the json true",g=C(),_=b("li"),_.innerHTML=""false" is converted to the json false",y=C(),S=b("li"),S.innerHTML=""null" is converted to the json null",T=C(),$=b("li"),$.innerHTML=""[1,2,3]" is converted to the json [1,2,3]",E=C(),M=b("li"),I=Y(L),A=Y(" is converted to the json "),P=b("code"),P.textContent='{"a":1,"b":2}',R=C(),N=b("li"),N.textContent="numeric strings are converted to json number",U=C(),j=b("li"),j.textContent="double quoted strings are left as they are (aka. without normalizations)",V=C(),K=b("li"),K.textContent="any other string (empty string too) is double quoted",J=Y(` + Alternatively, if you want to avoid the string value normalizations, you can wrap your + data inside an object, eg.`),ee=b("code"),ee.textContent='{"data": anything}',p(i,"class","content"),p(t,"class","alert alert-warning m-b-0 m-t-10"),p(e,"class","block")},m(Se,ke){v(Se,e,ke),w(e,t),w(t,i),w(i,l),w(i,s),w(i,o),w(i,r),w(i,a),w(i,u),w(i,f),w(i,c),w(i,d),w(i,m),w(m,h),w(m,g),w(m,_),w(m,y),w(m,S),w(m,T),w(m,$),w(m,E),w(m,M),w(M,I),w(M,A),w(M,P),w(m,R),w(m,N),w(m,U),w(m,j),w(m,V),w(m,K),w(i,J),w(i,ee),oe=!0},i(Se){oe||(Se&&nt(()=>{oe&&(X||(X=ze(e,vt,{duration:150},!0)),X.run(1))}),oe=!0)},o(Se){Se&&(X||(X=ze(e,vt,{duration:150},!1)),X.run(0)),oe=!1},d(Se){Se&&k(e),Se&&X&&X.end()}}}function tM(n){let e,t,i,l,s,o,r,a,u,f,c;e=new fe({props:{class:"form-field m-b-sm",name:"fields."+n[1]+".maxSize",$$slots:{default:[QE,({uniqueId:_})=>({10:_}),({uniqueId:_})=>_?1024:0]},$$scope:{ctx:n}}});function d(_,y){return _[2]?eM:xE}let m=d(n),h=m(n),g=n[2]&&lh();return{c(){H(e.$$.fragment),t=C(),i=b("button"),l=b("strong"),l.textContent="String value normalizations",s=C(),h.c(),r=C(),g&&g.c(),a=ge(),p(l,"class","txt"),p(i,"type","button"),p(i,"class",o="btn btn-sm "+(n[2]?"btn-secondary":"btn-hint btn-transparent"))},m(_,y){F(e,_,y),v(_,t,y),v(_,i,y),w(i,l),w(i,s),h.m(i,null),v(_,r,y),g&&g.m(_,y),v(_,a,y),u=!0,f||(c=B(i,"click",n[5]),f=!0)},p(_,y){const S={};y&2&&(S.name="fields."+_[1]+".maxSize"),y&3073&&(S.$$scope={dirty:y,ctx:_}),e.$set(S),m!==(m=d(_))&&(h.d(1),h=m(_),h&&(h.c(),h.m(i,null))),(!u||y&4&&o!==(o="btn btn-sm "+(_[2]?"btn-secondary":"btn-hint btn-transparent")))&&p(i,"class",o),_[2]?g?y&4&&O(g,1):(g=lh(),g.c(),O(g,1),g.m(a.parentNode,a)):g&&(re(),D(g,1,1,()=>{g=null}),ae())},i(_){u||(O(e.$$.fragment,_),O(g),u=!0)},o(_){D(e.$$.fragment,_),D(g),u=!1},d(_){_&&(k(t),k(i),k(r),k(a)),q(e,_),h.d(),g&&g.d(_),f=!1,c()}}}function nM(n){let e,t,i;const l=[{key:n[1]},n[3]];function s(r){n[6](r)}let o={$$slots:{options:[tM]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&10?kt(l,[a&2&&{key:r[1]},a&8&&Ft(r[3])]):{};a&2055&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function iM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e,r=!1;const a=h=>t(0,s.maxSize=h.target.value<<0,s),u=()=>{t(2,r=!r)};function f(h){s=h,t(0,s)}function c(h){Pe.call(this,n,h)}function d(h){Pe.call(this,n,h)}function m(h){Pe.call(this,n,h)}return n.$$set=h=>{e=je(je({},e),Ut(h)),t(3,l=lt(e,i)),"field"in h&&t(0,s=h.field),"key"in h&&t(1,o=h.key)},[s,o,r,l,a,u,f,c,d,m]}class lM extends ye{constructor(e){super(),be(this,e,iM,nM,_e,{field:0,key:1})}}function sM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Min"),l=C(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10])},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].min),r||(a=B(s,"input",n[4]),r=!0)},p(u,f){f&1024&&i!==(i=u[10])&&p(e,"for",i),f&1024&&o!==(o=u[10])&&p(s,"id",o),f&1&&St(s.value)!==u[0].min&&ce(s,u[0].min)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function oM(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Max"),l=C(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10]),p(s,"min",r=n[0].min)},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),ce(s,n[0].max),a||(u=B(s,"input",n[5]),a=!0)},p(f,c){c&1024&&i!==(i=f[10])&&p(e,"for",i),c&1024&&o!==(o=f[10])&&p(s,"id",o),c&1&&r!==(r=f[0].min)&&p(s,"min",r),c&1&&St(s.value)!==f[0].max&&ce(s,f[0].max)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function rM(n){let e,t,i,l,s,o,r;return i=new fe({props:{class:"form-field",name:"fields."+n[1]+".min",$$slots:{default:[sM,({uniqueId:a})=>({10:a}),({uniqueId:a})=>a?1024:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".max",$$slots:{default:[oM,({uniqueId:a})=>({10:a}),({uniqueId:a})=>a?1024:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,u){v(a,e,u),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),r=!0},p(a,u){const f={};u&2&&(f.name="fields."+a[1]+".min"),u&3073&&(f.$$scope={dirty:u,ctx:a}),i.$set(f);const c={};u&2&&(c.name="fields."+a[1]+".max"),u&3073&&(c.$$scope={dirty:u,ctx:a}),o.$set(c)},i(a){r||(O(i.$$.fragment,a),O(o.$$.fragment,a),r=!0)},o(a){D(i.$$.fragment,a),D(o.$$.fragment,a),r=!1},d(a){a&&k(e),q(i),q(o)}}}function aM(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="No decimals",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[10]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[10])},m(c,d){v(c,e,d),e.checked=n[0].onlyInt,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[3]),Me(He.call(null,r,{text:"Existing decimal numbers will not be affected."}))],u=!0)},p(c,d){d&1024&&t!==(t=c[10])&&p(e,"id",t),d&1&&(e.checked=c[0].onlyInt),d&1024&&a!==(a=c[10])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function uM(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle",name:"fields."+n[1]+".onlyInt",$$slots:{default:[aM,({uniqueId:i})=>({10:i}),({uniqueId:i})=>i?1024:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="fields."+i[1]+".onlyInt"),l&3073&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function fM(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[6](r)}let o={$$slots:{optionsFooter:[uM],options:[rM]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};a&2051&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function cM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;function r(){s.onlyInt=this.checked,t(0,s)}function a(){s.min=St(this.value),t(0,s)}function u(){s.max=St(this.value),t(0,s)}function f(h){s=h,t(0,s)}function c(h){Pe.call(this,n,h)}function d(h){Pe.call(this,n,h)}function m(h){Pe.call(this,n,h)}return n.$$set=h=>{e=je(je({},e),Ut(h)),t(2,l=lt(e,i)),"field"in h&&t(0,s=h.field),"key"in h&&t(1,o=h.key)},[s,o,l,r,a,u,f,c,d,m]}class dM extends ye{constructor(e){super(),be(this,e,cM,fM,_e,{field:0,key:1})}}function pM(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Min length"),l=C(),s=b("input"),p(e,"for",i=n[12]),p(s,"type","number"),p(s,"id",o=n[12]),p(s,"step","1"),p(s,"min","0"),p(s,"placeholder","No min limit"),s.value=r=n[0].min||""},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),a||(u=B(s,"input",n[3]),a=!0)},p(f,c){c&4096&&i!==(i=f[12])&&p(e,"for",i),c&4096&&o!==(o=f[12])&&p(s,"id",o),c&1&&r!==(r=f[0].min||"")&&s.value!==r&&(s.value=r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function mM(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Max length"),l=C(),s=b("input"),p(e,"for",i=n[12]),p(s,"type","number"),p(s,"id",o=n[12]),p(s,"step","1"),p(s,"placeholder","Up to 71 chars"),p(s,"min",r=n[0].min||0),p(s,"max","71"),s.value=a=n[0].max||""},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),u||(f=B(s,"input",n[4]),u=!0)},p(c,d){d&4096&&i!==(i=c[12])&&p(e,"for",i),d&4096&&o!==(o=c[12])&&p(s,"id",o),d&1&&r!==(r=c[0].min||0)&&p(s,"min",r),d&1&&a!==(a=c[0].max||"")&&s.value!==a&&(s.value=a)},d(c){c&&(k(e),k(l),k(s)),u=!1,f()}}}function hM(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Bcrypt cost"),l=C(),s=b("input"),p(e,"for",i=n[12]),p(s,"type","number"),p(s,"id",o=n[12]),p(s,"placeholder","Default to 10"),p(s,"step","1"),p(s,"min","6"),p(s,"max","31"),s.value=r=n[0].cost||""},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),a||(u=B(s,"input",n[5]),a=!0)},p(f,c){c&4096&&i!==(i=f[12])&&p(e,"for",i),c&4096&&o!==(o=f[12])&&p(s,"id",o),c&1&&r!==(r=f[0].cost||"")&&s.value!==r&&(s.value=r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function _M(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Validation pattern"),l=C(),s=b("input"),p(e,"for",i=n[12]),p(s,"type","text"),p(s,"id",o=n[12]),p(s,"placeholder","ex. ^\\w+$")},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].pattern),r||(a=B(s,"input",n[6]),r=!0)},p(u,f){f&4096&&i!==(i=u[12])&&p(e,"for",i),f&4096&&o!==(o=u[12])&&p(s,"id",o),f&1&&s.value!==u[0].pattern&&ce(s,u[0].pattern)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function gM(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;return i=new fe({props:{class:"form-field",name:"fields."+n[1]+".min",$$slots:{default:[pM,({uniqueId:h})=>({12:h}),({uniqueId:h})=>h?4096:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".max",$$slots:{default:[mM,({uniqueId:h})=>({12:h}),({uniqueId:h})=>h?4096:0]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field",name:"fields."+n[1]+".cost",$$slots:{default:[hM,({uniqueId:h})=>({12:h}),({uniqueId:h})=>h?4096:0]},$$scope:{ctx:n}}}),d=new fe({props:{class:"form-field",name:"fields."+n[1]+".pattern",$$slots:{default:[_M,({uniqueId:h})=>({12:h}),({uniqueId:h})=>h?4096:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),H(u.$$.fragment),f=C(),c=b("div"),H(d.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(a,"class","col-sm-6"),p(c,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(h,g){v(h,e,g),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),F(u,a,null),w(e,f),w(e,c),F(d,c,null),m=!0},p(h,g){const _={};g&2&&(_.name="fields."+h[1]+".min"),g&12289&&(_.$$scope={dirty:g,ctx:h}),i.$set(_);const y={};g&2&&(y.name="fields."+h[1]+".max"),g&12289&&(y.$$scope={dirty:g,ctx:h}),o.$set(y);const S={};g&2&&(S.name="fields."+h[1]+".cost"),g&12289&&(S.$$scope={dirty:g,ctx:h}),u.$set(S);const T={};g&2&&(T.name="fields."+h[1]+".pattern"),g&12289&&(T.$$scope={dirty:g,ctx:h}),d.$set(T)},i(h){m||(O(i.$$.fragment,h),O(o.$$.fragment,h),O(u.$$.fragment,h),O(d.$$.fragment,h),m=!0)},o(h){D(i.$$.fragment,h),D(o.$$.fragment,h),D(u.$$.fragment,h),D(d.$$.fragment,h),m=!1},d(h){h&&k(e),q(i),q(o),q(u),q(d)}}}function bM(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[7](r)}let o={$$slots:{options:[gM]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[8]),e.$on("remove",n[9]),e.$on("duplicate",n[10]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};a&8195&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function yM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;function r(){t(0,s.cost=11,s)}const a=_=>t(0,s.min=_.target.value<<0,s),u=_=>t(0,s.max=_.target.value<<0,s),f=_=>t(0,s.cost=_.target.value<<0,s);function c(){s.pattern=this.value,t(0,s)}function d(_){s=_,t(0,s)}function m(_){Pe.call(this,n,_)}function h(_){Pe.call(this,n,_)}function g(_){Pe.call(this,n,_)}return n.$$set=_=>{e=je(je({},e),Ut(_)),t(2,l=lt(e,i)),"field"in _&&t(0,s=_.field),"key"in _&&t(1,o=_.key)},n.$$.update=()=>{n.$$.dirty&1&&z.isEmpty(s.id)&&r()},[s,o,l,a,u,f,c,d,m,h,g]}class kM extends ye{constructor(e){super(),be(this,e,yM,bM,_e,{field:0,key:1})}}function vM(n){let e,t,i,l,s;return{c(){e=b("hr"),t=C(),i=b("button"),i.innerHTML=' New collection',p(i,"type","button"),p(i,"class","btn btn-transparent btn-block btn-sm")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),l||(s=B(i,"click",n[14]),l=!0)},p:te,d(o){o&&(k(e),k(t),k(i)),l=!1,s()}}}function wM(n){let e,t,i;function l(o){n[15](o)}let s={id:n[24],searchable:n[5].length>5,selectPlaceholder:"Select collection *",noOptionsText:"No collections found",selectionKey:"id",items:n[5],readonly:!n[25]||n[0].id,$$slots:{afterOptions:[vM]},$$scope:{ctx:n}};return n[0].collectionId!==void 0&&(s.keyOfSelected=n[0].collectionId),e=new xn({props:s}),ie.push(()=>ve(e,"keyOfSelected",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&16777216&&(a.id=o[24]),r&32&&(a.searchable=o[5].length>5),r&32&&(a.items=o[5]),r&33554433&&(a.readonly=!o[25]||o[0].id),r&67108872&&(a.$$scope={dirty:r,ctx:o}),!t&&r&1&&(t=!0,a.keyOfSelected=o[0].collectionId,$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function SM(n){let e,t,i;function l(o){n[16](o)}let s={id:n[24],items:n[6],readonly:!n[25]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new xn({props:s}),ie.push(()=>ve(e,"keyOfSelected",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&16777216&&(a.id=o[24]),r&33554432&&(a.readonly=!o[25]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function TM(n){let e,t,i,l,s,o,r,a,u,f;return i=new fe({props:{class:"form-field required "+(n[25]?"":"readonly"),inlineError:!0,name:"fields."+n[1]+".collectionId",$$slots:{default:[wM,({uniqueId:c})=>({24:c}),({uniqueId:c})=>c?16777216:0]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field form-field-single-multiple-select "+(n[25]?"":"readonly"),inlineError:!0,$$slots:{default:[SM,({uniqueId:c})=>({24:c}),({uniqueId:c})=>c?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),H(i.$$.fragment),l=C(),s=b("div"),o=C(),H(r.$$.fragment),a=C(),u=b("div"),p(e,"class","separator"),p(s,"class","separator"),p(u,"class","separator")},m(c,d){v(c,e,d),v(c,t,d),F(i,c,d),v(c,l,d),v(c,s,d),v(c,o,d),F(r,c,d),v(c,a,d),v(c,u,d),f=!0},p(c,d){const m={};d&33554432&&(m.class="form-field required "+(c[25]?"":"readonly")),d&2&&(m.name="fields."+c[1]+".collectionId"),d&117440553&&(m.$$scope={dirty:d,ctx:c}),i.$set(m);const h={};d&33554432&&(h.class="form-field form-field-single-multiple-select "+(c[25]?"":"readonly")),d&117440516&&(h.$$scope={dirty:d,ctx:c}),r.$set(h)},i(c){f||(O(i.$$.fragment,c),O(r.$$.fragment,c),f=!0)},o(c){D(i.$$.fragment,c),D(r.$$.fragment,c),f=!1},d(c){c&&(k(e),k(t),k(l),k(s),k(o),k(a),k(u)),q(i,c),q(r,c)}}}function sh(n){let e,t,i,l,s,o;return t=new fe({props:{class:"form-field",name:"fields."+n[1]+".minSelect",$$slots:{default:[$M,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),s=new fe({props:{class:"form-field",name:"fields."+n[1]+".maxSelect",$$slots:{default:[CM,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),H(t.$$.fragment),i=C(),l=b("div"),H(s.$$.fragment),p(e,"class","col-sm-6"),p(l,"class","col-sm-6")},m(r,a){v(r,e,a),F(t,e,null),v(r,i,a),v(r,l,a),F(s,l,null),o=!0},p(r,a){const u={};a&2&&(u.name="fields."+r[1]+".minSelect"),a&83886081&&(u.$$scope={dirty:a,ctx:r}),t.$set(u);const f={};a&2&&(f.name="fields."+r[1]+".maxSelect"),a&83886081&&(f.$$scope={dirty:a,ctx:r}),s.$set(f)},i(r){o||(O(t.$$.fragment,r),O(s.$$.fragment,r),o=!0)},o(r){D(t.$$.fragment,r),D(s.$$.fragment,r),o=!1},d(r){r&&(k(e),k(i),k(l)),q(t),q(s)}}}function $M(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Min select"),l=C(),s=b("input"),p(e,"for",i=n[24]),p(s,"type","number"),p(s,"id",o=n[24]),p(s,"step","1"),p(s,"min","0"),p(s,"placeholder","No min limit"),s.value=r=n[0].minSelect||""},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),a||(u=B(s,"input",n[11]),a=!0)},p(f,c){c&16777216&&i!==(i=f[24])&&p(e,"for",i),c&16777216&&o!==(o=f[24])&&p(s,"id",o),c&1&&r!==(r=f[0].minSelect||"")&&s.value!==r&&(s.value=r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function CM(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Max select"),l=C(),s=b("input"),p(e,"for",i=n[24]),p(s,"type","number"),p(s,"id",o=n[24]),p(s,"step","1"),p(s,"placeholder","Default to single"),p(s,"min",r=n[0].minSelect||1)},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),ce(s,n[0].maxSelect),a||(u=B(s,"input",n[12]),a=!0)},p(f,c){c&16777216&&i!==(i=f[24])&&p(e,"for",i),c&16777216&&o!==(o=f[24])&&p(s,"id",o),c&1&&r!==(r=f[0].minSelect||1)&&p(s,"min",r),c&1&&St(s.value)!==f[0].maxSelect&&ce(s,f[0].maxSelect)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function OM(n){let e,t,i,l,s,o,r,a,u,f,c,d;function m(g){n[13](g)}let h={id:n[24],items:n[7]};return n[0].cascadeDelete!==void 0&&(h.keyOfSelected=n[0].cascadeDelete),a=new xn({props:h}),ie.push(()=>ve(a,"keyOfSelected",m)),{c(){e=b("label"),t=b("span"),t.textContent="Cascade delete",i=C(),l=b("i"),r=C(),H(a.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",o=n[24])},m(g,_){var y,S;v(g,e,_),w(e,t),w(e,i),w(e,l),v(g,r,_),F(a,g,_),f=!0,c||(d=Me(s=He.call(null,l,{text:[`Whether on ${((y=n[4])==null?void 0:y.name)||"relation"} record deletion to delete also the current corresponding collection record(s).`,n[2]?null:`For "Multiple" relation fields the cascade delete is triggered only when all ${((S=n[4])==null?void 0:S.name)||"relation"} ids are removed from the corresponding record.`].filter(Boolean).join(` + +`),position:"top"})),c=!0)},p(g,_){var S,T;s&&Rt(s.update)&&_&20&&s.update.call(null,{text:[`Whether on ${((S=g[4])==null?void 0:S.name)||"relation"} record deletion to delete also the current corresponding collection record(s).`,g[2]?null:`For "Multiple" relation fields the cascade delete is triggered only when all ${((T=g[4])==null?void 0:T.name)||"relation"} ids are removed from the corresponding record.`].filter(Boolean).join(` + +`),position:"top"}),(!f||_&16777216&&o!==(o=g[24]))&&p(e,"for",o);const y={};_&16777216&&(y.id=g[24]),!u&&_&1&&(u=!0,y.keyOfSelected=g[0].cascadeDelete,$e(()=>u=!1)),a.$set(y)},i(g){f||(O(a.$$.fragment,g),f=!0)},o(g){D(a.$$.fragment,g),f=!1},d(g){g&&(k(e),k(r)),q(a,g),c=!1,d()}}}function EM(n){let e,t,i,l,s,o=!n[2]&&sh(n);return l=new fe({props:{class:"form-field",name:"fields."+n[1]+".cascadeDelete",$$slots:{default:[OM,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),o&&o.c(),t=C(),i=b("div"),H(l.$$.fragment),p(i,"class","col-sm-12"),p(e,"class","grid grid-sm")},m(r,a){v(r,e,a),o&&o.m(e,null),w(e,t),w(e,i),F(l,i,null),s=!0},p(r,a){r[2]?o&&(re(),D(o,1,1,()=>{o=null}),ae()):o?(o.p(r,a),a&4&&O(o,1)):(o=sh(r),o.c(),O(o,1),o.m(e,t));const u={};a&2&&(u.name="fields."+r[1]+".cascadeDelete"),a&83886101&&(u.$$scope={dirty:a,ctx:r}),l.$set(u)},i(r){s||(O(o),O(l.$$.fragment,r),s=!0)},o(r){D(o),D(l.$$.fragment,r),s=!1},d(r){r&&k(e),o&&o.d(),q(l)}}}function MM(n){let e,t,i,l,s;const o=[{key:n[1]},n[8]];function r(f){n[17](f)}let a={$$slots:{options:[EM],default:[TM,({interactive:f})=>({25:f}),({interactive:f})=>f?33554432:0]},$$scope:{ctx:n}};for(let f=0;fve(e,"field",r)),e.$on("rename",n[18]),e.$on("remove",n[19]),e.$on("duplicate",n[20]);let u={};return l=new nf({props:u}),n[21](l),l.$on("save",n[22]),{c(){H(e.$$.fragment),i=C(),H(l.$$.fragment)},m(f,c){F(e,f,c),v(f,i,c),F(l,f,c),s=!0},p(f,[c]){const d=c&258?kt(o,[c&2&&{key:f[1]},c&256&&Ft(f[8])]):{};c&100663359&&(d.$$scope={dirty:c,ctx:f}),!t&&c&1&&(t=!0,d.field=f[0],$e(()=>t=!1)),e.$set(d);const m={};l.$set(m)},i(f){s||(O(e.$$.fragment,f),O(l.$$.fragment,f),s=!0)},o(f){D(e.$$.fragment,f),D(l.$$.fragment,f),s=!1},d(f){f&&k(i),q(e,f),n[21](null),q(l,f)}}}function DM(n,e,t){let i,l;const s=["field","key"];let o=lt(e,s),r;Qe(n,En,N=>t(10,r=N));let{field:a}=e,{key:u=""}=e;const f=[{label:"Single",value:!0},{label:"Multiple",value:!1}],c=[{label:"False",value:!1},{label:"True",value:!0}];let d=null,m=a.maxSelect<=1,h=m;function g(){t(0,a.maxSelect=1,a),t(0,a.collectionId=null,a),t(0,a.cascadeDelete=!1,a),t(2,m=!0),t(9,h=m)}const _=N=>t(0,a.minSelect=N.target.value<<0,a);function y(){a.maxSelect=St(this.value),t(0,a),t(9,h),t(2,m)}function S(N){n.$$.not_equal(a.cascadeDelete,N)&&(a.cascadeDelete=N,t(0,a),t(9,h),t(2,m))}const T=()=>d==null?void 0:d.show();function $(N){n.$$.not_equal(a.collectionId,N)&&(a.collectionId=N,t(0,a),t(9,h),t(2,m))}function E(N){m=N,t(2,m)}function M(N){a=N,t(0,a),t(9,h),t(2,m)}function L(N){Pe.call(this,n,N)}function I(N){Pe.call(this,n,N)}function A(N){Pe.call(this,n,N)}function P(N){ie[N?"unshift":"push"](()=>{d=N,t(3,d)})}const R=N=>{var U,j;(j=(U=N==null?void 0:N.detail)==null?void 0:U.collection)!=null&&j.id&&N.detail.collection.type!="view"&&t(0,a.collectionId=N.detail.collection.id,a)};return n.$$set=N=>{e=je(je({},e),Ut(N)),t(8,o=lt(e,s)),"field"in N&&t(0,a=N.field),"key"in N&&t(1,u=N.key)},n.$$.update=()=>{n.$$.dirty&1024&&t(5,i=r.filter(N=>!N.system&&N.type!="view")),n.$$.dirty&516&&h!=m&&(t(9,h=m),m?(t(0,a.minSelect=0,a),t(0,a.maxSelect=1,a)):t(0,a.maxSelect=999,a)),n.$$.dirty&1&&typeof a.maxSelect>"u"&&g(),n.$$.dirty&1025&&t(4,l=r.find(N=>N.id==a.collectionId)||null)},[a,u,m,d,l,i,f,c,o,h,r,_,y,S,T,$,E,M,L,I,A,P,R]}class IM extends ye{constructor(e){super(),be(this,e,DM,MM,_e,{field:0,key:1})}}function LM(n){let e,t,i,l,s,o;function r(u){n[7](u)}let a={id:n[14],placeholder:"Choices: eg. optionA, optionB",required:!0,readonly:!n[15]};return n[0].values!==void 0&&(a.value=n[0].values),t=new _o({props:a}),ie.push(()=>ve(t,"value",r)),{c(){e=b("div"),H(t.$$.fragment)},m(u,f){v(u,e,f),F(t,e,null),l=!0,s||(o=Me(He.call(null,e,{text:"Choices (comma separated)",position:"top-left",delay:700})),s=!0)},p(u,f){const c={};f&16384&&(c.id=u[14]),f&32768&&(c.readonly=!u[15]),!i&&f&1&&(i=!0,c.value=u[0].values,$e(()=>i=!1)),t.$set(c)},i(u){l||(O(t.$$.fragment,u),l=!0)},o(u){D(t.$$.fragment,u),l=!1},d(u){u&&k(e),q(t),s=!1,o()}}}function AM(n){let e,t,i;function l(o){n[8](o)}let s={id:n[14],items:n[3],readonly:!n[15]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new xn({props:s}),ie.push(()=>ve(e,"keyOfSelected",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&16384&&(a.id=o[14]),r&32768&&(a.readonly=!o[15]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function PM(n){let e,t,i,l,s,o,r,a,u,f;return i=new fe({props:{class:"form-field required "+(n[15]?"":"readonly"),inlineError:!0,name:"fields."+n[1]+".values",$$slots:{default:[LM,({uniqueId:c})=>({14:c}),({uniqueId:c})=>c?16384:0]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field form-field-single-multiple-select "+(n[15]?"":"readonly"),inlineError:!0,$$slots:{default:[AM,({uniqueId:c})=>({14:c}),({uniqueId:c})=>c?16384:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),H(i.$$.fragment),l=C(),s=b("div"),o=C(),H(r.$$.fragment),a=C(),u=b("div"),p(e,"class","separator"),p(s,"class","separator"),p(u,"class","separator")},m(c,d){v(c,e,d),v(c,t,d),F(i,c,d),v(c,l,d),v(c,s,d),v(c,o,d),F(r,c,d),v(c,a,d),v(c,u,d),f=!0},p(c,d){const m={};d&32768&&(m.class="form-field required "+(c[15]?"":"readonly")),d&2&&(m.name="fields."+c[1]+".values"),d&114689&&(m.$$scope={dirty:d,ctx:c}),i.$set(m);const h={};d&32768&&(h.class="form-field form-field-single-multiple-select "+(c[15]?"":"readonly")),d&114692&&(h.$$scope={dirty:d,ctx:c}),r.$set(h)},i(c){f||(O(i.$$.fragment,c),O(r.$$.fragment,c),f=!0)},o(c){D(i.$$.fragment,c),D(r.$$.fragment,c),f=!1},d(c){c&&(k(e),k(t),k(l),k(s),k(o),k(a),k(u)),q(i,c),q(r,c)}}}function oh(n){let e,t;return e=new fe({props:{class:"form-field",name:"fields."+n[1]+".maxSelect",$$slots:{default:[NM,({uniqueId:i})=>({14:i}),({uniqueId:i})=>i?16384:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="fields."+i[1]+".maxSelect"),l&81921&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function NM(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Max select"),l=C(),s=b("input"),p(e,"for",i=n[14]),p(s,"id",o=n[14]),p(s,"type","number"),p(s,"step","1"),p(s,"min","2"),p(s,"max",r=n[0].values.length),p(s,"placeholder","Default to single")},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),ce(s,n[0].maxSelect),a||(u=B(s,"input",n[6]),a=!0)},p(f,c){c&16384&&i!==(i=f[14])&&p(e,"for",i),c&16384&&o!==(o=f[14])&&p(s,"id",o),c&1&&r!==(r=f[0].values.length)&&p(s,"max",r),c&1&&St(s.value)!==f[0].maxSelect&&ce(s,f[0].maxSelect)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function RM(n){let e,t,i=!n[2]&&oh(n);return{c(){i&&i.c(),e=ge()},m(l,s){i&&i.m(l,s),v(l,e,s),t=!0},p(l,s){l[2]?i&&(re(),D(i,1,1,()=>{i=null}),ae()):i?(i.p(l,s),s&4&&O(i,1)):(i=oh(l),i.c(),O(i,1),i.m(e.parentNode,e))},i(l){t||(O(i),t=!0)},o(l){D(i),t=!1},d(l){l&&k(e),i&&i.d(l)}}}function FM(n){let e,t,i;const l=[{key:n[1]},n[4]];function s(r){n[9](r)}let o={$$slots:{options:[RM],default:[PM,({interactive:r})=>({15:r}),({interactive:r})=>r?32768:0]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[10]),e.$on("remove",n[11]),e.$on("duplicate",n[12]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&18?kt(l,[a&2&&{key:r[1]},a&16&&Ft(r[4])]):{};a&98311&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function qM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;const r=[{label:"Single",value:!0},{label:"Multiple",value:!1}];let a=s.maxSelect<=1,u=a;function f(){t(0,s.maxSelect=1,s),t(0,s.values=[],s),t(2,a=!0),t(5,u=a)}function c(){s.maxSelect=St(this.value),t(0,s),t(5,u),t(2,a)}function d(S){n.$$.not_equal(s.values,S)&&(s.values=S,t(0,s),t(5,u),t(2,a))}function m(S){a=S,t(2,a)}function h(S){s=S,t(0,s),t(5,u),t(2,a)}function g(S){Pe.call(this,n,S)}function _(S){Pe.call(this,n,S)}function y(S){Pe.call(this,n,S)}return n.$$set=S=>{e=je(je({},e),Ut(S)),t(4,l=lt(e,i)),"field"in S&&t(0,s=S.field),"key"in S&&t(1,o=S.key)},n.$$.update=()=>{var S;n.$$.dirty&37&&u!=a&&(t(5,u=a),a?t(0,s.maxSelect=1,s):t(0,s.maxSelect=((S=s.values)==null?void 0:S.length)||2,s)),n.$$.dirty&1&&typeof s.maxSelect>"u"&&f()},[s,o,a,r,l,u,c,d,m,h,g,_,y]}class HM extends ye{constructor(e){super(),be(this,e,qM,FM,_e,{field:0,key:1})}}function jM(n){let e,t,i,l,s,o,r,a,u,f,c;return{c(){e=b("label"),t=b("span"),t.textContent="Min length",i=C(),l=b("i"),o=C(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[11]),p(r,"type","number"),p(r,"id",a=n[11]),p(r,"step","1"),p(r,"min","0"),p(r,"placeholder","No min limit"),r.value=u=n[0].min||""},m(d,m){v(d,e,m),w(e,t),w(e,i),w(e,l),v(d,o,m),v(d,r,m),f||(c=[Me(He.call(null,l,"Clear the field or set it to 0 for no limit.")),B(r,"input",n[3])],f=!0)},p(d,m){m&2048&&s!==(s=d[11])&&p(e,"for",s),m&2048&&a!==(a=d[11])&&p(r,"id",a),m&1&&u!==(u=d[0].min||"")&&r.value!==u&&(r.value=u)},d(d){d&&(k(e),k(o),k(r)),f=!1,De(c)}}}function zM(n){let e,t,i,l,s,o,r,a,u,f,c,d;return{c(){e=b("label"),t=b("span"),t.textContent="Max length",i=C(),l=b("i"),o=C(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[11]),p(r,"type","number"),p(r,"id",a=n[11]),p(r,"step","1"),p(r,"placeholder","Default to max 5000 characters"),p(r,"min",u=n[0].min||0),r.value=f=n[0].max||""},m(m,h){v(m,e,h),w(e,t),w(e,i),w(e,l),v(m,o,h),v(m,r,h),c||(d=[Me(He.call(null,l,"Clear the field or set it to 0 to fallback to the default limit.")),B(r,"input",n[4])],c=!0)},p(m,h){h&2048&&s!==(s=m[11])&&p(e,"for",s),h&2048&&a!==(a=m[11])&&p(r,"id",a),h&1&&u!==(u=m[0].min||0)&&p(r,"min",u),h&1&&f!==(f=m[0].max||"")&&r.value!==f&&(r.value=f)},d(m){m&&(k(e),k(o),k(r)),c=!1,De(d)}}}function UM(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;return{c(){e=b("label"),t=Y("Validation pattern"),l=C(),s=b("input"),r=C(),a=b("div"),u=b("p"),f=Y("Ex. "),c=b("code"),c.textContent="^[a-z0-9]+$",p(e,"for",i=n[11]),p(s,"type","text"),p(s,"id",o=n[11]),p(a,"class","help-block")},m(h,g){v(h,e,g),w(e,t),v(h,l,g),v(h,s,g),ce(s,n[0].pattern),v(h,r,g),v(h,a,g),w(a,u),w(u,f),w(u,c),d||(m=B(s,"input",n[5]),d=!0)},p(h,g){g&2048&&i!==(i=h[11])&&p(e,"for",i),g&2048&&o!==(o=h[11])&&p(s,"id",o),g&1&&s.value!==h[0].pattern&&ce(s,h[0].pattern)},d(h){h&&(k(e),k(l),k(s),k(r),k(a)),d=!1,m()}}}function VM(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g;return{c(){e=b("label"),t=b("span"),t.textContent="Autogenerate pattern",i=C(),l=b("i"),o=C(),r=b("input"),u=C(),f=b("div"),c=b("p"),d=Y("Ex. "),m=b("code"),m.textContent="[a-z0-9]{30}",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[11]),p(r,"type","text"),p(r,"id",a=n[11]),p(f,"class","help-block")},m(_,y){v(_,e,y),w(e,t),w(e,i),w(e,l),v(_,o,y),v(_,r,y),ce(r,n[0].autogeneratePattern),v(_,u,y),v(_,f,y),w(f,c),w(c,d),w(c,m),h||(g=[Me(He.call(null,l,"Set and autogenerate text matching the pattern on missing record create value.")),B(r,"input",n[6])],h=!0)},p(_,y){y&2048&&s!==(s=_[11])&&p(e,"for",s),y&2048&&a!==(a=_[11])&&p(r,"id",a),y&1&&r.value!==_[0].autogeneratePattern&&ce(r,_[0].autogeneratePattern)},d(_){_&&(k(e),k(o),k(r),k(u),k(f)),h=!1,De(g)}}}function BM(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;return i=new fe({props:{class:"form-field",name:"fields."+n[1]+".min",$$slots:{default:[jM,({uniqueId:h})=>({11:h}),({uniqueId:h})=>h?2048:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"fields."+n[1]+".max",$$slots:{default:[zM,({uniqueId:h})=>({11:h}),({uniqueId:h})=>h?2048:0]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field",name:"fields."+n[1]+".pattern",$$slots:{default:[UM,({uniqueId:h})=>({11:h}),({uniqueId:h})=>h?2048:0]},$$scope:{ctx:n}}}),d=new fe({props:{class:"form-field",name:"fields."+n[1]+".pattern",$$slots:{default:[VM,({uniqueId:h})=>({11:h}),({uniqueId:h})=>h?2048:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),H(u.$$.fragment),f=C(),c=b("div"),H(d.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(a,"class","col-sm-6"),p(c,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(h,g){v(h,e,g),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),F(u,a,null),w(e,f),w(e,c),F(d,c,null),m=!0},p(h,g){const _={};g&2&&(_.name="fields."+h[1]+".min"),g&6145&&(_.$$scope={dirty:g,ctx:h}),i.$set(_);const y={};g&2&&(y.name="fields."+h[1]+".max"),g&6145&&(y.$$scope={dirty:g,ctx:h}),o.$set(y);const S={};g&2&&(S.name="fields."+h[1]+".pattern"),g&6145&&(S.$$scope={dirty:g,ctx:h}),u.$set(S);const T={};g&2&&(T.name="fields."+h[1]+".pattern"),g&6145&&(T.$$scope={dirty:g,ctx:h}),d.$set(T)},i(h){m||(O(i.$$.fragment,h),O(o.$$.fragment,h),O(u.$$.fragment,h),O(d.$$.fragment,h),m=!0)},o(h){D(i.$$.fragment,h),D(o.$$.fragment,h),D(u.$$.fragment,h),D(d.$$.fragment,h),m=!1},d(h){h&&k(e),q(i),q(o),q(u),q(d)}}}function WM(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[7](r)}let o={$$slots:{options:[BM]},$$scope:{ctx:n}};for(let r=0;rve(e,"field",s)),e.$on("rename",n[8]),e.$on("remove",n[9]),e.$on("duplicate",n[10]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};a&4099&&(u.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function YM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;const r=g=>t(0,s.min=g.target.value<<0,s),a=g=>t(0,s.max=g.target.value<<0,s);function u(){s.pattern=this.value,t(0,s)}function f(){s.autogeneratePattern=this.value,t(0,s)}function c(g){s=g,t(0,s)}function d(g){Pe.call(this,n,g)}function m(g){Pe.call(this,n,g)}function h(g){Pe.call(this,n,g)}return n.$$set=g=>{e=je(je({},e),Ut(g)),t(2,l=lt(e,i)),"field"in g&&t(0,s=g.field),"key"in g&&t(1,o=g.key)},[s,o,l,r,a,u,f,c,d,m,h]}class KM extends ye{constructor(e){super(),be(this,e,YM,WM,_e,{field:0,key:1})}}function JM(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[3](r)}let o={};for(let r=0;rve(e,"field",s)),e.$on("rename",n[4]),e.$on("remove",n[5]),e.$on("duplicate",n[6]),{c(){H(e.$$.fragment)},m(r,a){F(e,r,a),i=!0},p(r,[a]){const u=a&6?kt(l,[a&2&&{key:r[1]},a&4&&Ft(r[2])]):{};!t&&a&1&&(t=!0,u.field=r[0],$e(()=>t=!1)),e.$set(u)},i(r){i||(O(e.$$.fragment,r),i=!0)},o(r){D(e.$$.fragment,r),i=!1},d(r){q(e,r)}}}function ZM(n,e,t){const i=["field","key"];let l=lt(e,i),{field:s}=e,{key:o=""}=e;function r(c){s=c,t(0,s)}function a(c){Pe.call(this,n,c)}function u(c){Pe.call(this,n,c)}function f(c){Pe.call(this,n,c)}return n.$$set=c=>{e=je(je({},e),Ut(c)),t(2,l=lt(e,i)),"field"in c&&t(0,s=c.field),"key"in c&&t(1,o=c.key)},[s,o,l,r,a,u,f]}class GM extends ye{constructor(e){super(),be(this,e,ZM,JM,_e,{field:0,key:1})}}function rh(n,e,t){const i=n.slice();return i[22]=e[t],i[23]=e,i[24]=t,i}function XM(n){let e,t,i,l;function s(f){n[8](f,n[22],n[23],n[24])}function o(){return n[9](n[24])}function r(){return n[10](n[24])}var a=n[1][n[22].type];function u(f,c){let d={key:f[5](f[22]),collection:f[0]};return f[22]!==void 0&&(d.field=f[22]),{props:d}}return a&&(e=jt(a,u(n)),ie.push(()=>ve(e,"field",s)),e.$on("remove",o),e.$on("duplicate",r),e.$on("rename",n[11])),{c(){e&&H(e.$$.fragment),i=C()},m(f,c){e&&F(e,f,c),v(f,i,c),l=!0},p(f,c){if(n=f,c&1&&a!==(a=n[1][n[22].type])){if(e){re();const d=e;D(d.$$.fragment,1,0,()=>{q(d,1)}),ae()}a?(e=jt(a,u(n)),ie.push(()=>ve(e,"field",s)),e.$on("remove",o),e.$on("duplicate",r),e.$on("rename",n[11]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,i.parentNode,i)):e=null}else if(a){const d={};c&1&&(d.key=n[5](n[22])),c&1&&(d.collection=n[0]),!t&&c&1&&(t=!0,d.field=n[22],$e(()=>t=!1)),e.$set(d)}},i(f){l||(e&&O(e.$$.fragment,f),l=!0)},o(f){e&&D(e.$$.fragment,f),l=!1},d(f){f&&k(i),e&&q(e,f)}}}function ah(n,e){let t,i,l,s;function o(a){e[12](a)}let r={index:e[24],disabled:e[22]._toDelete,dragHandleClass:"drag-handle-wrapper",$$slots:{default:[XM]},$$scope:{ctx:e}};return e[0].fields!==void 0&&(r.list=e[0].fields),i=new ho({props:r}),ie.push(()=>ve(i,"list",o)),i.$on("drag",e[13]),i.$on("sort",e[14]),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u&1&&(f.index=e[24]),u&1&&(f.disabled=e[22]._toDelete),u&33554433&&(f.$$scope={dirty:u,ctx:e}),!l&&u&1&&(l=!0,f.list=e[0].fields,$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function QM(n){let e,t=[],i=new Map,l,s,o,r,a,u,f,c,d,m=pe(n[0].fields);const h=y=>y[22];for(let y=0;yve(f,"collection",g)),{c(){e=b("div");for(let y=0;yc=!1)),f.$set(T)},i(y){if(!d){for(let S=0;St(18,l=A));let{collection:s}=e,o;const r={text:KM,number:dM,bool:oE,email:lk,url:GM,editor:EE,date:wE,select:HM,json:lM,file:XE,relation:IM,password:kM,autodate:iE};function a(A){s.fields[A]&&(s.fields.splice(A,1),t(0,s))}function u(A){const P=s.fields[A];if(!P)return;P.onMountSelect=!1;const R=structuredClone(P);R.id="",R.system=!1,R.name=c(R.name+"_copy"),R.onMountSelect=!0,s.fields.splice(A+1,0,R),t(0,s)}function f(A="text"){const P=z.initSchemaField({name:c(),type:A});P.onMountSelect=!0;const R=s.fields.findLastIndex(N=>N.type!="autodate");P.type!="autodate"&&R>=0?s.fields.splice(R+1,0,P):s.fields.push(P),t(0,s)}function c(A="field"){var j;let P=A,R=2,N=((j=A.match(/\d+$/))==null?void 0:j[0])||"",U=N?A.substring(0,A.length-N.length):A;for(;d(P);)P=U+((N<<0)+R),R++;return P}function d(A){var P;return!!((P=s==null?void 0:s.fields)!=null&&P.find(R=>R.name===A))}function m(A){return i.findIndex(P=>P===A)}function h(A,P){var R,N;!((R=s==null?void 0:s.fields)!=null&&R.length)||A===P||!P||(N=s==null?void 0:s.fields)!=null&&N.find(U=>U.name==A&&!U._toDelete)||t(0,s.indexes=s.indexes.map(U=>z.replaceIndexColumn(U,A,P)),s)}function g(){const A=s.fields||[],P=A.filter(N=>!N.system),R=structuredClone(l[s.type]);t(0,s.fields=R.fields,s);for(let N of A){if(!N.system)continue;const U=s.fields.findIndex(j=>j.name==N.name);U<0||t(0,s.fields[U]=Object.assign(s.fields[U],N),s)}for(let N of P)s.fields.push(N)}function _(A,P,R,N){R[N]=A,t(0,s)}const y=A=>a(A),S=A=>u(A),T=A=>h(A.detail.oldName,A.detail.newName);function $(A){n.$$.not_equal(s.fields,A)&&(s.fields=A,t(0,s))}const E=A=>{if(!A.detail)return;const P=A.detail.target;P.style.opacity=0,setTimeout(()=>{var R;(R=P==null?void 0:P.style)==null||R.removeProperty("opacity")},0),A.detail.dataTransfer.setDragImage(P,0,0)},M=()=>{Wt({})},L=A=>f(A.detail);function I(A){s=A,t(0,s)}return n.$$set=A=>{"collection"in A&&t(0,s=A.collection)},n.$$.update=()=>{n.$$.dirty&1&&typeof s.fields>"u"&&t(0,s.fields=[],s),n.$$.dirty&129&&!s.id&&o!=s.type&&(t(7,o=s.type),g()),n.$$.dirty&1&&(i=s.fields.filter(A=>!A._toDelete))},[s,r,a,u,f,m,h,o,_,y,S,T,$,E,M,L,I]}class eD extends ye{constructor(e){super(),be(this,e,xM,QM,_e,{collection:0})}}function uh(n,e,t){const i=n.slice();return i[9]=e[t],i}function tD(n){let e,t,i,l;function s(a){n[5](a)}var o=n[1];function r(a,u){let f={id:a[8],placeholder:"eg. SELECT id, name from posts",language:"sql-select",minHeight:"150"};return a[0].viewQuery!==void 0&&(f.value=a[0].viewQuery),{props:f}}return o&&(e=jt(o,r(n)),ie.push(()=>ve(e,"value",s)),e.$on("change",n[6])),{c(){e&&H(e.$$.fragment),i=ge()},m(a,u){e&&F(e,a,u),v(a,i,u),l=!0},p(a,u){if(u&2&&o!==(o=a[1])){if(e){re();const f=e;D(f.$$.fragment,1,0,()=>{q(f,1)}),ae()}o?(e=jt(o,r(a)),ie.push(()=>ve(e,"value",s)),e.$on("change",a[6]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,i.parentNode,i)):e=null}else if(o){const f={};u&256&&(f.id=a[8]),!t&&u&1&&(t=!0,f.value=a[0].viewQuery,$e(()=>t=!1)),e.$set(f)}},i(a){l||(e&&O(e.$$.fragment,a),l=!0)},o(a){e&&D(e.$$.fragment,a),l=!1},d(a){a&&k(i),e&&q(e,a)}}}function nD(n){let e;return{c(){e=b("textarea"),e.disabled=!0,p(e,"rows","7"),p(e,"placeholder","Loading...")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function fh(n){let e,t,i=pe(n[3]),l=[];for(let s=0;s
  • Wildcard columns (*) are not supported.
  • The query must have a unique id column. +
    + If your query doesn't have a suitable one, you can use the universal + (ROW_NUMBER() OVER()) as id.
  • Expressions must be aliased with a valid formatted field name (eg. + MAX(balance) as maxBalance).
  • `,u=C(),g&&g.c(),f=ge(),p(t,"class","txt"),p(e,"for",i=n[8]),p(a,"class","help-block")},m(_,y){v(_,e,y),w(e,t),v(_,l,y),m[s].m(_,y),v(_,r,y),v(_,a,y),v(_,u,y),g&&g.m(_,y),v(_,f,y),c=!0},p(_,y){(!c||y&256&&i!==(i=_[8]))&&p(e,"for",i);let S=s;s=h(_),s===S?m[s].p(_,y):(re(),D(m[S],1,1,()=>{m[S]=null}),ae(),o=m[s],o?o.p(_,y):(o=m[s]=d[s](_),o.c()),O(o,1),o.m(r.parentNode,r)),_[3].length?g?g.p(_,y):(g=fh(_),g.c(),g.m(f.parentNode,f)):g&&(g.d(1),g=null)},i(_){c||(O(o),c=!0)},o(_){D(o),c=!1},d(_){_&&(k(e),k(l),k(r),k(a),k(u),k(f)),m[s].d(_),g&&g.d(_)}}}function lD(n){let e,t;return e=new fe({props:{class:"form-field required "+(n[3].length?"error":""),name:"viewQuery",$$slots:{default:[iD,({uniqueId:i})=>({8:i}),({uniqueId:i})=>i?256:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&8&&(s.class="form-field required "+(i[3].length?"error":"")),l&4367&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function sD(n,e,t){let i;Qe(n,Sn,c=>t(4,i=c));let{collection:l}=e,s,o=!1,r=[];function a(c){t(3,r=[]);const d=z.getNestedVal(c,"fields",null);if(z.isEmpty(d))return;if(d!=null&&d.message){r.push(d==null?void 0:d.message);return}const m=z.extractColumnsFromQuery(l==null?void 0:l.viewQuery);z.removeByValue(m,"id"),z.removeByValue(m,"created"),z.removeByValue(m,"updated");for(let h in d)for(let g in d[h]){const _=d[h][g].message,y=m[h]||h;r.push(z.sentenize(y+": "+_))}}Yt(async()=>{t(2,o=!0);try{t(1,s=(await Ot(async()=>{const{default:c}=await import("./CodeEditor-CPgcqnd5.js");return{default:c}},__vite__mapDeps([12,1]),import.meta.url)).default)}catch(c){console.warn(c)}t(2,o=!1)});function u(c){n.$$.not_equal(l.viewQuery,c)&&(l.viewQuery=c,t(0,l))}const f=()=>{r.length&&fi("fields")};return n.$$set=c=>{"collection"in c&&t(0,l=c.collection)},n.$$.update=()=>{n.$$.dirty&16&&a(i)},[l,s,o,r,i,u,f]}class oD extends ye{constructor(e){super(),be(this,e,sD,lD,_e,{collection:0})}}function dh(n,e,t){const i=n.slice();return i[15]=e[t],i}function ph(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A=pe(n[4]),P=[];for(let R=0;R@request filter:",c=C(),d=b("div"),d.innerHTML="@request.headers.* @request.query.* @request.body.* @request.auth.*",m=C(),h=b("hr"),g=C(),_=b("p"),_.innerHTML=`You could also add constraints and query other collections using the + @collection filter:`,y=C(),S=b("div"),S.innerHTML="@collection.ANY_COLLECTION_NAME.*",T=C(),$=b("hr"),E=C(),M=b("p"),M.innerHTML=`Example rule: +
    @request.auth.id != "" && created > "2022-01-01 00:00:00"`,p(l,"class","m-b-0"),p(o,"class","inline-flex flex-gap-5"),p(a,"class","m-t-10 m-b-5"),p(f,"class","m-b-0"),p(d,"class","inline-flex flex-gap-5"),p(h,"class","m-t-10 m-b-5"),p(_,"class","m-b-0"),p(S,"class","inline-flex flex-gap-5"),p($,"class","m-t-10 m-b-5"),p(i,"class","content"),p(t,"class","alert alert-warning m-0")},m(R,N){v(R,e,N),w(e,t),w(t,i),w(i,l),w(i,s),w(i,o);for(let U=0;U{I&&(L||(L=ze(e,vt,{duration:150},!0)),L.run(1))}),I=!0)},o(R){R&&(L||(L=ze(e,vt,{duration:150},!1)),L.run(0)),I=!1},d(R){R&&k(e),pt(P,R),R&&L&&L.end()}}}function mh(n){let e,t=n[15]+"",i;return{c(){e=b("code"),i=Y(t)},m(l,s){v(l,e,s),w(e,i)},p(l,s){s&16&&t!==(t=l[15]+"")&&ue(i,t)},d(l){l&&k(e)}}}function hh(n){let e=!n[3].includes(n[15]),t,i=e&&mh(n);return{c(){i&&i.c(),t=ge()},m(l,s){i&&i.m(l,s),v(l,t,s)},p(l,s){s&24&&(e=!l[3].includes(l[15])),e?i?i.p(l,s):(i=mh(l),i.c(),i.m(t.parentNode,t)):i&&(i.d(1),i=null)},d(l){l&&k(t),i&&i.d(l)}}}function _h(n){let e,t,i,l,s,o,r,a,u;function f(_){n[8](_)}let c={label:"Create rule",formKey:"createRule",collection:n[0],$$slots:{afterLabel:[rD,({isSuperuserOnly:_})=>({14:_}),({isSuperuserOnly:_})=>_?16384:0]},$$scope:{ctx:n}};n[0].createRule!==void 0&&(c.rule=n[0].createRule),e=new tl({props:c}),ie.push(()=>ve(e,"rule",f));function d(_){n[9](_)}let m={label:"Update rule",formKey:"updateRule",collection:n[0]};n[0].updateRule!==void 0&&(m.rule=n[0].updateRule),l=new tl({props:m}),ie.push(()=>ve(l,"rule",d));function h(_){n[10](_)}let g={label:"Delete rule",formKey:"deleteRule",collection:n[0]};return n[0].deleteRule!==void 0&&(g.rule=n[0].deleteRule),r=new tl({props:g}),ie.push(()=>ve(r,"rule",h)),{c(){H(e.$$.fragment),i=C(),H(l.$$.fragment),o=C(),H(r.$$.fragment)},m(_,y){F(e,_,y),v(_,i,y),F(l,_,y),v(_,o,y),F(r,_,y),u=!0},p(_,y){const S={};y&1&&(S.collection=_[0]),y&278528&&(S.$$scope={dirty:y,ctx:_}),!t&&y&1&&(t=!0,S.rule=_[0].createRule,$e(()=>t=!1)),e.$set(S);const T={};y&1&&(T.collection=_[0]),!s&&y&1&&(s=!0,T.rule=_[0].updateRule,$e(()=>s=!1)),l.$set(T);const $={};y&1&&($.collection=_[0]),!a&&y&1&&(a=!0,$.rule=_[0].deleteRule,$e(()=>a=!1)),r.$set($)},i(_){u||(O(e.$$.fragment,_),O(l.$$.fragment,_),O(r.$$.fragment,_),u=!0)},o(_){D(e.$$.fragment,_),D(l.$$.fragment,_),D(r.$$.fragment,_),u=!1},d(_){_&&(k(i),k(o)),q(e,_),q(l,_),q(r,_)}}}function gh(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-information-line link-hint")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,{text:'The Create rule is executed after a "dry save" of the submitted data, giving you access to the main record fields as in every other rule.',position:"top"})),t=!0)},d(l){l&&k(e),t=!1,i()}}}function rD(n){let e,t=!n[14]&&gh();return{c(){t&&t.c(),e=ge()},m(i,l){t&&t.m(i,l),v(i,e,l)},p(i,l){i[14]?t&&(t.d(1),t=null):t||(t=gh(),t.c(),t.m(e.parentNode,e))},d(i){i&&k(e),t&&t.d(i)}}}function bh(n){let e,t,i,l,s,o,r,a,u,f,c;function d(_,y){return _[2]?uD:aD}let m=d(n),h=m(n),g=n[2]&&yh(n);return{c(){e=b("hr"),t=C(),i=b("button"),l=b("strong"),l.textContent="Additional auth collection rules",s=C(),h.c(),r=C(),g&&g.c(),a=ge(),p(l,"class","txt"),p(i,"type","button"),p(i,"class",o="btn btn-sm m-b-sm "+(n[2]?"btn-secondary":"btn-hint btn-transparent"))},m(_,y){v(_,e,y),v(_,t,y),v(_,i,y),w(i,l),w(i,s),h.m(i,null),v(_,r,y),g&&g.m(_,y),v(_,a,y),u=!0,f||(c=B(i,"click",n[11]),f=!0)},p(_,y){m!==(m=d(_))&&(h.d(1),h=m(_),h&&(h.c(),h.m(i,null))),(!u||y&4&&o!==(o="btn btn-sm m-b-sm "+(_[2]?"btn-secondary":"btn-hint btn-transparent")))&&p(i,"class",o),_[2]?g?(g.p(_,y),y&4&&O(g,1)):(g=yh(_),g.c(),O(g,1),g.m(a.parentNode,a)):g&&(re(),D(g,1,1,()=>{g=null}),ae())},i(_){u||(O(g),u=!0)},o(_){D(g),u=!1},d(_){_&&(k(e),k(t),k(i),k(r),k(a)),h.d(),g&&g.d(_),f=!1,c()}}}function aD(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function uD(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line txt-sm")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function yh(n){let e,t,i,l,s,o,r,a;function u(m){n[12](m)}let f={label:"Authentication rule",formKey:"authRule",placeholder:"",collection:n[0],$$slots:{default:[fD]},$$scope:{ctx:n}};n[0].authRule!==void 0&&(f.rule=n[0].authRule),t=new tl({props:f}),ie.push(()=>ve(t,"rule",u));function c(m){n[13](m)}let d={label:"Manage rule",formKey:"manageRule",placeholder:"",required:n[0].manageRule!==null,collection:n[0],$$slots:{default:[cD]},$$scope:{ctx:n}};return n[0].manageRule!==void 0&&(d.rule=n[0].manageRule),s=new tl({props:d}),ie.push(()=>ve(s,"rule",c)),{c(){e=b("div"),H(t.$$.fragment),l=C(),H(s.$$.fragment),p(e,"class","block")},m(m,h){v(m,e,h),F(t,e,null),w(e,l),F(s,e,null),a=!0},p(m,h){const g={};h&1&&(g.collection=m[0]),h&262144&&(g.$$scope={dirty:h,ctx:m}),!i&&h&1&&(i=!0,g.rule=m[0].authRule,$e(()=>i=!1)),t.$set(g);const _={};h&1&&(_.required=m[0].manageRule!==null),h&1&&(_.collection=m[0]),h&262144&&(_.$$scope={dirty:h,ctx:m}),!o&&h&1&&(o=!0,_.rule=m[0].manageRule,$e(()=>o=!1)),s.$set(_)},i(m){a||(O(t.$$.fragment,m),O(s.$$.fragment,m),m&&nt(()=>{a&&(r||(r=ze(e,vt,{duration:150},!0)),r.run(1))}),a=!0)},o(m){D(t.$$.fragment,m),D(s.$$.fragment,m),m&&(r||(r=ze(e,vt,{duration:150},!1)),r.run(0)),a=!1},d(m){m&&k(e),q(t),q(s),m&&r&&r.end()}}}function fD(n){let e,t,i,l,s,o,r;return{c(){e=b("p"),e.textContent=`This rule is executed every time before authentication allowing you to restrict who + can authenticate.`,t=C(),i=b("p"),i.innerHTML=`For example, to allow only verified users you can set it to + verified = true.`,l=C(),s=b("p"),s.textContent="Leave it empty to allow anyone with an account to authenticate.",o=C(),r=b("p"),r.textContent='To disable authentication entirely you can change it to "Set superusers only".'},m(a,u){v(a,e,u),v(a,t,u),v(a,i,u),v(a,l,u),v(a,s,u),v(a,o,u),v(a,r,u)},p:te,d(a){a&&(k(e),k(t),k(i),k(l),k(s),k(o),k(r))}}}function cD(n){let e,t,i;return{c(){e=b("p"),e.innerHTML=`This rule is executed in addition to the create and update API + rules.`,t=C(),i=b("p"),i.textContent=`It enables superuser-like permissions to allow fully managing the auth record(s), eg. + changing the password without requiring to enter the old one, directly updating the + verified state or email, etc.`},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},p:te,d(l){l&&(k(e),k(t),k(i))}}}function dD(n){var N,U;let e,t,i,l,s,o=n[1]?"Hide available fields":"Show available fields",r,a,u,f,c,d,m,h,g,_,y,S,T,$,E=n[1]&&ph(n);function M(j){n[6](j)}let L={label:"List/Search rule",formKey:"listRule",collection:n[0]};n[0].listRule!==void 0&&(L.rule=n[0].listRule),f=new tl({props:L}),ie.push(()=>ve(f,"rule",M));function I(j){n[7](j)}let A={label:"View rule",formKey:"viewRule",collection:n[0]};n[0].viewRule!==void 0&&(A.rule=n[0].viewRule),m=new tl({props:A}),ie.push(()=>ve(m,"rule",I));let P=((N=n[0])==null?void 0:N.type)!=="view"&&_h(n),R=((U=n[0])==null?void 0:U.type)==="auth"&&bh(n);return{c(){e=b("div"),t=b("div"),i=b("p"),i.innerHTML=`All rules follow the + PocketBase filter syntax and operators + .`,l=C(),s=b("button"),r=Y(o),a=C(),E&&E.c(),u=C(),H(f.$$.fragment),d=C(),H(m.$$.fragment),g=C(),P&&P.c(),_=C(),R&&R.c(),y=ge(),p(s,"type","button"),p(s,"class","expand-handle txt-sm txt-bold txt-nowrap link-hint"),p(t,"class","flex txt-sm txt-hint m-b-5"),p(e,"class","block m-b-sm handle")},m(j,V){v(j,e,V),w(e,t),w(t,i),w(t,l),w(t,s),w(s,r),w(e,a),E&&E.m(e,null),v(j,u,V),F(f,j,V),v(j,d,V),F(m,j,V),v(j,g,V),P&&P.m(j,V),v(j,_,V),R&&R.m(j,V),v(j,y,V),S=!0,T||($=B(s,"click",n[5]),T=!0)},p(j,[V]){var ee,X;(!S||V&2)&&o!==(o=j[1]?"Hide available fields":"Show available fields")&&ue(r,o),j[1]?E?(E.p(j,V),V&2&&O(E,1)):(E=ph(j),E.c(),O(E,1),E.m(e,null)):E&&(re(),D(E,1,1,()=>{E=null}),ae());const K={};V&1&&(K.collection=j[0]),!c&&V&1&&(c=!0,K.rule=j[0].listRule,$e(()=>c=!1)),f.$set(K);const J={};V&1&&(J.collection=j[0]),!h&&V&1&&(h=!0,J.rule=j[0].viewRule,$e(()=>h=!1)),m.$set(J),((ee=j[0])==null?void 0:ee.type)!=="view"?P?(P.p(j,V),V&1&&O(P,1)):(P=_h(j),P.c(),O(P,1),P.m(_.parentNode,_)):P&&(re(),D(P,1,1,()=>{P=null}),ae()),((X=j[0])==null?void 0:X.type)==="auth"?R?(R.p(j,V),V&1&&O(R,1)):(R=bh(j),R.c(),O(R,1),R.m(y.parentNode,y)):R&&(re(),D(R,1,1,()=>{R=null}),ae())},i(j){S||(O(E),O(f.$$.fragment,j),O(m.$$.fragment,j),O(P),O(R),S=!0)},o(j){D(E),D(f.$$.fragment,j),D(m.$$.fragment,j),D(P),D(R),S=!1},d(j){j&&(k(e),k(u),k(d),k(g),k(_),k(y)),E&&E.d(),q(f,j),q(m,j),P&&P.d(j),R&&R.d(j),T=!1,$()}}}function pD(n,e,t){let i,l,{collection:s}=e,o=!1,r=s.manageRule!==null||s.authRule!=="";const a=()=>t(1,o=!o);function u(y){n.$$.not_equal(s.listRule,y)&&(s.listRule=y,t(0,s))}function f(y){n.$$.not_equal(s.viewRule,y)&&(s.viewRule=y,t(0,s))}function c(y){n.$$.not_equal(s.createRule,y)&&(s.createRule=y,t(0,s))}function d(y){n.$$.not_equal(s.updateRule,y)&&(s.updateRule=y,t(0,s))}function m(y){n.$$.not_equal(s.deleteRule,y)&&(s.deleteRule=y,t(0,s))}const h=()=>{t(2,r=!r)};function g(y){n.$$.not_equal(s.authRule,y)&&(s.authRule=y,t(0,s))}function _(y){n.$$.not_equal(s.manageRule,y)&&(s.manageRule=y,t(0,s))}return n.$$set=y=>{"collection"in y&&t(0,s=y.collection)},n.$$.update=()=>{var y;n.$$.dirty&1&&t(4,i=z.getAllCollectionIdentifiers(s)),n.$$.dirty&1&&t(3,l=(y=s.fields)==null?void 0:y.filter(S=>S.hidden).map(S=>S.name))},[s,o,r,l,i,a,u,f,c,d,m,h,g,_]}class mD extends ye{constructor(e){super(),be(this,e,pD,dD,_e,{collection:0})}}function kh(n,e,t){const i=n.slice();return i[19]=e[t],i}function vh(n,e,t){const i=n.slice();return i[19]=e[t],i}function wh(n,e,t){const i=n.slice();return i[19]=e[t],i}function Sh(n){let e;return{c(){e=b("p"),e.textContent="All data associated with the removed fields will be permanently deleted!"},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function Th(n){let e,t,i,l,s=n[3]&&$h(n),o=!n[4]&&Ch(n);return{c(){e=b("h6"),e.textContent="Changes:",t=C(),i=b("ul"),s&&s.c(),l=C(),o&&o.c(),p(i,"class","changes-list svelte-xqpcsf")},m(r,a){v(r,e,a),v(r,t,a),v(r,i,a),s&&s.m(i,null),w(i,l),o&&o.m(i,null)},p(r,a){r[3]?s?s.p(r,a):(s=$h(r),s.c(),s.m(i,l)):s&&(s.d(1),s=null),r[4]?o&&(o.d(1),o=null):o?o.p(r,a):(o=Ch(r),o.c(),o.m(i,null))},d(r){r&&(k(e),k(t),k(i)),s&&s.d(),o&&o.d()}}}function $h(n){var m,h;let e,t,i,l,s=((m=n[1])==null?void 0:m.name)+"",o,r,a,u,f,c=((h=n[2])==null?void 0:h.name)+"",d;return{c(){e=b("li"),t=b("div"),i=Y(`Renamed collection + `),l=b("strong"),o=Y(s),r=C(),a=b("i"),u=C(),f=b("strong"),d=Y(c),p(l,"class","txt-strikethrough txt-hint"),p(a,"class","ri-arrow-right-line txt-sm"),p(f,"class","txt"),p(t,"class","inline-flex"),p(e,"class","svelte-xqpcsf")},m(g,_){v(g,e,_),w(e,t),w(t,i),w(t,l),w(l,o),w(t,r),w(t,a),w(t,u),w(t,f),w(f,d)},p(g,_){var y,S;_&2&&s!==(s=((y=g[1])==null?void 0:y.name)+"")&&ue(o,s),_&4&&c!==(c=((S=g[2])==null?void 0:S.name)+"")&&ue(d,c)},d(g){g&&k(e)}}}function Ch(n){let e,t,i,l=pe(n[6]),s=[];for(let f=0;f',i=C(),l=b("div"),s=b("p"),s.textContent=`If any of the collection changes is part of another collection rule, filter or view query, + you'll have to update it manually!`,o=C(),u&&u.c(),r=C(),f&&f.c(),a=ge(),p(t,"class","icon"),p(l,"class","content txt-bold"),p(e,"class","alert alert-warning")},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),w(l,s),w(l,o),u&&u.m(l,null),v(c,r,d),f&&f.m(c,d),v(c,a,d)},p(c,d){c[7].length?u||(u=Sh(),u.c(),u.m(l,null)):u&&(u.d(1),u=null),c[9]?f?f.p(c,d):(f=Th(c),f.c(),f.m(a.parentNode,a)):f&&(f.d(1),f=null)},d(c){c&&(k(e),k(r),k(a)),u&&u.d(),f&&f.d(c)}}}function _D(n){let e;return{c(){e=b("h4"),e.textContent="Confirm collection changes"},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function gD(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Cancel',t=C(),i=b("button"),i.innerHTML='Confirm',e.autofocus=!0,p(e,"type","button"),p(e,"class","btn btn-transparent"),p(i,"type","button"),p(i,"class","btn btn-expanded")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),e.focus(),l||(s=[B(e,"click",n[12]),B(i,"click",n[13])],l=!0)},p:te,d(o){o&&(k(e),k(t),k(i)),l=!1,De(s)}}}function bD(n){let e,t,i={class:"confirm-changes-panel",popup:!0,$$slots:{footer:[gD],header:[_D],default:[hD]},$$scope:{ctx:n}};return e=new ln({props:i}),n[14](e),e.$on("hide",n[15]),e.$on("show",n[16]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&67109854&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[14](null),q(e,l)}}}function yD(n,e,t){let i,l,s,o,r,a;const u=_t();let f,c,d,m;async function h(M,L,I=!0){t(1,c=M),t(2,d=L),m=I,await fn(),i||s.length||o.length||r.length?f==null||f.show():_()}function g(){f==null||f.hide()}function _(){g(),u("confirm",m)}const y=()=>g(),S=()=>_();function T(M){ie[M?"unshift":"push"](()=>{f=M,t(5,f)})}function $(M){Pe.call(this,n,M)}function E(M){Pe.call(this,n,M)}return n.$$.update=()=>{var M,L,I;n.$$.dirty&6&&t(3,i=(c==null?void 0:c.name)!=(d==null?void 0:d.name)),n.$$.dirty&4&&t(4,l=(d==null?void 0:d.type)==="view"),n.$$.dirty&4&&t(8,s=((M=d==null?void 0:d.fields)==null?void 0:M.filter(A=>A.id&&!A._toDelete&&A._originalName!=A.name))||[]),n.$$.dirty&4&&t(7,o=((L=d==null?void 0:d.fields)==null?void 0:L.filter(A=>A.id&&A._toDelete))||[]),n.$$.dirty&6&&t(6,r=((I=d==null?void 0:d.fields)==null?void 0:I.filter(A=>{var R;const P=(R=c==null?void 0:c.fields)==null?void 0:R.find(N=>N.id==A.id);return P?P.maxSelect!=1&&A.maxSelect==1:!1}))||[]),n.$$.dirty&24&&t(9,a=!l||i)},[g,c,d,i,l,f,r,o,s,a,_,h,y,S,T,$,E]}class kD extends ye{constructor(e){super(),be(this,e,yD,bD,_e,{show:11,hide:0})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[0]}}function Dh(n,e,t){const i=n.slice();return i[58]=e[t][0],i[59]=e[t][1],i}function vD(n){let e,t,i;function l(o){n[43](o)}let s={};return n[2]!==void 0&&(s.collection=n[2]),e=new eD({props:s}),ie.push(()=>ve(e,"collection",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&4&&(t=!0,a.collection=o[2],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function wD(n){let e,t,i;function l(o){n[42](o)}let s={};return n[2]!==void 0&&(s.collection=n[2]),e=new oD({props:s}),ie.push(()=>ve(e,"collection",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&4&&(t=!0,a.collection=o[2],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function Ih(n){let e,t,i,l;function s(r){n[44](r)}let o={};return n[2]!==void 0&&(o.collection=n[2]),t=new mD({props:o}),ie.push(()=>ve(t,"collection",s)),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","tab-item active")},m(r,a){v(r,e,a),F(t,e,null),l=!0},p(r,a){const u={};!i&&a[0]&4&&(i=!0,u.collection=r[2],$e(()=>i=!1)),t.$set(u)},i(r){l||(O(t.$$.fragment,r),l=!0)},o(r){D(t.$$.fragment,r),l=!1},d(r){r&&k(e),q(t)}}}function Lh(n){let e,t,i,l;function s(r){n[45](r)}let o={};return n[2]!==void 0&&(o.collection=n[2]),t=new yO({props:o}),ie.push(()=>ve(t,"collection",s)),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","tab-item"),x(e,"active",n[3]===ss)},m(r,a){v(r,e,a),F(t,e,null),l=!0},p(r,a){const u={};!i&&a[0]&4&&(i=!0,u.collection=r[2],$e(()=>i=!1)),t.$set(u),(!l||a[0]&8)&&x(e,"active",r[3]===ss)},i(r){l||(O(t.$$.fragment,r),l=!0)},o(r){D(t.$$.fragment,r),l=!1},d(r){r&&k(e),q(t)}}}function SD(n){let e,t,i,l,s,o,r;const a=[wD,vD],u=[];function f(m,h){return m[16]?0:1}i=f(n),l=u[i]=a[i](n);let c=!n[14]&&n[3]===to&&Ih(n),d=n[17]&&Lh(n);return{c(){e=b("div"),t=b("div"),l.c(),s=C(),c&&c.c(),o=C(),d&&d.c(),p(t,"class","tab-item"),x(t,"active",n[3]===Xi),p(e,"class","tabs-content svelte-xyiw1b")},m(m,h){v(m,e,h),w(e,t),u[i].m(t,null),w(e,s),c&&c.m(e,null),w(e,o),d&&d.m(e,null),r=!0},p(m,h){let g=i;i=f(m),i===g?u[i].p(m,h):(re(),D(u[g],1,1,()=>{u[g]=null}),ae(),l=u[i],l?l.p(m,h):(l=u[i]=a[i](m),l.c()),O(l,1),l.m(t,null)),(!r||h[0]&8)&&x(t,"active",m[3]===Xi),!m[14]&&m[3]===to?c?(c.p(m,h),h[0]&16392&&O(c,1)):(c=Ih(m),c.c(),O(c,1),c.m(e,o)):c&&(re(),D(c,1,1,()=>{c=null}),ae()),m[17]?d?(d.p(m,h),h[0]&131072&&O(d,1)):(d=Lh(m),d.c(),O(d,1),d.m(e,null)):d&&(re(),D(d,1,1,()=>{d=null}),ae())},i(m){r||(O(l),O(c),O(d),r=!0)},o(m){D(l),D(c),D(d),r=!1},d(m){m&&k(e),u[i].d(),c&&c.d(),d&&d.d()}}}function Ah(n){let e,t,i,l,s,o,r;return o=new Hn({props:{class:"dropdown dropdown-right m-t-5",$$slots:{default:[TD]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),i=b("div"),l=b("i"),s=C(),H(o.$$.fragment),p(e,"class","flex-fill"),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"aria-label","More collection options"),p(i,"class","btn btn-sm btn-circle btn-transparent flex-gap-0")},m(a,u){v(a,e,u),v(a,t,u),v(a,i,u),w(i,l),w(i,s),F(o,i,null),r=!0},p(a,u){const f={};u[2]&1&&(f.$$scope={dirty:u,ctx:a}),o.$set(f)},i(a){r||(O(o.$$.fragment,a),r=!0)},o(a){D(o.$$.fragment,a),r=!1},d(a){a&&(k(e),k(t),k(i)),q(o)}}}function TD(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("button"),e.innerHTML=' Duplicate',t=C(),i=b("hr"),l=C(),s=b("button"),s.innerHTML=' Truncate',o=C(),r=b("button"),r.innerHTML=' Delete',p(e,"type","button"),p(e,"class","dropdown-item"),p(e,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item txt-danger"),p(s,"role","menuitem"),p(r,"type","button"),p(r,"class","dropdown-item txt-danger"),p(r,"role","menuitem")},m(f,c){v(f,e,c),v(f,t,c),v(f,i,c),v(f,l,c),v(f,s,c),v(f,o,c),v(f,r,c),a||(u=[B(e,"click",n[33]),B(s,"click",n[34]),B(r,"click",On(tt(n[35])))],a=!0)},p:te,d(f){f&&(k(e),k(t),k(i),k(l),k(s),k(o),k(r)),a=!1,De(u)}}}function Ph(n){let e,t,i,l;return i=new Hn({props:{class:"dropdown dropdown-right dropdown-nowrap m-t-5",$$slots:{default:[$D]},$$scope:{ctx:n}}}),{c(){e=b("i"),t=C(),H(i.$$.fragment),p(e,"class","ri-arrow-down-s-fill"),p(e,"aria-hidden","true")},m(s,o){v(s,e,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o[0]&68|o[2]&1&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(i.$$.fragment,s),l=!0)},o(s){D(i.$$.fragment,s),l=!1},d(s){s&&(k(e),k(t)),q(i,s)}}}function Nh(n){let e,t,i,l,s,o=n[59]+"",r,a,u,f,c;function d(){return n[37](n[58])}return{c(){e=b("button"),t=b("i"),l=C(),s=b("span"),r=Y(o),a=Y(" collection"),u=C(),p(t,"class",i=js(z.getCollectionTypeIcon(n[58]))+" svelte-xyiw1b"),p(t,"aria-hidden","true"),p(s,"class","txt"),p(e,"type","button"),p(e,"role","menuitem"),p(e,"class","dropdown-item closable"),x(e,"selected",n[58]==n[2].type)},m(m,h){v(m,e,h),w(e,t),w(e,l),w(e,s),w(s,r),w(s,a),w(e,u),f||(c=B(e,"click",d),f=!0)},p(m,h){n=m,h[0]&64&&i!==(i=js(z.getCollectionTypeIcon(n[58]))+" svelte-xyiw1b")&&p(t,"class",i),h[0]&64&&o!==(o=n[59]+"")&&ue(r,o),h[0]&68&&x(e,"selected",n[58]==n[2].type)},d(m){m&&k(e),f=!1,c()}}}function $D(n){let e,t=pe(Object.entries(n[6])),i=[];for(let l=0;l{N=null}),ae()):N?(N.p(j,V),V[0]&4&&O(N,1)):(N=Ph(j),N.c(),O(N,1),N.m(d,null)),(!A||V[0]&4&&$!==($=j[2].id?-1:0))&&p(d,"tabindex",$),(!A||V[0]&4&&E!==(E=j[2].id?"":"button"))&&p(d,"role",E),(!A||V[0]&4&&M!==(M="btn btn-sm p-r-10 p-l-10 "+(j[2].id?"btn-transparent":"btn-outline")))&&p(d,"class",M),(!A||V[0]&4)&&x(d,"btn-disabled",!!j[2].id),j[2].system?U||(U=Rh(),U.c(),U.m(I.parentNode,I)):U&&(U.d(1),U=null)},i(j){A||(O(N),A=!0)},o(j){D(N),A=!1},d(j){j&&(k(e),k(l),k(s),k(f),k(c),k(L),k(I)),N&&N.d(),U&&U.d(j),P=!1,R()}}}function Fh(n){let e,t,i,l,s,o;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(r,a){v(r,e,a),l=!0,s||(o=Me(t=He.call(null,e,n[11])),s=!0)},p(r,a){t&&Rt(t.update)&&a[0]&2048&&t.update.call(null,r[11])},i(r){l||(r&&nt(()=>{l&&(i||(i=ze(e,Mt,{duration:150,start:.7},!0)),i.run(1))}),l=!0)},o(r){r&&(i||(i=ze(e,Mt,{duration:150,start:.7},!1)),i.run(0)),l=!1},d(r){r&&k(e),r&&i&&i.end(),s=!1,o()}}}function qh(n){var a,u,f,c,d,m,h;let e,t,i,l=!z.isEmpty((a=n[5])==null?void 0:a.listRule)||!z.isEmpty((u=n[5])==null?void 0:u.viewRule)||!z.isEmpty((f=n[5])==null?void 0:f.createRule)||!z.isEmpty((c=n[5])==null?void 0:c.updateRule)||!z.isEmpty((d=n[5])==null?void 0:d.deleteRule)||!z.isEmpty((m=n[5])==null?void 0:m.authRule)||!z.isEmpty((h=n[5])==null?void 0:h.manageRule),s,o,r=l&&Hh();return{c(){e=b("button"),t=b("span"),t.textContent="API Rules",i=C(),r&&r.c(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","tab-item"),x(e,"active",n[3]===to)},m(g,_){v(g,e,_),w(e,t),w(e,i),r&&r.m(e,null),s||(o=B(e,"click",n[40]),s=!0)},p(g,_){var y,S,T,$,E,M,L;_[0]&32&&(l=!z.isEmpty((y=g[5])==null?void 0:y.listRule)||!z.isEmpty((S=g[5])==null?void 0:S.viewRule)||!z.isEmpty((T=g[5])==null?void 0:T.createRule)||!z.isEmpty(($=g[5])==null?void 0:$.updateRule)||!z.isEmpty((E=g[5])==null?void 0:E.deleteRule)||!z.isEmpty((M=g[5])==null?void 0:M.authRule)||!z.isEmpty((L=g[5])==null?void 0:L.manageRule)),l?r?_[0]&32&&O(r,1):(r=Hh(),r.c(),O(r,1),r.m(e,null)):r&&(re(),D(r,1,1,()=>{r=null}),ae()),_[0]&8&&x(e,"active",g[3]===to)},d(g){g&&k(e),r&&r.d(),s=!1,o()}}}function Hh(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,"Has errors")),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function jh(n){let e,t,i,l=n[5]&&n[24](n[5],n[12].concat(["manageRule","authRule"])),s,o,r=l&&zh();return{c(){e=b("button"),t=b("span"),t.textContent="Options",i=C(),r&&r.c(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","tab-item"),x(e,"active",n[3]===ss)},m(a,u){v(a,e,u),w(e,t),w(e,i),r&&r.m(e,null),s||(o=B(e,"click",n[41]),s=!0)},p(a,u){u[0]&4128&&(l=a[5]&&a[24](a[5],a[12].concat(["manageRule","authRule"]))),l?r?u[0]&4128&&O(r,1):(r=zh(),r.c(),O(r,1),r.m(e,null)):r&&(re(),D(r,1,1,()=>{r=null}),ae()),u[0]&8&&x(e,"active",a[3]===ss)},d(a){a&&k(e),r&&r.d(),s=!1,o()}}}function zh(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,"Has errors")),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function OD(n){let e,t=n[2].id?"Edit collection":"New collection",i,l,s,o,r,a,u,f,c,d,m,h=n[16]?"Query":"Fields",g,_,y=!z.isEmpty(n[11]),S,T,$,E,M,L=!!n[2].id&&!n[2].system&&Ah(n);r=new fe({props:{class:"form-field collection-field-name required m-b-0",name:"name",$$slots:{default:[CD,({uniqueId:R})=>({57:R}),({uniqueId:R})=>[0,R?67108864:0]]},$$scope:{ctx:n}}});let I=y&&Fh(n),A=!n[14]&&qh(n),P=n[17]&&jh(n);return{c(){e=b("h4"),i=Y(t),l=C(),L&&L.c(),s=C(),o=b("form"),H(r.$$.fragment),a=C(),u=b("input"),f=C(),c=b("div"),d=b("button"),m=b("span"),g=Y(h),_=C(),I&&I.c(),S=C(),A&&A.c(),T=C(),P&&P.c(),p(e,"class","upsert-panel-title svelte-xyiw1b"),p(u,"type","submit"),p(u,"class","hidden"),p(u,"tabindex","-1"),p(o,"class","block"),p(m,"class","txt"),p(d,"type","button"),p(d,"class","tab-item"),x(d,"active",n[3]===Xi),p(c,"class","tabs-header stretched")},m(R,N){v(R,e,N),w(e,i),v(R,l,N),L&&L.m(R,N),v(R,s,N),v(R,o,N),F(r,o,null),w(o,a),w(o,u),v(R,f,N),v(R,c,N),w(c,d),w(d,m),w(m,g),w(d,_),I&&I.m(d,null),w(c,S),A&&A.m(c,null),w(c,T),P&&P.m(c,null),$=!0,E||(M=[B(o,"submit",tt(n[38])),B(d,"click",n[39])],E=!0)},p(R,N){(!$||N[0]&4)&&t!==(t=R[2].id?"Edit collection":"New collection")&&ue(i,t),R[2].id&&!R[2].system?L?(L.p(R,N),N[0]&4&&O(L,1)):(L=Ah(R),L.c(),O(L,1),L.m(s.parentNode,s)):L&&(re(),D(L,1,1,()=>{L=null}),ae());const U={};N[0]&163908|N[1]&67108864|N[2]&1&&(U.$$scope={dirty:N,ctx:R}),r.$set(U),(!$||N[0]&65536)&&h!==(h=R[16]?"Query":"Fields")&&ue(g,h),N[0]&2048&&(y=!z.isEmpty(R[11])),y?I?(I.p(R,N),N[0]&2048&&O(I,1)):(I=Fh(R),I.c(),O(I,1),I.m(d,null)):I&&(re(),D(I,1,1,()=>{I=null}),ae()),(!$||N[0]&8)&&x(d,"active",R[3]===Xi),R[14]?A&&(A.d(1),A=null):A?A.p(R,N):(A=qh(R),A.c(),A.m(c,T)),R[17]?P?P.p(R,N):(P=jh(R),P.c(),P.m(c,null)):P&&(P.d(1),P=null)},i(R){$||(O(L),O(r.$$.fragment,R),O(I),$=!0)},o(R){D(L),D(r.$$.fragment,R),D(I),$=!1},d(R){R&&(k(e),k(l),k(s),k(o),k(f),k(c)),L&&L.d(R),q(r),I&&I.d(),A&&A.d(),P&&P.d(),E=!1,De(M)}}}function Uh(n){let e,t,i,l,s,o;return l=new Hn({props:{class:"dropdown dropdown-upside dropdown-right dropdown-nowrap m-b-5",$$slots:{default:[ED]},$$scope:{ctx:n}}}),{c(){e=b("button"),t=b("i"),i=C(),H(l.$$.fragment),p(t,"class","ri-arrow-down-s-line"),p(t,"aria-hidden","true"),p(e,"type","button"),p(e,"class","btn p-l-5 p-r-5 flex-gap-0"),e.disabled=s=!n[13]||n[9]},m(r,a){v(r,e,a),w(e,t),w(e,i),F(l,e,null),o=!0},p(r,a){const u={};a[2]&1&&(u.$$scope={dirty:a,ctx:r}),l.$set(u),(!o||a[0]&8704&&s!==(s=!r[13]||r[9]))&&(e.disabled=s)},i(r){o||(O(l.$$.fragment,r),o=!0)},o(r){D(l.$$.fragment,r),o=!1},d(r){r&&k(e),q(l)}}}function ED(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Save and continue',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[32]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function MD(n){let e,t,i,l,s,o,r=n[2].id?"Save changes":"Create",a,u,f,c,d,m,h=n[2].id&&Uh(n);return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=C(),l=b("div"),s=b("button"),o=b("span"),a=Y(r),f=C(),h&&h.c(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[9],p(o,"class","txt"),p(s,"type","button"),p(s,"class","btn btn-expanded"),p(s,"title","Save and close"),s.disabled=u=!n[13]||n[9],x(s,"btn-loading",n[9]),p(l,"class","btns-group no-gap")},m(g,_){v(g,e,_),w(e,t),v(g,i,_),v(g,l,_),w(l,s),w(s,o),w(o,a),w(l,f),h&&h.m(l,null),c=!0,d||(m=[B(e,"click",n[30]),B(s,"click",n[31])],d=!0)},p(g,_){(!c||_[0]&512)&&(e.disabled=g[9]),(!c||_[0]&4)&&r!==(r=g[2].id?"Save changes":"Create")&&ue(a,r),(!c||_[0]&8704&&u!==(u=!g[13]||g[9]))&&(s.disabled=u),(!c||_[0]&512)&&x(s,"btn-loading",g[9]),g[2].id?h?(h.p(g,_),_[0]&4&&O(h,1)):(h=Uh(g),h.c(),O(h,1),h.m(l,null)):h&&(re(),D(h,1,1,()=>{h=null}),ae())},i(g){c||(O(h),c=!0)},o(g){D(h),c=!1},d(g){g&&(k(e),k(i),k(l)),h&&h.d(),d=!1,De(m)}}}function DD(n){let e,t,i,l,s={class:"overlay-panel-lg colored-header collection-panel",escClose:!1,overlayClose:!n[9],beforeHide:n[46],$$slots:{footer:[MD],header:[OD],default:[SD]},$$scope:{ctx:n}};e=new ln({props:s}),n[47](e),e.$on("hide",n[48]),e.$on("show",n[49]);let o={};return i=new kD({props:o}),n[50](i),i.$on("confirm",n[51]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(r,a){F(e,r,a),v(r,t,a),F(i,r,a),l=!0},p(r,a){const u={};a[0]&512&&(u.overlayClose=!r[9]),a[0]&1040&&(u.beforeHide=r[46]),a[0]&260716|a[2]&1&&(u.$$scope={dirty:a,ctx:r}),e.$set(u);const f={};i.$set(f)},i(r){l||(O(e.$$.fragment,r),O(i.$$.fragment,r),l=!0)},o(r){D(e.$$.fragment,r),D(i.$$.fragment,r),l=!1},d(r){r&&k(t),n[47](null),q(e,r),n[50](null),q(i,r)}}}const Xi="schema",to="api_rules",ss="options",ID="base",Vh="auth",Bh="view";function La(n){return JSON.stringify(n)}function LD(n,e,t){let i,l,s,o,r,a,u,f,c;Qe(n,Mu,Fe=>t(29,u=Fe)),Qe(n,Qn,Fe=>t(52,f=Fe)),Qe(n,Sn,Fe=>t(5,c=Fe));const d={};d[ID]="Base",d[Bh]="View",d[Vh]="Auth";const m=_t();let h,g,_=null,y={},S=!1,T=!1,$=Xi,E=La(y),M="",L=[];function I(Fe){t(3,$=Fe)}function A(Fe){return N(Fe),t(10,T=!0),I(Xi),h==null?void 0:h.show()}function P(){return h==null?void 0:h.hide()}function R(){t(10,T=!1),P()}async function N(Fe){Wt({}),typeof Fe<"u"?(t(27,_=Fe),t(2,y=structuredClone(Fe))):(t(27,_=null),t(2,y=structuredClone(u.base)),y.fields.push({type:"autodate",name:"created",onCreate:!0}),y.fields.push({type:"autodate",name:"updated",onCreate:!0,onUpdate:!0})),t(2,y.fields=y.fields||[],y),t(2,y._originalName=y.name||"",y),await fn(),t(28,E=La(y))}function U(Fe=!0){y.id?g==null||g.show(_,y,Fe):j(Fe)}function j(Fe=!0){if(S)return;t(9,S=!0);const Dt=V(),Gt=!y.id;let mn;Gt?mn=me.collections.create(Dt):mn=me.collections.update(y.id,Dt),mn.then(hn=>{Ds(),Mw(hn),Fe?(t(10,T=!1),P()):N(hn),tn(y.id?"Successfully updated collection.":"Successfully created collection."),m("save",{isNew:Gt,collection:hn}),Gt&&Nn(Qn,f=hn,f)}).catch(hn=>{me.error(hn)}).finally(()=>{t(9,S=!1)})}function V(){const Fe=Object.assign({},y);Fe.fields=Fe.fields.slice(0);for(let Dt=Fe.fields.length-1;Dt>=0;Dt--)Fe.fields[Dt]._toDelete&&Fe.fields.splice(Dt,1);return Fe}function K(){_!=null&&_.id&&pn(`Do you really want to delete all "${_.name}" records, including their cascade delete references and files?`,()=>me.collections.truncate(_.id).then(()=>{R(),tn(`Successfully truncated collection "${_.name}".`),m("truncate")}).catch(Fe=>{me.error(Fe)}))}function J(){_!=null&&_.id&&pn(`Do you really want to delete collection "${_.name}" and all its records?`,()=>me.collections.delete(_.id).then(()=>{R(),tn(`Successfully deleted collection "${_.name}".`),m("delete",_),Dw(_)}).catch(Fe=>{me.error(Fe)}))}function ee(Fe){t(2,y.type=Fe,y),t(2,y=Object.assign(structuredClone(u[Fe]),y)),fi("fields")}function X(){r?pn("You have unsaved changes. Do you really want to discard them?",()=>{oe()}):oe()}async function oe(){const Fe=_?structuredClone(_):null;if(Fe){if(Fe.id="",Fe.created="",Fe.updated="",Fe.name+="_duplicate",!z.isEmpty(Fe.fields))for(const Dt of Fe.fields)Dt.id="";if(!z.isEmpty(Fe.indexes))for(let Dt=0;DtP(),Ce=()=>U(),We=()=>U(!1),st=()=>X(),et=()=>K(),Be=()=>J(),rt=Fe=>{t(2,y.name=z.slugify(Fe.target.value),y),Fe.target.value=y.name},Je=Fe=>ee(Fe),at=()=>{a&&U()},Ht=()=>I(Xi),Te=()=>I(to),Ze=()=>I(ss);function ot(Fe){y=Fe,t(2,y),t(27,_)}function Le(Fe){y=Fe,t(2,y),t(27,_)}function Ve(Fe){y=Fe,t(2,y),t(27,_)}function we(Fe){y=Fe,t(2,y),t(27,_)}const Oe=()=>r&&T?(pn("You have unsaved changes. Do you really want to close the panel?",()=>{t(10,T=!1),P()}),!1):!0;function ut(Fe){ie[Fe?"unshift":"push"](()=>{h=Fe,t(7,h)})}function Ne(Fe){Pe.call(this,n,Fe)}function xe(Fe){Pe.call(this,n,Fe)}function qt(Fe){ie[Fe?"unshift":"push"](()=>{g=Fe,t(8,g)})}const Zt=Fe=>j(Fe.detail);return n.$$.update=()=>{var Fe;n.$$.dirty[0]&536870912&&t(12,L=Object.keys(u.base||{})),n.$$.dirty[0]&4&&y.type==="view"&&(t(2,y.createRule=null,y),t(2,y.updateRule=null,y),t(2,y.deleteRule=null,y),t(2,y.indexes=[],y)),n.$$.dirty[0]&134217732&&y.name&&(_==null?void 0:_.name)!=y.name&&y.indexes.length>0&&t(2,y.indexes=(Fe=y.indexes)==null?void 0:Fe.map(Dt=>z.replaceIndexTableName(Dt,y.name)),y),n.$$.dirty[0]&4&&t(17,i=y.type===Vh),n.$$.dirty[0]&4&&t(16,l=y.type===Bh),n.$$.dirty[0]&32&&(c.fields||c.viewQuery?t(11,M=z.getNestedVal(c,"fields.message")||"Has errors"):t(11,M="")),n.$$.dirty[0]&4&&t(15,s=!!y.id&&y.system),n.$$.dirty[0]&4&&t(14,o=!!y.id&&y.system&&y.name=="_superusers"),n.$$.dirty[0]&268435460&&t(4,r=E!=La(y)),n.$$.dirty[0]&20&&t(13,a=!y.id||r),n.$$.dirty[0]&12&&$===ss&&y.type!=="auth"&&I(Xi)},[I,P,y,$,r,c,d,h,g,S,T,M,L,a,o,s,l,i,U,j,K,J,ee,X,Se,A,R,_,E,u,ke,Ce,We,st,et,Be,rt,Je,at,Ht,Te,Ze,ot,Le,Ve,we,Oe,ut,Ne,xe,qt,Zt]}class nf extends ye{constructor(e){super(),be(this,e,LD,DD,_e,{changeTab:0,show:25,hide:1,forceHide:26},null,[-1,-1,-1])}get changeTab(){return this.$$.ctx[0]}get show(){return this.$$.ctx[25]}get hide(){return this.$$.ctx[1]}get forceHide(){return this.$$.ctx[26]}}function AD(n){let e,t,i;return{c(){e=b("span"),p(e,"class","dragline svelte-y9un12"),x(e,"dragging",n[1])},m(l,s){v(l,e,s),n[4](e),t||(i=[B(e,"mousedown",n[5]),B(e,"touchstart",n[2])],t=!0)},p(l,[s]){s&2&&x(e,"dragging",l[1])},i:te,o:te,d(l){l&&k(e),n[4](null),t=!1,De(i)}}}function PD(n,e,t){const i=_t();let{tolerance:l=0}=e,s,o=0,r=0,a=0,u=0,f=!1;function c(_){_.stopPropagation(),o=_.clientX,r=_.clientY,a=_.clientX-s.offsetLeft,u=_.clientY-s.offsetTop,document.addEventListener("touchmove",m),document.addEventListener("mousemove",m),document.addEventListener("touchend",d),document.addEventListener("mouseup",d)}function d(_){f&&(_.preventDefault(),t(1,f=!1),s.classList.remove("no-pointer-events"),i("dragstop",{event:_,elem:s})),document.removeEventListener("touchmove",m),document.removeEventListener("mousemove",m),document.removeEventListener("touchend",d),document.removeEventListener("mouseup",d)}function m(_){let y=_.clientX-o,S=_.clientY-r,T=_.clientX-a,$=_.clientY-u;!f&&Math.abs(T-s.offsetLeft){s=_,t(0,s)})}const g=_=>{_.button==0&&c(_)};return n.$$set=_=>{"tolerance"in _&&t(3,l=_.tolerance)},[s,f,c,l,h,g]}class ND extends ye{constructor(e){super(),be(this,e,PD,AD,_e,{tolerance:3})}}function RD(n){let e,t,i,l,s;const o=n[5].default,r=Lt(o,n,n[4],null);return l=new ND({}),l.$on("dragstart",n[7]),l.$on("dragging",n[8]),l.$on("dragstop",n[9]),{c(){e=b("aside"),r&&r.c(),i=C(),H(l.$$.fragment),p(e,"class",t="page-sidebar "+n[0])},m(a,u){v(a,e,u),r&&r.m(e,null),n[6](e),v(a,i,u),F(l,a,u),s=!0},p(a,[u]){r&&r.p&&(!s||u&16)&&Pt(r,o,a,a[4],s?At(o,a[4],u,null):Nt(a[4]),null),(!s||u&1&&t!==(t="page-sidebar "+a[0]))&&p(e,"class",t)},i(a){s||(O(r,a),O(l.$$.fragment,a),s=!0)},o(a){D(r,a),D(l.$$.fragment,a),s=!1},d(a){a&&(k(e),k(i)),r&&r.d(a),n[6](null),q(l,a)}}}const Wh="@superuserSidebarWidth";function FD(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,o,r,a=localStorage.getItem(Wh)||null;function u(m){ie[m?"unshift":"push"](()=>{o=m,t(1,o),t(2,a)})}const f=()=>{t(3,r=o.offsetWidth)},c=m=>{t(2,a=r+m.detail.diffX+"px")},d=()=>{z.triggerResize()};return n.$$set=m=>{"class"in m&&t(0,s=m.class),"$$scope"in m&&t(4,l=m.$$scope)},n.$$.update=()=>{n.$$.dirty&6&&a&&o&&(t(1,o.style.width=a,o),localStorage.setItem(Wh,a))},[s,o,a,r,l,i,u,f,c,d]}class sk extends ye{constructor(e){super(),be(this,e,FD,RD,_e,{class:0})}}function Yh(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-alert-line txt-sm link-hint"),p(e,"title",""),p(e,"aria-hidden","true")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,"OAuth2 auth is enabled but the collection doesn't have any registered providers")),t=!0)},d(l){l&&k(e),t=!1,i()}}}function qD(n){let e;return{c(){e=b("i"),p(e,"class","ri-pushpin-line m-l-auto svelte-5oh3nd")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function HD(n){let e;return{c(){e=b("i"),p(e,"class","ri-unpin-line svelte-5oh3nd")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function jD(n){var $,E;let e,t,i,l,s,o=n[0].name+"",r,a,u,f,c,d,m,h,g,_=n[0].type=="auth"&&(($=n[0].oauth2)==null?void 0:$.enabled)&&!((E=n[0].oauth2.providers)!=null&&E.length)&&Yh();function y(M,L){return M[1]?HD:qD}let S=y(n),T=S(n);return{c(){var M;e=b("a"),t=b("i"),l=C(),s=b("span"),r=Y(o),a=C(),_&&_.c(),u=C(),f=b("span"),T.c(),p(t,"class",i=js(z.getCollectionTypeIcon(n[0].type))+" svelte-5oh3nd"),p(t,"aria-hidden","true"),p(s,"class","txt"),p(f,"class","btn btn-xs btn-circle btn-hint btn-transparent btn-pin-collection m-l-auto svelte-5oh3nd"),p(f,"aria-label","Pin collection"),p(f,"aria-hidden","true"),p(e,"href",d="/collections?collectionId="+n[0].id),p(e,"class","sidebar-list-item svelte-5oh3nd"),p(e,"title",m=n[0].name),x(e,"active",((M=n[2])==null?void 0:M.id)===n[0].id)},m(M,L){v(M,e,L),w(e,t),w(e,l),w(e,s),w(s,r),w(e,a),_&&_.m(e,null),w(e,u),w(e,f),T.m(f,null),h||(g=[Me(c=He.call(null,f,{position:"right",text:(n[1]?"Unpin":"Pin")+" collection"})),B(f,"click",On(tt(n[5]))),Me(Un.call(null,e))],h=!0)},p(M,[L]){var I,A,P;L&1&&i!==(i=js(z.getCollectionTypeIcon(M[0].type))+" svelte-5oh3nd")&&p(t,"class",i),L&1&&o!==(o=M[0].name+"")&&ue(r,o),M[0].type=="auth"&&((I=M[0].oauth2)!=null&&I.enabled)&&!((A=M[0].oauth2.providers)!=null&&A.length)?_||(_=Yh(),_.c(),_.m(e,u)):_&&(_.d(1),_=null),S!==(S=y(M))&&(T.d(1),T=S(M),T&&(T.c(),T.m(f,null))),c&&Rt(c.update)&&L&2&&c.update.call(null,{position:"right",text:(M[1]?"Unpin":"Pin")+" collection"}),L&1&&d!==(d="/collections?collectionId="+M[0].id)&&p(e,"href",d),L&1&&m!==(m=M[0].name)&&p(e,"title",m),L&5&&x(e,"active",((P=M[2])==null?void 0:P.id)===M[0].id)},i:te,o:te,d(M){M&&k(e),_&&_.d(),T.d(),h=!1,De(g)}}}function zD(n,e,t){let i,l;Qe(n,Qn,u=>t(2,l=u));let{collection:s}=e,{pinnedIds:o}=e;function r(u){o.includes(u.id)?z.removeByValue(o,u.id):o.push(u.id),t(4,o)}const a=()=>r(s);return n.$$set=u=>{"collection"in u&&t(0,s=u.collection),"pinnedIds"in u&&t(4,o=u.pinnedIds)},n.$$.update=()=>{n.$$.dirty&17&&t(1,i=o.includes(s.id))},[s,i,l,r,o,a]}class lf extends ye{constructor(e){super(),be(this,e,zD,jD,_e,{collection:0,pinnedIds:4})}}function Kh(n,e,t){const i=n.slice();return i[25]=e[t],i}function Jh(n,e,t){const i=n.slice();return i[25]=e[t],i}function Zh(n,e,t){const i=n.slice();return i[25]=e[t],i}function Gh(n){let e,t,i=[],l=new Map,s,o,r=pe(n[2]);const a=u=>u[25].id;for(let u=0;uve(i,"pinnedIds",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&4&&(f.collection=e[25]),!l&&u[0]&2&&(l=!0,f.pinnedIds=e[1],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function Qh(n){let e,t=[],i=new Map,l,s,o=n[2].length&&xh(),r=pe(n[8]);const a=u=>u[25].id;for(let u=0;uve(i,"pinnedIds",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&256&&(f.collection=e[25]),!l&&u[0]&2&&(l=!0,f.pinnedIds=e[1],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function t_(n){let e,t,i,l,s,o,r,a,u,f,c,d=!n[4].length&&n_(n),m=(n[6]||n[4].length)&&i_(n);return{c(){e=b("button"),t=b("span"),t.textContent="System",i=C(),d&&d.c(),r=C(),m&&m.c(),a=ge(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","sidebar-title m-b-xs"),p(e,"aria-label",l=n[6]?"Expand system collections":"Collapse system collections"),p(e,"aria-expanded",s=n[6]||n[4].length),e.disabled=o=n[4].length,x(e,"link-hint",!n[4].length)},m(h,g){v(h,e,g),w(e,t),w(e,i),d&&d.m(e,null),v(h,r,g),m&&m.m(h,g),v(h,a,g),u=!0,f||(c=B(e,"click",n[19]),f=!0)},p(h,g){h[4].length?d&&(d.d(1),d=null):d?d.p(h,g):(d=n_(h),d.c(),d.m(e,null)),(!u||g[0]&64&&l!==(l=h[6]?"Expand system collections":"Collapse system collections"))&&p(e,"aria-label",l),(!u||g[0]&80&&s!==(s=h[6]||h[4].length))&&p(e,"aria-expanded",s),(!u||g[0]&16&&o!==(o=h[4].length))&&(e.disabled=o),(!u||g[0]&16)&&x(e,"link-hint",!h[4].length),h[6]||h[4].length?m?(m.p(h,g),g[0]&80&&O(m,1)):(m=i_(h),m.c(),O(m,1),m.m(a.parentNode,a)):m&&(re(),D(m,1,1,()=>{m=null}),ae())},i(h){u||(O(m),u=!0)},o(h){D(m),u=!1},d(h){h&&(k(e),k(r),k(a)),d&&d.d(),m&&m.d(h),f=!1,c()}}}function n_(n){let e,t;return{c(){e=b("i"),p(e,"class",t="ri-arrow-"+(n[6]?"up":"down")+"-s-line"),p(e,"aria-hidden","true")},m(i,l){v(i,e,l)},p(i,l){l[0]&64&&t!==(t="ri-arrow-"+(i[6]?"up":"down")+"-s-line")&&p(e,"class",t)},d(i){i&&k(e)}}}function i_(n){let e=[],t=new Map,i,l,s=pe(n[7]);const o=r=>r[25].id;for(let r=0;rve(i,"pinnedIds",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&128&&(f.collection=e[25]),!l&&u[0]&2&&(l=!0,f.pinnedIds=e[1],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function s_(n){let e;return{c(){e=b("p"),e.textContent="No collections found.",p(e,"class","txt-hint m-t-10 m-b-10 txt-center")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function o_(n){let e,t,i,l;return{c(){e=b("footer"),t=b("button"),t.innerHTML=' New collection',p(t,"type","button"),p(t,"class","btn btn-block btn-outline"),p(e,"class","sidebar-footer")},m(s,o){v(s,e,o),w(e,t),i||(l=B(t,"click",n[21]),i=!0)},p:te,d(s){s&&k(e),i=!1,l()}}}function UD(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T=n[2].length&&Gh(n),$=n[8].length&&Qh(n),E=n[7].length&&t_(n),M=n[4].length&&!n[3].length&&s_(),L=!n[11]&&o_(n);return{c(){e=b("header"),t=b("div"),i=b("div"),l=b("button"),l.innerHTML='',s=C(),o=b("input"),r=C(),a=b("hr"),u=C(),f=b("div"),T&&T.c(),c=C(),$&&$.c(),d=C(),E&&E.c(),m=C(),M&&M.c(),h=C(),L&&L.c(),g=ge(),p(l,"type","button"),p(l,"class","btn btn-xs btn-transparent btn-circle btn-clear"),x(l,"hidden",!n[9]),p(i,"class","form-field-addon"),p(o,"type","text"),p(o,"placeholder","Search collections..."),p(o,"name","collections-search"),p(t,"class","form-field search"),x(t,"active",n[9]),p(e,"class","sidebar-header"),p(a,"class","m-t-5 m-b-xs"),p(f,"class","sidebar-content"),x(f,"fade",n[10]),x(f,"sidebar-content-compact",n[3].length>20)},m(I,A){v(I,e,A),w(e,t),w(t,i),w(i,l),w(t,s),w(t,o),ce(o,n[0]),v(I,r,A),v(I,a,A),v(I,u,A),v(I,f,A),T&&T.m(f,null),w(f,c),$&&$.m(f,null),w(f,d),E&&E.m(f,null),w(f,m),M&&M.m(f,null),v(I,h,A),L&&L.m(I,A),v(I,g,A),_=!0,y||(S=[B(l,"click",n[15]),B(o,"input",n[16])],y=!0)},p(I,A){(!_||A[0]&512)&&x(l,"hidden",!I[9]),A[0]&1&&o.value!==I[0]&&ce(o,I[0]),(!_||A[0]&512)&&x(t,"active",I[9]),I[2].length?T?(T.p(I,A),A[0]&4&&O(T,1)):(T=Gh(I),T.c(),O(T,1),T.m(f,c)):T&&(re(),D(T,1,1,()=>{T=null}),ae()),I[8].length?$?($.p(I,A),A[0]&256&&O($,1)):($=Qh(I),$.c(),O($,1),$.m(f,d)):$&&(re(),D($,1,1,()=>{$=null}),ae()),I[7].length?E?(E.p(I,A),A[0]&128&&O(E,1)):(E=t_(I),E.c(),O(E,1),E.m(f,m)):E&&(re(),D(E,1,1,()=>{E=null}),ae()),I[4].length&&!I[3].length?M||(M=s_(),M.c(),M.m(f,null)):M&&(M.d(1),M=null),(!_||A[0]&1024)&&x(f,"fade",I[10]),(!_||A[0]&8)&&x(f,"sidebar-content-compact",I[3].length>20),I[11]?L&&(L.d(1),L=null):L?L.p(I,A):(L=o_(I),L.c(),L.m(g.parentNode,g))},i(I){_||(O(T),O($),O(E),_=!0)},o(I){D(T),D($),D(E),_=!1},d(I){I&&(k(e),k(r),k(a),k(u),k(f),k(h),k(g)),T&&T.d(),$&&$.d(),E&&E.d(),M&&M.d(),L&&L.d(I),y=!1,De(S)}}}function VD(n){let e,t,i,l;e=new sk({props:{class:"collection-sidebar",$$slots:{default:[UD]},$$scope:{ctx:n}}});let s={};return i=new nf({props:s}),n[22](i),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(o,r){F(e,o,r),v(o,t,r),F(i,o,r),l=!0},p(o,r){const a={};r[0]&4095|r[1]&2&&(a.$$scope={dirty:r,ctx:o}),e.$set(a);const u={};i.$set(u)},i(o){l||(O(e.$$.fragment,o),O(i.$$.fragment,o),l=!0)},o(o){D(e.$$.fragment,o),D(i.$$.fragment,o),l=!1},d(o){o&&k(t),q(e,o),n[22](null),q(i,o)}}}const r_="@pinnedCollections";function BD(){setTimeout(()=>{const n=document.querySelector(".collection-sidebar .sidebar-list-item.active");n&&(n==null||n.scrollIntoView({block:"nearest"}))},0)}function WD(n,e,t){let i,l,s,o,r,a,u,f,c,d;Qe(n,En,N=>t(13,u=N)),Qe(n,Qn,N=>t(14,f=N)),Qe(n,gr,N=>t(10,c=N)),Qe(n,Dl,N=>t(11,d=N));let m,h="",g=[],_=!1,y;S();function S(){t(1,g=[]);try{const N=localStorage.getItem(r_);N&&t(1,g=JSON.parse(N)||[])}catch{}}function T(){t(1,g=g.filter(N=>!!u.find(U=>U.id==N)))}const $=()=>t(0,h="");function E(){h=this.value,t(0,h)}function M(N){g=N,t(1,g)}function L(N){g=N,t(1,g)}const I=()=>{i.length||t(6,_=!_)};function A(N){g=N,t(1,g)}const P=()=>m==null?void 0:m.show();function R(N){ie[N?"unshift":"push"](()=>{m=N,t(5,m)})}return n.$$.update=()=>{n.$$.dirty[0]&8192&&u&&(T(),BD()),n.$$.dirty[0]&1&&t(4,i=h.replace(/\s+/g,"").toLowerCase()),n.$$.dirty[0]&1&&t(9,l=h!==""),n.$$.dirty[0]&2&&g&&localStorage.setItem(r_,JSON.stringify(g)),n.$$.dirty[0]&8209&&t(3,s=u.filter(N=>N.id==h||N.name.replace(/\s+/g,"").toLowerCase().includes(i))),n.$$.dirty[0]&10&&t(2,o=s.filter(N=>g.includes(N.id))),n.$$.dirty[0]&10&&t(8,r=s.filter(N=>!N.system&&!g.includes(N.id))),n.$$.dirty[0]&10&&t(7,a=s.filter(N=>N.system&&!g.includes(N.id))),n.$$.dirty[0]&20484&&f!=null&&f.id&&y!=f.id&&(t(12,y=f.id),f.system&&!o.find(N=>N.id==f.id)?t(6,_=!0):t(6,_=!1))},[h,g,o,s,i,m,_,a,r,l,c,d,y,u,f,$,E,M,L,I,A,P,R]}class YD extends ye{constructor(e){super(),be(this,e,WD,VD,_e,{},null,[-1,-1])}}function KD(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function JD(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("div"),t=b("div"),i=Y(n[2]),l=C(),s=b("div"),o=Y(n[1]),r=Y(" UTC"),p(t,"class","date"),p(s,"class","time svelte-5pjd03"),p(e,"class","datetime svelte-5pjd03")},m(f,c){v(f,e,c),w(e,t),w(t,i),w(e,l),w(e,s),w(s,o),w(s,r),a||(u=Me(He.call(null,e,n[3])),a=!0)},p(f,c){c&4&&ue(i,f[2]),c&2&&ue(o,f[1])},d(f){f&&k(e),a=!1,u()}}}function ZD(n){let e;function t(s,o){return s[0]?JD:KD}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:te,o:te,d(s){s&&k(e),l.d(s)}}}function GD(n,e,t){let i,l,{date:s=""}=e;const o={get text(){return z.formatToLocalDate(s)+" Local"}};return n.$$set=r=>{"date"in r&&t(0,s=r.date)},n.$$.update=()=>{n.$$.dirty&1&&t(2,i=s?s.substring(0,10):null),n.$$.dirty&1&&t(1,l=s?s.substring(10,19):null)},[s,l,i,o]}class sf extends ye{constructor(e){super(),be(this,e,GD,ZD,_e,{date:0})}}function a_(n){let e;function t(s,o){return s[4]==="image"?QD:XD}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function XD(n){let e,t;return{c(){e=b("object"),t=Y("Cannot preview the file."),p(e,"title",n[2]),p(e,"data",n[1])},m(i,l){v(i,e,l),w(e,t)},p(i,l){l&4&&p(e,"title",i[2]),l&2&&p(e,"data",i[1])},d(i){i&&k(e)}}}function QD(n){let e,t,i;return{c(){e=b("img"),vn(e.src,t=n[1])||p(e,"src",t),p(e,"alt",i="Preview "+n[2])},m(l,s){v(l,e,s)},p(l,s){s&2&&!vn(e.src,t=l[1])&&p(e,"src",t),s&4&&i!==(i="Preview "+l[2])&&p(e,"alt",i)},d(l){l&&k(e)}}}function xD(n){var l;let e=(l=n[3])==null?void 0:l.isActive(),t,i=e&&a_(n);return{c(){i&&i.c(),t=ge()},m(s,o){i&&i.m(s,o),v(s,t,o)},p(s,o){var r;o&8&&(e=(r=s[3])==null?void 0:r.isActive()),e?i?i.p(s,o):(i=a_(s),i.c(),i.m(t.parentNode,t)):i&&(i.d(1),i=null)},d(s){s&&k(t),i&&i.d(s)}}}function eI(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","overlay-close")},m(l,s){v(l,e,s),t||(i=B(e,"click",tt(n[0])),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function tI(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("a"),t=Y(n[2]),i=C(),l=b("i"),s=C(),o=b("div"),r=C(),a=b("button"),a.textContent="Close",p(l,"class","ri-external-link-line"),p(e,"href",n[1]),p(e,"title",n[2]),p(e,"target","_blank"),p(e,"rel","noreferrer noopener"),p(e,"class","link-hint txt-ellipsis inline-flex"),p(o,"class","flex-fill"),p(a,"type","button"),p(a,"class","btn btn-transparent")},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),v(c,s,d),v(c,o,d),v(c,r,d),v(c,a,d),u||(f=B(a,"click",n[0]),u=!0)},p(c,d){d&4&&ue(t,c[2]),d&2&&p(e,"href",c[1]),d&4&&p(e,"title",c[2])},d(c){c&&(k(e),k(s),k(o),k(r),k(a)),u=!1,f()}}}function nI(n){let e,t,i={class:"preview preview-"+n[4],btnClose:!1,popup:!0,$$slots:{footer:[tI],header:[eI],default:[xD]},$$scope:{ctx:n}};return e=new ln({props:i}),n[7](e),e.$on("show",n[8]),e.$on("hide",n[9]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.class="preview preview-"+l[4]),s&1054&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[7](null),q(e,l)}}}function iI(n,e,t){let i,l,s,o,r="";function a(m){m!==""&&(t(1,r=m),o==null||o.show())}function u(){return o==null?void 0:o.hide()}function f(m){ie[m?"unshift":"push"](()=>{o=m,t(3,o)})}function c(m){Pe.call(this,n,m)}function d(m){Pe.call(this,n,m)}return n.$$.update=()=>{n.$$.dirty&2&&t(6,i=r.indexOf("?")),n.$$.dirty&66&&t(2,l=r.substring(r.lastIndexOf("/")+1,i>0?i:void 0)),n.$$.dirty&4&&t(4,s=z.getFileType(l))},[u,r,l,o,s,a,i,f,c,d]}class lI extends ye{constructor(e){super(),be(this,e,iI,nI,_e,{show:5,hide:0})}get show(){return this.$$.ctx[5]}get hide(){return this.$$.ctx[0]}}function sI(n){let e,t,i,l,s;function o(u,f){return u[3]==="image"?uI:u[3]==="video"||u[3]==="audio"?aI:rI}let r=o(n),a=r(n);return{c(){e=b("a"),a.c(),p(e,"draggable",!1),p(e,"class",t="thumb "+(n[1]?`thumb-${n[1]}`:"")),p(e,"href",n[6]),p(e,"target","_blank"),p(e,"rel","noreferrer"),p(e,"title",i=(n[7]?"Preview":"Download")+" "+n[0])},m(u,f){v(u,e,f),a.m(e,null),l||(s=B(e,"click",On(n[11])),l=!0)},p(u,f){r===(r=o(u))&&a?a.p(u,f):(a.d(1),a=r(u),a&&(a.c(),a.m(e,null))),f&2&&t!==(t="thumb "+(u[1]?`thumb-${u[1]}`:""))&&p(e,"class",t),f&64&&p(e,"href",u[6]),f&129&&i!==(i=(u[7]?"Preview":"Download")+" "+u[0])&&p(e,"title",i)},d(u){u&&k(e),a.d(),l=!1,s()}}}function oI(n){let e,t;return{c(){e=b("div"),p(e,"class",t="thumb "+(n[1]?`thumb-${n[1]}`:""))},m(i,l){v(i,e,l)},p(i,l){l&2&&t!==(t="thumb "+(i[1]?`thumb-${i[1]}`:""))&&p(e,"class",t)},d(i){i&&k(e)}}}function rI(n){let e;return{c(){e=b("i"),p(e,"class","ri-file-3-line")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function aI(n){let e;return{c(){e=b("i"),p(e,"class","ri-video-line")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function uI(n){let e,t,i,l,s;return{c(){e=b("img"),p(e,"draggable",!1),p(e,"loading","lazy"),vn(e.src,t=n[5])||p(e,"src",t),p(e,"alt",n[0]),p(e,"title",i="Preview "+n[0])},m(o,r){v(o,e,r),l||(s=B(e,"error",n[8]),l=!0)},p(o,r){r&32&&!vn(e.src,t=o[5])&&p(e,"src",t),r&1&&p(e,"alt",o[0]),r&1&&i!==(i="Preview "+o[0])&&p(e,"title",i)},d(o){o&&k(e),l=!1,s()}}}function u_(n){let e,t,i={};return e=new lI({props:i}),n[12](e),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,s){const o={};e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[12](null),q(e,l)}}}function fI(n){let e,t,i;function l(a,u){return a[2]?oI:sI}let s=l(n),o=s(n),r=n[7]&&u_(n);return{c(){o.c(),e=C(),r&&r.c(),t=ge()},m(a,u){o.m(a,u),v(a,e,u),r&&r.m(a,u),v(a,t,u),i=!0},p(a,[u]){s===(s=l(a))&&o?o.p(a,u):(o.d(1),o=s(a),o&&(o.c(),o.m(e.parentNode,e))),a[7]?r?(r.p(a,u),u&128&&O(r,1)):(r=u_(a),r.c(),O(r,1),r.m(t.parentNode,t)):r&&(re(),D(r,1,1,()=>{r=null}),ae())},i(a){i||(O(r),i=!0)},o(a){D(r),i=!1},d(a){a&&(k(e),k(t)),o.d(a),r&&r.d(a)}}}function cI(n,e,t){let i,l,{record:s=null}=e,{filename:o=""}=e,{size:r=""}=e,a,u="",f="",c="",d=!0;m();async function m(){t(2,d=!0);try{t(10,c=await me.getSuperuserFileToken(s.collectionId))}catch(y){console.warn("File token failure:",y)}t(2,d=!1)}function h(){t(5,u="")}const g=y=>{l&&(y.preventDefault(),a==null||a.show(f))};function _(y){ie[y?"unshift":"push"](()=>{a=y,t(4,a)})}return n.$$set=y=>{"record"in y&&t(9,s=y.record),"filename"in y&&t(0,o=y.filename),"size"in y&&t(1,r=y.size)},n.$$.update=()=>{n.$$.dirty&1&&t(3,i=z.getFileType(o)),n.$$.dirty&9&&t(7,l=["image","audio","video"].includes(i)||o.endsWith(".pdf")),n.$$.dirty&1541&&t(6,f=d?"":me.files.getURL(s,o,{token:c})),n.$$.dirty&1541&&t(5,u=d?"":me.files.getURL(s,o,{thumb:"100x100",token:c}))},[o,r,d,i,a,u,f,l,h,s,c,g,_]}class of extends ye{constructor(e){super(),be(this,e,cI,fI,_e,{record:9,filename:0,size:1})}}function f_(n,e,t){const i=n.slice();return i[7]=e[t],i[8]=e,i[9]=t,i}function c_(n,e,t){const i=n.slice();i[7]=e[t];const l=z.toArray(i[0][i[7].name]).slice(0,5);return i[10]=l,i}function d_(n,e,t){const i=n.slice();return i[13]=e[t],i}function p_(n){let e,t;return e=new of({props:{record:n[0],filename:n[13],size:"xs"}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&1&&(s.record=i[0]),l&3&&(s.filename=i[13]),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function m_(n){let e=!z.isEmpty(n[13]),t,i,l=e&&p_(n);return{c(){l&&l.c(),t=ge()},m(s,o){l&&l.m(s,o),v(s,t,o),i=!0},p(s,o){o&3&&(e=!z.isEmpty(s[13])),e?l?(l.p(s,o),o&3&&O(l,1)):(l=p_(s),l.c(),O(l,1),l.m(t.parentNode,t)):l&&(re(),D(l,1,1,()=>{l=null}),ae())},i(s){i||(O(l),i=!0)},o(s){D(l),i=!1},d(s){s&&k(t),l&&l.d(s)}}}function h_(n){let e,t,i=pe(n[10]),l=[];for(let o=0;oD(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;ove(e,"record",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};!t&&r&5&&(t=!0,a.record=n[0].expand[n[7].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function g_(n){let e,t,i,l,s,o=n[9]>0&&dI();const r=[mI,pI],a=[];function u(f,c){var d;return f[7].type=="relation"&&((d=f[0].expand)!=null&&d[f[7].name])?0:1}return t=u(n),i=a[t]=r[t](n),{c(){o&&o.c(),e=C(),i.c(),l=ge()},m(f,c){o&&o.m(f,c),v(f,e,c),a[t].m(f,c),v(f,l,c),s=!0},p(f,c){let d=t;t=u(f),t===d?a[t].p(f,c):(re(),D(a[d],1,1,()=>{a[d]=null}),ae(),i=a[t],i?i.p(f,c):(i=a[t]=r[t](f),i.c()),O(i,1),i.m(l.parentNode,l))},i(f){s||(O(i),s=!0)},o(f){D(i),s=!1},d(f){f&&(k(e),k(l)),o&&o.d(f),a[t].d(f)}}}function hI(n){let e,t,i,l=pe(n[1]),s=[];for(let c=0;cD(s[c],1,1,()=>{s[c]=null});let r=pe(n[2]),a=[];for(let c=0;cD(a[c],1,1,()=>{a[c]=null});let f=null;return r.length||(f=__(n)),{c(){for(let c=0;ct(4,l=f));let{record:s}=e,o=[],r=[];function a(){const f=(i==null?void 0:i.fields)||[];if(t(1,o=f.filter(c=>!c.hidden&&c.presentable&&c.type=="file")),t(2,r=f.filter(c=>!c.hidden&&c.presentable&&c.type!="file")),!o.length&&!r.length){const c=f.find(d=>{var m;return!d.hidden&&d.type=="file"&&d.maxSelect==1&&((m=d.mimeTypes)==null?void 0:m.find(h=>h.startsWith("image/")))});c&&o.push(c)}}function u(f,c){n.$$.not_equal(s.expand[c.name],f)&&(s.expand[c.name]=f,t(0,s))}return n.$$set=f=>{"record"in f&&t(0,s=f.record)},n.$$.update=()=>{n.$$.dirty&17&&t(3,i=l==null?void 0:l.find(f=>f.id==(s==null?void 0:s.collectionId))),n.$$.dirty&8&&i&&a()},[s,o,r,i,l,u]}class ok extends ye{constructor(e){super(),be(this,e,_I,hI,_e,{record:0})}}function gI(n){let e,t,i,l,s,o,r,a,u,f;return t=new ok({props:{record:n[0]}}),{c(){e=b("div"),H(t.$$.fragment),i=C(),l=b("a"),s=b("i"),p(s,"class","ri-external-link-line txt-sm"),p(l,"href",o="#/collections?collectionId="+n[0].collectionId+"&recordId="+n[0].id),p(l,"target","_blank"),p(l,"class","inline-flex link-hint"),p(l,"rel","noopener noreferrer"),p(e,"class","record-info svelte-69icne")},m(c,d){v(c,e,d),F(t,e,null),w(e,i),w(e,l),w(l,s),a=!0,u||(f=[Me(r=He.call(null,l,{text:`Open relation record in new tab: +`+z.truncate(JSON.stringify(z.truncateObject(b_(n[0],"expand")),null,2),800,!0),class:"code",position:"left"})),B(l,"click",On(n[1])),B(l,"keydown",On(n[2]))],u=!0)},p(c,[d]){const m={};d&1&&(m.record=c[0]),t.$set(m),(!a||d&1&&o!==(o="#/collections?collectionId="+c[0].collectionId+"&recordId="+c[0].id))&&p(l,"href",o),r&&Rt(r.update)&&d&1&&r.update.call(null,{text:`Open relation record in new tab: +`+z.truncate(JSON.stringify(z.truncateObject(b_(c[0],"expand")),null,2),800,!0),class:"code",position:"left"})},i(c){a||(O(t.$$.fragment,c),a=!0)},o(c){D(t.$$.fragment,c),a=!1},d(c){c&&k(e),q(t),u=!1,De(f)}}}function b_(n,...e){const t=Object.assign({},n);for(let i of e)delete t[i];return t}function bI(n,e,t){let{record:i}=e;function l(o){Pe.call(this,n,o)}function s(o){Pe.call(this,n,o)}return n.$$set=o=>{"record"in o&&t(0,i=o.record)},[i,l,s]}class Ur extends ye{constructor(e){super(),be(this,e,bI,gI,_e,{record:0})}}function y_(n,e,t){const i=n.slice();return i[19]=e[t],i[9]=t,i}function k_(n,e,t){const i=n.slice();return i[14]=e[t],i}function v_(n,e,t){const i=n.slice();return i[7]=e[t],i[9]=t,i}function w_(n,e,t){const i=n.slice();return i[7]=e[t],i[9]=t,i}function yI(n){const e=n.slice(),t=z.toArray(e[3]);e[17]=t;const i=e[2]?10:500;return e[18]=i,e}function kI(n){var s,o;const e=n.slice(),t=z.toArray(e[3]);e[10]=t;const i=z.toArray((o=(s=e[0])==null?void 0:s.expand)==null?void 0:o[e[1].name]);e[11]=i;const l=e[2]?20:500;return e[12]=l,e}function vI(n){const e=n.slice(),t=z.trimQuotedValue(JSON.stringify(e[3]))||'""';return e[6]=t,e}function wI(n){let e,t;return{c(){e=b("div"),t=Y(n[3]),p(e,"class","block txt-break fallback-block svelte-jdf51v")},m(i,l){v(i,e,l),w(e,t)},p(i,l){l&8&&ue(t,i[3])},i:te,o:te,d(i){i&&k(e)}}}function SI(n){let e,t=z.truncate(n[3])+"",i,l;return{c(){e=b("span"),i=Y(t),p(e,"class","txt txt-ellipsis"),p(e,"title",l=z.truncate(n[3]))},m(s,o){v(s,e,o),w(e,i)},p(s,o){o&8&&t!==(t=z.truncate(s[3])+"")&&ue(i,t),o&8&&l!==(l=z.truncate(s[3]))&&p(e,"title",l)},i:te,o:te,d(s){s&&k(e)}}}function TI(n){let e,t=[],i=new Map,l,s,o=pe(n[17].slice(0,n[18]));const r=u=>u[9]+u[19];for(let u=0;un[18]&&T_();return{c(){e=b("div");for(let u=0;uu[18]?a||(a=T_(),a.c(),a.m(e,null)):a&&(a.d(1),a=null),(!s||f&2)&&x(e,"multiple",u[1].maxSelect!=1)},i(u){if(!s){for(let f=0;fn[12]&&O_();return{c(){e=b("div"),i.c(),l=C(),u&&u.c(),p(e,"class","inline-flex")},m(f,c){v(f,e,c),r[t].m(e,null),w(e,l),u&&u.m(e,null),s=!0},p(f,c){let d=t;t=a(f),t===d?r[t].p(f,c):(re(),D(r[d],1,1,()=>{r[d]=null}),ae(),i=r[t],i?i.p(f,c):(i=r[t]=o[t](f),i.c()),O(i,1),i.m(e,l)),f[10].length>f[12]?u||(u=O_(),u.c(),u.m(e,null)):u&&(u.d(1),u=null)},i(f){s||(O(i),s=!0)},o(f){D(i),s=!1},d(f){f&&k(e),r[t].d(),u&&u.d()}}}function CI(n){let e,t=[],i=new Map,l=pe(z.toArray(n[3]));const s=o=>o[9]+o[7];for(let o=0;o{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function MI(n){let e,t=z.truncate(n[3])+"",i,l,s;return{c(){e=b("a"),i=Y(t),p(e,"class","txt-ellipsis"),p(e,"href",n[3]),p(e,"target","_blank"),p(e,"rel","noopener noreferrer")},m(o,r){v(o,e,r),w(e,i),l||(s=[Me(He.call(null,e,"Open in new tab")),B(e,"click",On(n[5]))],l=!0)},p(o,r){r&8&&t!==(t=z.truncate(o[3])+"")&&ue(i,t),r&8&&p(e,"href",o[3])},i:te,o:te,d(o){o&&k(e),l=!1,De(s)}}}function DI(n){let e,t;return{c(){e=b("span"),t=Y(n[3]),p(e,"class","txt")},m(i,l){v(i,e,l),w(e,t)},p(i,l){l&8&&ue(t,i[3])},i:te,o:te,d(i){i&&k(e)}}}function II(n){let e,t=n[3]?"True":"False",i;return{c(){e=b("span"),i=Y(t),p(e,"class","label"),x(e,"label-success",!!n[3])},m(l,s){v(l,e,s),w(e,i)},p(l,s){s&8&&t!==(t=l[3]?"True":"False")&&ue(i,t),s&8&&x(e,"label-success",!!l[3])},i:te,o:te,d(l){l&&k(e)}}}function LI(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function AI(n){let e,t,i,l;const s=[jI,HI],o=[];function r(a,u){return a[2]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function PI(n){let e,t,i,l,s,o,r,a;t=new ai({props:{value:n[3]}});let u=n[0].collectionName=="_superusers"&&n[0].id==n[4].id&&D_();return{c(){e=b("div"),H(t.$$.fragment),i=C(),l=b("div"),s=Y(n[3]),o=C(),u&&u.c(),r=ge(),p(l,"class","txt txt-ellipsis"),p(e,"class","label")},m(f,c){v(f,e,c),F(t,e,null),w(e,i),w(e,l),w(l,s),v(f,o,c),u&&u.m(f,c),v(f,r,c),a=!0},p(f,c){const d={};c&8&&(d.value=f[3]),t.$set(d),(!a||c&8)&&ue(s,f[3]),f[0].collectionName=="_superusers"&&f[0].id==f[4].id?u||(u=D_(),u.c(),u.m(r.parentNode,r)):u&&(u.d(1),u=null)},i(f){a||(O(t.$$.fragment,f),a=!0)},o(f){D(t.$$.fragment,f),a=!1},d(f){f&&(k(e),k(o),k(r)),q(t),u&&u.d(f)}}}function S_(n,e){let t,i,l;return i=new of({props:{record:e[0],filename:e[19],size:"sm"}}),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(s,o){v(s,t,o),F(i,s,o),l=!0},p(s,o){e=s;const r={};o&1&&(r.record=e[0]),o&12&&(r.filename=e[19]),i.$set(r)},i(s){l||(O(i.$$.fragment,s),l=!0)},o(s){D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(i,s)}}}function T_(n){let e;return{c(){e=Y("...")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function NI(n){let e,t=pe(n[10].slice(0,n[12])),i=[];for(let l=0;lr[9]+r[7];for(let r=0;r500&&M_(n);return{c(){e=b("span"),i=Y(t),l=C(),r&&r.c(),s=ge(),p(e,"class","txt")},m(a,u){v(a,e,u),w(e,i),v(a,l,u),r&&r.m(a,u),v(a,s,u),o=!0},p(a,u){(!o||u&8)&&t!==(t=z.truncate(a[6],500,!0)+"")&&ue(i,t),a[6].length>500?r?(r.p(a,u),u&8&&O(r,1)):(r=M_(a),r.c(),O(r,1),r.m(s.parentNode,s)):r&&(re(),D(r,1,1,()=>{r=null}),ae())},i(a){o||(O(r),o=!0)},o(a){D(r),o=!1},d(a){a&&(k(e),k(l),k(s)),r&&r.d(a)}}}function jI(n){let e,t=z.truncate(n[6])+"",i;return{c(){e=b("span"),i=Y(t),p(e,"class","txt txt-ellipsis")},m(l,s){v(l,e,s),w(e,i)},p(l,s){s&8&&t!==(t=z.truncate(l[6])+"")&&ue(i,t)},i:te,o:te,d(l){l&&k(e)}}}function M_(n){let e,t;return e=new ai({props:{value:JSON.stringify(n[3],null,2)}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&8&&(s.value=JSON.stringify(i[3],null,2)),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function D_(n){let e;return{c(){e=b("span"),e.textContent="You",p(e,"class","label label-warning")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function zI(n){let e,t,i,l,s;const o=[PI,AI,LI,II,DI,MI,EI,OI,CI,$I,TI,SI,wI],r=[];function a(f,c){return c&8&&(e=null),f[1].primaryKey?0:f[1].type==="json"?1:(e==null&&(e=!!z.isEmpty(f[3])),e?2:f[1].type==="bool"?3:f[1].type==="number"?4:f[1].type==="url"?5:f[1].type==="editor"?6:f[1].type==="date"||f[1].type==="autodate"?7:f[1].type==="select"?8:f[1].type==="relation"?9:f[1].type==="file"?10:f[2]?11:12)}function u(f,c){return c===1?vI(f):c===9?kI(f):c===10?yI(f):f}return t=a(n,-1),i=r[t]=o[t](u(n,t)),{c(){i.c(),l=ge()},m(f,c){r[t].m(f,c),v(f,l,c),s=!0},p(f,[c]){let d=t;t=a(f,c),t===d?r[t].p(u(f,t),c):(re(),D(r[d],1,1,()=>{r[d]=null}),ae(),i=r[t],i?i.p(u(f,t),c):(i=r[t]=o[t](u(f,t)),i.c()),O(i,1),i.m(l.parentNode,l))},i(f){s||(O(i),s=!0)},o(f){D(i),s=!1},d(f){f&&k(l),r[t].d(f)}}}function UI(n,e,t){let i,l;Qe(n,Fr,u=>t(4,l=u));let{record:s}=e,{field:o}=e,{short:r=!1}=e;function a(u){Pe.call(this,n,u)}return n.$$set=u=>{"record"in u&&t(0,s=u.record),"field"in u&&t(1,o=u.field),"short"in u&&t(2,r=u.short)},n.$$.update=()=>{n.$$.dirty&3&&t(3,i=s==null?void 0:s[o.name])},[s,o,r,i,l,a]}class rk extends ye{constructor(e){super(),be(this,e,UI,zI,_e,{record:0,field:1,short:2})}}function I_(n,e,t){const i=n.slice();return i[13]=e[t],i}function L_(n){let e,t,i=n[13].name+"",l,s,o,r,a;return r=new rk({props:{field:n[13],record:n[3]}}),{c(){e=b("tr"),t=b("td"),l=Y(i),s=C(),o=b("td"),H(r.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(o,"class","col-field svelte-1nt58f7")},m(u,f){v(u,e,f),w(e,t),w(t,l),w(e,s),w(e,o),F(r,o,null),a=!0},p(u,f){(!a||f&1)&&i!==(i=u[13].name+"")&&ue(l,i);const c={};f&1&&(c.field=u[13]),f&8&&(c.record=u[3]),r.$set(c)},i(u){a||(O(r.$$.fragment,u),a=!0)},o(u){D(r.$$.fragment,u),a=!1},d(u){u&&k(e),q(r)}}}function A_(n){let e,t,i,l,s,o;return s=new sf({props:{date:n[3].created}}),{c(){e=b("tr"),t=b("td"),t.textContent="created",i=C(),l=b("td"),H(s.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(l,"class","col-field svelte-1nt58f7")},m(r,a){v(r,e,a),w(e,t),w(e,i),w(e,l),F(s,l,null),o=!0},p(r,a){const u={};a&8&&(u.date=r[3].created),s.$set(u)},i(r){o||(O(s.$$.fragment,r),o=!0)},o(r){D(s.$$.fragment,r),o=!1},d(r){r&&k(e),q(s)}}}function P_(n){let e,t,i,l,s,o;return s=new sf({props:{date:n[3].updated}}),{c(){e=b("tr"),t=b("td"),t.textContent="updated",i=C(),l=b("td"),H(s.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(l,"class","col-field svelte-1nt58f7")},m(r,a){v(r,e,a),w(e,t),w(e,i),w(e,l),F(s,l,null),o=!0},p(r,a){const u={};a&8&&(u.date=r[3].updated),s.$set(u)},i(r){o||(O(s.$$.fragment,r),o=!0)},o(r){D(s.$$.fragment,r),o=!1},d(r){r&&k(e),q(s)}}}function VI(n){var M;let e,t,i,l,s,o,r,a,u,f,c=(n[3].id||"...")+"",d,m,h,g,_;a=new ai({props:{value:n[3].id}});let y=pe((M=n[0])==null?void 0:M.fields),S=[];for(let L=0;LD(S[L],1,1,()=>{S[L]=null});let $=n[3].created&&A_(n),E=n[3].updated&&P_(n);return{c(){e=b("table"),t=b("tbody"),i=b("tr"),l=b("td"),l.textContent="id",s=C(),o=b("td"),r=b("div"),H(a.$$.fragment),u=C(),f=b("span"),d=Y(c),m=C();for(let L=0;L{$=null}),ae()),L[3].updated?E?(E.p(L,I),I&8&&O(E,1)):(E=P_(L),E.c(),O(E,1),E.m(t,null)):E&&(re(),D(E,1,1,()=>{E=null}),ae()),(!_||I&16)&&x(e,"table-loading",L[4])},i(L){if(!_){O(a.$$.fragment,L);for(let I=0;IClose',p(e,"type","button"),p(e,"class","btn btn-transparent")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[7]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function YI(n){let e,t,i={class:"record-preview-panel "+(n[5]?"overlay-panel-xl":"overlay-panel-lg"),$$slots:{footer:[WI],header:[BI],default:[VI]},$$scope:{ctx:n}};return e=new ln({props:i}),n[8](e),e.$on("hide",n[9]),e.$on("show",n[10]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&32&&(o.class="record-preview-panel "+(l[5]?"overlay-panel-xl":"overlay-panel-lg")),s&65561&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[8](null),q(e,l)}}}function KI(n,e,t){let i,{collection:l}=e,s,o={},r=!1;function a(_){return f(_),s==null?void 0:s.show()}function u(){return t(4,r=!1),s==null?void 0:s.hide()}async function f(_){t(3,o={}),t(4,r=!0),t(3,o=await c(_)||{}),t(4,r=!1)}async function c(_){if(_&&typeof _=="string"){try{return await me.collection(l.id).getOne(_)}catch(y){y.isAbort||(u(),console.warn("resolveModel:",y),$i(`Unable to load record with id "${_}"`))}return null}return _}const d=()=>u();function m(_){ie[_?"unshift":"push"](()=>{s=_,t(2,s)})}function h(_){Pe.call(this,n,_)}function g(_){Pe.call(this,n,_)}return n.$$set=_=>{"collection"in _&&t(0,l=_.collection)},n.$$.update=()=>{var _;n.$$.dirty&1&&t(5,i=!!((_=l==null?void 0:l.fields)!=null&&_.find(y=>y.type==="editor")))},[l,u,s,o,r,i,a,d,m,h,g]}class JI extends ye{constructor(e){super(),be(this,e,KI,YI,_e,{collection:0,show:6,hide:1})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[1]}}function ZI(n){let e,t,i,l;return{c(){e=b("i"),p(e,"class","ri-calendar-event-line txt-disabled")},m(s,o){v(s,e,o),i||(l=Me(t=He.call(null,e,{text:n[0].join(` +`),position:"left"})),i=!0)},p(s,[o]){t&&Rt(t.update)&&o&1&&t.update.call(null,{text:s[0].join(` +`),position:"left"})},i:te,o:te,d(s){s&&k(e),i=!1,l()}}}const GI="yyyy-MM-dd HH:mm:ss.SSS";function XI(n,e,t){let i,l;Qe(n,En,a=>t(2,l=a));let{record:s}=e,o=[];function r(){t(0,o=[]);const a=i.fields||[];for(let u of a)u.type=="autodate"&&o.push(u.name+": "+z.formatToLocalDate(s[u.name],GI)+" Local")}return n.$$set=a=>{"record"in a&&t(1,s=a.record)},n.$$.update=()=>{n.$$.dirty&6&&(i=s&&l.find(a=>a.id==s.collectionId)),n.$$.dirty&2&&s&&r()},[o,s,l]}class QI extends ye{constructor(e){super(),be(this,e,XI,ZI,_e,{record:1})}}function N_(n,e,t){const i=n.slice();return i[9]=e[t],i}function xI(n){let e;return{c(){e=b("h6"),e.textContent="No linked OAuth2 providers.",p(e,"class","txt-hint txt-center m-t-sm m-b-sm")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function eL(n){let e,t=pe(n[1]),i=[];for(let l=0;l',p(e,"class","block txt-center")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function R_(n){let e,t,i,l,s,o,r=n[4](n[9].provider)+"",a,u,f,c,d=n[9].providerId+"",m,h,g,_,y,S;function T(){return n[6](n[9])}return{c(){var $;e=b("div"),t=b("figure"),i=b("img"),s=C(),o=b("span"),a=Y(r),u=C(),f=b("div"),c=Y("ID: "),m=Y(d),h=C(),g=b("button"),g.innerHTML='',_=C(),vn(i.src,l="./images/oauth2/"+(($=n[3](n[9].provider))==null?void 0:$.logo))||p(i,"src",l),p(i,"alt","Provider logo"),p(t,"class","provider-logo"),p(o,"class","txt"),p(f,"class","txt-hint"),p(g,"type","button"),p(g,"class","btn btn-transparent link-hint btn-circle btn-sm m-l-auto"),p(e,"class","list-item")},m($,E){v($,e,E),w(e,t),w(t,i),w(e,s),w(e,o),w(o,a),w(e,u),w(e,f),w(f,c),w(f,m),w(e,h),w(e,g),w(e,_),y||(S=B(g,"click",T),y=!0)},p($,E){var M;n=$,E&2&&!vn(i.src,l="./images/oauth2/"+((M=n[3](n[9].provider))==null?void 0:M.logo))&&p(i,"src",l),E&2&&r!==(r=n[4](n[9].provider)+"")&&ue(a,r),E&2&&d!==(d=n[9].providerId+"")&&ue(m,d)},d($){$&&k(e),y=!1,S()}}}function nL(n){let e;function t(s,o){var r;return s[2]?tL:(r=s[0])!=null&&r.id&&s[1].length?eL:xI}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:te,o:te,d(s){s&&k(e),l.d(s)}}}function iL(n,e,t){const i=_t();let{record:l}=e,s=[],o=!1;function r(d){return ef.find(m=>m.key==d+"Auth")||{}}function a(d){var m;return((m=r(d))==null?void 0:m.title)||z.sentenize(d,!1)}async function u(){if(!(l!=null&&l.id)){t(1,s=[]),t(2,o=!1);return}t(2,o=!0);try{t(1,s=await me.collection("_externalAuths").getFullList({filter:me.filter("collectionRef = {:collectionId} && recordRef = {:recordId}",{collectionId:l.collectionId,recordId:l.id})}))}catch(d){me.error(d)}t(2,o=!1)}function f(d){!(l!=null&&l.id)||!d||pn(`Do you really want to unlink the ${a(d.provider)} provider?`,()=>me.collection("_externalAuths").delete(d.id).then(()=>{tn(`Successfully unlinked the ${a(d.provider)} provider.`),i("unlink",d.provider),u()}).catch(m=>{me.error(m)}))}u();const c=d=>f(d);return n.$$set=d=>{"record"in d&&t(0,l=d.record)},[l,s,o,r,a,f,c]}class lL extends ye{constructor(e){super(),be(this,e,iL,nL,_e,{record:0})}}function sL(n){let e,t,i,l,s,o,r,a,u,f;return s=new ai({props:{value:n[1]}}),{c(){e=b("div"),t=b("span"),i=Y(n[1]),l=C(),H(s.$$.fragment),o=C(),r=b("i"),p(t,"class","secret svelte-1md8247"),p(r,"class","ri-refresh-line txt-sm link-hint"),p(r,"aria-label","Refresh"),p(e,"class","flex flex-gap-5 p-5")},m(c,d){v(c,e,d),w(e,t),w(t,i),n[6](t),w(e,l),F(s,e,null),w(e,o),w(e,r),a=!0,u||(f=[Me(He.call(null,r,"Refresh")),B(r,"click",n[4])],u=!0)},p(c,d){(!a||d&2)&&ue(i,c[1]);const m={};d&2&&(m.value=c[1]),s.$set(m)},i(c){a||(O(s.$$.fragment,c),a=!0)},o(c){D(s.$$.fragment,c),a=!1},d(c){c&&k(e),n[6](null),q(s),u=!1,De(f)}}}function oL(n){let e,t,i,l,s,o,r,a,u,f;function c(m){n[7](m)}let d={class:"dropdown dropdown-upside dropdown-center dropdown-nowrap",$$slots:{default:[sL]},$$scope:{ctx:n}};return n[3]!==void 0&&(d.active=n[3]),l=new Hn({props:d}),ie.push(()=>ve(l,"active",c)),l.$on("show",n[4]),{c(){e=b("button"),t=b("i"),i=C(),H(l.$$.fragment),p(t,"class","ri-sparkling-line"),p(t,"aria-hidden","true"),p(e,"tabindex","-1"),p(e,"type","button"),p(e,"aria-label","Generate"),p(e,"class",o="btn btn-circle "+n[0]+" svelte-1md8247")},m(m,h){v(m,e,h),w(e,t),w(e,i),F(l,e,null),a=!0,u||(f=Me(r=He.call(null,e,n[3]?"":"Generate")),u=!0)},p(m,[h]){const g={};h&518&&(g.$$scope={dirty:h,ctx:m}),!s&&h&8&&(s=!0,g.active=m[3],$e(()=>s=!1)),l.$set(g),(!a||h&1&&o!==(o="btn btn-circle "+m[0]+" svelte-1md8247"))&&p(e,"class",o),r&&Rt(r.update)&&h&8&&r.update.call(null,m[3]?"":"Generate")},i(m){a||(O(l.$$.fragment,m),a=!0)},o(m){D(l.$$.fragment,m),a=!1},d(m){m&&k(e),q(l),u=!1,f()}}}function rL(n,e,t){const i=_t();let{class:l="btn-sm btn-hint btn-transparent"}=e,{length:s=32}=e,o="",r,a=!1;async function u(){if(t(1,o=z.randomSecret(s)),i("generate",o),await fn(),r){let d=document.createRange();d.selectNode(r),window.getSelection().removeAllRanges(),window.getSelection().addRange(d)}}function f(d){ie[d?"unshift":"push"](()=>{r=d,t(2,r)})}function c(d){a=d,t(3,a)}return n.$$set=d=>{"class"in d&&t(0,l=d.class),"length"in d&&t(5,s=d.length)},[l,o,r,a,u,s,f,c]}class aL extends ye{constructor(e){super(),be(this,e,rL,oL,_e,{class:0,length:5})}}function F_(n){let e,t,i,l,s=n[0].emailVisibility?"On":"Off",o,r,a,u;return{c(){e=b("div"),t=b("button"),i=b("span"),l=Y("Public: "),o=Y(s),p(i,"class","txt"),p(t,"type","button"),p(t,"class",r="btn btn-sm btn-transparent "+(n[0].emailVisibility?"btn-success":"btn-hint")),p(e,"class","form-field-addon email-visibility-addon svelte-1751a4d")},m(f,c){v(f,e,c),w(e,t),w(t,i),w(i,l),w(i,o),a||(u=[Me(He.call(null,t,{text:"Make email public or private",position:"top-right"})),B(t,"click",tt(n[7]))],a=!0)},p(f,c){c&1&&s!==(s=f[0].emailVisibility?"On":"Off")&&ue(o,s),c&1&&r!==(r="btn btn-sm btn-transparent "+(f[0].emailVisibility?"btn-success":"btn-hint"))&&p(t,"class",r)},d(f){f&&k(e),a=!1,De(u)}}}function uL(n){let e,t,i,l,s,o,r,a,u,f,c,d,m=!n[5]&&F_(n);return{c(){e=b("label"),t=b("i"),i=C(),l=b("span"),l.textContent="email",o=C(),m&&m.c(),r=C(),a=b("input"),p(t,"class",z.getFieldTypeIcon("email")),p(l,"class","txt"),p(e,"for",s=n[14]),p(a,"type","email"),a.autofocus=n[1],p(a,"autocomplete","off"),p(a,"id",u=n[14]),a.required=f=n[4].required,p(a,"class","svelte-1751a4d")},m(h,g){v(h,e,g),w(e,t),w(e,i),w(e,l),v(h,o,g),m&&m.m(h,g),v(h,r,g),v(h,a,g),ce(a,n[0].email),n[1]&&a.focus(),c||(d=B(a,"input",n[8]),c=!0)},p(h,g){g&16384&&s!==(s=h[14])&&p(e,"for",s),h[5]?m&&(m.d(1),m=null):m?m.p(h,g):(m=F_(h),m.c(),m.m(r.parentNode,r)),g&2&&(a.autofocus=h[1]),g&16384&&u!==(u=h[14])&&p(a,"id",u),g&16&&f!==(f=h[4].required)&&(a.required=f),g&1&&a.value!==h[0].email&&ce(a,h[0].email)},d(h){h&&(k(e),k(o),k(r),k(a)),m&&m.d(h),c=!1,d()}}}function q_(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle",name:"verified",$$slots:{default:[fL,({uniqueId:i})=>({14:i}),({uniqueId:i})=>i?16384:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&49156&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function fL(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Change password"),p(e,"type","checkbox"),p(e,"id",t=n[14]),p(l,"for",o=n[14])},m(u,f){v(u,e,f),e.checked=n[2],v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[9]),r=!0)},p(u,f){f&16384&&t!==(t=u[14])&&p(e,"id",t),f&4&&(e.checked=u[2]),f&16384&&o!==(o=u[14])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function H_(n){let e,t,i,l,s,o,r,a,u;return l=new fe({props:{class:"form-field required",name:"password",$$slots:{default:[cL,({uniqueId:f})=>({14:f}),({uniqueId:f})=>f?16384:0]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[dL,({uniqueId:f})=>({14:f}),({uniqueId:f})=>f?16384:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),p(i,"class","col-sm-6"),p(o,"class","col-sm-6"),p(t,"class","grid"),x(t,"p-t-xs",n[2]),p(e,"class","block")},m(f,c){v(f,e,c),w(e,t),w(t,i),F(l,i,null),w(t,s),w(t,o),F(r,o,null),u=!0},p(f,c){const d={};c&49161&&(d.$$scope={dirty:c,ctx:f}),l.$set(d);const m={};c&49153&&(m.$$scope={dirty:c,ctx:f}),r.$set(m),(!u||c&4)&&x(t,"p-t-xs",f[2])},i(f){u||(O(l.$$.fragment,f),O(r.$$.fragment,f),f&&nt(()=>{u&&(a||(a=ze(e,vt,{duration:150},!0)),a.run(1))}),u=!0)},o(f){D(l.$$.fragment,f),D(r.$$.fragment,f),f&&(a||(a=ze(e,vt,{duration:150},!1)),a.run(0)),u=!1},d(f){f&&k(e),q(l),q(r),f&&a&&a.end()}}}function cL(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h;return c=new aL({props:{length:Math.max(15,n[3].min||0)}}),{c(){e=b("label"),t=b("i"),i=C(),l=b("span"),l.textContent="Password",o=C(),r=b("input"),u=C(),f=b("div"),H(c.$$.fragment),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[14]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[14]),r.required=!0,p(f,"class","form-field-addon")},m(g,_){v(g,e,_),w(e,t),w(e,i),w(e,l),v(g,o,_),v(g,r,_),ce(r,n[0].password),v(g,u,_),v(g,f,_),F(c,f,null),d=!0,m||(h=B(r,"input",n[10]),m=!0)},p(g,_){(!d||_&16384&&s!==(s=g[14]))&&p(e,"for",s),(!d||_&16384&&a!==(a=g[14]))&&p(r,"id",a),_&1&&r.value!==g[0].password&&ce(r,g[0].password);const y={};_&8&&(y.length=Math.max(15,g[3].min||0)),c.$set(y)},i(g){d||(O(c.$$.fragment,g),d=!0)},o(g){D(c.$$.fragment,g),d=!1},d(g){g&&(k(e),k(o),k(r),k(u),k(f)),q(c),m=!1,h()}}}function dL(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=b("i"),i=C(),l=b("span"),l.textContent="Password confirm",o=C(),r=b("input"),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[14]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[14]),r.required=!0},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),v(c,o,d),v(c,r,d),ce(r,n[0].passwordConfirm),u||(f=B(r,"input",n[11]),u=!0)},p(c,d){d&16384&&s!==(s=c[14])&&p(e,"for",s),d&16384&&a!==(a=c[14])&&p(r,"id",a),d&1&&r.value!==c[0].passwordConfirm&&ce(r,c[0].passwordConfirm)},d(c){c&&(k(e),k(o),k(r)),u=!1,f()}}}function j_(n){let e,t,i;return t=new fe({props:{class:"form-field form-field-toggle",name:"verified",$$slots:{default:[pL,({uniqueId:l})=>({14:l}),({uniqueId:l})=>l?16384:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","col-lg-12")},m(l,s){v(l,e,s),F(t,e,null),i=!0},p(l,s){const o={};s&49155&&(o.$$scope={dirty:s,ctx:l}),t.$set(o)},i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function pL(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Verified"),p(e,"type","checkbox"),p(e,"id",t=n[14]),p(l,"for",o=n[14])},m(u,f){v(u,e,f),e.checked=n[0].verified,v(u,i,f),v(u,l,f),w(l,s),r||(a=[B(e,"change",n[12]),B(e,"change",tt(n[13]))],r=!0)},p(u,f){f&16384&&t!==(t=u[14])&&p(e,"id",t),f&1&&(e.checked=u[0].verified),f&16384&&o!==(o=u[14])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,De(a)}}}function mL(n){var d;let e,t,i,l,s,o,r,a;i=new fe({props:{class:"form-field "+((d=n[4])!=null&&d.required?"required":""),name:"email",$$slots:{default:[uL,({uniqueId:m})=>({14:m}),({uniqueId:m})=>m?16384:0]},$$scope:{ctx:n}}});let u=!n[1]&&q_(n),f=(n[1]||n[2])&&H_(n),c=!n[5]&&j_(n);return{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),u&&u.c(),o=C(),f&&f.c(),r=C(),c&&c.c(),p(t,"class","col-lg-12"),p(s,"class","col-lg-12"),p(e,"class","grid m-b-base")},m(m,h){v(m,e,h),w(e,t),F(i,t,null),w(e,l),w(e,s),u&&u.m(s,null),w(s,o),f&&f.m(s,null),w(e,r),c&&c.m(e,null),a=!0},p(m,[h]){var _;const g={};h&16&&(g.class="form-field "+((_=m[4])!=null&&_.required?"required":"")),h&49203&&(g.$$scope={dirty:h,ctx:m}),i.$set(g),m[1]?u&&(re(),D(u,1,1,()=>{u=null}),ae()):u?(u.p(m,h),h&2&&O(u,1)):(u=q_(m),u.c(),O(u,1),u.m(s,o)),m[1]||m[2]?f?(f.p(m,h),h&6&&O(f,1)):(f=H_(m),f.c(),O(f,1),f.m(s,null)):f&&(re(),D(f,1,1,()=>{f=null}),ae()),m[5]?c&&(re(),D(c,1,1,()=>{c=null}),ae()):c?(c.p(m,h),h&32&&O(c,1)):(c=j_(m),c.c(),O(c,1),c.m(e,null))},i(m){a||(O(i.$$.fragment,m),O(u),O(f),O(c),a=!0)},o(m){D(i.$$.fragment,m),D(u),D(f),D(c),a=!1},d(m){m&&k(e),q(i),u&&u.d(),f&&f.d(),c&&c.d()}}}function hL(n,e,t){let i,l,s,{record:o}=e,{collection:r}=e,{isNew:a=!(o!=null&&o.id)}=e,u=!1;const f=()=>t(0,o.emailVisibility=!o.emailVisibility,o);function c(){o.email=this.value,t(0,o),t(2,u)}function d(){u=this.checked,t(2,u)}function m(){o.password=this.value,t(0,o),t(2,u)}function h(){o.passwordConfirm=this.value,t(0,o),t(2,u)}function g(){o.verified=this.checked,t(0,o),t(2,u)}const _=y=>{a||pn("Do you really want to manually change the verified account state?",()=>{},()=>{t(0,o.verified=!y.target.checked,o)})};return n.$$set=y=>{"record"in y&&t(0,o=y.record),"collection"in y&&t(6,r=y.collection),"isNew"in y&&t(1,a=y.isNew)},n.$$.update=()=>{var y,S;n.$$.dirty&64&&t(5,i=(r==null?void 0:r.name)=="_superusers"),n.$$.dirty&64&&t(4,l=((y=r==null?void 0:r.fields)==null?void 0:y.find(T=>T.name=="email"))||{}),n.$$.dirty&64&&t(3,s=((S=r==null?void 0:r.fields)==null?void 0:S.find(T=>T.name=="password"))||{}),n.$$.dirty&4&&(u||(t(0,o.password=void 0,o),t(0,o.passwordConfirm=void 0,o),fi("password"),fi("passwordConfirm")))},[o,a,u,s,l,i,r,f,c,d,m,h,g,_]}class _L extends ye{constructor(e){super(),be(this,e,hL,mL,_e,{record:0,collection:6,isNew:1})}}function z_(n){let e;function t(s,o){return s[1].primaryKey?bL:gL}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function gL(n){let e,t;return{c(){e=b("i"),p(e,"class",t=z.getFieldTypeIcon(n[1].type))},m(i,l){v(i,e,l)},p(i,l){l&2&&t!==(t=z.getFieldTypeIcon(i[1].type))&&p(e,"class",t)},d(i){i&&k(e)}}}function bL(n){let e;return{c(){e=b("i"),p(e,"class",z.getFieldTypeIcon("primary"))},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function U_(n){let e;return{c(){e=b("small"),e.textContent="Hidden",p(e,"class","label label-sm label-danger")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function yL(n){let e,t,i,l=n[1].name+"",s,o,r,a,u=n[2]&&z_(n),f=n[1].hidden&&U_();const c=n[4].default,d=Lt(c,n,n[3],null);return{c(){e=b("label"),u&&u.c(),t=C(),i=b("span"),s=Y(l),o=C(),f&&f.c(),r=C(),d&&d.c(),p(i,"class","txt"),p(e,"for",n[0])},m(m,h){v(m,e,h),u&&u.m(e,null),w(e,t),w(e,i),w(i,s),w(e,o),f&&f.m(e,null),w(e,r),d&&d.m(e,null),a=!0},p(m,[h]){m[2]?u?u.p(m,h):(u=z_(m),u.c(),u.m(e,t)):u&&(u.d(1),u=null),(!a||h&2)&&l!==(l=m[1].name+"")&&ue(s,l),m[1].hidden?f||(f=U_(),f.c(),f.m(e,r)):f&&(f.d(1),f=null),d&&d.p&&(!a||h&8)&&Pt(d,c,m,m[3],a?At(c,m[3],h,null):Nt(m[3]),null),(!a||h&1)&&p(e,"for",m[0])},i(m){a||(O(d,m),a=!0)},o(m){D(d,m),a=!1},d(m){m&&k(e),u&&u.d(),f&&f.d(),d&&d.d(m)}}}function kL(n,e,t){let{$$slots:i={},$$scope:l}=e,{uniqueId:s}=e,{field:o}=e,{icon:r=!0}=e;return n.$$set=a=>{"uniqueId"in a&&t(0,s=a.uniqueId),"field"in a&&t(1,o=a.field),"icon"in a&&t(2,r=a.icon),"$$scope"in a&&t(3,l=a.$$scope)},[s,o,r,l,i]}class ti extends ye{constructor(e){super(),be(this,e,kL,yL,_e,{uniqueId:0,field:1,icon:2})}}function vL(n){let e,t,i,l,s,o,r;return l=new ti({props:{uniqueId:n[3],field:n[1],icon:!1}}),{c(){e=b("input"),i=C(),H(l.$$.fragment),p(e,"type","checkbox"),p(e,"id",t=n[3])},m(a,u){v(a,e,u),e.checked=n[0],v(a,i,u),F(l,a,u),s=!0,o||(r=B(e,"change",n[2]),o=!0)},p(a,u){(!s||u&8&&t!==(t=a[3]))&&p(e,"id",t),u&1&&(e.checked=a[0]);const f={};u&8&&(f.uniqueId=a[3]),u&2&&(f.field=a[1]),l.$set(f)},i(a){s||(O(l.$$.fragment,a),s=!0)},o(a){D(l.$$.fragment,a),s=!1},d(a){a&&(k(e),k(i)),q(l,a),o=!1,r()}}}function wL(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[vL,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field form-field-toggle "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function SL(n,e,t){let{field:i}=e,{value:l=!1}=e;function s(){l=this.checked,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class TL extends ye{constructor(e){super(),be(this,e,SL,wL,_e,{field:1,value:0})}}function V_(n){let e,t,i,l;return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","link-hint clear-btn svelte-11df51y"),p(e,"class","form-field-addon")},m(s,o){v(s,e,o),w(e,t),i||(l=[Me(He.call(null,t,"Clear")),B(t,"click",n[5])],i=!0)},p:te,d(s){s&&k(e),i=!1,De(l)}}}function $L(n){let e,t,i,l,s,o,r;e=new ti({props:{uniqueId:n[8],field:n[1]}});let a=n[0]&&!n[1].required&&V_(n);function u(d){n[6](d)}function f(d){n[7](d)}let c={id:n[8],options:z.defaultFlatpickrOptions()};return n[2]!==void 0&&(c.value=n[2]),n[0]!==void 0&&(c.formattedValue=n[0]),l=new tf({props:c}),ie.push(()=>ve(l,"value",u)),ie.push(()=>ve(l,"formattedValue",f)),l.$on("close",n[3]),{c(){H(e.$$.fragment),t=C(),a&&a.c(),i=C(),H(l.$$.fragment)},m(d,m){F(e,d,m),v(d,t,m),a&&a.m(d,m),v(d,i,m),F(l,d,m),r=!0},p(d,m){const h={};m&256&&(h.uniqueId=d[8]),m&2&&(h.field=d[1]),e.$set(h),d[0]&&!d[1].required?a?a.p(d,m):(a=V_(d),a.c(),a.m(i.parentNode,i)):a&&(a.d(1),a=null);const g={};m&256&&(g.id=d[8]),!s&&m&4&&(s=!0,g.value=d[2],$e(()=>s=!1)),!o&&m&1&&(o=!0,g.formattedValue=d[0],$e(()=>o=!1)),l.$set(g)},i(d){r||(O(e.$$.fragment,d),O(l.$$.fragment,d),r=!0)},o(d){D(e.$$.fragment,d),D(l.$$.fragment,d),r=!1},d(d){d&&(k(t),k(i)),q(e,d),a&&a.d(d),q(l,d)}}}function CL(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[$L,({uniqueId:i})=>({8:i}),({uniqueId:i})=>i?256:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&775&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function OL(n,e,t){let{field:i}=e,{value:l=void 0}=e,s=l;function o(c){c.detail&&c.detail.length==3&&t(0,l=c.detail[1])}function r(){t(0,l="")}const a=()=>r();function u(c){s=c,t(2,s),t(0,l)}function f(c){l=c,t(0,l)}return n.$$set=c=>{"field"in c&&t(1,i=c.field),"value"in c&&t(0,l=c.value)},n.$$.update=()=>{n.$$.dirty&1&&l&&l.length>19&&t(0,l=l.substring(0,19)),n.$$.dirty&5&&s!=l&&t(2,s=l)},[l,i,s,o,r,a,u,f]}class EL extends ye{constructor(e){super(),be(this,e,OL,CL,_e,{field:1,value:0})}}function B_(n,e,t){const i=n.slice();i[44]=e[t];const l=i[19](i[44]);return i[45]=l,i}function W_(n,e,t){const i=n.slice();return i[48]=e[t],i}function Y_(n,e,t){const i=n.slice();return i[51]=e[t],i}function ML(n){let e,t,i=[],l=new Map,s,o,r,a,u,f,c,d,m,h,g,_=pe(n[7]);const y=S=>S[51].id;for(let S=0;S<_.length;S+=1){let T=Y_(n,_,S),$=y(T);l.set($,i[S]=K_($,T))}return a=new Hr({props:{value:n[4],placeholder:"Record search term or filter...",autocompleteCollection:n[8]}}),a.$on("submit",n[30]),d=new Pu({props:{class:"files-list",vThreshold:100,$$slots:{default:[NL]},$$scope:{ctx:n}}}),d.$on("vScrollEnd",n[32]),{c(){e=b("div"),t=b("aside");for(let S=0;SNew record',c=C(),H(d.$$.fragment),p(t,"class","file-picker-sidebar"),p(f,"type","button"),p(f,"class","btn btn-pill btn-transparent btn-hint p-l-xs p-r-xs"),p(r,"class","flex m-b-base flex-gap-10"),p(o,"class","file-picker-content"),p(e,"class","file-picker")},m(S,T){v(S,e,T),w(e,t);for(let $=0;$file field.",p(e,"class","txt-center txt-hint")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function K_(n,e){let t,i=e[51].name+"",l,s,o,r;function a(){return e[29](e[51])}return{key:n,first:null,c(){var u;t=b("button"),l=Y(i),s=C(),p(t,"type","button"),p(t,"class","sidebar-item"),x(t,"active",((u=e[8])==null?void 0:u.id)==e[51].id),this.first=t},m(u,f){v(u,t,f),w(t,l),w(t,s),o||(r=B(t,"click",tt(a)),o=!0)},p(u,f){var c;e=u,f[0]&128&&i!==(i=e[51].name+"")&&ue(l,i),f[0]&384&&x(t,"active",((c=e[8])==null?void 0:c.id)==e[51].id)},d(u){u&&k(t),o=!1,r()}}}function IL(n){var s;let e,t,i,l=((s=n[4])==null?void 0:s.length)&&J_(n);return{c(){e=b("div"),t=b("span"),t.textContent="No records with images found.",i=C(),l&&l.c(),p(t,"class","txt txt-hint"),p(e,"class","inline-flex")},m(o,r){v(o,e,r),w(e,t),w(e,i),l&&l.m(e,null)},p(o,r){var a;(a=o[4])!=null&&a.length?l?l.p(o,r):(l=J_(o),l.c(),l.m(e,null)):l&&(l.d(1),l=null)},d(o){o&&k(e),l&&l.d()}}}function LL(n){let e=[],t=new Map,i,l=pe(n[5]);const s=o=>o[44].id;for(let o=0;oClear filter',p(e,"type","button"),p(e,"class","btn btn-hint btn-sm")},m(l,s){v(l,e,s),t||(i=B(e,"click",tt(n[17])),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function AL(n){let e;return{c(){e=b("i"),p(e,"class","ri-file-3-line")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function PL(n){let e,t,i;return{c(){e=b("img"),p(e,"loading","lazy"),vn(e.src,t=me.files.getURL(n[44],n[48],{thumb:"100x100"}))||p(e,"src",t),p(e,"alt",i=n[48])},m(l,s){v(l,e,s)},p(l,s){s[0]&32&&!vn(e.src,t=me.files.getURL(l[44],l[48],{thumb:"100x100"}))&&p(e,"src",t),s[0]&32&&i!==(i=l[48])&&p(e,"alt",i)},d(l){l&&k(e)}}}function Z_(n){let e,t,i,l,s,o;function r(f,c){return c[0]&32&&(t=null),t==null&&(t=!!z.hasImageExtension(f[48])),t?PL:AL}let a=r(n,[-1,-1]),u=a(n);return{c(){e=b("button"),u.c(),i=C(),p(e,"type","button"),p(e,"class","thumb handle"),x(e,"thumb-warning",n[16](n[44],n[48]))},m(f,c){v(f,e,c),u.m(e,null),w(e,i),s||(o=[Me(l=He.call(null,e,n[48]+` +(record: `+n[44].id+")")),B(e,"click",tt(function(){Rt(n[20](n[44],n[48]))&&n[20](n[44],n[48]).apply(this,arguments)}))],s=!0)},p(f,c){n=f,a===(a=r(n,c))&&u?u.p(n,c):(u.d(1),u=a(n),u&&(u.c(),u.m(e,i))),l&&Rt(l.update)&&c[0]&32&&l.update.call(null,n[48]+` +(record: `+n[44].id+")"),c[0]&589856&&x(e,"thumb-warning",n[16](n[44],n[48]))},d(f){f&&k(e),u.d(),s=!1,De(o)}}}function G_(n,e){let t,i,l=pe(e[45]),s=[];for(let o=0;o',p(e,"class","block txt-center")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function NL(n){let e,t;function i(r,a){if(r[15])return LL;if(!r[6])return IL}let l=i(n),s=l&&l(n),o=n[6]&&X_();return{c(){s&&s.c(),e=C(),o&&o.c(),t=ge()},m(r,a){s&&s.m(r,a),v(r,e,a),o&&o.m(r,a),v(r,t,a)},p(r,a){l===(l=i(r))&&s?s.p(r,a):(s&&s.d(1),s=l&&l(r),s&&(s.c(),s.m(e.parentNode,e))),r[6]?o||(o=X_(),o.c(),o.m(t.parentNode,t)):o&&(o.d(1),o=null)},d(r){r&&(k(e),k(t)),s&&s.d(r),o&&o.d(r)}}}function RL(n){let e,t,i,l;const s=[DL,ML],o=[];function r(a,u){return a[7].length?1:0}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function FL(n){let e,t;return{c(){e=b("h4"),t=Y(n[0])},m(i,l){v(i,e,l),w(e,t)},p(i,l){l[0]&1&&ue(t,i[0])},d(i){i&&k(e)}}}function Q_(n){let e,t;return e=new fe({props:{class:"form-field file-picker-size-select",$$slots:{default:[qL,({uniqueId:i})=>({23:i}),({uniqueId:i})=>[i?8388608:0]]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&8402944|l[1]&8388608&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function qL(n){let e,t,i;function l(o){n[28](o)}let s={upside:!0,id:n[23],items:n[11],disabled:!n[13],selectPlaceholder:"Select size"};return n[12]!==void 0&&(s.keyOfSelected=n[12]),e=new xn({props:s}),ie.push(()=>ve(e,"keyOfSelected",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r[0]&8388608&&(a.id=o[23]),r[0]&2048&&(a.items=o[11]),r[0]&8192&&(a.disabled=!o[13]),!t&&r[0]&4096&&(t=!0,a.keyOfSelected=o[12],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function HL(n){var h;let e,t,i,l=z.hasImageExtension((h=n[9])==null?void 0:h.name),s,o,r,a,u,f,c,d,m=l&&Q_(n);return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=C(),m&&m.c(),s=C(),o=b("button"),r=b("span"),a=Y(n[1]),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent m-r-auto"),e.disabled=n[6],p(r,"class","txt"),p(o,"type","button"),p(o,"class","btn btn-expanded"),o.disabled=u=!n[13]},m(g,_){v(g,e,_),w(e,t),v(g,i,_),m&&m.m(g,_),v(g,s,_),v(g,o,_),w(o,r),w(r,a),f=!0,c||(d=[B(e,"click",n[2]),B(o,"click",n[21])],c=!0)},p(g,_){var y;(!f||_[0]&64)&&(e.disabled=g[6]),_[0]&512&&(l=z.hasImageExtension((y=g[9])==null?void 0:y.name)),l?m?(m.p(g,_),_[0]&512&&O(m,1)):(m=Q_(g),m.c(),O(m,1),m.m(s.parentNode,s)):m&&(re(),D(m,1,1,()=>{m=null}),ae()),(!f||_[0]&2)&&ue(a,g[1]),(!f||_[0]&8192&&u!==(u=!g[13]))&&(o.disabled=u)},i(g){f||(O(m),f=!0)},o(g){D(m),f=!1},d(g){g&&(k(e),k(i),k(s),k(o)),m&&m.d(g),c=!1,De(d)}}}function jL(n){let e,t,i,l;const s=[{popup:!0},{class:"file-picker-popup"},n[22]];let o={$$slots:{footer:[HL],header:[FL],default:[RL]},$$scope:{ctx:n}};for(let a=0;at(27,u=Te));const f=_t(),c="file_picker_"+z.randomString(5);let{title:d="Select a file"}=e,{submitText:m="Insert"}=e,{fileTypes:h=["image","document","video","audio","file"]}=e,g,_,y="",S=[],T=1,$=0,E=!1,M=[],L=[],I=[],A={},P={},R="";function N(){return K(!0),g==null?void 0:g.show()}function U(){return g==null?void 0:g.hide()}function j(){t(5,S=[]),t(9,P={}),t(12,R="")}function V(){t(4,y="")}async function K(Te=!1){if(A!=null&&A.id){t(6,E=!0),Te&&j();try{const Ze=Te?1:T+1,ot=z.getAllCollectionIdentifiers(A);let Le=z.normalizeSearchFilter(y,ot)||"";Le&&(Le+=" && "),Le+="("+L.map(we=>`${we.name}:length>0`).join("||")+")";const Ve=await me.collection(A.id).getList(Ze,x_,{filter:Le,sort:"-created",fields:"*:excerpt(100)",skipTotal:1,requestKey:c+"loadImagePicker"});t(5,S=z.filterDuplicatesByKey(S.concat(Ve.items))),T=Ve.page,t(26,$=Ve.items.length),t(6,E=!1)}catch(Ze){Ze.isAbort||(me.error(Ze),t(6,E=!1))}}}function J(){var Ze;let Te=["100x100"];if((Ze=P==null?void 0:P.record)!=null&&Ze.id){for(const ot of L)if(z.toArray(P.record[ot.name]).includes(P.name)){Te=Te.concat(z.toArray(ot.thumbs));break}}t(11,I=[{label:"Original size",value:""}]);for(const ot of Te)I.push({label:`${ot} thumb`,value:ot});R&&!Te.includes(R)&&t(12,R="")}function ee(Te){let Ze=[];for(const ot of L){const Le=z.toArray(Te[ot.name]);for(const Ve of Le)h.includes(z.getFileType(Ve))&&Ze.push(Ve)}return Ze}function X(Te,Ze){t(9,P={record:Te,name:Ze})}function oe(){o&&(f("submit",Object.assign({size:R},P)),U())}function Se(Te){R=Te,t(12,R)}const ke=Te=>{t(8,A=Te)},Ce=Te=>t(4,y=Te.detail),We=()=>_==null?void 0:_.show(),st=()=>{s&&K()};function et(Te){ie[Te?"unshift":"push"](()=>{g=Te,t(3,g)})}function Be(Te){Pe.call(this,n,Te)}function rt(Te){Pe.call(this,n,Te)}function Je(Te){ie[Te?"unshift":"push"](()=>{_=Te,t(10,_)})}const at=Te=>{z.removeByKey(S,"id",Te.detail.record.id),S.unshift(Te.detail.record),t(5,S);const Ze=ee(Te.detail.record);Ze.length>0&&X(Te.detail.record,Ze[0])},Ht=Te=>{var Ze;((Ze=P==null?void 0:P.record)==null?void 0:Ze.id)==Te.detail.id&&t(9,P={}),z.removeByKey(S,"id",Te.detail.id),t(5,S)};return n.$$set=Te=>{e=je(je({},e),Ut(Te)),t(22,a=lt(e,r)),"title"in Te&&t(0,d=Te.title),"submitText"in Te&&t(1,m=Te.submitText),"fileTypes"in Te&&t(24,h=Te.fileTypes)},n.$$.update=()=>{var Te;n.$$.dirty[0]&134217728&&t(7,M=u.filter(Ze=>Ze.type!=="view"&&!!z.toArray(Ze.fields).find(ot=>{var Le,Ve;return ot.type==="file"&&!ot.protected&&(!((Le=ot.mimeTypes)!=null&&Le.length)||!!((Ve=ot.mimeTypes)!=null&&Ve.find(we=>we.startsWith("image/"))))}))),n.$$.dirty[0]&384&&!(A!=null&&A.id)&&M.length>0&&t(8,A=M[0]),n.$$.dirty[0]&256&&(L=(Te=A==null?void 0:A.fields)==null?void 0:Te.filter(Ze=>Ze.type==="file"&&!Ze.protected)),n.$$.dirty[0]&256&&A!=null&&A.id&&(V(),J()),n.$$.dirty[0]&512&&P!=null&&P.name&&J(),n.$$.dirty[0]&280&&typeof y<"u"&&A!=null&&A.id&&g!=null&&g.isActive()&&K(!0),n.$$.dirty[0]&512&&t(16,i=(Ze,ot)=>{var Le;return(P==null?void 0:P.name)==ot&&((Le=P==null?void 0:P.record)==null?void 0:Le.id)==Ze.id}),n.$$.dirty[0]&32&&t(15,l=S.find(Ze=>ee(Ze).length>0)),n.$$.dirty[0]&67108928&&t(14,s=!E&&$==x_),n.$$.dirty[0]&576&&t(13,o=!E&&!!(P!=null&&P.name))},[d,m,U,g,y,S,E,M,A,P,_,I,R,o,s,l,i,V,K,ee,X,oe,a,c,h,N,$,u,Se,ke,Ce,We,st,et,Be,rt,Je,at,Ht]}class UL extends ye{constructor(e){super(),be(this,e,zL,jL,_e,{title:0,submitText:1,fileTypes:24,show:25,hide:2},null,[-1,-1])}get show(){return this.$$.ctx[25]}get hide(){return this.$$.ctx[2]}}function VL(n){let e;return{c(){e=b("div"),p(e,"class","tinymce-wrapper")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function BL(n){let e,t,i;function l(o){n[6](o)}let s={id:n[11],conf:n[5]};return n[0]!==void 0&&(s.value=n[0]),e=new Tu({props:s}),ie.push(()=>ve(e,"value",l)),e.$on("init",n[7]),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r&2048&&(a.id=o[11]),r&32&&(a.conf=o[5]),!t&&r&1&&(t=!0,a.value=o[0],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function WL(n){let e,t,i,l,s,o;e=new ti({props:{uniqueId:n[11],field:n[1]}});const r=[BL,VL],a=[];function u(f,c){return f[4]?0:1}return i=u(n),l=a[i]=r[i](n),{c(){H(e.$$.fragment),t=C(),l.c(),s=ge()},m(f,c){F(e,f,c),v(f,t,c),a[i].m(f,c),v(f,s,c),o=!0},p(f,c){const d={};c&2048&&(d.uniqueId=f[11]),c&2&&(d.field=f[1]),e.$set(d);let m=i;i=u(f),i===m?a[i].p(f,c):(re(),D(a[m],1,1,()=>{a[m]=null}),ae(),l=a[i],l?l.p(f,c):(l=a[i]=r[i](f),l.c()),O(l,1),l.m(s.parentNode,s))},i(f){o||(O(e.$$.fragment,f),O(l),o=!0)},o(f){D(e.$$.fragment,f),D(l),o=!1},d(f){f&&(k(t),k(s)),q(e,f),a[i].d(f)}}}function YL(n){let e,t,i,l;e=new fe({props:{class:"form-field form-field-editor "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[WL,({uniqueId:o})=>({11:o}),({uniqueId:o})=>o?2048:0]},$$scope:{ctx:n}}});let s={title:"Select an image",fileTypes:["image"]};return i=new UL({props:s}),n[8](i),i.$on("submit",n[9]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(o,r){F(e,o,r),v(o,t,r),F(i,o,r),l=!0},p(o,[r]){const a={};r&2&&(a.class="form-field form-field-editor "+(o[1].required?"required":"")),r&2&&(a.name=o[1].name),r&6207&&(a.$$scope={dirty:r,ctx:o}),e.$set(a);const u={};i.$set(u)},i(o){l||(O(e.$$.fragment,o),O(i.$$.fragment,o),l=!0)},o(o){D(e.$$.fragment,o),D(i.$$.fragment,o),l=!1},d(o){o&&k(t),q(e,o),n[8](null),q(i,o)}}}function KL(n,e,t){let i,{field:l}=e,{value:s=""}=e,o,r,a=!1,u=null;Yt(async()=>(typeof s>"u"&&t(0,s=""),u=setTimeout(()=>{t(4,a=!0)},100),()=>{clearTimeout(u)}));function f(h){s=h,t(0,s)}const c=h=>{t(3,r=h.detail.editor),r.on("collections_file_picker",()=>{o==null||o.show()})};function d(h){ie[h?"unshift":"push"](()=>{o=h,t(2,o)})}const m=h=>{r==null||r.execCommand("InsertImage",!1,me.files.getURL(h.detail.record,h.detail.name,{thumb:h.detail.size}))};return n.$$set=h=>{"field"in h&&t(1,l=h.field),"value"in h&&t(0,s=h.value)},n.$$.update=()=>{n.$$.dirty&2&&t(5,i=Object.assign(z.defaultEditorOptions(),{convert_urls:l.convertURLs,relative_urls:!1})),n.$$.dirty&1&&typeof s>"u"&&t(0,s="")},[s,l,o,r,a,i,f,c,d,m]}class JL extends ye{constructor(e){super(),be(this,e,KL,YL,_e,{field:1,value:0})}}function ZL(n){let e,t,i,l,s,o,r,a;return e=new ti({props:{uniqueId:n[3],field:n[1]}}),{c(){H(e.$$.fragment),t=C(),i=b("input"),p(i,"type","email"),p(i,"id",l=n[3]),i.required=s=n[1].required},m(u,f){F(e,u,f),v(u,t,f),v(u,i,f),ce(i,n[0]),o=!0,r||(a=B(i,"input",n[2]),r=!0)},p(u,f){const c={};f&8&&(c.uniqueId=u[3]),f&2&&(c.field=u[1]),e.$set(c),(!o||f&8&&l!==(l=u[3]))&&p(i,"id",l),(!o||f&2&&s!==(s=u[1].required))&&(i.required=s),f&1&&i.value!==u[0]&&ce(i,u[0])},i(u){o||(O(e.$$.fragment,u),o=!0)},o(u){D(e.$$.fragment,u),o=!1},d(u){u&&(k(t),k(i)),q(e,u),r=!1,a()}}}function GL(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[ZL,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function XL(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=this.value,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class QL extends ye{constructor(e){super(),be(this,e,XL,GL,_e,{field:1,value:0})}}function xL(n){let e,t;return{c(){e=b("i"),p(e,"class","ri-file-line"),p(e,"alt",t=n[0].name)},m(i,l){v(i,e,l)},p(i,l){l&1&&t!==(t=i[0].name)&&p(e,"alt",t)},d(i){i&&k(e)}}}function eA(n){let e,t,i;return{c(){e=b("img"),p(e,"draggable",!1),vn(e.src,t=n[2])||p(e,"src",t),p(e,"width",n[1]),p(e,"height",n[1]),p(e,"alt",i=n[0].name)},m(l,s){v(l,e,s)},p(l,s){s&4&&!vn(e.src,t=l[2])&&p(e,"src",t),s&2&&p(e,"width",l[1]),s&2&&p(e,"height",l[1]),s&1&&i!==(i=l[0].name)&&p(e,"alt",i)},d(l){l&&k(e)}}}function tA(n){let e;function t(s,o){return s[2]?eA:xL}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:te,o:te,d(s){s&&k(e),l.d(s)}}}function nA(n,e,t){let i,{file:l}=e,{size:s=50}=e;function o(){z.hasImageExtension(l==null?void 0:l.name)?z.generateThumb(l,s,s).then(r=>{t(2,i=r)}).catch(r=>{t(2,i=""),console.warn("Unable to generate thumb: ",r)}):t(2,i="")}return n.$$set=r=>{"file"in r&&t(0,l=r.file),"size"in r&&t(1,s=r.size)},n.$$.update=()=>{n.$$.dirty&1&&typeof l<"u"&&o()},t(2,i=""),[l,s,i]}class iA extends ye{constructor(e){super(),be(this,e,nA,tA,_e,{file:0,size:1})}}function eg(n,e,t){const i=n.slice();return i[29]=e[t],i[31]=t,i}function tg(n,e,t){const i=n.slice();i[34]=e[t],i[31]=t;const l=i[2].includes(i[34]);return i[35]=l,i}function lA(n){let e,t,i;function l(){return n[17](n[34])}return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove")},m(s,o){v(s,e,o),t||(i=[Me(He.call(null,e,"Remove file")),B(e,"click",l)],t=!0)},p(s,o){n=s},d(s){s&&k(e),t=!1,De(i)}}}function sA(n){let e,t,i;function l(){return n[16](n[34])}return{c(){e=b("button"),e.innerHTML='Restore',p(e,"type","button"),p(e,"class","btn btn-sm btn-danger btn-transparent")},m(s,o){v(s,e,o),t||(i=B(e,"click",l),t=!0)},p(s,o){n=s},d(s){s&&k(e),t=!1,i()}}}function oA(n){let e,t,i,l,s,o,r=n[34]+"",a,u,f,c,d,m;i=new of({props:{record:n[3],filename:n[34]}});function h(y,S){return y[35]?sA:lA}let g=h(n),_=g(n);return{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),o=b("a"),a=Y(r),c=C(),d=b("div"),_.c(),x(t,"fade",n[35]),p(o,"draggable",!1),p(o,"href",u=me.files.getURL(n[3],n[34],{token:n[10]})),p(o,"class",f="txt-ellipsis "+(n[35]?"txt-strikethrough txt-hint":"link-primary")),p(o,"title","Download"),p(o,"target","_blank"),p(o,"rel","noopener noreferrer"),p(s,"class","content"),p(d,"class","actions"),p(e,"class","list-item"),x(e,"dragging",n[32]),x(e,"dragover",n[33])},m(y,S){v(y,e,S),w(e,t),F(i,t,null),w(e,l),w(e,s),w(s,o),w(o,a),w(e,c),w(e,d),_.m(d,null),m=!0},p(y,S){const T={};S[0]&8&&(T.record=y[3]),S[0]&32&&(T.filename=y[34]),i.$set(T),(!m||S[0]&36)&&x(t,"fade",y[35]),(!m||S[0]&32)&&r!==(r=y[34]+"")&&ue(a,r),(!m||S[0]&1064&&u!==(u=me.files.getURL(y[3],y[34],{token:y[10]})))&&p(o,"href",u),(!m||S[0]&36&&f!==(f="txt-ellipsis "+(y[35]?"txt-strikethrough txt-hint":"link-primary")))&&p(o,"class",f),g===(g=h(y))&&_?_.p(y,S):(_.d(1),_=g(y),_&&(_.c(),_.m(d,null))),(!m||S[1]&2)&&x(e,"dragging",y[32]),(!m||S[1]&4)&&x(e,"dragover",y[33])},i(y){m||(O(i.$$.fragment,y),m=!0)},o(y){D(i.$$.fragment,y),m=!1},d(y){y&&k(e),q(i),_.d()}}}function ng(n,e){let t,i,l,s;function o(a){e[18](a)}let r={group:e[4].name+"_uploaded",index:e[31],disabled:!e[6],$$slots:{default:[oA,({dragging:a,dragover:u})=>({32:a,33:u}),({dragging:a,dragover:u})=>[0,(a?2:0)|(u?4:0)]]},$$scope:{ctx:e}};return e[0]!==void 0&&(r.list=e[0]),i=new ho({props:r}),ie.push(()=>ve(i,"list",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&16&&(f.group=e[4].name+"_uploaded"),u[0]&32&&(f.index=e[31]),u[0]&64&&(f.disabled=!e[6]),u[0]&1068|u[1]&70&&(f.$$scope={dirty:u,ctx:e}),!l&&u[0]&1&&(l=!0,f.list=e[0],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function rA(n){let e,t,i,l,s,o,r,a,u=n[29].name+"",f,c,d,m,h,g,_;i=new iA({props:{file:n[29]}});function y(){return n[19](n[31])}return{c(){e=b("div"),t=b("figure"),H(i.$$.fragment),l=C(),s=b("div"),o=b("small"),o.textContent="New",r=C(),a=b("span"),f=Y(u),d=C(),m=b("button"),m.innerHTML='',p(t,"class","thumb"),p(o,"class","label label-success m-r-5"),p(a,"class","txt"),p(s,"class","filename m-r-auto"),p(s,"title",c=n[29].name),p(m,"type","button"),p(m,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove"),p(e,"class","list-item"),x(e,"dragging",n[32]),x(e,"dragover",n[33])},m(S,T){v(S,e,T),w(e,t),F(i,t,null),w(e,l),w(e,s),w(s,o),w(s,r),w(s,a),w(a,f),w(e,d),w(e,m),h=!0,g||(_=[Me(He.call(null,m,"Remove file")),B(m,"click",y)],g=!0)},p(S,T){n=S;const $={};T[0]&2&&($.file=n[29]),i.$set($),(!h||T[0]&2)&&u!==(u=n[29].name+"")&&ue(f,u),(!h||T[0]&2&&c!==(c=n[29].name))&&p(s,"title",c),(!h||T[1]&2)&&x(e,"dragging",n[32]),(!h||T[1]&4)&&x(e,"dragover",n[33])},i(S){h||(O(i.$$.fragment,S),h=!0)},o(S){D(i.$$.fragment,S),h=!1},d(S){S&&k(e),q(i),g=!1,De(_)}}}function ig(n,e){let t,i,l,s;function o(a){e[20](a)}let r={group:e[4].name+"_new",index:e[31],disabled:!e[6],$$slots:{default:[rA,({dragging:a,dragover:u})=>({32:a,33:u}),({dragging:a,dragover:u})=>[0,(a?2:0)|(u?4:0)]]},$$scope:{ctx:e}};return e[1]!==void 0&&(r.list=e[1]),i=new ho({props:r}),ie.push(()=>ve(i,"list",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&16&&(f.group=e[4].name+"_new"),u[0]&2&&(f.index=e[31]),u[0]&64&&(f.disabled=!e[6]),u[0]&2|u[1]&70&&(f.$$scope={dirty:u,ctx:e}),!l&&u[0]&2&&(l=!0,f.list=e[1],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function aA(n){let e,t,i,l=[],s=new Map,o,r=[],a=new Map,u,f,c,d,m,h,g,_,y,S,T,$;e=new ti({props:{uniqueId:n[28],field:n[4]}});let E=pe(n[5]);const M=A=>A[34]+A[3].id;for(let A=0;AA[29].name+A[31];for(let A=0;A({28:o}),({uniqueId:o})=>[o?268435456:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","block")},m(o,r){v(o,e,r),F(t,e,null),i=!0,l||(s=[B(e,"dragover",tt(n[25])),B(e,"dragleave",n[26]),B(e,"drop",n[15])],l=!0)},p(o,r){const a={};r[0]&528&&(a.class=` + form-field form-field-list form-field-file + `+(o[4].required?"required":"")+` + `+(o[9]?"dragover":"")+` + `),r[0]&16&&(a.name=o[4].name),r[0]&268439039|r[1]&64&&(a.$$scope={dirty:r,ctx:o}),t.$set(a)},i(o){i||(O(t.$$.fragment,o),i=!0)},o(o){D(t.$$.fragment,o),i=!1},d(o){o&&k(e),q(t),l=!1,De(s)}}}function fA(n,e,t){let i,l,s,{record:o}=e,{field:r}=e,{value:a=""}=e,{uploadedFiles:u=[]}=e,{deletedFileNames:f=[]}=e,c,d,m=!1,h="";function g(V){z.removeByValue(f,V),t(2,f)}function _(V){z.pushUnique(f,V),t(2,f)}function y(V){z.isEmpty(u[V])||u.splice(V,1),t(1,u)}function S(){d==null||d.dispatchEvent(new CustomEvent("change",{detail:{value:a,uploadedFiles:u,deletedFileNames:f},bubbles:!0}))}function T(V){var J;V.preventDefault(),t(9,m=!1);const K=((J=V.dataTransfer)==null?void 0:J.files)||[];if(!(s||!K.length)){for(const ee of K){const X=l.length+u.length-f.length;if(r.maxSelect<=X)break;u.push(ee)}t(1,u)}}Yt(async()=>{t(10,h=await me.getSuperuserFileToken(o.collectionId))});const $=V=>g(V),E=V=>_(V);function M(V){a=V,t(0,a),t(6,i),t(4,r)}const L=V=>y(V);function I(V){u=V,t(1,u)}function A(V){ie[V?"unshift":"push"](()=>{c=V,t(7,c)})}const P=()=>{for(let V of c.files)u.push(V);t(1,u),t(7,c.value=null,c)},R=()=>c==null?void 0:c.click();function N(V){ie[V?"unshift":"push"](()=>{d=V,t(8,d)})}const U=()=>{t(9,m=!0)},j=()=>{t(9,m=!1)};return n.$$set=V=>{"record"in V&&t(3,o=V.record),"field"in V&&t(4,r=V.field),"value"in V&&t(0,a=V.value),"uploadedFiles"in V&&t(1,u=V.uploadedFiles),"deletedFileNames"in V&&t(2,f=V.deletedFileNames)},n.$$.update=()=>{n.$$.dirty[0]&2&&(Array.isArray(u)||t(1,u=z.toArray(u))),n.$$.dirty[0]&4&&(Array.isArray(f)||t(2,f=z.toArray(f))),n.$$.dirty[0]&16&&t(6,i=r.maxSelect!=1),n.$$.dirty[0]&65&&z.isEmpty(a)&&t(0,a=i?[]:""),n.$$.dirty[0]&1&&t(5,l=z.toArray(a)),n.$$.dirty[0]&54&&t(11,s=(l.length||u.length)&&r.maxSelect<=l.length+u.length-f.length),n.$$.dirty[0]&6&&(u!==-1||f!==-1)&&S()},[a,u,f,o,r,l,i,c,d,m,h,s,g,_,y,T,$,E,M,L,I,A,P,R,N,U,j]}class cA extends ye{constructor(e){super(),be(this,e,fA,uA,_e,{record:3,field:4,value:0,uploadedFiles:1,deletedFileNames:2},null,[-1,-1])}}function dA(n){let e;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function pA(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-fill txt-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function mA(n){let e,t,i,l;function s(a,u){return a[4]?pA:dA}let o=s(n),r=o(n);return{c(){e=b("span"),r.c(),p(e,"class","json-state svelte-p6ecb8")},m(a,u){v(a,e,u),r.m(e,null),i||(l=Me(t=He.call(null,e,{position:"left",text:n[4]?"Valid JSON":"Invalid JSON"})),i=!0)},p(a,u){o!==(o=s(a))&&(r.d(1),r=o(a),r&&(r.c(),r.m(e,null))),t&&Rt(t.update)&&u&16&&t.update.call(null,{position:"left",text:a[4]?"Valid JSON":"Invalid JSON"})},d(a){a&&k(e),r.d(),i=!1,l()}}}function hA(n){let e;return{c(){e=b("input"),p(e,"type","text"),p(e,"class","txt-mono"),e.value="Loading...",e.disabled=!0},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function _A(n){let e,t,i;var l=n[3];function s(o,r){return{props:{id:o[6],maxHeight:"500",language:"json",value:o[2]}}}return l&&(e=jt(l,s(n)),e.$on("change",n[5])),{c(){e&&H(e.$$.fragment),t=ge()},m(o,r){e&&F(e,o,r),v(o,t,r),i=!0},p(o,r){if(r&8&&l!==(l=o[3])){if(e){re();const a=e;D(a.$$.fragment,1,0,()=>{q(a,1)}),ae()}l?(e=jt(l,s(o)),e.$on("change",o[5]),H(e.$$.fragment),O(e.$$.fragment,1),F(e,t.parentNode,t)):e=null}else if(l){const a={};r&64&&(a.id=o[6]),r&4&&(a.value=o[2]),e.$set(a)}},i(o){i||(e&&O(e.$$.fragment,o),i=!0)},o(o){e&&D(e.$$.fragment,o),i=!1},d(o){o&&k(t),e&&q(e,o)}}}function gA(n){let e,t,i,l,s,o;e=new ti({props:{uniqueId:n[6],field:n[1],$$slots:{default:[mA]},$$scope:{ctx:n}}});const r=[_A,hA],a=[];function u(f,c){return f[3]?0:1}return i=u(n),l=a[i]=r[i](n),{c(){H(e.$$.fragment),t=C(),l.c(),s=ge()},m(f,c){F(e,f,c),v(f,t,c),a[i].m(f,c),v(f,s,c),o=!0},p(f,c){const d={};c&64&&(d.uniqueId=f[6]),c&2&&(d.field=f[1]),c&144&&(d.$$scope={dirty:c,ctx:f}),e.$set(d);let m=i;i=u(f),i===m?a[i].p(f,c):(re(),D(a[m],1,1,()=>{a[m]=null}),ae(),l=a[i],l?l.p(f,c):(l=a[i]=r[i](f),l.c()),O(l,1),l.m(s.parentNode,s))},i(f){o||(O(e.$$.fragment,f),O(l),o=!0)},o(f){D(e.$$.fragment,f),D(l),o=!1},d(f){f&&(k(t),k(s)),q(e,f),a[i].d(f)}}}function bA(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[gA,({uniqueId:i})=>({6:i}),({uniqueId:i})=>i?64:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&223&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function lg(n){return typeof n=="string"&&ak(n)?n:JSON.stringify(typeof n>"u"?null:n,null,2)}function ak(n){try{return JSON.parse(n===""?null:n),!0}catch{}return!1}function yA(n,e,t){let i,{field:l}=e,{value:s=void 0}=e,o,r=lg(s);Yt(async()=>{try{t(3,o=(await Ot(async()=>{const{default:u}=await import("./CodeEditor-CPgcqnd5.js");return{default:u}},__vite__mapDeps([12,1]),import.meta.url)).default)}catch(u){console.warn(u)}});const a=u=>{t(2,r=u.detail),t(0,s=r.trim())};return n.$$set=u=>{"field"in u&&t(1,l=u.field),"value"in u&&t(0,s=u.value)},n.$$.update=()=>{n.$$.dirty&5&&s!==(r==null?void 0:r.trim())&&(t(2,r=lg(s)),t(0,s=r)),n.$$.dirty&4&&t(4,i=ak(r))},[s,l,r,o,i,a]}class kA extends ye{constructor(e){super(),be(this,e,yA,bA,_e,{field:1,value:0})}}function vA(n){let e,t,i,l,s,o,r,a,u,f;return e=new ti({props:{uniqueId:n[3],field:n[1]}}),{c(){H(e.$$.fragment),t=C(),i=b("input"),p(i,"type","number"),p(i,"id",l=n[3]),i.required=s=n[1].required,p(i,"min",o=n[1].min),p(i,"max",r=n[1].max),p(i,"step","any")},m(c,d){F(e,c,d),v(c,t,d),v(c,i,d),ce(i,n[0]),a=!0,u||(f=B(i,"input",n[2]),u=!0)},p(c,d){const m={};d&8&&(m.uniqueId=c[3]),d&2&&(m.field=c[1]),e.$set(m),(!a||d&8&&l!==(l=c[3]))&&p(i,"id",l),(!a||d&2&&s!==(s=c[1].required))&&(i.required=s),(!a||d&2&&o!==(o=c[1].min))&&p(i,"min",o),(!a||d&2&&r!==(r=c[1].max))&&p(i,"max",r),d&1&&St(i.value)!==c[0]&&ce(i,c[0])},i(c){a||(O(e.$$.fragment,c),a=!0)},o(c){D(e.$$.fragment,c),a=!1},d(c){c&&(k(t),k(i)),q(e,c),u=!1,f()}}}function wA(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[vA,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function SA(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=St(this.value),t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class TA extends ye{constructor(e){super(),be(this,e,SA,wA,_e,{field:1,value:0})}}function $A(n){let e,t,i,l,s,o,r,a;return e=new ti({props:{uniqueId:n[3],field:n[1]}}),{c(){H(e.$$.fragment),t=C(),i=b("input"),p(i,"type","password"),p(i,"id",l=n[3]),p(i,"autocomplete","new-password"),i.required=s=n[1].required},m(u,f){F(e,u,f),v(u,t,f),v(u,i,f),ce(i,n[0]),o=!0,r||(a=B(i,"input",n[2]),r=!0)},p(u,f){const c={};f&8&&(c.uniqueId=u[3]),f&2&&(c.field=u[1]),e.$set(c),(!o||f&8&&l!==(l=u[3]))&&p(i,"id",l),(!o||f&2&&s!==(s=u[1].required))&&(i.required=s),f&1&&i.value!==u[0]&&ce(i,u[0])},i(u){o||(O(e.$$.fragment,u),o=!0)},o(u){D(e.$$.fragment,u),o=!1},d(u){u&&(k(t),k(i)),q(e,u),r=!1,a()}}}function CA(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[$A,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function OA(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=this.value,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class EA extends ye{constructor(e){super(),be(this,e,OA,CA,_e,{field:1,value:0})}}function sg(n){return typeof n=="function"?{threshold:100,callback:n}:n||{}}function MA(n,e){e=sg(e),e!=null&&e.callback&&e.callback();function t(i){if(!(e!=null&&e.callback))return;i.target.scrollHeight-i.target.clientHeight-i.target.scrollTop<=e.threshold&&e.callback()}return n.addEventListener("scroll",t),n.addEventListener("resize",t),{update(i){e=sg(i)},destroy(){n.removeEventListener("scroll",t),n.removeEventListener("resize",t)}}}function og(n,e,t){const i=n.slice();return i[50]=e[t],i[52]=t,i}function rg(n,e,t){const i=n.slice();i[50]=e[t];const l=i[9](i[50]);return i[6]=l,i}function ag(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='
    New record
    ',p(e,"type","button"),p(e,"class","btn btn-pill btn-transparent btn-hint p-l-xs p-r-xs")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[31]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function ug(n){let e,t=!n[13]&&fg(n);return{c(){t&&t.c(),e=ge()},m(i,l){t&&t.m(i,l),v(i,e,l)},p(i,l){i[13]?t&&(t.d(1),t=null):t?t.p(i,l):(t=fg(i),t.c(),t.m(e.parentNode,e))},d(i){i&&k(e),t&&t.d(i)}}}function fg(n){var s;let e,t,i,l=((s=n[2])==null?void 0:s.length)&&cg(n);return{c(){e=b("div"),t=b("span"),t.textContent="No records found.",i=C(),l&&l.c(),p(t,"class","txt txt-hint"),p(e,"class","list-item")},m(o,r){v(o,e,r),w(e,t),w(e,i),l&&l.m(e,null)},p(o,r){var a;(a=o[2])!=null&&a.length?l?l.p(o,r):(l=cg(o),l.c(),l.m(e,null)):l&&(l.d(1),l=null)},d(o){o&&k(e),l&&l.d()}}}function cg(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-sm")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[35]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function DA(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-blank-circle-line txt-disabled")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function IA(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-fill txt-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function dg(n){let e,t,i,l;function s(){return n[32](n[50])}return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","btn btn-sm btn-circle btn-transparent btn-hint m-l-auto"),p(e,"class","actions nonintrusive")},m(o,r){v(o,e,r),w(e,t),i||(l=[Me(He.call(null,t,"Edit")),B(t,"keydown",On(n[27])),B(t,"click",On(s))],i=!0)},p(o,r){n=o},d(o){o&&k(e),i=!1,De(l)}}}function pg(n,e){let t,i,l,s,o,r,a,u;function f(_,y){return _[6]?IA:DA}let c=f(e),d=c(e);s=new Ur({props:{record:e[50]}});let m=!e[11]&&dg(e);function h(){return e[33](e[50])}function g(..._){return e[34](e[50],..._)}return{key:n,first:null,c(){t=b("div"),d.c(),i=C(),l=b("div"),H(s.$$.fragment),o=C(),m&&m.c(),p(l,"class","content"),p(t,"tabindex","0"),p(t,"class","list-item handle"),x(t,"selected",e[6]),x(t,"disabled",!e[6]&&e[4]>1&&!e[10]),this.first=t},m(_,y){v(_,t,y),d.m(t,null),w(t,i),w(t,l),F(s,l,null),w(t,o),m&&m.m(t,null),r=!0,a||(u=[B(t,"click",h),B(t,"keydown",g)],a=!0)},p(_,y){e=_,c!==(c=f(e))&&(d.d(1),d=c(e),d&&(d.c(),d.m(t,i)));const S={};y[0]&256&&(S.record=e[50]),s.$set(S),e[11]?m&&(m.d(1),m=null):m?m.p(e,y):(m=dg(e),m.c(),m.m(t,null)),(!r||y[0]&768)&&x(t,"selected",e[6]),(!r||y[0]&1808)&&x(t,"disabled",!e[6]&&e[4]>1&&!e[10])},i(_){r||(O(s.$$.fragment,_),r=!0)},o(_){D(s.$$.fragment,_),r=!1},d(_){_&&k(t),d.d(),q(s),m&&m.d(),a=!1,De(u)}}}function mg(n){let e;return{c(){e=b("div"),e.innerHTML='
    ',p(e,"class","list-item")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function hg(n){let e,t=n[6].length+"",i,l,s,o;return{c(){e=Y("("),i=Y(t),l=Y(" of MAX "),s=Y(n[4]),o=Y(")")},m(r,a){v(r,e,a),v(r,i,a),v(r,l,a),v(r,s,a),v(r,o,a)},p(r,a){a[0]&64&&t!==(t=r[6].length+"")&&ue(i,t),a[0]&16&&ue(s,r[4])},d(r){r&&(k(e),k(i),k(l),k(s),k(o))}}}function LA(n){let e;return{c(){e=b("p"),e.textContent="No selected records.",p(e,"class","txt-hint")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function AA(n){let e,t,i=pe(n[6]),l=[];for(let o=0;oD(l[o],1,1,()=>{l[o]=null});return{c(){e=b("div");for(let o=0;o',s=C(),p(l,"type","button"),p(l,"title","Remove"),p(l,"class","btn btn-circle btn-transparent btn-hint btn-xs"),p(e,"class","label"),x(e,"label-danger",n[53]),x(e,"label-warning",n[54])},m(f,c){v(f,e,c),F(t,e,null),w(e,i),w(e,l),v(f,s,c),o=!0,r||(a=B(l,"click",u),r=!0)},p(f,c){n=f;const d={};c[0]&64&&(d.record=n[50]),t.$set(d),(!o||c[1]&4194304)&&x(e,"label-danger",n[53]),(!o||c[1]&8388608)&&x(e,"label-warning",n[54])},i(f){o||(O(t.$$.fragment,f),o=!0)},o(f){D(t.$$.fragment,f),o=!1},d(f){f&&(k(e),k(s)),q(t),r=!1,a()}}}function _g(n){let e,t,i;function l(o){n[38](o)}let s={index:n[52],$$slots:{default:[PA,({dragging:o,dragover:r})=>({53:o,54:r}),({dragging:o,dragover:r})=>[0,(o?4194304:0)|(r?8388608:0)]]},$$scope:{ctx:n}};return n[6]!==void 0&&(s.list=n[6]),e=new ho({props:s}),ie.push(()=>ve(e,"list",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r[0]&64|r[1]&79691776&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&64&&(t=!0,a.list=o[6],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function NA(n){let e,t,i,l,s,o=[],r=new Map,a,u,f,c,d,m,h,g,_,y,S,T;t=new Hr({props:{value:n[2],autocompleteCollection:n[5]}}),t.$on("submit",n[30]);let $=!n[11]&&ag(n),E=pe(n[8]);const M=U=>U[50].id;for(let U=0;U1&&hg(n);const P=[AA,LA],R=[];function N(U,j){return U[6].length?0:1}return h=N(n),g=R[h]=P[h](n),{c(){e=b("div"),H(t.$$.fragment),i=C(),$&&$.c(),l=C(),s=b("div");for(let U=0;U1?A?A.p(U,j):(A=hg(U),A.c(),A.m(c,null)):A&&(A.d(1),A=null);let K=h;h=N(U),h===K?R[h].p(U,j):(re(),D(R[K],1,1,()=>{R[K]=null}),ae(),g=R[h],g?g.p(U,j):(g=R[h]=P[h](U),g.c()),O(g,1),g.m(_.parentNode,_))},i(U){if(!y){O(t.$$.fragment,U);for(let j=0;jCancel',t=C(),i=b("button"),i.innerHTML='Set selection',p(e,"type","button"),p(e,"class","btn btn-transparent"),p(i,"type","button"),p(i,"class","btn")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),l||(s=[B(e,"click",n[28]),B(i,"click",n[29])],l=!0)},p:te,d(o){o&&(k(e),k(t),k(i)),l=!1,De(s)}}}function qA(n){let e,t,i,l;const s=[{popup:!0},{class:"overlay-panel-xl"},n[19]];let o={$$slots:{footer:[FA],header:[RA],default:[NA]},$$scope:{ctx:n}};for(let a=0;at(26,m=Oe));const h=_t(),g="picker_"+z.randomString(5);let{value:_}=e,{field:y}=e,S,T,$="",E=[],M=[],L=1,I=0,A=!1,P=!1;function R(){return t(2,$=""),t(8,E=[]),t(6,M=[]),j(),V(!0),S==null?void 0:S.show()}function N(){return S==null?void 0:S.hide()}function U(){var Ne;let Oe="";const ut=(Ne=s==null?void 0:s.fields)==null?void 0:Ne.filter(xe=>!xe.hidden&&xe.presentable&&xe.type=="relation");for(const xe of ut){const qt=z.getExpandPresentableRelField(xe,m,2);qt&&(Oe!=""&&(Oe+=","),Oe+=qt)}return Oe}async function j(){const Oe=z.toArray(_);if(!l||!Oe.length)return;t(24,P=!0);let ut=[];const Ne=Oe.slice(),xe=[];for(;Ne.length>0;){const qt=[];for(const Zt of Ne.splice(0,Go))qt.push(`id="${Zt}"`);xe.push(me.collection(l).getFullList({batch:Go,filter:qt.join("||"),fields:"*:excerpt(200)",expand:U(),requestKey:null}))}try{await Promise.all(xe).then(qt=>{ut=ut.concat(...qt)}),t(6,M=[]);for(const qt of Oe){const Zt=z.findByKey(ut,"id",qt);Zt&&M.push(Zt)}$.trim()||t(8,E=z.filterDuplicatesByKey(M.concat(E))),t(24,P=!1)}catch(qt){qt.isAbort||(me.error(qt),t(24,P=!1))}}async function V(Oe=!1){if(l){t(3,A=!0),Oe&&($.trim()?t(8,E=[]):t(8,E=z.toArray(M).slice()));try{const ut=Oe?1:L+1,Ne=z.getAllCollectionIdentifiers(s),xe=await me.collection(l).getList(ut,Go,{filter:z.normalizeSearchFilter($,Ne),sort:o?"":"-created",fields:"*:excerpt(200)",skipTotal:1,expand:U(),requestKey:g+"loadList"});t(8,E=z.filterDuplicatesByKey(E.concat(xe.items))),L=xe.page,t(23,I=xe.items.length),t(3,A=!1)}catch(ut){ut.isAbort||(me.error(ut),t(3,A=!1))}}}function K(Oe){i==1?t(6,M=[Oe]):u&&(z.pushOrReplaceByKey(M,Oe),t(6,M))}function J(Oe){z.removeByKey(M,"id",Oe.id),t(6,M)}function ee(Oe){f(Oe)?J(Oe):K(Oe)}function X(){var Oe;i!=1?t(20,_=M.map(ut=>ut.id)):t(20,_=((Oe=M==null?void 0:M[0])==null?void 0:Oe.id)||""),h("save",M),N()}function oe(Oe){Pe.call(this,n,Oe)}const Se=()=>N(),ke=()=>X(),Ce=Oe=>t(2,$=Oe.detail),We=()=>T==null?void 0:T.show(),st=Oe=>T==null?void 0:T.show(Oe.id),et=Oe=>ee(Oe),Be=(Oe,ut)=>{(ut.code==="Enter"||ut.code==="Space")&&(ut.preventDefault(),ut.stopPropagation(),ee(Oe))},rt=()=>t(2,$=""),Je=()=>{a&&!A&&V()},at=Oe=>J(Oe);function Ht(Oe){M=Oe,t(6,M)}function Te(Oe){ie[Oe?"unshift":"push"](()=>{S=Oe,t(1,S)})}function Ze(Oe){Pe.call(this,n,Oe)}function ot(Oe){Pe.call(this,n,Oe)}function Le(Oe){ie[Oe?"unshift":"push"](()=>{T=Oe,t(7,T)})}const Ve=Oe=>{z.removeByKey(E,"id",Oe.detail.record.id),E.unshift(Oe.detail.record),t(8,E),K(Oe.detail.record)},we=Oe=>{z.removeByKey(E,"id",Oe.detail.id),t(8,E),J(Oe.detail)};return n.$$set=Oe=>{e=je(je({},e),Ut(Oe)),t(19,d=lt(e,c)),"value"in Oe&&t(20,_=Oe.value),"field"in Oe&&t(21,y=Oe.field)},n.$$.update=()=>{n.$$.dirty[0]&2097152&&t(4,i=(y==null?void 0:y.maxSelect)||null),n.$$.dirty[0]&2097152&&t(25,l=y==null?void 0:y.collectionId),n.$$.dirty[0]&100663296&&t(5,s=m.find(Oe=>Oe.id==l)||null),n.$$.dirty[0]&6&&typeof $<"u"&&S!=null&&S.isActive()&&V(!0),n.$$.dirty[0]&32&&t(11,o=(s==null?void 0:s.type)==="view"),n.$$.dirty[0]&16777224&&t(13,r=A||P),n.$$.dirty[0]&8388608&&t(12,a=I==Go),n.$$.dirty[0]&80&&t(10,u=i<=0||i>M.length),n.$$.dirty[0]&64&&t(9,f=function(Oe){return z.findByKey(M,"id",Oe.id)})},[N,S,$,A,i,s,M,T,E,f,u,o,a,r,V,K,J,ee,X,d,_,y,R,I,P,l,m,oe,Se,ke,Ce,We,st,et,Be,rt,Je,at,Ht,Te,Ze,ot,Le,Ve,we]}class jA extends ye{constructor(e){super(),be(this,e,HA,qA,_e,{value:20,field:21,show:22,hide:0},null,[-1,-1])}get show(){return this.$$.ctx[22]}get hide(){return this.$$.ctx[0]}}function gg(n,e,t){const i=n.slice();return i[22]=e[t],i[24]=t,i}function bg(n,e,t){const i=n.slice();return i[27]=e[t],i}function yg(n){let e,t,i,l;return{c(){e=b("i"),p(e,"class","ri-error-warning-line link-hint m-l-auto flex-order-10")},m(s,o){v(s,e,o),i||(l=Me(t=He.call(null,e,{position:"left",text:"The following relation ids were removed from the list because they are missing or invalid: "+n[6].join(", ")})),i=!0)},p(s,o){t&&Rt(t.update)&&o&64&&t.update.call(null,{position:"left",text:"The following relation ids were removed from the list because they are missing or invalid: "+s[6].join(", ")})},d(s){s&&k(e),i=!1,l()}}}function zA(n){let e,t=n[6].length&&yg(n);return{c(){t&&t.c(),e=ge()},m(i,l){t&&t.m(i,l),v(i,e,l)},p(i,l){i[6].length?t?t.p(i,l):(t=yg(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){i&&k(e),t&&t.d(i)}}}function kg(n){let e,t=n[5]&&vg(n);return{c(){t&&t.c(),e=ge()},m(i,l){t&&t.m(i,l),v(i,e,l)},p(i,l){i[5]?t?t.p(i,l):(t=vg(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){i&&k(e),t&&t.d(i)}}}function vg(n){let e,t=pe(z.toArray(n[0]).slice(0,10)),i=[];for(let l=0;l ',p(e,"class","list-item")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function UA(n){let e,t,i,l,s,o,r,a,u,f;i=new Ur({props:{record:n[22]}});function c(){return n[11](n[22])}return{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),o=b("button"),o.innerHTML='',r=C(),p(t,"class","content"),p(o,"type","button"),p(o,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove"),p(s,"class","actions"),p(e,"class","list-item"),x(e,"dragging",n[25]),x(e,"dragover",n[26])},m(d,m){v(d,e,m),w(e,t),F(i,t,null),w(e,l),w(e,s),w(s,o),v(d,r,m),a=!0,u||(f=[Me(He.call(null,o,"Remove")),B(o,"click",c)],u=!0)},p(d,m){n=d;const h={};m&16&&(h.record=n[22]),i.$set(h),(!a||m&33554432)&&x(e,"dragging",n[25]),(!a||m&67108864)&&x(e,"dragover",n[26])},i(d){a||(O(i.$$.fragment,d),a=!0)},o(d){D(i.$$.fragment,d),a=!1},d(d){d&&(k(e),k(r)),q(i),u=!1,De(f)}}}function Sg(n,e){let t,i,l,s;function o(a){e[12](a)}let r={group:e[2].name+"_relation",index:e[24],disabled:!e[7],$$slots:{default:[UA,({dragging:a,dragover:u})=>({25:a,26:u}),({dragging:a,dragover:u})=>(a?33554432:0)|(u?67108864:0)]},$$scope:{ctx:e}};return e[4]!==void 0&&(r.list=e[4]),i=new ho({props:r}),ie.push(()=>ve(i,"list",o)),i.$on("sort",e[13]),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u&4&&(f.group=e[2].name+"_relation"),u&16&&(f.index=e[24]),u&128&&(f.disabled=!e[7]),u&1174405136&&(f.$$scope={dirty:u,ctx:e}),!l&&u&16&&(l=!0,f.list=e[4],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function VA(n){let e,t,i,l,s=[],o=new Map,r,a,u,f,c,d;e=new ti({props:{uniqueId:n[21],field:n[2],$$slots:{default:[zA]},$$scope:{ctx:n}}});let m=pe(n[4]);const h=_=>_[22].id;for(let _=0;_ Open picker',p(l,"class","relations-list svelte-1ynw0pc"),p(u,"type","button"),p(u,"class","btn btn-transparent btn-sm btn-block"),p(a,"class","list-item list-item-btn"),p(i,"class","list")},m(_,y){F(e,_,y),v(_,t,y),v(_,i,y),w(i,l);for(let S=0;S({21:r}),({uniqueId:r})=>r?2097152:0]},$$scope:{ctx:n}};e=new fe({props:s}),n[15](e);let o={value:n[0],field:n[2]};return i=new jA({props:o}),n[16](i),i.$on("save",n[17]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(r,a){F(e,r,a),v(r,t,a),F(i,r,a),l=!0},p(r,[a]){const u={};a&4&&(u.class="form-field form-field-list "+(r[2].required?"required":"")),a&4&&(u.name=r[2].name),a&1075839223&&(u.$$scope={dirty:a,ctx:r}),e.$set(u);const f={};a&1&&(f.value=r[0]),a&4&&(f.field=r[2]),i.$set(f)},i(r){l||(O(e.$$.fragment,r),O(i.$$.fragment,r),l=!0)},o(r){D(e.$$.fragment,r),D(i.$$.fragment,r),l=!1},d(r){r&&k(t),n[15](null),q(e,r),n[16](null),q(i,r)}}}const Tg=100;function WA(n,e,t){let i,l;Qe(n,En,I=>t(18,l=I));let{field:s}=e,{value:o}=e,{picker:r}=e,a,u=[],f=!1,c,d=[];function m(){if(f)return!1;const I=z.toArray(o);return t(4,u=u.filter(A=>I.includes(A.id))),I.length!=u.length}async function h(){var U,j;const I=z.toArray(o);if(t(4,u=[]),t(6,d=[]),!(s!=null&&s.collectionId)||!I.length){t(5,f=!1);return}t(5,f=!0);let A="";const P=(j=(U=l.find(V=>V.id==s.collectionId))==null?void 0:U.fields)==null?void 0:j.filter(V=>!V.hidden&&V.presentable&&V.type=="relation");for(const V of P){const K=z.getExpandPresentableRelField(V,l,2);K&&(A!=""&&(A+=","),A+=K)}const R=I.slice(),N=[];for(;R.length>0;){const V=[];for(const K of R.splice(0,Tg))V.push(`id="${K}"`);N.push(me.collection(s.collectionId).getFullList(Tg,{filter:V.join("||"),fields:"*:excerpt(200)",expand:A,requestKey:null}))}try{let V=[];await Promise.all(N).then(K=>{V=V.concat(...K)});for(const K of I){const J=z.findByKey(V,"id",K);J?u.push(J):d.push(K)}t(4,u),_()}catch(V){me.error(V)}t(5,f=!1)}function g(I){z.removeByKey(u,"id",I.id),t(4,u),_()}function _(){var I;i?t(0,o=u.map(A=>A.id)):t(0,o=((I=u[0])==null?void 0:I.id)||"")}so(()=>{clearTimeout(c)});const y=I=>g(I);function S(I){u=I,t(4,u)}const T=()=>{_()},$=()=>r==null?void 0:r.show();function E(I){ie[I?"unshift":"push"](()=>{a=I,t(3,a)})}function M(I){ie[I?"unshift":"push"](()=>{r=I,t(1,r)})}const L=I=>{var A;t(4,u=I.detail||[]),t(0,o=i?u.map(P=>P.id):((A=u[0])==null?void 0:A.id)||"")};return n.$$set=I=>{"field"in I&&t(2,s=I.field),"value"in I&&t(0,o=I.value),"picker"in I&&t(1,r=I.picker)},n.$$.update=()=>{n.$$.dirty&4&&t(7,i=s.maxSelect!=1),n.$$.dirty&9&&typeof o<"u"&&(a==null||a.changed()),n.$$.dirty&1041&&m()&&(t(5,f=!0),clearTimeout(c),t(10,c=setTimeout(h,0)))},[o,r,s,a,u,f,d,i,g,_,c,y,S,T,$,E,M,L]}class YA extends ye{constructor(e){super(),be(this,e,WA,BA,_e,{field:2,value:0,picker:1})}}function $g(n){let e,t,i,l;return{c(){e=b("div"),t=Y("Select up to "),i=Y(n[2]),l=Y(" items."),p(e,"class","help-block")},m(s,o){v(s,e,o),w(e,t),w(e,i),w(e,l)},p(s,o){o&4&&ue(i,s[2])},d(s){s&&k(e)}}}function KA(n){var c,d;let e,t,i,l,s,o,r;e=new ti({props:{uniqueId:n[5],field:n[1]}});function a(m){n[4](m)}let u={id:n[5],toggle:!n[1].required||n[3],multiple:n[3],closable:!n[3]||((c=n[0])==null?void 0:c.length)>=n[1].maxSelect,items:n[1].values,searchable:((d=n[1].values)==null?void 0:d.length)>5};n[0]!==void 0&&(u.selected=n[0]),i=new ds({props:u}),ie.push(()=>ve(i,"selected",a));let f=n[1].maxSelect!=1&&$g(n);return{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),s=C(),f&&f.c(),o=ge()},m(m,h){F(e,m,h),v(m,t,h),F(i,m,h),v(m,s,h),f&&f.m(m,h),v(m,o,h),r=!0},p(m,h){var y,S;const g={};h&32&&(g.uniqueId=m[5]),h&2&&(g.field=m[1]),e.$set(g);const _={};h&32&&(_.id=m[5]),h&10&&(_.toggle=!m[1].required||m[3]),h&8&&(_.multiple=m[3]),h&11&&(_.closable=!m[3]||((y=m[0])==null?void 0:y.length)>=m[1].maxSelect),h&2&&(_.items=m[1].values),h&2&&(_.searchable=((S=m[1].values)==null?void 0:S.length)>5),!l&&h&1&&(l=!0,_.selected=m[0],$e(()=>l=!1)),i.$set(_),m[1].maxSelect!=1?f?f.p(m,h):(f=$g(m),f.c(),f.m(o.parentNode,o)):f&&(f.d(1),f=null)},i(m){r||(O(e.$$.fragment,m),O(i.$$.fragment,m),r=!0)},o(m){D(e.$$.fragment,m),D(i.$$.fragment,m),r=!1},d(m){m&&(k(t),k(s),k(o)),q(e,m),q(i,m),f&&f.d(m)}}}function JA(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[KA,({uniqueId:i})=>({5:i}),({uniqueId:i})=>i?32:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&111&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function ZA(n,e,t){let i,l,{field:s}=e,{value:o=void 0}=e;function r(a){o=a,t(0,o),t(3,i),t(1,s),t(2,l)}return n.$$set=a=>{"field"in a&&t(1,s=a.field),"value"in a&&t(0,o=a.value)},n.$$.update=()=>{n.$$.dirty&2&&t(3,i=s.maxSelect!=1),n.$$.dirty&9&&typeof o>"u"&&t(0,o=i?[]:""),n.$$.dirty&2&&t(2,l=s.maxSelect||s.values.length),n.$$.dirty&15&&i&&Array.isArray(o)&&(t(0,o=o.filter(a=>s.values.includes(a))),o.length>l&&t(0,o=o.slice(o.length-l)))},[o,s,l,i,r]}class GA extends ye{constructor(e){super(),be(this,e,ZA,JA,_e,{field:1,value:0})}}function XA(n){let e,t,i,l=[n[3]],s={};for(let o=0;o{r&&(t(1,r.style.height="",r),t(1,r.style.height=Math.min(r.scrollHeight,o)+"px",r))},0)}function f(m){if((m==null?void 0:m.code)==="Enter"&&!(m!=null&&m.shiftKey)&&!(m!=null&&m.isComposing)){m.preventDefault();const h=r.closest("form");h!=null&&h.requestSubmit&&h.requestSubmit()}}Yt(()=>(u(),()=>clearTimeout(a)));function c(m){ie[m?"unshift":"push"](()=>{r=m,t(1,r)})}function d(){s=this.value,t(0,s)}return n.$$set=m=>{e=je(je({},e),Ut(m)),t(3,l=lt(e,i)),"value"in m&&t(0,s=m.value),"maxHeight"in m&&t(4,o=m.maxHeight)},n.$$.update=()=>{n.$$.dirty&1&&typeof s!==void 0&&u()},[s,r,f,l,o,c,d]}class xA extends ye{constructor(e){super(),be(this,e,QA,XA,_e,{value:0,maxHeight:4})}}function eP(n){let e,t,i,l,s;e=new ti({props:{uniqueId:n[6],field:n[1]}});function o(a){n[5](a)}let r={id:n[6],required:n[3],placeholder:n[2]?"Leave empty to autogenerate...":""};return n[0]!==void 0&&(r.value=n[0]),i=new xA({props:r}),ie.push(()=>ve(i,"value",o)),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(a,u){F(e,a,u),v(a,t,u),F(i,a,u),s=!0},p(a,u){const f={};u&64&&(f.uniqueId=a[6]),u&2&&(f.field=a[1]),e.$set(f);const c={};u&64&&(c.id=a[6]),u&8&&(c.required=a[3]),u&4&&(c.placeholder=a[2]?"Leave empty to autogenerate...":""),!l&&u&1&&(l=!0,c.value=a[0],$e(()=>l=!1)),i.$set(c)},i(a){s||(O(e.$$.fragment,a),O(i.$$.fragment,a),s=!0)},o(a){D(e.$$.fragment,a),D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(e,a),q(i,a)}}}function tP(n){let e,t;return e=new fe({props:{class:"form-field "+(n[3]?"required":""),name:n[1].name,$$slots:{default:[eP,({uniqueId:i})=>({6:i}),({uniqueId:i})=>i?64:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&8&&(s.class="form-field "+(i[3]?"required":"")),l&2&&(s.name=i[1].name),l&207&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function nP(n,e,t){let i,l,{original:s}=e,{field:o}=e,{value:r=void 0}=e;function a(u){r=u,t(0,r)}return n.$$set=u=>{"original"in u&&t(4,s=u.original),"field"in u&&t(1,o=u.field),"value"in u&&t(0,r=u.value)},n.$$.update=()=>{n.$$.dirty&18&&t(2,i=!z.isEmpty(o.autogeneratePattern)&&!(s!=null&&s.id)),n.$$.dirty&6&&t(3,l=o.required&&!i)},[r,o,i,l,s,a]}class iP extends ye{constructor(e){super(),be(this,e,nP,tP,_e,{original:4,field:1,value:0})}}function lP(n){let e,t,i,l,s,o,r,a;return e=new ti({props:{uniqueId:n[3],field:n[1]}}),{c(){H(e.$$.fragment),t=C(),i=b("input"),p(i,"type","url"),p(i,"id",l=n[3]),i.required=s=n[1].required},m(u,f){F(e,u,f),v(u,t,f),v(u,i,f),ce(i,n[0]),o=!0,r||(a=B(i,"input",n[2]),r=!0)},p(u,f){const c={};f&8&&(c.uniqueId=u[3]),f&2&&(c.field=u[1]),e.$set(c),(!o||f&8&&l!==(l=u[3]))&&p(i,"id",l),(!o||f&2&&s!==(s=u[1].required))&&(i.required=s),f&1&&i.value!==u[0]&&ce(i,u[0])},i(u){o||(O(e.$$.fragment,u),o=!0)},o(u){D(e.$$.fragment,u),o=!1},d(u){u&&(k(t),k(i)),q(e,u),r=!1,a()}}}function sP(n){let e,t;return e=new fe({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[lP,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function oP(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=this.value,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class rP extends ye{constructor(e){super(),be(this,e,oP,sP,_e,{field:1,value:0})}}function Cg(n,e,t){const i=n.slice();return i[6]=e[t],i}function Og(n,e,t){const i=n.slice();return i[6]=e[t],i}function Eg(n,e){let t,i,l=e[6].title+"",s,o,r,a;function u(){return e[5](e[6])}return{key:n,first:null,c(){t=b("button"),i=b("div"),s=Y(l),o=C(),p(i,"class","txt"),p(t,"class","tab-item svelte-1maocj6"),x(t,"active",e[1]===e[6].language),this.first=t},m(f,c){v(f,t,c),w(t,i),w(i,s),w(t,o),r||(a=B(t,"click",u),r=!0)},p(f,c){e=f,c&4&&l!==(l=e[6].title+"")&&ue(s,l),c&6&&x(t,"active",e[1]===e[6].language)},d(f){f&&k(t),r=!1,a()}}}function Mg(n,e){let t,i,l,s,o,r,a=e[6].title+"",u,f,c,d,m;return i=new Qu({props:{language:e[6].language,content:e[6].content}}),{key:n,first:null,c(){t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),o=b("em"),r=b("a"),u=Y(a),f=Y(" SDK"),d=C(),p(r,"href",c=e[6].url),p(r,"target","_blank"),p(r,"rel","noopener noreferrer"),p(o,"class","txt-sm txt-hint"),p(s,"class","txt-right"),p(t,"class","tab-item svelte-1maocj6"),x(t,"active",e[1]===e[6].language),this.first=t},m(h,g){v(h,t,g),F(i,t,null),w(t,l),w(t,s),w(s,o),w(o,r),w(r,u),w(r,f),w(t,d),m=!0},p(h,g){e=h;const _={};g&4&&(_.language=e[6].language),g&4&&(_.content=e[6].content),i.$set(_),(!m||g&4)&&a!==(a=e[6].title+"")&&ue(u,a),(!m||g&4&&c!==(c=e[6].url))&&p(r,"href",c),(!m||g&6)&&x(t,"active",e[1]===e[6].language)},i(h){m||(O(i.$$.fragment,h),m=!0)},o(h){D(i.$$.fragment,h),m=!1},d(h){h&&k(t),q(i)}}}function aP(n){let e,t,i=[],l=new Map,s,o,r=[],a=new Map,u,f,c=pe(n[2]);const d=g=>g[6].language;for(let g=0;gg[6].language;for(let g=0;gt(1,r=u.language);return n.$$set=u=>{"class"in u&&t(0,l=u.class),"js"in u&&t(3,s=u.js),"dart"in u&&t(4,o=u.dart)},n.$$.update=()=>{n.$$.dirty&2&&r&&localStorage.setItem(Dg,r),n.$$.dirty&24&&t(2,i=[{title:"JavaScript",language:"javascript",content:s,url:"https://github.com/pocketbase/js-sdk"},{title:"Dart",language:"dart",content:o,url:"https://github.com/pocketbase/dart-sdk"}])},[l,r,i,s,o,a]}class fP extends ye{constructor(e){super(),be(this,e,uP,aP,_e,{class:0,js:3,dart:4})}}function cP(n){let e,t,i,l,s=z.displayValue(n[1])+"",o,r,a,u,f,c,d;return u=new fe({props:{class:"form-field m-b-0 m-t-sm",name:"duration",$$slots:{default:[pP,({uniqueId:m})=>({20:m}),({uniqueId:m})=>m?1048576:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("p"),i=Y(`Generate a non-refreshable auth token for + `),l=b("strong"),o=Y(s),r=Y(":"),a=C(),H(u.$$.fragment),p(e,"id",n[8])},m(m,h){v(m,e,h),w(e,t),w(t,i),w(t,l),w(l,o),w(l,r),w(e,a),F(u,e,null),f=!0,c||(d=B(e,"submit",tt(n[9])),c=!0)},p(m,h){(!f||h&2)&&s!==(s=z.displayValue(m[1])+"")&&ue(o,s);const g={};h&3145761&&(g.$$scope={dirty:h,ctx:m}),u.$set(g)},i(m){f||(O(u.$$.fragment,m),f=!0)},o(m){D(u.$$.fragment,m),f=!1},d(m){m&&k(e),q(u),c=!1,d()}}}function dP(n){let e,t,i,l=n[3].authStore.token+"",s,o,r,a,u,f;return r=new ai({props:{value:n[3].authStore.token}}),u=new fP({props:{js:` + import PocketBase from 'pocketbase'; + + const token = "..."; + + const pb = new PocketBase('${n[7]}'); + + pb.authStore.save(token, null); + `,dart:` + import 'package:pocketbase/pocketbase.dart'; + + final token = "..."; + + final pb = PocketBase('${n[7]}'); + + pb.authStore.save(token, null); + `}}),{c(){e=b("div"),t=b("div"),i=b("span"),s=Y(l),o=C(),H(r.$$.fragment),a=C(),H(u.$$.fragment),p(i,"class","txt token-holder svelte-1i56uix"),p(t,"class","content txt-bold"),p(e,"class","alert alert-success")},m(c,d){v(c,e,d),w(e,t),w(t,i),w(i,s),w(t,o),F(r,t,null),v(c,a,d),F(u,c,d),f=!0},p(c,d){(!f||d&8)&&l!==(l=c[3].authStore.token+"")&&ue(s,l);const m={};d&8&&(m.value=c[3].authStore.token),r.$set(m);const h={};d&128&&(h.js=` + import PocketBase from 'pocketbase'; + + const token = "..."; + + const pb = new PocketBase('${c[7]}'); + + pb.authStore.save(token, null); + `),d&128&&(h.dart=` + import 'package:pocketbase/pocketbase.dart'; + + final token = "..."; + + final pb = PocketBase('${c[7]}'); + + pb.authStore.save(token, null); + `),u.$set(h)},i(c){f||(O(r.$$.fragment,c),O(u.$$.fragment,c),f=!0)},o(c){D(r.$$.fragment,c),D(u.$$.fragment,c),f=!1},d(c){c&&(k(e),k(a)),q(r),q(u,c)}}}function pP(n){let e,t,i,l,s,o,r,a,u,f;return{c(){var c,d;e=b("label"),t=Y("Token duration (in seconds)"),l=C(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","number"),p(s,"id",o=n[20]),p(s,"placeholder",r="Default to the collection setting ("+(((d=(c=n[0])==null?void 0:c.authToken)==null?void 0:d.duration)||0)+"s)"),p(s,"min","0"),p(s,"step","1"),s.value=a=n[5]||""},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),u||(f=B(s,"input",n[14]),u=!0)},p(c,d){var m,h;d&1048576&&i!==(i=c[20])&&p(e,"for",i),d&1048576&&o!==(o=c[20])&&p(s,"id",o),d&1&&r!==(r="Default to the collection setting ("+(((h=(m=c[0])==null?void 0:m.authToken)==null?void 0:h.duration)||0)+"s)")&&p(s,"placeholder",r),d&32&&a!==(a=c[5]||"")&&s.value!==a&&(s.value=a)},d(c){c&&(k(e),k(l),k(s)),u=!1,f()}}}function mP(n){let e,t,i,l,s,o;const r=[dP,cP],a=[];function u(f,c){var d,m;return(m=(d=f[3])==null?void 0:d.authStore)!=null&&m.token?0:1}return i=u(n),l=a[i]=r[i](n),{c(){e=b("div"),t=C(),l.c(),s=ge(),p(e,"class","clearfix")},m(f,c){v(f,e,c),v(f,t,c),a[i].m(f,c),v(f,s,c),o=!0},p(f,c){let d=i;i=u(f),i===d?a[i].p(f,c):(re(),D(a[d],1,1,()=>{a[d]=null}),ae(),l=a[i],l?l.p(f,c):(l=a[i]=r[i](f),l.c()),O(l,1),l.m(s.parentNode,s))},i(f){o||(O(l),o=!0)},o(f){D(l),o=!1},d(f){f&&(k(e),k(t),k(s)),a[i].d(f)}}}function hP(n){let e;return{c(){e=b("h4"),e.textContent="Impersonate auth token"},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function _P(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Generate token",p(t,"class","txt"),p(e,"type","submit"),p(e,"form",n[8]),p(e,"class","btn btn-expanded"),e.disabled=n[6],x(e,"btn-loading",n[6])},m(s,o){v(s,e,o),w(e,t),i||(l=B(e,"click",n[13]),i=!0)},p(s,o){o&64&&(e.disabled=s[6]),o&64&&x(e,"btn-loading",s[6])},d(s){s&&k(e),i=!1,l()}}}function gP(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Generate a new one",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-secondary btn-expanded"),e.disabled=n[6]},m(s,o){v(s,e,o),w(e,t),i||(l=B(e,"click",n[12]),i=!0)},p(s,o){o&64&&(e.disabled=s[6])},d(s){s&&k(e),i=!1,l()}}}function bP(n){let e,t,i,l,s,o;function r(f,c){var d,m;return(m=(d=f[3])==null?void 0:d.authStore)!=null&&m.token?gP:_P}let a=r(n),u=a(n);return{c(){e=b("button"),t=b("span"),t.textContent="Close",i=C(),u.c(),l=ge(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[6]},m(f,c){v(f,e,c),w(e,t),v(f,i,c),u.m(f,c),v(f,l,c),s||(o=B(e,"click",n[2]),s=!0)},p(f,c){c&64&&(e.disabled=f[6]),a===(a=r(f))&&u?u.p(f,c):(u.d(1),u=a(f),u&&(u.c(),u.m(l.parentNode,l)))},d(f){f&&(k(e),k(i),k(l)),u.d(f),s=!1,o()}}}function yP(n){let e,t,i={overlayClose:!1,escClose:!n[6],beforeHide:n[15],popup:!0,$$slots:{footer:[bP],header:[hP],default:[mP]},$$scope:{ctx:n}};return e=new ln({props:i}),n[16](e),e.$on("show",n[17]),e.$on("hide",n[18]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&64&&(o.escClose=!l[6]),s&64&&(o.beforeHide=l[15]),s&2097387&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[16](null),q(e,l)}}}function kP(n,e,t){let i;const l=_t(),s="impersonate_"+z.randomString(5);let{collection:o}=e,{record:r}=e,a,u=0,f=!1,c;function d(){r&&(a==null||a.show())}function m(){a==null||a.hide(),g()}async function h(){if(!(f||!o||!r)){t(6,f=!0);try{t(3,c=await me.collection(o.name).impersonate(r.id,u)),l("submit",c)}catch(L){me.error(L)}t(6,f=!1)}}function g(){t(5,u=0),t(3,c=void 0)}const _=()=>g(),y=()=>h(),S=L=>t(5,u=L.target.value<<0),T=()=>!f;function $(L){ie[L?"unshift":"push"](()=>{a=L,t(4,a)})}function E(L){Pe.call(this,n,L)}function M(L){Pe.call(this,n,L)}return n.$$set=L=>{"collection"in L&&t(0,o=L.collection),"record"in L&&t(1,r=L.record)},n.$$.update=()=>{n.$$.dirty&8&&t(7,i=z.getApiExampleUrl(c==null?void 0:c.baseURL))},[o,r,m,c,a,u,f,i,s,h,g,d,_,y,S,T,$,E,M]}class vP extends ye{constructor(e){super(),be(this,e,kP,yP,_e,{collection:0,record:1,show:11,hide:2})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[2]}}function Ig(n,e,t){const i=n.slice();return i[82]=e[t],i[83]=e,i[84]=t,i}function Lg(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g;return{c(){e=b("div"),t=b("div"),i=b("div"),i.innerHTML='',l=C(),s=b("div"),o=Y(`The record has previous unsaved changes. + `),r=b("button"),r.textContent="Restore draft",a=C(),u=b("button"),u.innerHTML='',f=C(),c=b("div"),p(i,"class","icon"),p(r,"type","button"),p(r,"class","btn btn-sm btn-secondary"),p(s,"class","flex flex-gap-xs"),p(u,"type","button"),p(u,"class","close"),p(u,"aria-label","Discard draft"),p(t,"class","alert alert-info m-0"),p(c,"class","clearfix p-b-base"),p(e,"class","block")},m(_,y){v(_,e,y),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),w(s,r),w(t,a),w(t,u),w(e,f),w(e,c),m=!0,h||(g=[B(r,"click",n[46]),Me(He.call(null,u,"Discard draft")),B(u,"click",tt(n[47]))],h=!0)},p:te,i(_){m||(d&&d.end(1),m=!0)},o(_){_&&(d=mu(e,vt,{duration:150})),m=!1},d(_){_&&k(e),_&&d&&d.end(),h=!1,De(g)}}}function Ag(n){let e,t,i;return t=new QI({props:{record:n[3]}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","form-field-addon")},m(l,s){v(l,e,s),F(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.record=l[3]),t.$set(o)},i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function wP(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S=!n[6]&&Ag(n);return{c(){var T,$;e=b("label"),t=b("i"),i=C(),l=b("span"),l.textContent="id",s=C(),o=b("span"),a=C(),S&&S.c(),u=C(),f=b("input"),p(t,"class",js(z.getFieldTypeIcon("primary"))+" svelte-qc5ngu"),p(l,"class","txt"),p(o,"class","flex-fill"),p(e,"for",r=n[85]),p(f,"type","text"),p(f,"id",c=n[85]),p(f,"placeholder",d=!n[7]&&!z.isEmpty((T=n[19])==null?void 0:T.autogeneratePattern)?"Leave empty to auto generate...":""),p(f,"minlength",m=($=n[19])==null?void 0:$.min),f.readOnly=h=!n[6]},m(T,$){v(T,e,$),w(e,t),w(e,i),w(e,l),w(e,s),w(e,o),v(T,a,$),S&&S.m(T,$),v(T,u,$),v(T,f,$),ce(f,n[3].id),g=!0,_||(y=B(f,"input",n[48]),_=!0)},p(T,$){var E,M;(!g||$[2]&8388608&&r!==(r=T[85]))&&p(e,"for",r),T[6]?S&&(re(),D(S,1,1,()=>{S=null}),ae()):S?(S.p(T,$),$[0]&64&&O(S,1)):(S=Ag(T),S.c(),O(S,1),S.m(u.parentNode,u)),(!g||$[2]&8388608&&c!==(c=T[85]))&&p(f,"id",c),(!g||$[0]&524416&&d!==(d=!T[7]&&!z.isEmpty((E=T[19])==null?void 0:E.autogeneratePattern)?"Leave empty to auto generate...":""))&&p(f,"placeholder",d),(!g||$[0]&524288&&m!==(m=(M=T[19])==null?void 0:M.min))&&p(f,"minlength",m),(!g||$[0]&64&&h!==(h=!T[6]))&&(f.readOnly=h),$[0]&8&&f.value!==T[3].id&&ce(f,T[3].id)},i(T){g||(O(S),g=!0)},o(T){D(S),g=!1},d(T){T&&(k(e),k(a),k(u),k(f)),S&&S.d(T),_=!1,y()}}}function Pg(n){let e,t,i,l,s;function o(u){n[49](u)}let r={isNew:n[6],collection:n[0]};n[3]!==void 0&&(r.record=n[3]),e=new _L({props:r}),ie.push(()=>ve(e,"record",o));let a=n[16].length&&Ng();return{c(){H(e.$$.fragment),i=C(),a&&a.c(),l=ge()},m(u,f){F(e,u,f),v(u,i,f),a&&a.m(u,f),v(u,l,f),s=!0},p(u,f){const c={};f[0]&64&&(c.isNew=u[6]),f[0]&1&&(c.collection=u[0]),!t&&f[0]&8&&(t=!0,c.record=u[3],$e(()=>t=!1)),e.$set(c),u[16].length?a||(a=Ng(),a.c(),a.m(l.parentNode,l)):a&&(a.d(1),a=null)},i(u){s||(O(e.$$.fragment,u),s=!0)},o(u){D(e.$$.fragment,u),s=!1},d(u){u&&(k(i),k(l)),q(e,u),a&&a.d(u)}}}function Ng(n){let e;return{c(){e=b("hr")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function SP(n){let e,t,i;function l(o){n[63](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new EA({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function TP(n){let e,t,i;function l(o){n[62](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new YA({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function $P(n){let e,t,i,l,s;function o(f){n[59](f,n[82])}function r(f){n[60](f,n[82])}function a(f){n[61](f,n[82])}let u={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(u.value=n[3][n[82].name]),n[4][n[82].name]!==void 0&&(u.uploadedFiles=n[4][n[82].name]),n[5][n[82].name]!==void 0&&(u.deletedFileNames=n[5][n[82].name]),e=new cA({props:u}),ie.push(()=>ve(e,"value",o)),ie.push(()=>ve(e,"uploadedFiles",r)),ie.push(()=>ve(e,"deletedFileNames",a)),{c(){H(e.$$.fragment)},m(f,c){F(e,f,c),s=!0},p(f,c){n=f;const d={};c[0]&65536&&(d.field=n[82]),c[0]&4&&(d.original=n[2]),c[0]&8&&(d.record=n[3]),!t&&c[0]&65544&&(t=!0,d.value=n[3][n[82].name],$e(()=>t=!1)),!i&&c[0]&65552&&(i=!0,d.uploadedFiles=n[4][n[82].name],$e(()=>i=!1)),!l&&c[0]&65568&&(l=!0,d.deletedFileNames=n[5][n[82].name],$e(()=>l=!1)),e.$set(d)},i(f){s||(O(e.$$.fragment,f),s=!0)},o(f){D(e.$$.fragment,f),s=!1},d(f){q(e,f)}}}function CP(n){let e,t,i;function l(o){n[58](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new kA({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function OP(n){let e,t,i;function l(o){n[57](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new GA({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function EP(n){let e,t,i;function l(o){n[56](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new EL({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function MP(n){let e,t,i;function l(o){n[55](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new JL({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function DP(n){let e,t,i;function l(o){n[54](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new rP({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function IP(n){let e,t,i;function l(o){n[53](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new QL({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function LP(n){let e,t,i;function l(o){n[52](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new TL({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function AP(n){let e,t,i;function l(o){n[51](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new TA({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function PP(n){let e,t,i;function l(o){n[50](o,n[82])}let s={field:n[82],original:n[2],record:n[3]};return n[3][n[82].name]!==void 0&&(s.value=n[3][n[82].name]),e=new iP({props:s}),ie.push(()=>ve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&65536&&(a.field=n[82]),r[0]&4&&(a.original=n[2]),r[0]&8&&(a.record=n[3]),!t&&r[0]&65544&&(t=!0,a.value=n[3][n[82].name],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function Rg(n,e){let t,i,l,s,o;const r=[PP,AP,LP,IP,DP,MP,EP,OP,CP,$P,TP,SP],a=[];function u(f,c){return f[82].type==="text"?0:f[82].type==="number"?1:f[82].type==="bool"?2:f[82].type==="email"?3:f[82].type==="url"?4:f[82].type==="editor"?5:f[82].type==="date"?6:f[82].type==="select"?7:f[82].type==="json"?8:f[82].type==="file"?9:f[82].type==="relation"?10:f[82].type==="password"?11:-1}return~(i=u(e))&&(l=a[i]=r[i](e)),{key:n,first:null,c(){t=ge(),l&&l.c(),s=ge(),this.first=t},m(f,c){v(f,t,c),~i&&a[i].m(f,c),v(f,s,c),o=!0},p(f,c){e=f;let d=i;i=u(e),i===d?~i&&a[i].p(e,c):(l&&(re(),D(a[d],1,1,()=>{a[d]=null}),ae()),~i?(l=a[i],l?l.p(e,c):(l=a[i]=r[i](e),l.c()),O(l,1),l.m(s.parentNode,s)):l=null)},i(f){o||(O(l),o=!0)},o(f){D(l),o=!1},d(f){f&&(k(t),k(s)),~i&&a[i].d(f)}}}function Fg(n){let e,t,i;return t=new lL({props:{record:n[3]}}),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","tab-item"),x(e,"active",n[15]===no)},m(l,s){v(l,e,s),F(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.record=l[3]),t.$set(o),(!i||s[0]&32768)&&x(e,"active",l[15]===no)},i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function NP(n){let e,t,i,l,s,o,r=[],a=new Map,u,f,c,d,m=!n[8]&&n[12]&&!n[7]&&Lg(n);l=new fe({props:{class:"form-field "+(n[6]?"":"readonly"),name:"id",$$slots:{default:[wP,({uniqueId:S})=>({85:S}),({uniqueId:S})=>[0,0,S?8388608:0]]},$$scope:{ctx:n}}});let h=n[9]&&Pg(n),g=pe(n[16]);const _=S=>S[82].name;for(let S=0;S{m=null}),ae());const $={};T[0]&64&&($.class="form-field "+(S[6]?"":"readonly")),T[0]&524488|T[2]&25165824&&($.$$scope={dirty:T,ctx:S}),l.$set($),S[9]?h?(h.p(S,T),T[0]&512&&O(h,1)):(h=Pg(S),h.c(),O(h,1),h.m(t,o)):h&&(re(),D(h,1,1,()=>{h=null}),ae()),T[0]&65596&&(g=pe(S[16]),re(),r=yt(r,T,_,1,S,g,a,t,zt,Rg,null,Ig),ae()),(!f||T[0]&128)&&x(t,"no-pointer-events",S[7]),(!f||T[0]&32768)&&x(t,"active",S[15]===Ml),S[9]&&!S[17]&&!S[6]?y?(y.p(S,T),T[0]&131648&&O(y,1)):(y=Fg(S),y.c(),O(y,1),y.m(e,null)):y&&(re(),D(y,1,1,()=>{y=null}),ae())},i(S){if(!f){O(m),O(l.$$.fragment,S),O(h);for(let T=0;T{d=null}),ae()):d?(d.p(h,g),g[0]&64&&O(d,1)):(d=qg(h),d.c(),O(d,1),d.m(f.parentNode,f))},i(h){c||(O(d),c=!0)},o(h){D(d),c=!1},d(h){h&&(k(e),k(u),k(f)),d&&d.d(h)}}}function FP(n){let e,t,i;return{c(){e=b("span"),t=C(),i=b("h4"),i.textContent="Loading...",p(e,"class","loader loader-sm"),p(i,"class","panel-title txt-hint svelte-qc5ngu")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},p:te,i:te,o:te,d(l){l&&(k(e),k(t),k(i))}}}function qg(n){let e,t,i,l,s,o,r;return o=new Hn({props:{class:"dropdown dropdown-right dropdown-nowrap",$$slots:{default:[qP]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=C(),i=b("div"),l=b("i"),s=C(),H(o.$$.fragment),p(e,"class","flex-fill"),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"aria-label","More record options"),p(i,"class","btn btn-sm btn-circle btn-transparent flex-gap-0")},m(a,u){v(a,e,u),v(a,t,u),v(a,i,u),w(i,l),w(i,s),F(o,i,null),r=!0},p(a,u){const f={};u[0]&2564|u[2]&16777216&&(f.$$scope={dirty:u,ctx:a}),o.$set(f)},i(a){r||(O(o.$$.fragment,a),r=!0)},o(a){D(o.$$.fragment,a),r=!1},d(a){a&&(k(e),k(t),k(i)),q(o)}}}function Hg(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send verification email',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[39]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function jg(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send password reset email',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[40]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function qP(n){let e,t,i,l,s,o,r,a,u,f=n[9]&&!n[2].verified&&n[2].email&&Hg(n),c=n[9]&&n[2].email&&jg(n);return{c(){f&&f.c(),e=C(),c&&c.c(),t=C(),i=b("button"),i.innerHTML=' Impersonate',l=C(),s=b("button"),s.innerHTML=' Duplicate',o=C(),r=b("button"),r.innerHTML=' Delete',p(i,"type","button"),p(i,"class","dropdown-item closable"),p(i,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(s,"role","menuitem"),p(r,"type","button"),p(r,"class","dropdown-item txt-danger closable"),p(r,"role","menuitem")},m(d,m){f&&f.m(d,m),v(d,e,m),c&&c.m(d,m),v(d,t,m),v(d,i,m),v(d,l,m),v(d,s,m),v(d,o,m),v(d,r,m),a||(u=[B(i,"click",n[41]),B(s,"click",n[42]),B(r,"click",On(tt(n[43])))],a=!0)},p(d,m){d[9]&&!d[2].verified&&d[2].email?f?f.p(d,m):(f=Hg(d),f.c(),f.m(e.parentNode,e)):f&&(f.d(1),f=null),d[9]&&d[2].email?c?c.p(d,m):(c=jg(d),c.c(),c.m(t.parentNode,t)):c&&(c.d(1),c=null)},d(d){d&&(k(e),k(t),k(i),k(l),k(s),k(o),k(r)),f&&f.d(d),c&&c.d(d),a=!1,De(u)}}}function zg(n){let e,t,i,l,s,o;return{c(){e=b("div"),t=b("button"),t.textContent="Account",i=C(),l=b("button"),l.textContent="Authorized providers",p(t,"type","button"),p(t,"class","tab-item"),x(t,"active",n[15]===Ml),p(l,"type","button"),p(l,"class","tab-item"),x(l,"active",n[15]===no),p(e,"class","tabs-header stretched")},m(r,a){v(r,e,a),w(e,t),w(e,i),w(e,l),s||(o=[B(t,"click",n[44]),B(l,"click",n[45])],s=!0)},p(r,a){a[0]&32768&&x(t,"active",r[15]===Ml),a[0]&32768&&x(l,"active",r[15]===no)},d(r){r&&k(e),s=!1,De(o)}}}function HP(n){let e,t,i,l,s;const o=[FP,RP],r=[];function a(f,c){return f[7]?0:1}e=a(n),t=r[e]=o[e](n);let u=n[9]&&!n[17]&&!n[6]&&zg(n);return{c(){t.c(),i=C(),u&&u.c(),l=ge()},m(f,c){r[e].m(f,c),v(f,i,c),u&&u.m(f,c),v(f,l,c),s=!0},p(f,c){let d=e;e=a(f),e===d?r[e].p(f,c):(re(),D(r[d],1,1,()=>{r[d]=null}),ae(),t=r[e],t?t.p(f,c):(t=r[e]=o[e](f),t.c()),O(t,1),t.m(i.parentNode,i)),f[9]&&!f[17]&&!f[6]?u?u.p(f,c):(u=zg(f),u.c(),u.m(l.parentNode,l)):u&&(u.d(1),u=null)},i(f){s||(O(t),s=!0)},o(f){D(t),s=!1},d(f){f&&(k(i),k(l)),r[e].d(f),u&&u.d(f)}}}function Ug(n){let e,t,i,l,s,o;return l=new Hn({props:{class:"dropdown dropdown-upside dropdown-right dropdown-nowrap m-b-5",$$slots:{default:[jP]},$$scope:{ctx:n}}}),{c(){e=b("button"),t=b("i"),i=C(),H(l.$$.fragment),p(t,"class","ri-arrow-down-s-line"),p(t,"aria-hidden","true"),p(e,"type","button"),p(e,"class","btn p-l-5 p-r-5 flex-gap-0"),e.disabled=s=!n[18]||n[13]},m(r,a){v(r,e,a),w(e,t),w(e,i),F(l,e,null),o=!0},p(r,a){const u={};a[2]&16777216&&(u.$$scope={dirty:a,ctx:r}),l.$set(u),(!o||a[0]&270336&&s!==(s=!r[18]||r[13]))&&(e.disabled=s)},i(r){o||(O(l.$$.fragment,r),o=!0)},o(r){D(l.$$.fragment,r),o=!1},d(r){r&&k(e),q(l)}}}function jP(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Save and continue',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[38]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function zP(n){let e,t,i,l,s,o,r,a=n[6]?"Create":"Save changes",u,f,c,d,m,h,g=!n[6]&&Ug(n);return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",l=C(),s=b("div"),o=b("button"),r=b("span"),u=Y(a),c=C(),g&&g.c(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=i=n[13]||n[7],p(r,"class","txt"),p(o,"type","submit"),p(o,"form",n[21]),p(o,"title","Save and close"),p(o,"class","btn btn-expanded"),o.disabled=f=!n[18]||n[13],x(o,"btn-loading",n[13]||n[7]),p(s,"class","btns-group no-gap")},m(_,y){v(_,e,y),w(e,t),v(_,l,y),v(_,s,y),w(s,o),w(o,r),w(r,u),w(s,c),g&&g.m(s,null),d=!0,m||(h=B(e,"click",n[37]),m=!0)},p(_,y){(!d||y[0]&8320&&i!==(i=_[13]||_[7]))&&(e.disabled=i),(!d||y[0]&64)&&a!==(a=_[6]?"Create":"Save changes")&&ue(u,a),(!d||y[0]&270336&&f!==(f=!_[18]||_[13]))&&(o.disabled=f),(!d||y[0]&8320)&&x(o,"btn-loading",_[13]||_[7]),_[6]?g&&(re(),D(g,1,1,()=>{g=null}),ae()):g?(g.p(_,y),y[0]&64&&O(g,1)):(g=Ug(_),g.c(),O(g,1),g.m(s,null))},i(_){d||(O(g),d=!0)},o(_){D(g),d=!1},d(_){_&&(k(e),k(l),k(s)),g&&g.d(),m=!1,h()}}}function UP(n){let e,t,i,l,s={class:` + record-panel + `+(n[20]?"overlay-panel-xl":"overlay-panel-lg")+` + `+(n[9]&&!n[17]&&!n[6]?"colored-header":"")+` + `,btnClose:!n[7],escClose:!n[7],overlayClose:!n[7],beforeHide:n[64],$$slots:{footer:[zP],header:[HP],default:[NP]},$$scope:{ctx:n}};e=new ln({props:s}),n[65](e),e.$on("hide",n[66]),e.$on("show",n[67]);let o={record:n[3],collection:n[0]};return i=new vP({props:o}),n[68](i),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(r,a){F(e,r,a),v(r,t,a),F(i,r,a),l=!0},p(r,a){const u={};a[0]&1180224&&(u.class=` + record-panel + `+(r[20]?"overlay-panel-xl":"overlay-panel-lg")+` + `+(r[9]&&!r[17]&&!r[6]?"colored-header":"")+` + `),a[0]&128&&(u.btnClose=!r[7]),a[0]&128&&(u.escClose=!r[7]),a[0]&128&&(u.overlayClose=!r[7]),a[0]&16640&&(u.beforeHide=r[64]),a[0]&1031165|a[2]&16777216&&(u.$$scope={dirty:a,ctx:r}),e.$set(u);const f={};a[0]&8&&(f.record=r[3]),a[0]&1&&(f.collection=r[0]),i.$set(f)},i(r){l||(O(e.$$.fragment,r),O(i.$$.fragment,r),l=!0)},o(r){D(e.$$.fragment,r),D(i.$$.fragment,r),l=!1},d(r){r&&k(t),n[65](null),q(e,r),n[68](null),q(i,r)}}}const Ml="form",no="providers";function VP(n,e,t){let i,l,s,o,r,a,u,f;const c=_t(),d="record_"+z.randomString(5);let{collection:m}=e,h,g,_={},y={},S=null,T=!1,$=!1,E={},M={},L=JSON.stringify(_),I=L,A=Ml,P=!0,R=!0,N=m,U=[];const j=["id"],V=j.concat("email","emailVisibility","verified","tokenKey","password");function K(le){return Se(le),t(14,$=!0),t(15,A=Ml),h==null?void 0:h.show()}function J(){return h==null?void 0:h.hide()}function ee(){t(14,$=!1),J()}function X(){t(34,N=m),h!=null&&h.isActive()&&(st(JSON.stringify(y)),ee())}async function oe(le){if(le&&typeof le=="string"){try{return await me.collection(m.id).getOne(le)}catch(Ee){Ee.isAbort||(ee(),console.warn("resolveModel:",Ee),$i(`Unable to load record with id "${le}"`))}return null}return le}async function Se(le){t(7,R=!0),Wt({}),t(4,E={}),t(5,M={}),t(2,_=typeof le=="string"?{id:le,collectionId:m==null?void 0:m.id,collectionName:m==null?void 0:m.name}:le||{}),t(3,y=structuredClone(_)),t(2,_=await oe(le)||{}),t(3,y=structuredClone(_)),await fn(),t(12,S=We()),!S||Be(y,S)?t(12,S=null):(delete S.password,delete S.passwordConfirm),t(32,L=JSON.stringify(y)),t(7,R=!1)}async function ke(le){var Re,Ke;Wt({}),t(2,_=le||{}),t(4,E={}),t(5,M={});const Ee=((Ke=(Re=m==null?void 0:m.fields)==null?void 0:Re.filter(Ae=>Ae.type!="file"))==null?void 0:Ke.map(Ae=>Ae.name))||[];for(let Ae in le)Ee.includes(Ae)||t(3,y[Ae]=le[Ae],y);await fn(),t(32,L=JSON.stringify(y)),rt()}function Ce(){return"record_draft_"+((m==null?void 0:m.id)||"")+"_"+((_==null?void 0:_.id)||"")}function We(le){try{const Ee=window.localStorage.getItem(Ce());if(Ee)return JSON.parse(Ee)}catch{}return le}function st(le){try{window.localStorage.setItem(Ce(),le)}catch(Ee){console.warn("updateDraft failure:",Ee),window.localStorage.removeItem(Ce())}}function et(){S&&(t(3,y=S),t(12,S=null))}function Be(le,Ee){var ft;const Re=structuredClone(le||{}),Ke=structuredClone(Ee||{}),Ae=(ft=m==null?void 0:m.fields)==null?void 0:ft.filter(Xt=>Xt.type==="file");for(let Xt of Ae)delete Re[Xt.name],delete Ke[Xt.name];const Ge=["expand","password","passwordConfirm"];for(let Xt of Ge)delete Re[Xt],delete Ke[Xt];return JSON.stringify(Re)==JSON.stringify(Ke)}function rt(){t(12,S=null),window.localStorage.removeItem(Ce())}async function Je(le=!0){var Ee;if(!(T||!u||!(m!=null&&m.id))){t(13,T=!0);try{const Re=Ht();let Ke;if(P?Ke=await me.collection(m.id).create(Re):Ke=await me.collection(m.id).update(y.id,Re),tn(P?"Successfully created record.":"Successfully updated record."),rt(),l&&(y==null?void 0:y.id)==((Ee=me.authStore.record)==null?void 0:Ee.id)&&Re.get("password"))return me.logout();le?ee():ke(Ke),c("save",{isNew:P,record:Ke})}catch(Re){me.error(Re)}t(13,T=!1)}}function at(){_!=null&&_.id&&pn("Do you really want to delete the selected record?",()=>me.collection(_.collectionId).delete(_.id).then(()=>{ee(),tn("Successfully deleted record."),c("delete",_)}).catch(le=>{me.error(le)}))}function Ht(){const le=structuredClone(y||{}),Ee=new FormData,Re={},Ke={};for(const Ae of(m==null?void 0:m.fields)||[])Ae.type=="autodate"||i&&Ae.type=="password"||(Re[Ae.name]=!0,Ae.type=="json"&&(Ke[Ae.name]=!0));i&&le.password&&(Re.password=!0),i&&le.passwordConfirm&&(Re.passwordConfirm=!0);for(const Ae in le)if(Re[Ae]){if(typeof le[Ae]>"u"&&(le[Ae]=null),Ke[Ae]&&le[Ae]!=="")try{JSON.parse(le[Ae])}catch(Ge){const ft={};throw ft[Ae]={code:"invalid_json",message:Ge.toString()},new Rn({status:400,response:{data:ft}})}z.addValueToFormData(Ee,Ae,le[Ae])}for(const Ae in E){const Ge=z.toArray(E[Ae]);for(const ft of Ge)Ee.append(Ae+"+",ft)}for(const Ae in M){const Ge=z.toArray(M[Ae]);for(const ft of Ge)Ee.append(Ae+"-",ft)}return Ee}function Te(){!(m!=null&&m.id)||!(_!=null&&_.email)||pn(`Do you really want to sent verification email to ${_.email}?`,()=>me.collection(m.id).requestVerification(_.email).then(()=>{tn(`Successfully sent verification email to ${_.email}.`)}).catch(le=>{me.error(le)}))}function Ze(){!(m!=null&&m.id)||!(_!=null&&_.email)||pn(`Do you really want to sent password reset email to ${_.email}?`,()=>me.collection(m.id).requestPasswordReset(_.email).then(()=>{tn(`Successfully sent password reset email to ${_.email}.`)}).catch(le=>{me.error(le)}))}function ot(){a?pn("You have unsaved changes. Do you really want to discard them?",()=>{Le()}):Le()}async function Le(){let le=_?structuredClone(_):null;if(le){const Ee=["file","autodate"],Re=(m==null?void 0:m.fields)||[];for(const Ke of Re)Ee.includes(Ke.type)&&delete le[Ke.name];le.id=""}rt(),K(le),await fn(),t(32,L="")}function Ve(le){(le.ctrlKey||le.metaKey)&&le.code=="KeyS"&&(le.preventDefault(),le.stopPropagation(),Je(!1))}const we=()=>J(),Oe=()=>Je(!1),ut=()=>Te(),Ne=()=>Ze(),xe=()=>g==null?void 0:g.show(),qt=()=>ot(),Zt=()=>at(),Fe=()=>t(15,A=Ml),Dt=()=>t(15,A=no),Gt=()=>et(),mn=()=>rt();function hn(){y.id=this.value,t(3,y)}function pi(le){y=le,t(3,y)}function Ci(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function gt(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function rn(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function sn(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function rl(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function It(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function al(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function ul(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function Hi(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function ji(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function fl(le,Ee){n.$$.not_equal(E[Ee.name],le)&&(E[Ee.name]=le,t(4,E))}function Mn(le,Ee){n.$$.not_equal(M[Ee.name],le)&&(M[Ee.name]=le,t(5,M))}function Rl(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}function cl(le,Ee){n.$$.not_equal(y[Ee.name],le)&&(y[Ee.name]=le,t(3,y))}const Z=()=>a&&$?(pn("You have unsaved changes. Do you really want to close the panel?",()=>{ee()}),!1):(Wt({}),rt(),!0);function Q(le){ie[le?"unshift":"push"](()=>{h=le,t(10,h)})}function se(le){Pe.call(this,n,le)}function he(le){Pe.call(this,n,le)}function qe(le){ie[le?"unshift":"push"](()=>{g=le,t(11,g)})}return n.$$set=le=>{"collection"in le&&t(0,m=le.collection)},n.$$.update=()=>{var le,Ee,Re;n.$$.dirty[0]&1&&t(9,i=(m==null?void 0:m.type)==="auth"),n.$$.dirty[0]&1&&t(17,l=(m==null?void 0:m.name)==="_superusers"),n.$$.dirty[0]&1&&t(20,s=!!((le=m==null?void 0:m.fields)!=null&&le.find(Ke=>Ke.type==="editor"))),n.$$.dirty[0]&1&&t(19,o=(Ee=m==null?void 0:m.fields)==null?void 0:Ee.find(Ke=>Ke.name==="id")),n.$$.dirty[0]&48&&t(36,r=z.hasNonEmptyProps(E)||z.hasNonEmptyProps(M)),n.$$.dirty[0]&8&&t(33,I=JSON.stringify(y)),n.$$.dirty[1]&38&&t(8,a=r||L!=I),n.$$.dirty[0]&4&&t(6,P=!_||!_.id),n.$$.dirty[0]&448&&t(18,u=!R&&(P||a)),n.$$.dirty[0]&128|n.$$.dirty[1]&4&&(R||st(I)),n.$$.dirty[0]&1|n.$$.dirty[1]&8&&m&&(N==null?void 0:N.id)!=(m==null?void 0:m.id)&&X(),n.$$.dirty[0]&512&&t(35,f=i?V:j),n.$$.dirty[0]&1|n.$$.dirty[1]&16&&t(16,U=((Re=m==null?void 0:m.fields)==null?void 0:Re.filter(Ke=>!f.includes(Ke.name)&&Ke.type!="autodate"))||[])},[m,J,_,y,E,M,P,R,a,i,h,g,S,T,$,A,U,l,u,o,s,d,ee,et,rt,Je,at,Te,Ze,ot,Ve,K,L,I,N,f,r,we,Oe,ut,Ne,xe,qt,Zt,Fe,Dt,Gt,mn,hn,pi,Ci,gt,rn,sn,rl,It,al,ul,Hi,ji,fl,Mn,Rl,cl,Z,Q,se,he,qe]}class rf extends ye{constructor(e){super(),be(this,e,VP,UP,_e,{collection:0,show:31,hide:1},null,[-1,-1,-1])}get show(){return this.$$.ctx[31]}get hide(){return this.$$.ctx[1]}}function BP(n){let e,t,i,l,s=(n[2]?"...":n[0])+"",o,r;return{c(){e=b("div"),t=b("span"),t.textContent="Total found:",i=C(),l=b("span"),o=Y(s),p(t,"class","txt"),p(l,"class","txt"),p(e,"class",r="inline-flex flex-gap-5 records-counter "+n[1])},m(a,u){v(a,e,u),w(e,t),w(e,i),w(e,l),w(l,o)},p(a,[u]){u&5&&s!==(s=(a[2]?"...":a[0])+"")&&ue(o,s),u&2&&r!==(r="inline-flex flex-gap-5 records-counter "+a[1])&&p(e,"class",r)},i:te,o:te,d(a){a&&k(e)}}}function WP(n,e,t){const i=_t();let{collection:l}=e,{filter:s=""}=e,{totalCount:o=0}=e,{class:r=void 0}=e,a=!1;async function u(){if(l!=null&&l.id){t(2,a=!0),t(0,o=0);try{const f=z.getAllCollectionIdentifiers(l),c=await me.collection(l.id).getList(1,1,{filter:z.normalizeSearchFilter(s,f),fields:"id",requestKey:"records_count"});t(0,o=c.totalItems),i("count",o),t(2,a=!1)}catch(f){f!=null&&f.isAbort||(t(2,a=!1),console.warn(f))}}}return n.$$set=f=>{"collection"in f&&t(3,l=f.collection),"filter"in f&&t(4,s=f.filter),"totalCount"in f&&t(0,o=f.totalCount),"class"in f&&t(1,r=f.class)},n.$$.update=()=>{n.$$.dirty&24&&l!=null&&l.id&&s!==-1&&u()},[o,r,a,l,s,u]}class YP extends ye{constructor(e){super(),be(this,e,WP,BP,_e,{collection:3,filter:4,totalCount:0,class:1,reload:5})}get reload(){return this.$$.ctx[5]}}function Vg(n,e,t){const i=n.slice();return i[57]=e[t],i}function Bg(n,e,t){const i=n.slice();return i[60]=e[t],i}function Wg(n,e,t){const i=n.slice();return i[60]=e[t],i}function Yg(n,e,t){const i=n.slice();return i[53]=e[t],i}function Kg(n){let e;function t(s,o){return s[9]?JP:KP}let i=t(n),l=i(n);return{c(){e=b("th"),l.c(),p(e,"class","bulk-select-col min-width")},m(s,o){v(s,e,o),l.m(e,null)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e,null)))},d(s){s&&k(e),l.d()}}}function KP(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),t=b("input"),l=C(),s=b("label"),p(t,"type","checkbox"),p(t,"id","checkbox_0"),t.disabled=i=!n[3].length,t.checked=n[13],p(s,"for","checkbox_0"),p(e,"class","form-field")},m(a,u){v(a,e,u),w(e,t),w(e,l),w(e,s),o||(r=B(t,"change",n[30]),o=!0)},p(a,u){u[0]&8&&i!==(i=!a[3].length)&&(t.disabled=i),u[0]&8192&&(t.checked=a[13])},d(a){a&&k(e),o=!1,r()}}}function JP(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function ZP(n){let e,t;return{c(){e=b("i"),p(e,"class",t=z.getFieldTypeIcon(n[60].type))},m(i,l){v(i,e,l)},p(i,l){l[0]&32768&&t!==(t=z.getFieldTypeIcon(i[60].type))&&p(e,"class",t)},d(i){i&&k(e)}}}function GP(n){let e;return{c(){e=b("i"),p(e,"class",z.getFieldTypeIcon("primary"))},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function XP(n){let e,t,i,l=n[60].name+"",s;function o(u,f){return u[60].primaryKey?GP:ZP}let r=o(n),a=r(n);return{c(){e=b("div"),a.c(),t=C(),i=b("span"),s=Y(l),p(i,"class","txt"),p(e,"class","col-header-content")},m(u,f){v(u,e,f),a.m(e,null),w(e,t),w(e,i),w(i,s)},p(u,f){r===(r=o(u))&&a?a.p(u,f):(a.d(1),a=r(u),a&&(a.c(),a.m(e,t))),f[0]&32768&&l!==(l=u[60].name+"")&&ue(s,l)},d(u){u&&k(e),a.d()}}}function Jg(n,e){let t,i,l,s;function o(a){e[31](a)}let r={class:"col-type-"+e[60].type+" col-field-"+e[60].name,name:e[60].name,$$slots:{default:[XP]},$$scope:{ctx:e}};return e[0]!==void 0&&(r.sort=e[0]),i=new ir({props:r}),ie.push(()=>ve(i,"sort",o)),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(a,u){v(a,t,u),F(i,a,u),s=!0},p(a,u){e=a;const f={};u[0]&32768&&(f.class="col-type-"+e[60].type+" col-field-"+e[60].name),u[0]&32768&&(f.name=e[60].name),u[0]&32768|u[2]&8&&(f.$$scope={dirty:u,ctx:e}),!l&&u[0]&1&&(l=!0,f.sort=e[0],$e(()=>l=!1)),i.$set(f)},i(a){s||(O(i.$$.fragment,a),s=!0)},o(a){D(i.$$.fragment,a),s=!1},d(a){a&&k(t),q(i,a)}}}function Zg(n){let e;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Toggle columns"),p(e,"class","btn btn-sm btn-transparent p-0")},m(t,i){v(t,e,i),n[32](e)},p:te,d(t){t&&k(e),n[32](null)}}}function Gg(n){let e;function t(s,o){return s[9]?xP:QP}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function QP(n){let e,t,i,l;function s(a,u){var f;if((f=a[1])!=null&&f.length)return tN;if(!a[16])return eN}let o=s(n),r=o&&o(n);return{c(){e=b("tr"),t=b("td"),i=b("h6"),i.textContent="No records found.",l=C(),r&&r.c(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,u){v(a,e,u),w(e,t),w(t,i),w(t,l),r&&r.m(t,null)},p(a,u){o===(o=s(a))&&r?r.p(a,u):(r&&r.d(1),r=o&&o(a),r&&(r.c(),r.m(t,null)))},d(a){a&&k(e),r&&r.d()}}}function xP(n){let e;return{c(){e=b("tr"),e.innerHTML=''},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function eN(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' New record',p(e,"type","button"),p(e,"class","btn btn-secondary btn-expanded m-t-sm")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[37]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function tN(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[36]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function Xg(n){let e,t,i,l,s,o,r,a,u,f;function c(){return n[33](n[57])}return{c(){e=b("td"),t=b("div"),i=b("input"),o=C(),r=b("label"),p(i,"type","checkbox"),p(i,"id",l="checkbox_"+n[57].id),i.checked=s=n[4][n[57].id],p(r,"for",a="checkbox_"+n[57].id),p(t,"class","form-field"),p(e,"class","bulk-select-col min-width")},m(d,m){v(d,e,m),w(e,t),w(t,i),w(t,o),w(t,r),u||(f=[B(i,"change",c),B(t,"click",On(n[28]))],u=!0)},p(d,m){n=d,m[0]&8&&l!==(l="checkbox_"+n[57].id)&&p(i,"id",l),m[0]&24&&s!==(s=n[4][n[57].id])&&(i.checked=s),m[0]&8&&a!==(a="checkbox_"+n[57].id)&&p(r,"for",a)},d(d){d&&k(e),u=!1,De(f)}}}function Qg(n,e){let t,i,l,s;return i=new rk({props:{short:!0,record:e[57],field:e[60]}}),{key:n,first:null,c(){t=b("td"),H(i.$$.fragment),p(t,"class",l="col-type-"+e[60].type+" col-field-"+e[60].name),this.first=t},m(o,r){v(o,t,r),F(i,t,null),s=!0},p(o,r){e=o;const a={};r[0]&8&&(a.record=e[57]),r[0]&32768&&(a.field=e[60]),i.$set(a),(!s||r[0]&32768&&l!==(l="col-type-"+e[60].type+" col-field-"+e[60].name))&&p(t,"class",l)},i(o){s||(O(i.$$.fragment,o),s=!0)},o(o){D(i.$$.fragment,o),s=!1},d(o){o&&k(t),q(i)}}}function xg(n,e){let t,i,l=[],s=new Map,o,r,a,u,f,c=!e[16]&&Xg(e),d=pe(e[15]);const m=_=>_[60].id;for(let _=0;_',p(r,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(_,y){v(_,t,y),c&&c.m(t,null),w(t,i);for(let S=0;SL[60].id;for(let L=0;L<_.length;L+=1){let I=Wg(n,_,L),A=y(I);o.set(A,s[L]=Jg(A,I))}let S=n[12].length&&Zg(n),T=pe(n[3]);const $=L=>L[16]?L[57]:L[57].id;for(let L=0;L({56:s}),({uniqueId:s})=>[0,s?33554432:0]]},$$scope:{ctx:e}}}),{key:n,first:null,c(){t=ge(),H(i.$$.fragment),this.first=t},m(s,o){v(s,t,o),F(i,s,o),l=!0},p(s,o){e=s;const r={};o[0]&4128|o[1]&33554432|o[2]&8&&(r.$$scope={dirty:o,ctx:e}),i.$set(r)},i(s){l||(O(i.$$.fragment,s),l=!0)},o(s){D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(i,s)}}}function lN(n){let e,t,i=[],l=new Map,s,o,r=pe(n[12]);const a=u=>u[53].id+u[53].name;for(let u=0;u{i=null}),ae())},i(l){t||(O(i),t=!0)},o(l){D(i),t=!1},d(l){l&&k(e),i&&i.d(l)}}}function i1(n){let e,t,i,l,s,o,r=n[6]===1?"record":"records",a,u,f,c,d,m,h,g,_,y,S;return{c(){e=b("div"),t=b("div"),i=Y("Selected "),l=b("strong"),s=Y(n[6]),o=C(),a=Y(r),u=C(),f=b("button"),f.innerHTML='Reset',c=C(),d=b("div"),m=C(),h=b("button"),h.innerHTML='Delete selected',p(t,"class","txt"),p(f,"type","button"),p(f,"class","btn btn-xs btn-transparent btn-outline p-l-5 p-r-5"),x(f,"btn-disabled",n[10]),p(d,"class","flex-fill"),p(h,"type","button"),p(h,"class","btn btn-sm btn-transparent btn-danger"),x(h,"btn-loading",n[10]),x(h,"btn-disabled",n[10]),p(e,"class","bulkbar")},m(T,$){v(T,e,$),w(e,t),w(t,i),w(t,l),w(l,s),w(t,o),w(t,a),w(e,u),w(e,f),w(e,c),w(e,d),w(e,m),w(e,h),_=!0,y||(S=[B(f,"click",n[40]),B(h,"click",n[41])],y=!0)},p(T,$){(!_||$[0]&64)&&ue(s,T[6]),(!_||$[0]&64)&&r!==(r=T[6]===1?"record":"records")&&ue(a,r),(!_||$[0]&1024)&&x(f,"btn-disabled",T[10]),(!_||$[0]&1024)&&x(h,"btn-loading",T[10]),(!_||$[0]&1024)&&x(h,"btn-disabled",T[10])},i(T){_||(T&&nt(()=>{_&&(g||(g=ze(e,Fn,{duration:150,y:5},!0)),g.run(1))}),_=!0)},o(T){T&&(g||(g=ze(e,Fn,{duration:150,y:5},!1)),g.run(0)),_=!1},d(T){T&&k(e),T&&g&&g.end(),y=!1,De(S)}}}function oN(n){let e,t,i,l,s={class:"table-wrapper",$$slots:{before:[sN],default:[nN]},$$scope:{ctx:n}};e=new Pu({props:s}),n[39](e);let o=n[6]&&i1(n);return{c(){H(e.$$.fragment),t=C(),o&&o.c(),i=ge()},m(r,a){F(e,r,a),v(r,t,a),o&&o.m(r,a),v(r,i,a),l=!0},p(r,a){const u={};a[0]&129851|a[2]&8&&(u.$$scope={dirty:a,ctx:r}),e.$set(u),r[6]?o?(o.p(r,a),a[0]&64&&O(o,1)):(o=i1(r),o.c(),O(o,1),o.m(i.parentNode,i)):o&&(re(),D(o,1,1,()=>{o=null}),ae())},i(r){l||(O(e.$$.fragment,r),O(o),l=!0)},o(r){D(e.$$.fragment,r),D(o),l=!1},d(r){r&&(k(t),k(i)),n[39](null),q(e,r),o&&o.d(r)}}}const rN=/^([\+\-])?(\w+)$/,l1=40;function aN(n,e,t){let i,l,s,o,r,a,u,f,c,d;Qe(n,En,Ne=>t(46,d=Ne));const m=_t();let{collection:h}=e,{sort:g=""}=e,{filter:_=""}=e,y,S=[],T=1,$=0,E={},M=!0,L=!1,I=0,A,P=[],R=[],N="";const U=["verified","emailVisibility"];function j(){h!=null&&h.id&&(P.length?localStorage.setItem(N,JSON.stringify(P)):localStorage.removeItem(N))}function V(){if(t(5,P=[]),!!(h!=null&&h.id))try{const Ne=localStorage.getItem(N);Ne&&t(5,P=JSON.parse(Ne)||[])}catch{}}function K(Ne){return!!S.find(xe=>xe.id==Ne)}async function J(){const Ne=T;for(let xe=1;xe<=Ne;xe++)(xe===1||u)&&await ee(xe,!1)}async function ee(Ne=1,xe=!0){var hn,pi,Ci;if(!(h!=null&&h.id))return;t(9,M=!0);let qt=g;const Zt=qt.match(rN),Fe=Zt?r.find(gt=>gt.name===Zt[2]):null;if(Zt&&Fe){const gt=((Ci=(pi=(hn=d==null?void 0:d.find(sn=>sn.id==Fe.collectionId))==null?void 0:hn.fields)==null?void 0:pi.filter(sn=>sn.presentable))==null?void 0:Ci.map(sn=>sn.name))||[],rn=[];for(const sn of gt)rn.push((Zt[1]||"")+Zt[2]+"."+sn);rn.length>0&&(qt=rn.join(","))}const Dt=z.getAllCollectionIdentifiers(h),Gt=o.map(gt=>gt.name+":excerpt(200)").concat(r.map(gt=>"expand."+gt.name+".*:excerpt(200)"));Gt.length&&Gt.unshift("*");const mn=[];for(const gt of r){const rn=z.getExpandPresentableRelField(gt,d,2);rn&&mn.push(rn)}return me.collection(h.id).getList(Ne,l1,{sort:qt,skipTotal:1,filter:z.normalizeSearchFilter(_,Dt),expand:mn.join(","),fields:Gt.join(","),requestKey:"records_list"}).then(async gt=>{var rn;if(Ne<=1&&X(),t(9,M=!1),t(8,T=gt.page),t(25,$=gt.items.length),m("load",S.concat(gt.items)),o.length)for(let sn of gt.items)sn._partial=!0;if(xe){const sn=++I;for(;(rn=gt.items)!=null&&rn.length&&I==sn;){const rl=gt.items.splice(0,20);for(let It of rl)z.pushOrReplaceByKey(S,It);t(3,S),await z.yieldToMain()}}else{for(let sn of gt.items)z.pushOrReplaceByKey(S,sn);t(3,S)}}).catch(gt=>{gt!=null&>.isAbort||(t(9,M=!1),console.warn(gt),X(),me.error(gt,!_||(gt==null?void 0:gt.status)!=400))})}function X(){y==null||y.resetVerticalScroll(),t(3,S=[]),t(8,T=1),t(25,$=0),t(4,E={})}function oe(){c?Se():ke()}function Se(){t(4,E={})}function ke(){for(const Ne of S)t(4,E[Ne.id]=Ne,E);t(4,E)}function Ce(Ne){E[Ne.id]?delete E[Ne.id]:t(4,E[Ne.id]=Ne,E),t(4,E)}function We(){pn(`Do you really want to delete the selected ${f===1?"record":"records"}?`,st)}async function st(){if(L||!f||!(h!=null&&h.id))return;let Ne=[];for(const xe of Object.keys(E))Ne.push(me.collection(h.id).delete(xe));return t(10,L=!0),Promise.all(Ne).then(()=>{tn(`Successfully deleted the selected ${f===1?"record":"records"}.`),m("delete",E),Se()}).catch(xe=>{me.error(xe)}).finally(()=>(t(10,L=!1),J()))}function et(Ne){Pe.call(this,n,Ne)}const Be=(Ne,xe)=>{xe.target.checked?z.removeByValue(P,Ne.id):z.pushUnique(P,Ne.id),t(5,P)},rt=()=>oe();function Je(Ne){g=Ne,t(0,g)}function at(Ne){ie[Ne?"unshift":"push"](()=>{A=Ne,t(11,A)})}const Ht=Ne=>Ce(Ne),Te=Ne=>m("select",Ne),Ze=(Ne,xe)=>{xe.code==="Enter"&&(xe.preventDefault(),m("select",Ne))},ot=()=>t(1,_=""),Le=()=>m("new"),Ve=()=>ee(T+1);function we(Ne){ie[Ne?"unshift":"push"](()=>{y=Ne,t(7,y)})}const Oe=()=>Se(),ut=()=>We();return n.$$set=Ne=>{"collection"in Ne&&t(22,h=Ne.collection),"sort"in Ne&&t(0,g=Ne.sort),"filter"in Ne&&t(1,_=Ne.filter)},n.$$.update=()=>{n.$$.dirty[0]&4194304&&h!=null&&h.id&&(N=h.id+"@hiddenColumns",V(),X()),n.$$.dirty[0]&4194304&&t(16,i=(h==null?void 0:h.type)==="view"),n.$$.dirty[0]&4194304&&t(27,l=(h==null?void 0:h.type)==="auth"&&h.name==="_superusers"),n.$$.dirty[0]&138412032&&t(26,s=((h==null?void 0:h.fields)||[]).filter(Ne=>!Ne.hidden&&(!l||!U.includes(Ne.name)))),n.$$.dirty[0]&67108864&&(o=s.filter(Ne=>Ne.type==="editor")),n.$$.dirty[0]&67108864&&(r=s.filter(Ne=>Ne.type==="relation")),n.$$.dirty[0]&67108896&&t(15,a=s.filter(Ne=>!P.includes(Ne.id))),n.$$.dirty[0]&4194307&&h!=null&&h.id&&g!==-1&&_!==-1&&ee(1),n.$$.dirty[0]&33554432&&t(14,u=$>=l1),n.$$.dirty[0]&16&&t(6,f=Object.keys(E).length),n.$$.dirty[0]&72&&t(13,c=S.length&&f===S.length),n.$$.dirty[0]&32&&P!==-1&&j(),n.$$.dirty[0]&67108864&&t(12,R=s.filter(Ne=>!Ne.primaryKey).map(Ne=>({id:Ne.id,name:Ne.name})))},[g,_,ee,S,E,P,f,y,T,M,L,A,R,c,u,a,i,m,oe,Se,Ce,We,h,K,J,$,s,l,et,Be,rt,Je,at,Ht,Te,Ze,ot,Le,Ve,we,Oe,ut]}class uN extends ye{constructor(e){super(),be(this,e,aN,oN,_e,{collection:22,sort:0,filter:1,hasRecord:23,reloadLoadedPages:24,load:2},null,[-1,-1,-1])}get hasRecord(){return this.$$.ctx[23]}get reloadLoadedPages(){return this.$$.ctx[24]}get load(){return this.$$.ctx[2]}}function fN(n){let e,t,i,l;return e=new YD({}),i=new di({props:{class:"flex-content",$$slots:{footer:[mN],default:[pN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o[0]&6135|o[1]&16384&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function cN(n){let e,t;return e=new di({props:{center:!0,$$slots:{default:[gN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&4112|l[1]&16384&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function dN(n){let e,t;return e=new di({props:{center:!0,$$slots:{default:[bN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[1]&16384&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function s1(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Edit collection"),p(e,"class","btn btn-transparent btn-circle")},m(l,s){v(l,e,s),t||(i=[Me(He.call(null,e,{text:"Edit collection",position:"right"})),B(e,"click",n[20])],t=!0)},p:te,d(l){l&&k(e),t=!1,De(i)}}}function o1(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' New record',p(e,"type","button"),p(e,"class","btn btn-expanded")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[23]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function pN(n){let e,t,i,l,s,o=n[2].name+"",r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P,R=!n[12]&&s1(n);c=new Au({}),c.$on("refresh",n[21]);let N=n[2].type!=="view"&&o1(n);y=new Hr({props:{value:n[0],autocompleteCollection:n[2]}}),y.$on("submit",n[24]);function U(K){n[26](K)}function j(K){n[27](K)}let V={collection:n[2]};return n[0]!==void 0&&(V.filter=n[0]),n[1]!==void 0&&(V.sort=n[1]),E=new uN({props:V}),n[25](E),ie.push(()=>ve(E,"filter",U)),ie.push(()=>ve(E,"sort",j)),E.$on("select",n[28]),E.$on("delete",n[29]),E.$on("new",n[30]),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Collections",l=C(),s=b("div"),r=Y(o),a=C(),u=b("div"),R&&R.c(),f=C(),H(c.$$.fragment),d=C(),m=b("div"),h=b("button"),h.innerHTML=' API Preview',g=C(),N&&N.c(),_=C(),H(y.$$.fragment),S=C(),T=b("div"),$=C(),H(E.$$.fragment),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(u,"class","inline-flex gap-5"),p(h,"type","button"),p(h,"class","btn btn-outline"),p(m,"class","btns-group"),p(e,"class","page-header"),p(T,"class","clearfix m-b-sm")},m(K,J){v(K,e,J),w(e,t),w(t,i),w(t,l),w(t,s),w(s,r),w(e,a),w(e,u),R&&R.m(u,null),w(u,f),F(c,u,null),w(e,d),w(e,m),w(m,h),w(m,g),N&&N.m(m,null),v(K,_,J),F(y,K,J),v(K,S,J),v(K,T,J),v(K,$,J),F(E,K,J),I=!0,A||(P=B(h,"click",n[22]),A=!0)},p(K,J){(!I||J[0]&4)&&o!==(o=K[2].name+"")&&ue(r,o),K[12]?R&&(R.d(1),R=null):R?R.p(K,J):(R=s1(K),R.c(),R.m(u,f)),K[2].type!=="view"?N?N.p(K,J):(N=o1(K),N.c(),N.m(m,null)):N&&(N.d(1),N=null);const ee={};J[0]&1&&(ee.value=K[0]),J[0]&4&&(ee.autocompleteCollection=K[2]),y.$set(ee);const X={};J[0]&4&&(X.collection=K[2]),!M&&J[0]&1&&(M=!0,X.filter=K[0],$e(()=>M=!1)),!L&&J[0]&2&&(L=!0,X.sort=K[1],$e(()=>L=!1)),E.$set(X)},i(K){I||(O(c.$$.fragment,K),O(y.$$.fragment,K),O(E.$$.fragment,K),I=!0)},o(K){D(c.$$.fragment,K),D(y.$$.fragment,K),D(E.$$.fragment,K),I=!1},d(K){K&&(k(e),k(_),k(S),k(T),k($)),R&&R.d(),q(c),N&&N.d(),q(y,K),n[25](null),q(E,K),A=!1,P()}}}function mN(n){let e,t,i;function l(o){n[19](o)}let s={class:"m-r-auto txt-sm txt-hint",collection:n[2],filter:n[0]};return n[10]!==void 0&&(s.totalCount=n[10]),e=new YP({props:s}),n[18](e),ie.push(()=>ve(e,"totalCount",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){const a={};r[0]&4&&(a.collection=o[2]),r[0]&1&&(a.filter=o[0]),!t&&r[0]&1024&&(t=!0,a.totalCount=o[10],$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){n[18](null),q(e,o)}}}function hN(n){let e,t,i,l,s;return{c(){e=b("h1"),e.textContent="Create your first collection to add records!",t=C(),i=b("button"),i.innerHTML=' Create new collection',p(e,"class","m-b-10"),p(i,"type","button"),p(i,"class","btn btn-expanded-lg btn-lg")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),l||(s=B(i,"click",n[17]),l=!0)},p:te,d(o){o&&(k(e),k(t),k(i)),l=!1,s()}}}function _N(n){let e;return{c(){e=b("h1"),e.textContent="You don't have any collections yet.",p(e,"class","m-b-10")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function gN(n){let e,t,i;function l(r,a){return r[12]?_N:hN}let s=l(n),o=s(n);return{c(){e=b("div"),t=b("div"),t.innerHTML='',i=C(),o.c(),p(t,"class","icon"),p(e,"class","placeholder-section m-b-base")},m(r,a){v(r,e,a),w(e,t),w(e,i),o.m(e,null)},p(r,a){s===(s=l(r))&&o?o.p(r,a):(o.d(1),o=s(r),o&&(o.c(),o.m(e,null)))},d(r){r&&k(e),o.d()}}}function bN(n){let e;return{c(){e=b("div"),e.innerHTML='

    Loading collections...

    ',p(e,"class","placeholder-section m-b-base")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function yN(n){let e,t,i,l,s,o,r,a,u,f,c;const d=[dN,cN,fN],m=[];function h(T,$){return T[3]&&!T[11].length?0:T[11].length?2:1}e=h(n),t=m[e]=d[e](n);let g={};l=new nf({props:g}),n[31](l),l.$on("truncate",n[32]);let _={};o=new $5({props:_}),n[33](o);let y={collection:n[2]};a=new rf({props:y}),n[34](a),a.$on("hide",n[35]),a.$on("save",n[36]),a.$on("delete",n[37]);let S={collection:n[2]};return f=new JI({props:S}),n[38](f),f.$on("hide",n[39]),{c(){t.c(),i=C(),H(l.$$.fragment),s=C(),H(o.$$.fragment),r=C(),H(a.$$.fragment),u=C(),H(f.$$.fragment)},m(T,$){m[e].m(T,$),v(T,i,$),F(l,T,$),v(T,s,$),F(o,T,$),v(T,r,$),F(a,T,$),v(T,u,$),F(f,T,$),c=!0},p(T,$){let E=e;e=h(T),e===E?m[e].p(T,$):(re(),D(m[E],1,1,()=>{m[E]=null}),ae(),t=m[e],t?t.p(T,$):(t=m[e]=d[e](T),t.c()),O(t,1),t.m(i.parentNode,i));const M={};l.$set(M);const L={};o.$set(L);const I={};$[0]&4&&(I.collection=T[2]),a.$set(I);const A={};$[0]&4&&(A.collection=T[2]),f.$set(A)},i(T){c||(O(t),O(l.$$.fragment,T),O(o.$$.fragment,T),O(a.$$.fragment,T),O(f.$$.fragment,T),c=!0)},o(T){D(t),D(l.$$.fragment,T),D(o.$$.fragment,T),D(a.$$.fragment,T),D(f.$$.fragment,T),c=!1},d(T){T&&(k(i),k(s),k(r),k(u)),m[e].d(T),n[31](null),q(l,T),n[33](null),q(o,T),n[34](null),q(a,T),n[38](null),q(f,T)}}}function kN(n,e,t){let i,l,s,o,r,a,u;Qe(n,Qn,Te=>t(2,l=Te)),Qe(n,cn,Te=>t(40,s=Te)),Qe(n,gr,Te=>t(3,o=Te)),Qe(n,Lu,Te=>t(16,r=Te)),Qe(n,En,Te=>t(11,a=Te)),Qe(n,Dl,Te=>t(12,u=Te));const f=new URLSearchParams(r);let c,d,m,h,g,_,y=f.get("filter")||"",S=f.get("sort")||"-@rowid",T=f.get("collectionId")||(l==null?void 0:l.id),$=0;Du(T);async function E(Te){await fn(),(l==null?void 0:l.type)==="view"?h.show(Te):m==null||m.show(Te)}function M(){t(14,T=l==null?void 0:l.id),t(0,y=""),t(1,S="-@rowid"),I({recordId:null}),L(),c==null||c.forceHide(),d==null||d.hide()}async function L(){if(!S)return;const Te=z.getAllCollectionIdentifiers(l),Ze=S.split(",").map(ot=>ot.startsWith("+")||ot.startsWith("-")?ot.substring(1):ot);Ze.filter(ot=>Te.includes(ot)).length!=Ze.length&&((l==null?void 0:l.type)!="view"?t(1,S="-@rowid"):Te.includes("created")?t(1,S="-created"):t(1,S=""))}function I(Te={}){const Ze=Object.assign({collectionId:(l==null?void 0:l.id)||"",filter:y,sort:S},Te);z.replaceHashQueryParams(Ze)}const A=()=>c==null?void 0:c.show();function P(Te){ie[Te?"unshift":"push"](()=>{_=Te,t(9,_)})}function R(Te){$=Te,t(10,$)}const N=()=>c==null?void 0:c.show(l),U=()=>{g==null||g.load(),_==null||_.reload()},j=()=>d==null?void 0:d.show(l),V=()=>m==null?void 0:m.show(),K=Te=>t(0,y=Te.detail);function J(Te){ie[Te?"unshift":"push"](()=>{g=Te,t(8,g)})}function ee(Te){y=Te,t(0,y)}function X(Te){S=Te,t(1,S)}const oe=Te=>{I({recordId:Te.detail.id});let Ze=Te.detail._partial?Te.detail.id:Te.detail;l.type==="view"?h==null||h.show(Ze):m==null||m.show(Ze)},Se=()=>{_==null||_.reload()},ke=()=>m==null?void 0:m.show();function Ce(Te){ie[Te?"unshift":"push"](()=>{c=Te,t(4,c)})}const We=()=>{g==null||g.load(),_==null||_.reload()};function st(Te){ie[Te?"unshift":"push"](()=>{d=Te,t(5,d)})}function et(Te){ie[Te?"unshift":"push"](()=>{m=Te,t(6,m)})}const Be=()=>{I({recordId:null})},rt=Te=>{y?_==null||_.reload():Te.detail.isNew&&t(10,$++,$),g==null||g.reloadLoadedPages()},Je=Te=>{(!y||g!=null&&g.hasRecord(Te.detail.id))&&t(10,$--,$),g==null||g.reloadLoadedPages()};function at(Te){ie[Te?"unshift":"push"](()=>{h=Te,t(7,h)})}const Ht=()=>{I({recordId:null})};return n.$$.update=()=>{n.$$.dirty[0]&65536&&t(15,i=new URLSearchParams(r)),n.$$.dirty[0]&49160&&!o&&i.get("collectionId")&&i.get("collectionId")!=T&&Ew(i.get("collectionId")),n.$$.dirty[0]&16388&&l!=null&&l.id&&T!=l.id&&M(),n.$$.dirty[0]&4&&l!=null&&l.id&&L(),n.$$.dirty[0]&8&&!o&&f.get("recordId")&&E(f.get("recordId")),n.$$.dirty[0]&15&&!o&&(S||y||l!=null&&l.id)&&I(),n.$$.dirty[0]&4&&Nn(cn,s=(l==null?void 0:l.name)||"Collections",s)},[y,S,l,o,c,d,m,h,g,_,$,a,u,I,T,i,r,A,P,R,N,U,j,V,K,J,ee,X,oe,Se,ke,Ce,We,st,et,Be,rt,Je,at,Ht]}class vN extends ye{constructor(e){super(),be(this,e,kN,yN,_e,{},null,[-1,-1])}}function r1(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),e.innerHTML='Sync',t=C(),i=b("a"),i.innerHTML=' Export collections',l=C(),s=b("a"),s.innerHTML=' Import collections',p(e,"class","sidebar-title"),p(i,"href","/settings/export-collections"),p(i,"class","sidebar-list-item"),p(s,"href","/settings/import-collections"),p(s,"class","sidebar-list-item")},m(a,u){v(a,e,u),v(a,t,u),v(a,i,u),v(a,l,u),v(a,s,u),o||(r=[Me(Ri.call(null,i,{path:"/settings/export-collections/?.*"})),Me(Un.call(null,i)),Me(Ri.call(null,s,{path:"/settings/import-collections/?.*"})),Me(Un.call(null,s))],o=!0)},d(a){a&&(k(e),k(t),k(i),k(l),k(s)),o=!1,De(r)}}}function wN(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h=!n[0]&&r1();return{c(){e=b("div"),t=b("div"),t.textContent="System",i=C(),l=b("a"),l.innerHTML=' Application',s=C(),o=b("a"),o.innerHTML=' Mail settings',r=C(),a=b("a"),a.innerHTML=' Files storage',u=C(),f=b("a"),f.innerHTML=' Backups',c=C(),h&&h.c(),p(t,"class","sidebar-title"),p(l,"href","/settings"),p(l,"class","sidebar-list-item"),p(o,"href","/settings/mail"),p(o,"class","sidebar-list-item"),p(a,"href","/settings/storage"),p(a,"class","sidebar-list-item"),p(f,"href","/settings/backups"),p(f,"class","sidebar-list-item"),p(e,"class","sidebar-content")},m(g,_){v(g,e,_),w(e,t),w(e,i),w(e,l),w(e,s),w(e,o),w(e,r),w(e,a),w(e,u),w(e,f),w(e,c),h&&h.m(e,null),d||(m=[Me(Ri.call(null,l,{path:"/settings"})),Me(Un.call(null,l)),Me(Ri.call(null,o,{path:"/settings/mail/?.*"})),Me(Un.call(null,o)),Me(Ri.call(null,a,{path:"/settings/storage/?.*"})),Me(Un.call(null,a)),Me(Ri.call(null,f,{path:"/settings/backups/?.*"})),Me(Un.call(null,f))],d=!0)},p(g,_){g[0]?h&&(h.d(1),h=null):h||(h=r1(),h.c(),h.m(e,null))},d(g){g&&k(e),h&&h.d(),d=!1,De(m)}}}function SN(n){let e,t;return e=new sk({props:{class:"settings-sidebar",$$slots:{default:[wN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&3&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function TN(n,e,t){let i;return Qe(n,Dl,l=>t(0,i=l)),[i]}class ps extends ye{constructor(e){super(),be(this,e,TN,SN,_e,{})}}function $N(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[8]),p(l,"for",o=n[8])},m(u,f){v(u,e,f),e.checked=n[0].batch.enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[4]),r=!0)},p(u,f){f&256&&t!==(t=u[8])&&p(e,"id",t),f&1&&(e.checked=u[0].batch.enabled),f&256&&o!==(o=u[8])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function CN(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Max allowed batch requests"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","number"),p(s,"id",o=n[8]),p(s,"min","0"),p(s,"step","1"),s.required=n[1]},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].batch.maxRequests),r||(a=B(s,"input",n[5]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&2&&(s.required=u[1]),f&1&&St(s.value)!==u[0].batch.maxRequests&&ce(s,u[0].batch.maxRequests)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function ON(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=b("span"),t.textContent="Max processing time (in seconds)",l=C(),s=b("input"),p(t,"class","txt"),p(e,"for",i=n[8]),p(s,"type","number"),p(s,"id",o=n[8]),p(s,"min","0"),p(s,"step","1"),s.required=n[1]},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].batch.timeout),r||(a=B(s,"input",n[6]),r=!0)},p(u,f){f&256&&i!==(i=u[8])&&p(e,"for",i),f&256&&o!==(o=u[8])&&p(s,"id",o),f&2&&(s.required=u[1]),f&1&&St(s.value)!==u[0].batch.timeout&&ce(s,u[0].batch.timeout)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function EN(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("label"),t=Y("Max body size (in bytes)"),l=C(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","number"),p(s,"id",o=n[8]),p(s,"min","0"),p(s,"step","1"),p(s,"placeholder","Default to 128MB"),s.value=r=n[0].batch.maxBodySize||""},m(f,c){v(f,e,c),w(e,t),v(f,l,c),v(f,s,c),a||(u=B(s,"input",n[7]),a=!0)},p(f,c){c&256&&i!==(i=f[8])&&p(e,"for",i),c&256&&o!==(o=f[8])&&p(s,"id",o),c&1&&r!==(r=f[0].batch.maxBodySize||"")&&s.value!==r&&(s.value=r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function MN(n){let e,t,i,l,s,o,r,a,u,f,c,d;return e=new fe({props:{class:"form-field form-field-toggle m-b-sm",name:"batch.enabled",$$slots:{default:[$N,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),s=new fe({props:{class:"form-field "+(n[1]?"required":""),name:"batch.maxRequests",$$slots:{default:[CN,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field "+(n[1]?"required":""),name:"batch.timeout",$$slots:{default:[ON,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),c=new fe({props:{class:"form-field",name:"batch.maxBodySize",$$slots:{default:[EN,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),i=b("div"),l=b("div"),H(s.$$.fragment),o=C(),r=b("div"),H(a.$$.fragment),u=C(),f=b("div"),H(c.$$.fragment),p(l,"class","col-lg-4"),p(r,"class","col-lg-4"),p(f,"class","col-lg-4"),p(i,"class","grid")},m(m,h){F(e,m,h),v(m,t,h),v(m,i,h),w(i,l),F(s,l,null),w(i,o),w(i,r),F(a,r,null),w(i,u),w(i,f),F(c,f,null),d=!0},p(m,h){const g={};h&769&&(g.$$scope={dirty:h,ctx:m}),e.$set(g);const _={};h&2&&(_.class="form-field "+(m[1]?"required":"")),h&771&&(_.$$scope={dirty:h,ctx:m}),s.$set(_);const y={};h&2&&(y.class="form-field "+(m[1]?"required":"")),h&771&&(y.$$scope={dirty:h,ctx:m}),a.$set(y);const S={};h&769&&(S.$$scope={dirty:h,ctx:m}),c.$set(S)},i(m){d||(O(e.$$.fragment,m),O(s.$$.fragment,m),O(a.$$.fragment,m),O(c.$$.fragment,m),d=!0)},o(m){D(e.$$.fragment,m),D(s.$$.fragment,m),D(a.$$.fragment,m),D(c.$$.fragment,m),d=!1},d(m){m&&(k(t),k(i)),q(e,m),q(s),q(a),q(c)}}}function DN(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function IN(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function a1(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function LN(n){let e,t,i,l,s,o;function r(c,d){return c[1]?IN:DN}let a=r(n),u=a(n),f=n[2]&&a1();return{c(){e=b("div"),e.innerHTML=' Batch API',t=C(),i=b("div"),l=C(),u.c(),s=C(),f&&f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),u.m(c,d),v(c,s,d),f&&f.m(c,d),v(c,o,d)},p(c,d){a!==(a=r(c))&&(u.d(1),u=a(c),u&&(u.c(),u.m(s.parentNode,s))),c[2]?f?d&4&&O(f,1):(f=a1(),f.c(),O(f,1),f.m(o.parentNode,o)):f&&(re(),D(f,1,1,()=>{f=null}),ae())},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),u.d(c),f&&f.d(c)}}}function AN(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[LN],default:[MN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&519&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function PN(n,e,t){let i,l,s;Qe(n,Sn,c=>t(3,s=c));let{formSettings:o}=e;function r(){o.batch.enabled=this.checked,t(0,o)}function a(){o.batch.maxRequests=St(this.value),t(0,o)}function u(){o.batch.timeout=St(this.value),t(0,o)}const f=c=>t(0,o.batch.maxBodySize=c.target.value<<0,o);return n.$$set=c=>{"formSettings"in c&&t(0,o=c.formSettings)},n.$$.update=()=>{var c;n.$$.dirty&8&&t(2,i=!z.isEmpty(s==null?void 0:s.batch)),n.$$.dirty&1&&t(1,l=!!((c=o.batch)!=null&&c.enabled))},[o,l,i,s,r,a,u,f]}class NN extends ye{constructor(e){super(),be(this,e,PN,AN,_e,{formSettings:0})}}function u1(n,e,t){const i=n.slice();return i[17]=e[t],i}function f1(n){let e,t=n[17]+"",i,l,s,o;function r(){return n[13](n[17])}return{c(){e=b("button"),i=Y(t),l=Y(" "),p(e,"type","button"),p(e,"class","label label-sm link-primary txt-mono")},m(a,u){v(a,e,u),w(e,i),v(a,l,u),s||(o=B(e,"click",r),s=!0)},p(a,u){n=a,u&4&&t!==(t=n[17]+"")&&ue(i,t)},d(a){a&&(k(e),k(l)),s=!1,o()}}}function RN(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;function y(E){n[11](E)}let S={id:n[16],placeholder:"Leave empty to disable"};n[0].trustedProxy.headers!==void 0&&(S.value=n[0].trustedProxy.headers),s=new _o({props:S}),ie.push(()=>ve(s,"value",y));let T=pe(n[2]),$=[];for(let E=0;Eo=!1)),s.$set(L),(!h||M&1)&&x(u,"hidden",z.isEmpty(E[0].trustedProxy.headers)),M&68){T=pe(E[2]);let I;for(I=0;Ive(r,"keyOfSelected",d)),{c(){e=b("label"),t=b("span"),t.textContent="IP priority selection",i=C(),l=b("i"),o=C(),H(r.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[16])},m(h,g){v(h,e,g),w(e,t),w(e,i),w(e,l),v(h,o,g),F(r,h,g),u=!0,f||(c=Me(He.call(null,l,{text:"This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.",position:"right"})),f=!0)},p(h,g){(!u||g&65536&&s!==(s=h[16]))&&p(e,"for",s);const _={};!a&&g&1&&(a=!0,_.keyOfSelected=h[0].trustedProxy.useLeftmostIP,$e(()=>a=!1)),r.$set(_)},i(h){u||(O(r.$$.fragment,h),u=!0)},o(h){D(r.$$.fragment,h),u=!1},d(h){h&&(k(e),k(o)),q(r,h),f=!1,c()}}}function qN(n){let e,t,i,l,s,o,r=(n[1].realIP||"N/A")+"",a,u,f,c,d,m,h,g,_,y,S=(n[1].possibleProxyHeader||"N/A")+"",T,$,E,M,L,I,A,P,R,N,U,j,V;return A=new fe({props:{class:"form-field m-b-0",name:"trustedProxy.headers",$$slots:{default:[RN,({uniqueId:K})=>({16:K}),({uniqueId:K})=>K?65536:0]},$$scope:{ctx:n}}}),N=new fe({props:{class:"form-field m-0",name:"trustedProxy.useLeftmostIP",$$slots:{default:[FN,({uniqueId:K})=>({16:K}),({uniqueId:K})=>K?65536:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),l=b("span"),l.textContent="Resolved user IP:",s=C(),o=b("strong"),a=Y(r),u=C(),f=b("i"),c=C(),d=b("br"),m=C(),h=b("div"),g=b("span"),g.textContent="Detected proxy header:",_=C(),y=b("strong"),T=Y(S),$=C(),E=b("div"),E.innerHTML=`

    When PocketBase is deployed on platforms like Fly or it is accessible through proxies such as + NGINX, requests from different users will originate from the same IP address (the IP of the proxy + connecting to your PocketBase app).

    In this case to retrieve the actual user IP (used for rate limiting, logging, etc.) you need to + properly configure your proxy and list below the trusted headers that PocketBase could use to + extract the user IP.

    When using such proxy, to avoid spoofing it is recommended to:

    • use headers that are controlled only by the proxy and cannot be manually set by the users
    • make sure that the PocketBase server can be accessed only through the proxy

    You can clear the headers field if PocketBase is not deployed behind a proxy.

    `,M=C(),L=b("div"),I=b("div"),H(A.$$.fragment),P=C(),R=b("div"),H(N.$$.fragment),p(f,"class","ri-information-line txt-sm link-hint"),p(i,"class","inline-flex flex-gap-5"),p(h,"class","inline-flex flex-gap-5"),p(t,"class","content"),p(e,"class","alert alert-info m-b-sm"),p(E,"class","content m-b-sm"),p(I,"class","col-lg-9"),p(R,"class","col-lg-3"),p(L,"class","grid grid-sm")},m(K,J){v(K,e,J),w(e,t),w(t,i),w(i,l),w(i,s),w(i,o),w(o,a),w(i,u),w(i,f),w(t,c),w(t,d),w(t,m),w(t,h),w(h,g),w(h,_),w(h,y),w(y,T),v(K,$,J),v(K,E,J),v(K,M,J),v(K,L,J),w(L,I),F(A,I,null),w(L,P),w(L,R),F(N,R,null),U=!0,j||(V=Me(He.call(null,f,`Must show your actual IP. +If not, set the correct proxy header.`)),j=!0)},p(K,J){(!U||J&2)&&r!==(r=(K[1].realIP||"N/A")+"")&&ue(a,r),(!U||J&2)&&S!==(S=(K[1].possibleProxyHeader||"N/A")+"")&&ue(T,S);const ee={};J&1114117&&(ee.$$scope={dirty:J,ctx:K}),A.$set(ee);const X={};J&1114113&&(X.$$scope={dirty:J,ctx:K}),N.$set(X)},i(K){U||(O(A.$$.fragment,K),O(N.$$.fragment,K),U=!0)},o(K){D(A.$$.fragment,K),D(N.$$.fragment,K),U=!1},d(K){K&&(k(e),k($),k(E),k(M),k(L)),q(A),q(N),j=!1,V()}}}function HN(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-alert-line txt-sm txt-hint")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,"The configured proxy header doesn't match with the detected one.")),t=!0)},d(l){l&&k(e),t=!1,i()}}}function jN(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-alert-line txt-sm txt-warning")},m(l,s){v(l,e,s),t||(i=Me(He.call(null,e,`Detected proxy header. +It is recommend to list it as trusted.`)),t=!0)},d(l){l&&k(e),t=!1,i()}}}function zN(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function UN(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function c1(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function VN(n){let e,t,i,l,s,o,r,a,u,f,c;function d(T,$){if($&43&&(o=null),!T[3]&&T[1].possibleProxyHeader)return jN;if(o==null&&(o=!!(T[3]&&!T[5]&&!T[0].trustedProxy.headers.includes(T[1].possibleProxyHeader))),o)return HN}let m=d(n,-1),h=m&&m(n);function g(T,$){return T[3]?UN:zN}let _=g(n),y=_(n),S=n[4]&&c1();return{c(){e=b("div"),t=b("i"),i=C(),l=b("span"),l.textContent="User IP proxy headers",s=C(),h&&h.c(),r=C(),a=b("div"),u=C(),y.c(),f=C(),S&&S.c(),c=ge(),p(t,"class","ri-route-line"),p(l,"class","txt"),p(e,"class","inline-flex"),p(a,"class","flex-fill")},m(T,$){v(T,e,$),w(e,t),w(e,i),w(e,l),w(e,s),h&&h.m(e,null),v(T,r,$),v(T,a,$),v(T,u,$),y.m(T,$),v(T,f,$),S&&S.m(T,$),v(T,c,$)},p(T,$){m!==(m=d(T,$))&&(h&&h.d(1),h=m&&m(T),h&&(h.c(),h.m(e,null))),_!==(_=g(T))&&(y.d(1),y=_(T),y&&(y.c(),y.m(f.parentNode,f))),T[4]?S?$&16&&O(S,1):(S=c1(),S.c(),O(S,1),S.m(c.parentNode,c)):S&&(re(),D(S,1,1,()=>{S=null}),ae())},d(T){T&&(k(e),k(r),k(a),k(u),k(f),k(c)),h&&h.d(),y.d(T),S&&S.d(T)}}}function BN(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[VN],default:[qN]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&1048639&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function WN(n,e,t){let i,l,s,o,r,a;Qe(n,Sn,T=>t(10,a=T));const u=["X-Forward-For","Fly-Client-IP","CF-Connecting-IP"];let{formSettings:f}=e,{healthData:c}=e,d="";function m(T){t(0,f.trustedProxy.headers=[T],f)}const h=[{label:"Use leftmost IP",value:!0},{label:"Use rightmost IP",value:!1}];function g(T){n.$$.not_equal(f.trustedProxy.headers,T)&&(f.trustedProxy.headers=T,t(0,f))}const _=()=>t(0,f.trustedProxy.headers=[],f),y=T=>m(T);function S(T){n.$$.not_equal(f.trustedProxy.useLeftmostIP,T)&&(f.trustedProxy.useLeftmostIP=T,t(0,f))}return n.$$set=T=>{"formSettings"in T&&t(0,f=T.formSettings),"healthData"in T&&t(1,c=T.healthData)},n.$$.update=()=>{n.$$.dirty&1&&t(9,i=JSON.stringify(f)),n.$$.dirty&768&&d!=i&&t(8,d=i),n.$$.dirty&768&&t(5,l=d!=i),n.$$.dirty&1024&&t(4,s=!z.isEmpty(a==null?void 0:a.trustedProxy)),n.$$.dirty&1&&t(3,o=!z.isEmpty(f.trustedProxy.headers)),n.$$.dirty&2&&t(2,r=c.possibleProxyHeader?[c.possibleProxyHeader].concat(u.filter(T=>T!=c.possibleProxyHeader)):u)},[f,c,r,o,s,l,m,h,d,i,a,g,_,y,S]}class YN extends ye{constructor(e){super(),be(this,e,WN,BN,_e,{formSettings:0,healthData:1})}}function d1(n,e,t){const i=n.slice();return i[5]=e[t],i}function p1(n){let e,t=(n[5].label||"")+"",i,l;return{c(){e=b("option"),i=Y(t),e.__value=l=n[5].value,ce(e,e.__value)},m(s,o){v(s,e,o),w(e,i)},p(s,o){o&2&&t!==(t=(s[5].label||"")+"")&&ue(i,t),o&2&&l!==(l=s[5].value)&&(e.__value=l,ce(e,e.__value))},d(s){s&&k(e)}}}function KN(n){let e,t,i,l,s,o,r=[{type:t=n[3].type||"text"},{list:n[2]},{value:n[0]},n[3]],a={};for(let c=0;c{t(0,s=u.target.value)};return n.$$set=u=>{e=je(je({},e),Ut(u)),t(3,l=lt(e,i)),"value"in u&&t(0,s=u.value),"options"in u&&t(1,o=u.options)},[s,o,r,l,a]}class ZN extends ye{constructor(e){super(),be(this,e,JN,KN,_e,{value:0,options:1})}}function m1(n,e,t){const i=n.slice();return i[15]=e[t],i[16]=e,i[17]=t,i}function GN(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[18]),p(l,"for",o=n[18])},m(u,f){v(u,e,f),e.checked=n[0].rateLimits.enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[6]),r=!0)},p(u,f){f&262144&&t!==(t=u[18])&&p(e,"id",t),f&1&&(e.checked=u[0].rateLimits.enabled),f&262144&&o!==(o=u[18])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function h1(n){let e,t,i,l,s,o=pe(n[0].rateLimits.rules||[]),r=[];for(let u=0;uD(r[u],1,1,()=>{r[u]=null});return{c(){e=b("table"),t=b("thead"),t.innerHTML="Rate limit label Max requests (per IP) Interval (in seconds) ",i=C(),l=b("tbody");for(let u=0;uve(e,"value",l)),{c(){H(e.$$.fragment)},m(o,r){F(e,o,r),i=!0},p(o,r){n=o;const a={};r&4&&(a.options=n[2]),!t&&r&1&&(t=!0,a.value=n[15].label,$e(()=>t=!1)),e.$set(a)},i(o){i||(O(e.$$.fragment,o),i=!0)},o(o){D(e.$$.fragment,o),i=!1},d(o){q(e,o)}}}function QN(n){let e,t,i;function l(){n[8].call(e,n[16],n[17])}return{c(){e=b("input"),p(e,"type","number"),e.required=!0,p(e,"placeholder","Max requests*"),p(e,"min","1"),p(e,"step","1")},m(s,o){v(s,e,o),ce(e,n[15].maxRequests),t||(i=B(e,"input",l),t=!0)},p(s,o){n=s,o&1&&St(e.value)!==n[15].maxRequests&&ce(e,n[15].maxRequests)},d(s){s&&k(e),t=!1,i()}}}function xN(n){let e,t,i;function l(){n[9].call(e,n[16],n[17])}return{c(){e=b("input"),p(e,"type","number"),e.required=!0,p(e,"placeholder","Interval*"),p(e,"min","1"),p(e,"step","1")},m(s,o){v(s,e,o),ce(e,n[15].duration),t||(i=B(e,"input",l),t=!0)},p(s,o){n=s,o&1&&St(e.value)!==n[15].duration&&ce(e,n[15].duration)},d(s){s&&k(e),t=!1,i()}}}function _1(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;i=new fe({props:{class:"form-field",name:"rateLimits.rules."+n[17]+".label",inlineError:!0,$$slots:{default:[XN]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"rateLimits.rules."+n[17]+".maxRequests",inlineError:!0,$$slots:{default:[QN]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field",name:"rateLimits.rules."+n[17]+".duration",inlineError:!0,$$slots:{default:[xN]},$$scope:{ctx:n}}});function y(){return n[10](n[17])}return{c(){e=b("tr"),t=b("td"),H(i.$$.fragment),l=C(),s=b("td"),H(o.$$.fragment),r=C(),a=b("td"),H(u.$$.fragment),f=C(),c=b("td"),d=b("button"),d.innerHTML='',m=C(),p(t,"class","col-tag"),p(s,"class","col-requests"),p(a,"class","col-burst"),p(d,"type","button"),p(d,"title","Remove rule"),p(d,"aria-label","Remove rule"),p(d,"class","btn btn-xs btn-circle btn-hint btn-transparent"),p(c,"class","col-action"),p(e,"class","rate-limit-row")},m(S,T){v(S,e,T),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),F(u,a,null),w(e,f),w(e,c),w(c,d),w(e,m),h=!0,g||(_=B(d,"click",y),g=!0)},p(S,T){n=S;const $={};T&524293&&($.$$scope={dirty:T,ctx:n}),i.$set($);const E={};T&524289&&(E.$$scope={dirty:T,ctx:n}),o.$set(E);const M={};T&524289&&(M.$$scope={dirty:T,ctx:n}),u.$set(M)},i(S){h||(O(i.$$.fragment,S),O(o.$$.fragment,S),O(u.$$.fragment,S),h=!0)},o(S){D(i.$$.fragment,S),D(o.$$.fragment,S),D(u.$$.fragment,S),h=!1},d(S){S&&k(e),q(i),q(o),q(u),g=!1,_()}}}function e7(n){let e,t,i=!z.isEmpty(n[0].rateLimits.rules),l,s,o,r,a,u,f,c;e=new fe({props:{class:"form-field form-field-toggle m-b-xs",name:"rateLimits.enabled",$$slots:{default:[GN,({uniqueId:m})=>({18:m}),({uniqueId:m})=>m?262144:0]},$$scope:{ctx:n}}});let d=i&&h1(n);return{c(){var m,h,g;H(e.$$.fragment),t=C(),d&&d.c(),l=C(),s=b("div"),o=b("button"),o.innerHTML=' Add rate limit rule',r=C(),a=b("a"),a.innerHTML="Learn more about the rate limit rules",p(o,"type","button"),p(o,"class","btn btn-sm btn-secondary m-r-auto"),x(o,"btn-danger",(g=(h=(m=n[1])==null?void 0:m.rateLimits)==null?void 0:h.rules)==null?void 0:g.message),p(a,"href","https://pocketbase.io/docs/@todo"),p(a,"class","txt-nowrap txt-sm link-hint"),p(a,"target","_blank"),p(a,"rel","noopener noreferrer"),p(s,"class","flex m-t-sm")},m(m,h){F(e,m,h),v(m,t,h),d&&d.m(m,h),v(m,l,h),v(m,s,h),w(s,o),w(s,r),w(s,a),u=!0,f||(c=B(o,"click",n[11]),f=!0)},p(m,h){var _,y,S;const g={};h&786433&&(g.$$scope={dirty:h,ctx:m}),e.$set(g),h&1&&(i=!z.isEmpty(m[0].rateLimits.rules)),i?d?(d.p(m,h),h&1&&O(d,1)):(d=h1(m),d.c(),O(d,1),d.m(l.parentNode,l)):d&&(re(),D(d,1,1,()=>{d=null}),ae()),(!u||h&2)&&x(o,"btn-danger",(S=(y=(_=m[1])==null?void 0:_.rateLimits)==null?void 0:y.rules)==null?void 0:S.message)},i(m){u||(O(e.$$.fragment,m),O(d),u=!0)},o(m){D(e.$$.fragment,m),D(d),u=!1},d(m){m&&(k(t),k(l),k(s)),q(e,m),d&&d.d(m),f=!1,c()}}}function g1(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){v(o,e,r),i=!0,l||(s=Me(He.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&nt(()=>{i&&(t||(t=ze(e,Mt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=ze(e,Mt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&k(e),o&&t&&t.end(),l=!1,s()}}}function t7(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function n7(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function i7(n){let e,t,i,l,s,o,r=n[3]&&g1();function a(c,d){return c[0].rateLimits.enabled?n7:t7}let u=a(n),f=u(n);return{c(){e=b("div"),e.innerHTML=' Rate limiting',t=C(),i=b("div"),l=C(),r&&r.c(),s=C(),f.c(),o=ge(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){v(c,e,d),v(c,t,d),v(c,i,d),v(c,l,d),r&&r.m(c,d),v(c,s,d),f.m(c,d),v(c,o,d)},p(c,d){c[3]?r?d&8&&O(r,1):(r=g1(),r.c(),O(r,1),r.m(s.parentNode,s)):r&&(re(),D(r,1,1,()=>{r=null}),ae()),u!==(u=a(c))&&(f.d(1),f=u(c),f&&(f.c(),f.m(o.parentNode,o)))},d(c){c&&(k(e),k(t),k(i),k(l),k(s),k(o)),r&&r.d(c),f.d(c)}}}function l7(n){let e,t;return e=new qi({props:{single:!0,$$slots:{header:[i7],default:[e7]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&524303&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function s7(n,e,t){let i,l,s;Qe(n,En,S=>t(12,l=S)),Qe(n,Sn,S=>t(1,s=S));let{formSettings:o}=e;const r=[{value:"*:list"},{value:"*:view"},{value:"*:create"},{value:"*:update"},{value:"*:delete"},{value:"*:file"},{value:"*:listAuthMethods"},{value:"*:authRefresh"},{value:"*:auth"},{value:"*:authWithPassword"},{value:"*:authWithOAuth2"},{value:"*:authWithOTP"},{value:"*:requestOTP"},{value:"*:requestPasswordReset"},{value:"*:confirmPasswordReset"},{value:"*:requestVerification"},{value:"*:confirmVerification"},{value:"*:requestEmailChange"},{value:"*:confirmEmailChange"}];let a=r;u();async function u(){await Du(),t(2,a=[]);for(let S of l)S.system||(a.push({value:S.name+":list"}),a.push({value:S.name+":view"}),S.type!="view"&&(a.push({value:S.name+":create"}),a.push({value:S.name+":update"}),a.push({value:S.name+":delete"})),S.type=="auth"&&(a.push({value:S.name+":listAuthMethods"}),a.push({value:S.name+":authRefresh"}),a.push({value:S.name+":auth"}),a.push({value:S.name+":authWithPassword"}),a.push({value:S.name+":authWithOAuth2"}),a.push({value:S.name+":authWithOTP"}),a.push({value:S.name+":requestOTP"}),a.push({value:S.name+":requestPasswordReset"}),a.push({value:S.name+":confirmPasswordReset"}),a.push({value:S.name+":requestVerification"}),a.push({value:S.name+":confirmVerification"}),a.push({value:S.name+":requestEmailChange"}),a.push({value:S.name+":confirmEmailChange"})),S.fields.find(T=>T.type=="file")&&a.push({value:S.name+":file"}));t(2,a=a.concat(r))}function f(){Wt({}),Array.isArray(o.rateLimits.rules)||t(0,o.rateLimits.rules=[],o),o.rateLimits.rules.push({label:"",maxRequests:300,duration:10}),t(0,o),o.rateLimits.rules.length==1&&t(0,o.rateLimits.enabled=!0,o)}function c(S){Wt({}),o.rateLimits.rules.splice(S,1),t(0,o),o.rateLimits.rules.length||t(0,o.rateLimits.enabled=!1,o)}function d(){o.rateLimits.enabled=this.checked,t(0,o)}function m(S,T){n.$$.not_equal(T.label,S)&&(T.label=S,t(0,o))}function h(S,T){S[T].maxRequests=St(this.value),t(0,o)}function g(S,T){S[T].duration=St(this.value),t(0,o)}const _=S=>c(S),y=()=>f();return n.$$set=S=>{"formSettings"in S&&t(0,o=S.formSettings)},n.$$.update=()=>{n.$$.dirty&2&&t(3,i=!z.isEmpty(s==null?void 0:s.rateLimits))},[o,s,a,i,f,c,d,m,h,g,_,y]}class o7 extends ye{constructor(e){super(),be(this,e,s7,l7,_e,{formSettings:0})}}function r7(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P,R,N,U,j,V;i=new fe({props:{class:"form-field required",name:"meta.appName",$$slots:{default:[u7,({uniqueId:Ce})=>({23:Ce}),({uniqueId:Ce})=>Ce?8388608:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field required",name:"meta.appURL",$$slots:{default:[f7,({uniqueId:Ce})=>({23:Ce}),({uniqueId:Ce})=>Ce?8388608:0]},$$scope:{ctx:n}}});function K(Ce){n[11](Ce)}let J={healthData:n[3]};n[0]!==void 0&&(J.formSettings=n[0]),f=new YN({props:J}),ie.push(()=>ve(f,"formSettings",K));function ee(Ce){n[12](Ce)}let X={};n[0]!==void 0&&(X.formSettings=n[0]),m=new o7({props:X}),ie.push(()=>ve(m,"formSettings",ee));function oe(Ce){n[13](Ce)}let Se={};n[0]!==void 0&&(Se.formSettings=n[0]),_=new NN({props:Se}),ie.push(()=>ve(_,"formSettings",oe)),$=new fe({props:{class:"form-field form-field-toggle m-0",name:"meta.hideControls",$$slots:{default:[c7,({uniqueId:Ce})=>({23:Ce}),({uniqueId:Ce})=>Ce?8388608:0]},$$scope:{ctx:n}}});let ke=n[4]&&b1(n);return{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),u=b("div"),H(f.$$.fragment),d=C(),H(m.$$.fragment),g=C(),H(_.$$.fragment),S=C(),T=b("div"),H($.$$.fragment),E=C(),M=b("div"),L=b("div"),I=C(),ke&&ke.c(),A=C(),P=b("button"),R=b("span"),R.textContent="Save changes",p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(u,"class","accordions"),p(a,"class","col-lg-12"),p(T,"class","col-lg-12"),p(e,"class","grid"),p(L,"class","flex-fill"),p(R,"class","txt"),p(P,"type","submit"),p(P,"class","btn btn-expanded"),P.disabled=N=!n[4]||n[2],x(P,"btn-loading",n[2]),p(M,"class","flex m-t-base")},m(Ce,We){v(Ce,e,We),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),w(a,u),F(f,u,null),w(u,d),F(m,u,null),w(u,g),F(_,u,null),w(e,S),w(e,T),F($,T,null),v(Ce,E,We),v(Ce,M,We),w(M,L),w(M,I),ke&&ke.m(M,null),w(M,A),w(M,P),w(P,R),U=!0,j||(V=B(P,"click",n[16]),j=!0)},p(Ce,We){const st={};We&25165825&&(st.$$scope={dirty:We,ctx:Ce}),i.$set(st);const et={};We&25165825&&(et.$$scope={dirty:We,ctx:Ce}),o.$set(et);const Be={};We&8&&(Be.healthData=Ce[3]),!c&&We&1&&(c=!0,Be.formSettings=Ce[0],$e(()=>c=!1)),f.$set(Be);const rt={};!h&&We&1&&(h=!0,rt.formSettings=Ce[0],$e(()=>h=!1)),m.$set(rt);const Je={};!y&&We&1&&(y=!0,Je.formSettings=Ce[0],$e(()=>y=!1)),_.$set(Je);const at={};We&25165825&&(at.$$scope={dirty:We,ctx:Ce}),$.$set(at),Ce[4]?ke?ke.p(Ce,We):(ke=b1(Ce),ke.c(),ke.m(M,A)):ke&&(ke.d(1),ke=null),(!U||We&20&&N!==(N=!Ce[4]||Ce[2]))&&(P.disabled=N),(!U||We&4)&&x(P,"btn-loading",Ce[2])},i(Ce){U||(O(i.$$.fragment,Ce),O(o.$$.fragment,Ce),O(f.$$.fragment,Ce),O(m.$$.fragment,Ce),O(_.$$.fragment,Ce),O($.$$.fragment,Ce),U=!0)},o(Ce){D(i.$$.fragment,Ce),D(o.$$.fragment,Ce),D(f.$$.fragment,Ce),D(m.$$.fragment,Ce),D(_.$$.fragment,Ce),D($.$$.fragment,Ce),U=!1},d(Ce){Ce&&(k(e),k(E),k(M)),q(i),q(o),q(f),q(m),q(_),q($),ke&&ke.d(),j=!1,V()}}}function a7(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function u7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Application name"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].meta.appName),r||(a=B(s,"input",n[9]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].meta.appName&&ce(s,u[0].meta.appName)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function f7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Application URL"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].meta.appURL),r||(a=B(s,"input",n[10]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].meta.appURL&&ce(s,u[0].meta.appURL)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function c7(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Hide collection create and edit controls",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[23]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[23])},m(c,d){v(c,e,d),e.checked=n[0].meta.hideControls,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[14]),Me(He.call(null,r,{text:"This could prevent making accidental schema changes when in production environment.",position:"right"}))],u=!0)},p(c,d){d&8388608&&t!==(t=c[23])&&p(e,"id",t),d&1&&(e.checked=c[0].meta.hideControls),d&8388608&&a!==(a=c[23])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function b1(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[2]},m(s,o){v(s,e,o),w(e,t),i||(l=B(e,"click",n[15]),i=!0)},p(s,o){o&4&&(e.disabled=s[2])},d(s){s&&k(e),i=!1,l()}}}function d7(n){let e,t,i,l,s,o,r,a,u;const f=[a7,r7],c=[];function d(m,h){return m[1]?0:1}return s=d(n),o=c[s]=f[s](n),{c(){e=b("header"),e.innerHTML='',t=C(),i=b("div"),l=b("form"),o.c(),p(e,"class","page-header"),p(l,"class","panel"),p(l,"autocomplete","off"),p(i,"class","wrapper")},m(m,h){v(m,e,h),v(m,t,h),v(m,i,h),w(i,l),c[s].m(l,null),r=!0,a||(u=B(l,"submit",tt(n[5])),a=!0)},p(m,h){let g=s;s=d(m),s===g?c[s].p(m,h):(re(),D(c[g],1,1,()=>{c[g]=null}),ae(),o=c[s],o?o.p(m,h):(o=c[s]=f[s](m),o.c()),O(o,1),o.m(l,null))},i(m){r||(O(o),r=!0)},o(m){D(o),r=!1},d(m){m&&(k(e),k(t),k(i)),c[s].d(),a=!1,u()}}}function p7(n){let e,t,i,l;return e=new ps({}),i=new di({props:{$$slots:{default:[d7]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,[o]){const r={};o&16777247&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function m7(n,e,t){let i,l,s,o;Qe(n,Dl,P=>t(17,l=P)),Qe(n,_r,P=>t(18,s=P)),Qe(n,cn,P=>t(19,o=P)),Nn(cn,o="Application settings",o);let r={},a={},u=!1,f=!1,c="",d={};h();async function m(){var P;try{t(3,d=((P=await me.health.check()||{})==null?void 0:P.data)||{})}catch(R){console.warn("Health check failed:",R)}}async function h(){t(1,u=!0);try{const P=await me.settings.getAll()||{};_(P),await m()}catch(P){me.error(P)}t(1,u=!1)}async function g(){if(!(f||!i)){t(2,f=!0);try{const P=await me.settings.update(z.filterRedactedProps(a));_(P),await m(),tn("Successfully saved application settings.")}catch(P){me.error(P)}t(2,f=!1)}}function _(P={}){var R,N;Nn(_r,s=(R=P==null?void 0:P.meta)==null?void 0:R.appName,s),Nn(Dl,l=!!((N=P==null?void 0:P.meta)!=null&&N.hideControls),l),t(0,a={meta:(P==null?void 0:P.meta)||{},batch:P.batch||{},trustedProxy:P.trustedProxy||{headers:[]},rateLimits:P.rateLimits||{tags:[]}}),t(7,r=JSON.parse(JSON.stringify(a)))}function y(){t(0,a=JSON.parse(JSON.stringify(r||{})))}function S(){a.meta.appName=this.value,t(0,a)}function T(){a.meta.appURL=this.value,t(0,a)}function $(P){a=P,t(0,a)}function E(P){a=P,t(0,a)}function M(P){a=P,t(0,a)}function L(){a.meta.hideControls=this.checked,t(0,a)}const I=()=>y(),A=()=>g();return n.$$.update=()=>{n.$$.dirty&128&&t(8,c=JSON.stringify(r)),n.$$.dirty&257&&t(4,i=c!=JSON.stringify(a))},[a,u,f,d,i,g,y,r,c,S,T,$,E,M,L,I,A]}class h7 extends ye{constructor(e){super(),be(this,e,m7,p7,_e,{})}}function _7(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=Y("Backup name"),l=C(),s=b("input"),r=C(),a=b("em"),a.textContent="Must be in the format [a-z0-9_-].zip",p(e,"for",i=n[15]),p(s,"type","text"),p(s,"id",o=n[15]),p(s,"placeholder","Leave empty to autogenerate"),p(s,"pattern","^[a-z0-9_-]+\\.zip$"),p(a,"class","help-block")},m(c,d){v(c,e,d),w(e,t),v(c,l,d),v(c,s,d),ce(s,n[2]),v(c,r,d),v(c,a,d),u||(f=B(s,"input",n[7]),u=!0)},p(c,d){d&32768&&i!==(i=c[15])&&p(e,"for",i),d&32768&&o!==(o=c[15])&&p(s,"id",o),d&4&&s.value!==c[2]&&ce(s,c[2])},d(c){c&&(k(e),k(l),k(s),k(r),k(a)),u=!1,f()}}}function g7(n){let e,t,i,l,s,o,r;return l=new fe({props:{class:"form-field m-0",name:"name",$$slots:{default:[_7,({uniqueId:a})=>({15:a}),({uniqueId:a})=>a?32768:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.innerHTML=`

    Please note that during the backup other concurrent write requests may fail since the + database will be temporary "locked" (this usually happens only during the ZIP generation).

    If you are using S3 storage for the collections file upload, you'll have to backup them + separately since they are not locally stored and will not be included in the final backup!

    `,t=C(),i=b("form"),H(l.$$.fragment),p(e,"class","alert alert-info"),p(i,"id",n[4]),p(i,"autocomplete","off")},m(a,u){v(a,e,u),v(a,t,u),v(a,i,u),F(l,i,null),s=!0,o||(r=B(i,"submit",tt(n[5])),o=!0)},p(a,u){const f={};u&98308&&(f.$$scope={dirty:u,ctx:a}),l.$set(f)},i(a){s||(O(l.$$.fragment,a),s=!0)},o(a){D(l.$$.fragment,a),s=!1},d(a){a&&(k(e),k(t),k(i)),q(l),o=!1,r()}}}function b7(n){let e;return{c(){e=b("h4"),e.textContent="Initialize new backup",p(e,"class","center txt-break")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function y7(n){let e,t,i,l,s,o,r;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=C(),l=b("button"),s=b("span"),s.textContent="Start backup",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[4]),p(l,"class","btn btn-expanded"),l.disabled=n[3],x(l,"btn-loading",n[3])},m(a,u){v(a,e,u),w(e,t),v(a,i,u),v(a,l,u),w(l,s),o||(r=B(e,"click",n[0]),o=!0)},p(a,u){u&8&&(e.disabled=a[3]),u&8&&(l.disabled=a[3]),u&8&&x(l,"btn-loading",a[3])},d(a){a&&(k(e),k(i),k(l)),o=!1,r()}}}function k7(n){let e,t,i={class:"backup-create-panel",beforeOpen:n[8],beforeHide:n[9],popup:!0,$$slots:{footer:[y7],header:[b7],default:[g7]},$$scope:{ctx:n}};return e=new ln({props:i}),n[10](e),e.$on("show",n[11]),e.$on("hide",n[12]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&8&&(o.beforeOpen=l[8]),s&8&&(o.beforeHide=l[9]),s&65548&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[10](null),q(e,l)}}}function v7(n,e,t){const i=_t(),l="backup_create_"+z.randomString(5);let s,o="",r=!1,a;function u(S){Wt({}),t(3,r=!1),t(2,o=S||""),s==null||s.show()}function f(){return s==null?void 0:s.hide()}async function c(){if(!r){t(3,r=!0),clearTimeout(a),a=setTimeout(()=>{f()},1500);try{await me.backups.create(o,{$cancelKey:l}),t(3,r=!1),f(),i("submit"),tn("Successfully generated new backup.")}catch(S){S.isAbort||me.error(S)}clearTimeout(a),t(3,r=!1)}}so(()=>{clearTimeout(a)});function d(){o=this.value,t(2,o)}const m=()=>r?(Ys("A backup has already been started, please wait."),!1):!0,h=()=>(r&&Ys("The backup was started but may take a while to complete. You can come back later.",4500),!0);function g(S){ie[S?"unshift":"push"](()=>{s=S,t(1,s)})}function _(S){Pe.call(this,n,S)}function y(S){Pe.call(this,n,S)}return[f,s,o,r,l,c,u,d,m,h,g,_,y]}class w7 extends ye{constructor(e){super(),be(this,e,v7,k7,_e,{show:6,hide:0})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[0]}}function S7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Backup name"),l=C(),s=b("input"),p(e,"for",i=n[15]),p(s,"type","text"),p(s,"id",o=n[15]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[2]),r||(a=B(s,"input",n[9]),r=!0)},p(u,f){f&32768&&i!==(i=u[15])&&p(e,"for",i),f&32768&&o!==(o=u[15])&&p(s,"id",o),f&4&&s.value!==u[2]&&ce(s,u[2])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function T7(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;return u=new ai({props:{value:n[1]}}),m=new fe({props:{class:"form-field required m-0",name:"name",$$slots:{default:[S7,({uniqueId:y})=>({15:y}),({uniqueId:y})=>y?32768:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.innerHTML=`

    Please proceed with caution and use it only with trusted backups!

    Backup restore is experimental and works only on UNIX based systems.

    The restore operation will attempt to replace your existing pb_data with the one from + the backup and will restart the application process.

    This means that on success all of your data (including app settings, users, superusers, etc.) will + be replaced with the ones from the backup.

    Nothing will happen if the backup is invalid or incompatible (ex. missing + data.db file).

    `,t=C(),i=b("div"),l=Y(`Type the backup name + `),s=b("div"),o=b("span"),r=Y(n[1]),a=C(),H(u.$$.fragment),f=Y(` + to confirm:`),c=C(),d=b("form"),H(m.$$.fragment),p(e,"class","alert alert-danger"),p(o,"class","txt"),p(s,"class","label"),p(i,"class","content m-b-xs"),p(d,"id",n[6]),p(d,"autocomplete","off")},m(y,S){v(y,e,S),v(y,t,S),v(y,i,S),w(i,l),w(i,s),w(s,o),w(o,r),w(s,a),F(u,s,null),w(i,f),v(y,c,S),v(y,d,S),F(m,d,null),h=!0,g||(_=B(d,"submit",tt(n[7])),g=!0)},p(y,S){(!h||S&2)&&ue(r,y[1]);const T={};S&2&&(T.value=y[1]),u.$set(T);const $={};S&98308&&($.$$scope={dirty:S,ctx:y}),m.$set($)},i(y){h||(O(u.$$.fragment,y),O(m.$$.fragment,y),h=!0)},o(y){D(u.$$.fragment,y),D(m.$$.fragment,y),h=!1},d(y){y&&(k(e),k(t),k(i),k(c),k(d)),q(u),q(m),g=!1,_()}}}function $7(n){let e,t,i,l;return{c(){e=b("h4"),t=Y("Restore "),i=b("strong"),l=Y(n[1]),p(e,"class","popup-title txt-ellipsis svelte-1fcgldh")},m(s,o){v(s,e,o),w(e,t),w(e,i),w(i,l)},p(s,o){o&2&&ue(l,s[1])},d(s){s&&k(e)}}}function C7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=Y("Cancel"),i=C(),l=b("button"),s=b("span"),s.textContent="Restore backup",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[4],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[4],x(l,"btn-loading",n[4])},m(u,f){v(u,e,f),w(e,t),v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"click",n[0]),r=!0)},p(u,f){f&16&&(e.disabled=u[4]),f&48&&o!==(o=!u[5]||u[4])&&(l.disabled=o),f&16&&x(l,"btn-loading",u[4])},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function O7(n){let e,t,i={class:"backup-restore-panel",overlayClose:!n[4],escClose:!n[4],beforeHide:n[10],popup:!0,$$slots:{footer:[C7],header:[$7],default:[T7]},$$scope:{ctx:n}};return e=new ln({props:i}),n[11](e),e.$on("show",n[12]),e.$on("hide",n[13]),{c(){H(e.$$.fragment)},m(l,s){F(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.overlayClose=!l[4]),s&16&&(o.escClose=!l[4]),s&16&&(o.beforeHide=l[10]),s&65590&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(O(e.$$.fragment,l),t=!0)},o(l){D(e.$$.fragment,l),t=!1},d(l){n[11](null),q(e,l)}}}function E7(n,e,t){let i;const l="backup_restore_"+z.randomString(5);let s,o="",r="",a=!1,u=null;function f(S){Wt({}),t(2,r=""),t(1,o=S),t(4,a=!1),s==null||s.show()}function c(){return s==null?void 0:s.hide()}async function d(){var S;if(!(!i||a)){clearTimeout(u),t(4,a=!0);try{await me.backups.restore(o),u=setTimeout(()=>{window.location.reload()},2e3)}catch(T){clearTimeout(u),T!=null&&T.isAbort||(t(4,a=!1),$i(((S=T.response)==null?void 0:S.message)||T.message))}}}so(()=>{clearTimeout(u)});function m(){r=this.value,t(2,r)}const h=()=>!a;function g(S){ie[S?"unshift":"push"](()=>{s=S,t(3,s)})}function _(S){Pe.call(this,n,S)}function y(S){Pe.call(this,n,S)}return n.$$.update=()=>{n.$$.dirty&6&&t(5,i=r!=""&&o==r)},[c,o,r,s,a,i,l,d,f,m,h,g,_,y]}class M7 extends ye{constructor(e){super(),be(this,e,E7,O7,_e,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function y1(n,e,t){const i=n.slice();return i[22]=e[t],i}function k1(n,e,t){const i=n.slice();return i[19]=e[t],i}function D7(n){let e=[],t=new Map,i,l,s=pe(n[3]);const o=a=>a[22].key;for(let a=0;aNo backups yet. ',p(e,"class","list-item list-item-placeholder svelte-1ulbkf5")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function w1(n,e){let t,i,l,s,o,r=e[22].key+"",a,u,f,c,d,m=z.formattedFileSize(e[22].size)+"",h,g,_,y,S,T,$,E,M,L,I,A,P,R,N,U,j,V,K,J;function ee(){return e[10](e[22])}function X(){return e[11](e[22])}function oe(){return e[12](e[22])}return{key:n,first:null,c(){t=b("div"),i=b("i"),l=C(),s=b("div"),o=b("span"),a=Y(r),f=C(),c=b("span"),d=Y("("),h=Y(m),g=Y(")"),_=C(),y=b("div"),S=b("button"),T=b("i"),E=C(),M=b("button"),L=b("i"),A=C(),P=b("button"),R=b("i"),U=C(),p(i,"class","ri-folder-zip-line"),p(o,"class","name backup-name svelte-1ulbkf5"),p(o,"title",u=e[22].key),p(c,"class","size txt-hint txt-nowrap"),p(s,"class","content"),p(T,"class","ri-download-line"),p(S,"type","button"),p(S,"class","btn btn-sm btn-circle btn-hint btn-transparent"),S.disabled=$=e[6][e[22].key]||e[5][e[22].key],p(S,"aria-label","Download"),x(S,"btn-loading",e[5][e[22].key]),p(L,"class","ri-restart-line"),p(M,"type","button"),p(M,"class","btn btn-sm btn-circle btn-hint btn-transparent"),M.disabled=I=e[6][e[22].key],p(M,"aria-label","Restore"),p(R,"class","ri-delete-bin-7-line"),p(P,"type","button"),p(P,"class","btn btn-sm btn-circle btn-hint btn-transparent"),P.disabled=N=e[6][e[22].key],p(P,"aria-label","Delete"),x(P,"btn-loading",e[6][e[22].key]),p(y,"class","actions nonintrusive"),p(t,"class","list-item svelte-1ulbkf5"),this.first=t},m(Se,ke){v(Se,t,ke),w(t,i),w(t,l),w(t,s),w(s,o),w(o,a),w(s,f),w(s,c),w(c,d),w(c,h),w(c,g),w(t,_),w(t,y),w(y,S),w(S,T),w(y,E),w(y,M),w(M,L),w(y,A),w(y,P),w(P,R),w(t,U),V=!0,K||(J=[Me(He.call(null,S,"Download")),B(S,"click",tt(ee)),Me(He.call(null,M,"Restore")),B(M,"click",tt(X)),Me(He.call(null,P,"Delete")),B(P,"click",tt(oe))],K=!0)},p(Se,ke){e=Se,(!V||ke&8)&&r!==(r=e[22].key+"")&&ue(a,r),(!V||ke&8&&u!==(u=e[22].key))&&p(o,"title",u),(!V||ke&8)&&m!==(m=z.formattedFileSize(e[22].size)+"")&&ue(h,m),(!V||ke&104&&$!==($=e[6][e[22].key]||e[5][e[22].key]))&&(S.disabled=$),(!V||ke&40)&&x(S,"btn-loading",e[5][e[22].key]),(!V||ke&72&&I!==(I=e[6][e[22].key]))&&(M.disabled=I),(!V||ke&72&&N!==(N=e[6][e[22].key]))&&(P.disabled=N),(!V||ke&72)&&x(P,"btn-loading",e[6][e[22].key])},i(Se){V||(Se&&nt(()=>{V&&(j||(j=ze(t,vt,{duration:150},!0)),j.run(1))}),V=!0)},o(Se){Se&&(j||(j=ze(t,vt,{duration:150},!1)),j.run(0)),V=!1},d(Se){Se&&k(t),Se&&j&&j.end(),K=!1,De(J)}}}function S1(n){let e;return{c(){e=b("div"),e.innerHTML=' ',p(e,"class","list-item list-item-loader svelte-1ulbkf5")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function L7(n){let e,t,i;return{c(){e=b("span"),t=C(),i=b("span"),i.textContent="Backup/restore operation is in process",p(e,"class","loader loader-sm"),p(i,"class","txt")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},d(l){l&&(k(e),k(t),k(i))}}}function A7(n){let e,t,i;return{c(){e=b("i"),t=C(),i=b("span"),i.textContent="Initialize new backup",p(e,"class","ri-play-circle-line"),p(i,"class","txt")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},d(l){l&&(k(e),k(t),k(i))}}}function P7(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g;const _=[I7,D7],y=[];function S(I,A){return I[4]?0:1}i=S(n),l=y[i]=_[i](n);function T(I,A){return I[7]?A7:L7}let $=T(n),E=$(n),M={};f=new w7({props:M}),n[14](f),f.$on("submit",n[15]);let L={};return d=new M7({props:L}),n[16](d),{c(){e=b("div"),t=b("div"),l.c(),s=C(),o=b("div"),r=b("button"),E.c(),u=C(),H(f.$$.fragment),c=C(),H(d.$$.fragment),p(t,"class","list-content svelte-1ulbkf5"),p(r,"type","button"),p(r,"class","btn btn-block btn-transparent"),r.disabled=a=n[4]||!n[7],p(o,"class","list-item list-item-btn"),p(e,"class","list list-compact")},m(I,A){v(I,e,A),w(e,t),y[i].m(t,null),w(e,s),w(e,o),w(o,r),E.m(r,null),v(I,u,A),F(f,I,A),v(I,c,A),F(d,I,A),m=!0,h||(g=B(r,"click",n[13]),h=!0)},p(I,[A]){let P=i;i=S(I),i===P?y[i].p(I,A):(re(),D(y[P],1,1,()=>{y[P]=null}),ae(),l=y[i],l?l.p(I,A):(l=y[i]=_[i](I),l.c()),O(l,1),l.m(t,null)),$!==($=T(I))&&(E.d(1),E=$(I),E&&(E.c(),E.m(r,null))),(!m||A&144&&a!==(a=I[4]||!I[7]))&&(r.disabled=a);const R={};f.$set(R);const N={};d.$set(N)},i(I){m||(O(l),O(f.$$.fragment,I),O(d.$$.fragment,I),m=!0)},o(I){D(l),D(f.$$.fragment,I),D(d.$$.fragment,I),m=!1},d(I){I&&(k(e),k(u),k(c)),y[i].d(),E.d(),n[14](null),q(f,I),n[16](null),q(d,I),h=!1,g()}}}function N7(n,e,t){let i,l,s=[],o=!1,r={},a={},u=!0;f(),h();async function f(){t(4,o=!0);try{t(3,s=await me.backups.getFullList()),s.sort((M,L)=>M.modifiedL.modified?-1:0),t(4,o=!1)}catch(M){M.isAbort||(me.error(M),t(4,o=!1))}}async function c(M){if(!r[M]){t(5,r[M]=!0,r);try{const L=await me.getSuperuserFileToken(),I=me.backups.getDownloadURL(L,M);z.download(I)}catch(L){L.isAbort||me.error(L)}delete r[M],t(5,r)}}function d(M){pn(`Do you really want to delete ${M}?`,()=>m(M))}async function m(M){if(!a[M]){t(6,a[M]=!0,a);try{await me.backups.delete(M),z.removeByKey(s,"name",M),f(),tn(`Successfully deleted ${M}.`)}catch(L){L.isAbort||me.error(L)}delete a[M],t(6,a)}}async function h(){var M;try{const L=await me.health.check({$autoCancel:!1}),I=u;t(7,u=((M=L==null?void 0:L.data)==null?void 0:M.canBackup)||!1),I!=u&&u&&f()}catch{}}Yt(()=>{let M=setInterval(()=>{h()},3e3);return()=>{clearInterval(M)}});const g=M=>c(M.key),_=M=>l.show(M.key),y=M=>d(M.key),S=()=>i==null?void 0:i.show();function T(M){ie[M?"unshift":"push"](()=>{i=M,t(1,i)})}const $=()=>{f()};function E(M){ie[M?"unshift":"push"](()=>{l=M,t(2,l)})}return[f,i,l,s,o,r,a,u,c,d,g,_,y,S,T,$,E]}class R7 extends ye{constructor(e){super(),be(this,e,N7,P7,_e,{loadBackups:0})}get loadBackups(){return this.$$.ctx[0]}}const F7=n=>({isTesting:n&4,testError:n&2,enabled:n&1}),T1=n=>({isTesting:n[2],testError:n[1],enabled:n[0].enabled});function q7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y(n[4]),p(e,"type","checkbox"),p(e,"id",t=n[23]),e.required=!0,p(l,"for",o=n[23])},m(u,f){v(u,e,f),e.checked=n[0].enabled,v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[9]),r=!0)},p(u,f){f&8388608&&t!==(t=u[23])&&p(e,"id",t),f&1&&(e.checked=u[0].enabled),f&16&&ue(s,u[4]),f&8388608&&o!==(o=u[23])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function $1(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M;return i=new fe({props:{class:"form-field required",name:n[3]+".endpoint",$$slots:{default:[H7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field required",name:n[3]+".bucket",$$slots:{default:[j7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field required",name:n[3]+".region",$$slots:{default:[z7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),d=new fe({props:{class:"form-field required",name:n[3]+".accessKey",$$slots:{default:[U7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),g=new fe({props:{class:"form-field required",name:n[3]+".secret",$$slots:{default:[V7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),S=new fe({props:{class:"form-field",name:n[3]+".forcePathStyle",$$slots:{default:[B7,({uniqueId:L})=>({23:L}),({uniqueId:L})=>L?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),H(u.$$.fragment),f=C(),c=b("div"),H(d.$$.fragment),m=C(),h=b("div"),H(g.$$.fragment),_=C(),y=b("div"),H(S.$$.fragment),T=C(),$=b("div"),p(t,"class","col-lg-6"),p(s,"class","col-lg-3"),p(a,"class","col-lg-3"),p(c,"class","col-lg-6"),p(h,"class","col-lg-6"),p(y,"class","col-lg-12"),p($,"class","col-lg-12"),p(e,"class","grid")},m(L,I){v(L,e,I),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),F(u,a,null),w(e,f),w(e,c),F(d,c,null),w(e,m),w(e,h),F(g,h,null),w(e,_),w(e,y),F(S,y,null),w(e,T),w(e,$),M=!0},p(L,I){const A={};I&8&&(A.name=L[3]+".endpoint"),I&8519681&&(A.$$scope={dirty:I,ctx:L}),i.$set(A);const P={};I&8&&(P.name=L[3]+".bucket"),I&8519681&&(P.$$scope={dirty:I,ctx:L}),o.$set(P);const R={};I&8&&(R.name=L[3]+".region"),I&8519681&&(R.$$scope={dirty:I,ctx:L}),u.$set(R);const N={};I&8&&(N.name=L[3]+".accessKey"),I&8519681&&(N.$$scope={dirty:I,ctx:L}),d.$set(N);const U={};I&8&&(U.name=L[3]+".secret"),I&8519713&&(U.$$scope={dirty:I,ctx:L}),g.$set(U);const j={};I&8&&(j.name=L[3]+".forcePathStyle"),I&8519681&&(j.$$scope={dirty:I,ctx:L}),S.$set(j)},i(L){M||(O(i.$$.fragment,L),O(o.$$.fragment,L),O(u.$$.fragment,L),O(d.$$.fragment,L),O(g.$$.fragment,L),O(S.$$.fragment,L),L&&nt(()=>{M&&(E||(E=ze(e,vt,{duration:150},!0)),E.run(1))}),M=!0)},o(L){D(i.$$.fragment,L),D(o.$$.fragment,L),D(u.$$.fragment,L),D(d.$$.fragment,L),D(g.$$.fragment,L),D(S.$$.fragment,L),L&&(E||(E=ze(e,vt,{duration:150},!1)),E.run(0)),M=!1},d(L){L&&k(e),q(i),q(o),q(u),q(d),q(g),q(S),L&&E&&E.end()}}}function H7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Endpoint"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].endpoint),r||(a=B(s,"input",n[10]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].endpoint&&ce(s,u[0].endpoint)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function j7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Bucket"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].bucket),r||(a=B(s,"input",n[11]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].bucket&&ce(s,u[0].bucket)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function z7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Region"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].region),r||(a=B(s,"input",n[12]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].region&&ce(s,u[0].region)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function U7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Access key"),l=C(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].accessKey),r||(a=B(s,"input",n[13]),r=!0)},p(u,f){f&8388608&&i!==(i=u[23])&&p(e,"for",i),f&8388608&&o!==(o=u[23])&&p(s,"id",o),f&1&&s.value!==u[0].accessKey&&ce(s,u[0].accessKey)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function V7(n){let e,t,i,l,s,o,r,a;function u(d){n[14](d)}function f(d){n[15](d)}let c={required:!0,id:n[23]};return n[5]!==void 0&&(c.mask=n[5]),n[0].secret!==void 0&&(c.value=n[0].secret),s=new xu({props:c}),ie.push(()=>ve(s,"mask",u)),ie.push(()=>ve(s,"value",f)),{c(){e=b("label"),t=Y("Secret"),l=C(),H(s.$$.fragment),p(e,"for",i=n[23])},m(d,m){v(d,e,m),w(e,t),v(d,l,m),F(s,d,m),a=!0},p(d,m){(!a||m&8388608&&i!==(i=d[23]))&&p(e,"for",i);const h={};m&8388608&&(h.id=d[23]),!o&&m&32&&(o=!0,h.mask=d[5],$e(()=>o=!1)),!r&&m&1&&(r=!0,h.value=d[0].secret,$e(()=>r=!1)),s.$set(h)},i(d){a||(O(s.$$.fragment,d),a=!0)},o(d){D(s.$$.fragment,d),a=!1},d(d){d&&(k(e),k(l)),q(s,d)}}}function B7(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.textContent="Force path-style addressing",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[23]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[23])},m(c,d){v(c,e,d),e.checked=n[0].forcePathStyle,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[16]),Me(He.call(null,r,{text:'Forces the request to use path-style addressing, eg. "https://s3.amazonaws.com/BUCKET/KEY" instead of the default "https://BUCKET.s3.amazonaws.com/KEY".',position:"top"}))],u=!0)},p(c,d){d&8388608&&t!==(t=c[23])&&p(e,"id",t),d&1&&(e.checked=c[0].forcePathStyle),d&8388608&&a!==(a=c[23])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function W7(n){let e,t,i,l,s;e=new fe({props:{class:"form-field form-field-toggle",$$slots:{default:[q7,({uniqueId:u})=>({23:u}),({uniqueId:u})=>u?8388608:0]},$$scope:{ctx:n}}});const o=n[8].default,r=Lt(o,n,n[17],T1);let a=n[0].enabled&&$1(n);return{c(){H(e.$$.fragment),t=C(),r&&r.c(),i=C(),a&&a.c(),l=ge()},m(u,f){F(e,u,f),v(u,t,f),r&&r.m(u,f),v(u,i,f),a&&a.m(u,f),v(u,l,f),s=!0},p(u,[f]){const c={};f&8519697&&(c.$$scope={dirty:f,ctx:u}),e.$set(c),r&&r.p&&(!s||f&131079)&&Pt(r,o,u,u[17],s?At(o,u[17],f,F7):Nt(u[17]),T1),u[0].enabled?a?(a.p(u,f),f&1&&O(a,1)):(a=$1(u),a.c(),O(a,1),a.m(l.parentNode,l)):a&&(re(),D(a,1,1,()=>{a=null}),ae())},i(u){s||(O(e.$$.fragment,u),O(r,u),O(a),s=!0)},o(u){D(e.$$.fragment,u),D(r,u),D(a),s=!1},d(u){u&&(k(t),k(i),k(l)),q(e,u),r&&r.d(u),a&&a.d(u)}}}const Aa="s3_test_request";function Y7(n,e,t){let{$$slots:i={},$$scope:l}=e,{originalConfig:s={}}=e,{config:o={}}=e,{configKey:r="s3"}=e,{toggleLabel:a="Enable S3"}=e,{testFilesystem:u="storage"}=e,{testError:f=null}=e,{isTesting:c=!1}=e,d=null,m=null,h=!1;function g(){t(5,h=!!(s!=null&&s.accessKey))}function _(P){t(2,c=!0),clearTimeout(m),m=setTimeout(()=>{y()},P)}async function y(){if(t(1,f=null),!o.enabled)return t(2,c=!1),f;me.cancelRequest(Aa),clearTimeout(d),d=setTimeout(()=>{me.cancelRequest(Aa),t(1,f=new Error("S3 test connection timeout.")),t(2,c=!1)},3e4),t(2,c=!0);let P;try{await me.settings.testS3(u,{$cancelKey:Aa})}catch(R){P=R}return P!=null&&P.isAbort||(t(1,f=P),t(2,c=!1),clearTimeout(d)),f}Yt(()=>()=>{clearTimeout(d),clearTimeout(m)});function S(){o.enabled=this.checked,t(0,o)}function T(){o.endpoint=this.value,t(0,o)}function $(){o.bucket=this.value,t(0,o)}function E(){o.region=this.value,t(0,o)}function M(){o.accessKey=this.value,t(0,o)}function L(P){h=P,t(5,h)}function I(P){n.$$.not_equal(o.secret,P)&&(o.secret=P,t(0,o))}function A(){o.forcePathStyle=this.checked,t(0,o)}return n.$$set=P=>{"originalConfig"in P&&t(6,s=P.originalConfig),"config"in P&&t(0,o=P.config),"configKey"in P&&t(3,r=P.configKey),"toggleLabel"in P&&t(4,a=P.toggleLabel),"testFilesystem"in P&&t(7,u=P.testFilesystem),"testError"in P&&t(1,f=P.testError),"isTesting"in P&&t(2,c=P.isTesting),"$$scope"in P&&t(17,l=P.$$scope)},n.$$.update=()=>{n.$$.dirty&64&&s!=null&&s.enabled&&(g(),_(100)),n.$$.dirty&9&&(o.enabled||fi(r))},[o,f,c,r,a,h,s,u,i,S,T,$,E,M,L,I,A,l]}class uk extends ye{constructor(e){super(),be(this,e,Y7,W7,_e,{originalConfig:6,config:0,configKey:3,toggleLabel:4,testFilesystem:7,testError:1,isTesting:2})}}function K7(n){let e,t,i,l,s,o,r;return{c(){e=b("button"),t=b("i"),l=C(),s=b("input"),p(t,"class","ri-upload-cloud-line"),p(e,"type","button"),p(e,"class",i="btn btn-circle btn-transparent "+n[0]),p(e,"aria-label","Upload backup"),x(e,"btn-loading",n[2]),x(e,"btn-disabled",n[2]),p(s,"type","file"),p(s,"accept","application/zip"),p(s,"class","hidden")},m(a,u){v(a,e,u),w(e,t),v(a,l,u),v(a,s,u),n[5](s),o||(r=[Me(He.call(null,e,"Upload backup")),B(e,"click",n[4]),B(s,"change",n[6])],o=!0)},p(a,[u]){u&1&&i!==(i="btn btn-circle btn-transparent "+a[0])&&p(e,"class",i),u&5&&x(e,"btn-loading",a[2]),u&5&&x(e,"btn-disabled",a[2])},i:te,o:te,d(a){a&&(k(e),k(l),k(s)),n[5](null),o=!1,De(r)}}}const C1="upload_backup";function J7(n,e,t){const i=_t();let{class:l=""}=e,s,o=!1;function r(){s&&t(1,s.value="",s)}function a(m){m&&pn(`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the source. + +Do you really want to upload "${m.name}"?`,()=>{u(m)},()=>{r()})}async function u(m){var g,_,y;if(o||!m)return;t(2,o=!0);const h=new FormData;h.set("file",m);try{await me.backups.upload(h,{requestKey:C1}),t(2,o=!1),i("success"),tn("Successfully uploaded a new backup.")}catch(S){S.isAbort||(t(2,o=!1),(y=(_=(g=S.response)==null?void 0:g.data)==null?void 0:_.file)!=null&&y.message?$i(S.response.data.file.message):me.error(S))}r()}so(()=>{me.cancelRequest(C1)});const f=()=>s==null?void 0:s.click();function c(m){ie[m?"unshift":"push"](()=>{s=m,t(1,s)})}const d=m=>{var h,g;a((g=(h=m==null?void 0:m.target)==null?void 0:h.files)==null?void 0:g[0])};return n.$$set=m=>{"class"in m&&t(0,l=m.class)},[l,s,o,a,f,c,d]}class Z7 extends ye{constructor(e){super(),be(this,e,J7,K7,_e,{class:0})}}function G7(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function X7(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function O1(n){var V,K,J;let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L;t=new fe({props:{class:"form-field form-field-toggle m-t-base m-b-0",$$slots:{default:[Q7,({uniqueId:ee})=>({31:ee}),({uniqueId:ee})=>[0,ee?1:0]]},$$scope:{ctx:n}}});let I=n[2]&&E1(n);function A(ee){n[24](ee)}function P(ee){n[25](ee)}function R(ee){n[26](ee)}let N={toggleLabel:"Store backups in S3 storage",testFilesystem:"backups",configKey:"backups.s3",originalConfig:(V=n[0].backups)==null?void 0:V.s3};n[1].backups.s3!==void 0&&(N.config=n[1].backups.s3),n[7]!==void 0&&(N.isTesting=n[7]),n[8]!==void 0&&(N.testError=n[8]),r=new uk({props:N}),ie.push(()=>ve(r,"config",A)),ie.push(()=>ve(r,"isTesting",P)),ie.push(()=>ve(r,"testError",R));let U=((J=(K=n[1].backups)==null?void 0:K.s3)==null?void 0:J.enabled)&&!n[9]&&!n[5]&&M1(n),j=n[9]&&D1(n);return{c(){e=b("form"),H(t.$$.fragment),i=C(),I&&I.c(),l=C(),s=b("div"),o=C(),H(r.$$.fragment),c=C(),d=b("div"),m=b("div"),h=C(),U&&U.c(),g=C(),j&&j.c(),_=C(),y=b("button"),S=b("span"),S.textContent="Save changes",p(s,"class","clearfix m-b-base"),p(m,"class","flex-fill"),p(S,"class","txt"),p(y,"type","submit"),p(y,"class","btn btn-expanded"),y.disabled=T=!n[9]||n[5],x(y,"btn-loading",n[5]),p(d,"class","flex"),p(e,"class","block"),p(e,"autocomplete","off")},m(ee,X){v(ee,e,X),F(t,e,null),w(e,i),I&&I.m(e,null),w(e,l),w(e,s),w(e,o),F(r,e,null),w(e,c),w(e,d),w(d,m),w(d,h),U&&U.m(d,null),w(d,g),j&&j.m(d,null),w(d,_),w(d,y),w(y,S),E=!0,M||(L=[B(y,"click",n[28]),B(e,"submit",tt(n[11]))],M=!0)},p(ee,X){var ke,Ce,We;const oe={};X[0]&4|X[1]&3&&(oe.$$scope={dirty:X,ctx:ee}),t.$set(oe),ee[2]?I?(I.p(ee,X),X[0]&4&&O(I,1)):(I=E1(ee),I.c(),O(I,1),I.m(e,l)):I&&(re(),D(I,1,1,()=>{I=null}),ae());const Se={};X[0]&1&&(Se.originalConfig=(ke=ee[0].backups)==null?void 0:ke.s3),!a&&X[0]&2&&(a=!0,Se.config=ee[1].backups.s3,$e(()=>a=!1)),!u&&X[0]&128&&(u=!0,Se.isTesting=ee[7],$e(()=>u=!1)),!f&&X[0]&256&&(f=!0,Se.testError=ee[8],$e(()=>f=!1)),r.$set(Se),(We=(Ce=ee[1].backups)==null?void 0:Ce.s3)!=null&&We.enabled&&!ee[9]&&!ee[5]?U?U.p(ee,X):(U=M1(ee),U.c(),U.m(d,g)):U&&(U.d(1),U=null),ee[9]?j?j.p(ee,X):(j=D1(ee),j.c(),j.m(d,_)):j&&(j.d(1),j=null),(!E||X[0]&544&&T!==(T=!ee[9]||ee[5]))&&(y.disabled=T),(!E||X[0]&32)&&x(y,"btn-loading",ee[5])},i(ee){E||(O(t.$$.fragment,ee),O(I),O(r.$$.fragment,ee),ee&&nt(()=>{E&&($||($=ze(e,vt,{duration:150},!0)),$.run(1))}),E=!0)},o(ee){D(t.$$.fragment,ee),D(I),D(r.$$.fragment,ee),ee&&($||($=ze(e,vt,{duration:150},!1)),$.run(0)),E=!1},d(ee){ee&&k(e),q(t),I&&I.d(),q(r),U&&U.d(),j&&j.d(),ee&&$&&$.end(),M=!1,De(L)}}}function Q7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=C(),l=b("label"),s=Y("Enable auto backups"),p(e,"type","checkbox"),p(e,"id",t=n[31]),p(l,"for",o=n[31])},m(u,f){v(u,e,f),e.checked=n[2],v(u,i,f),v(u,l,f),w(l,s),r||(a=B(e,"change",n[17]),r=!0)},p(u,f){f[1]&1&&t!==(t=u[31])&&p(e,"id",t),f[0]&4&&(e.checked=u[2]),f[1]&1&&o!==(o=u[31])&&p(l,"for",o)},d(u){u&&(k(e),k(i),k(l)),r=!1,a()}}}function E1(n){let e,t,i,l,s,o,r,a,u;return l=new fe({props:{class:"form-field required",name:"backups.cron",$$slots:{default:[eR,({uniqueId:f})=>({31:f}),({uniqueId:f})=>[0,f?1:0]]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field required",name:"backups.cronMaxKeep",$$slots:{default:[tR,({uniqueId:f})=>({31:f}),({uniqueId:f})=>[0,f?1:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),p(i,"class","col-lg-6"),p(o,"class","col-lg-6"),p(t,"class","grid p-t-base p-b-sm"),p(e,"class","block")},m(f,c){v(f,e,c),w(e,t),w(t,i),F(l,i,null),w(t,s),w(t,o),F(r,o,null),u=!0},p(f,c){const d={};c[0]&3|c[1]&3&&(d.$$scope={dirty:c,ctx:f}),l.$set(d);const m={};c[0]&2|c[1]&3&&(m.$$scope={dirty:c,ctx:f}),r.$set(m)},i(f){u||(O(l.$$.fragment,f),O(r.$$.fragment,f),f&&nt(()=>{u&&(a||(a=ze(e,vt,{duration:150},!0)),a.run(1))}),u=!0)},o(f){D(l.$$.fragment,f),D(r.$$.fragment,f),f&&(a||(a=ze(e,vt,{duration:150},!1)),a.run(0)),u=!1},d(f){f&&k(e),q(l),q(r),f&&a&&a.end()}}}function x7(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("button"),e.innerHTML='Every day at 00:00h',t=C(),i=b("button"),i.innerHTML='Every sunday at 00:00h',l=C(),s=b("button"),s.innerHTML='Every Mon and Wed at 00:00h',o=C(),r=b("button"),r.innerHTML='Every first day of the month at 00:00h',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(i,"type","button"),p(i,"class","dropdown-item closable"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(r,"type","button"),p(r,"class","dropdown-item closable")},m(f,c){v(f,e,c),v(f,t,c),v(f,i,c),v(f,l,c),v(f,s,c),v(f,o,c),v(f,r,c),a||(u=[B(e,"click",n[19]),B(i,"click",n[20]),B(s,"click",n[21]),B(r,"click",n[22])],a=!0)},p:te,d(f){f&&(k(e),k(t),k(i),k(l),k(s),k(o),k(r)),a=!1,De(u)}}}function eR(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P;return g=new Hn({props:{class:"dropdown dropdown-nowrap dropdown-right",$$slots:{default:[x7]},$$scope:{ctx:n}}}),{c(){var R,N;e=b("label"),t=Y("Cron expression"),l=C(),s=b("input"),a=C(),u=b("div"),f=b("button"),c=b("span"),c.textContent="Presets",d=C(),m=b("i"),h=C(),H(g.$$.fragment),_=C(),y=b("div"),S=b("p"),T=Y(`Supports numeric list, steps, ranges or + `),$=b("span"),$.textContent="macros",E=Y(`. + `),M=b("br"),L=Y(` + The timezone is in UTC.`),p(e,"for",i=n[31]),s.required=!0,p(s,"type","text"),p(s,"id",o=n[31]),p(s,"class","txt-lg txt-mono"),p(s,"placeholder","* * * * *"),s.autofocus=r=!((N=(R=n[0])==null?void 0:R.backups)!=null&&N.cron),p(c,"class","txt"),p(m,"class","ri-arrow-drop-down-fill"),p(f,"type","button"),p(f,"class","btn btn-sm btn-outline p-r-0"),p(u,"class","form-field-addon"),p($,"class","link-primary"),p(y,"class","help-block")},m(R,N){var U,j;v(R,e,N),w(e,t),v(R,l,N),v(R,s,N),ce(s,n[1].backups.cron),v(R,a,N),v(R,u,N),w(u,f),w(f,c),w(f,d),w(f,m),w(f,h),F(g,f,null),v(R,_,N),v(R,y,N),w(y,S),w(S,T),w(S,$),w(S,E),w(S,M),w(S,L),I=!0,(j=(U=n[0])==null?void 0:U.backups)!=null&&j.cron||s.focus(),A||(P=[B(s,"input",n[18]),Me(He.call(null,$,`@yearly +@annually +@monthly +@weekly +@daily +@midnight +@hourly`))],A=!0)},p(R,N){var j,V;(!I||N[1]&1&&i!==(i=R[31]))&&p(e,"for",i),(!I||N[1]&1&&o!==(o=R[31]))&&p(s,"id",o),(!I||N[0]&1&&r!==(r=!((V=(j=R[0])==null?void 0:j.backups)!=null&&V.cron)))&&(s.autofocus=r),N[0]&2&&s.value!==R[1].backups.cron&&ce(s,R[1].backups.cron);const U={};N[0]&2|N[1]&2&&(U.$$scope={dirty:N,ctx:R}),g.$set(U)},i(R){I||(O(g.$$.fragment,R),I=!0)},o(R){D(g.$$.fragment,R),I=!1},d(R){R&&(k(e),k(l),k(s),k(a),k(u),k(_),k(y)),q(g),A=!1,De(P)}}}function tR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Max @auto backups to keep"),l=C(),s=b("input"),p(e,"for",i=n[31]),p(s,"type","number"),p(s,"id",o=n[31]),p(s,"min","1")},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[1].backups.cronMaxKeep),r||(a=B(s,"input",n[23]),r=!0)},p(u,f){f[1]&1&&i!==(i=u[31])&&p(e,"for",i),f[1]&1&&o!==(o=u[31])&&p(s,"id",o),f[0]&2&&St(s.value)!==u[1].backups.cronMaxKeep&&ce(s,u[1].backups.cronMaxKeep)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function M1(n){let e;function t(s,o){return s[7]?lR:s[8]?iR:nR}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function nR(n){let e;return{c(){e=b("div"),e.innerHTML=' S3 connected successfully',p(e,"class","label label-sm label-success entrance-right")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function iR(n){let e,t,i,l;return{c(){e=b("div"),e.innerHTML=' Failed to establish S3 connection',p(e,"class","label label-sm label-warning entrance-right")},m(s,o){var r;v(s,e,o),i||(l=Me(t=He.call(null,e,(r=n[8].data)==null?void 0:r.message)),i=!0)},p(s,o){var r;t&&Rt(t.update)&&o[0]&256&&t.update.call(null,(r=s[8].data)==null?void 0:r.message)},d(s){s&&k(e),i=!1,l()}}}function lR(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function D1(n){let e,t,i,l,s;return{c(){e=b("button"),t=b("span"),t.textContent="Reset",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-hint btn-transparent"),e.disabled=i=!n[9]||n[5]},m(o,r){v(o,e,r),w(e,t),l||(s=B(e,"click",n[27]),l=!0)},p(o,r){r[0]&544&&i!==(i=!o[9]||o[5])&&(e.disabled=i)},d(o){o&&k(e),l=!1,s()}}}function sR(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P,R;m=new Au({props:{class:"btn-sm",tooltip:"Refresh"}}),m.$on("refresh",n[13]),g=new Z7({props:{class:"btn-sm"}}),g.$on("success",n[13]);let N={};y=new R7({props:N}),n[15](y);function U(J,ee){return J[6]?X7:G7}let j=U(n),V=j(n),K=n[6]&&!n[4]&&O1(n);return{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=C(),s=b("div"),o=Y(n[10]),r=C(),a=b("div"),u=b("div"),f=b("div"),c=b("span"),c.textContent="Backup and restore your PocketBase data",d=C(),H(m.$$.fragment),h=C(),H(g.$$.fragment),_=C(),H(y.$$.fragment),S=C(),T=b("hr"),$=C(),E=b("button"),M=b("span"),M.textContent="Backups options",L=C(),V.c(),I=C(),K&&K.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(c,"class","txt-xl"),p(f,"class","flex m-b-sm flex-gap-10"),p(M,"class","txt"),p(E,"type","button"),p(E,"class","btn btn-secondary"),E.disabled=n[4],x(E,"btn-loading",n[4]),p(u,"class","panel"),p(u,"autocomplete","off"),p(a,"class","wrapper")},m(J,ee){v(J,e,ee),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),v(J,r,ee),v(J,a,ee),w(a,u),w(u,f),w(f,c),w(f,d),F(m,f,null),w(f,h),F(g,f,null),w(u,_),F(y,u,null),w(u,S),w(u,T),w(u,$),w(u,E),w(E,M),w(E,L),V.m(E,null),w(u,I),K&&K.m(u,null),A=!0,P||(R=[B(E,"click",n[16]),B(u,"submit",tt(n[11]))],P=!0)},p(J,ee){(!A||ee[0]&1024)&&ue(o,J[10]);const X={};y.$set(X),j!==(j=U(J))&&(V.d(1),V=j(J),V&&(V.c(),V.m(E,null))),(!A||ee[0]&16)&&(E.disabled=J[4]),(!A||ee[0]&16)&&x(E,"btn-loading",J[4]),J[6]&&!J[4]?K?(K.p(J,ee),ee[0]&80&&O(K,1)):(K=O1(J),K.c(),O(K,1),K.m(u,null)):K&&(re(),D(K,1,1,()=>{K=null}),ae())},i(J){A||(O(m.$$.fragment,J),O(g.$$.fragment,J),O(y.$$.fragment,J),O(K),A=!0)},o(J){D(m.$$.fragment,J),D(g.$$.fragment,J),D(y.$$.fragment,J),D(K),A=!1},d(J){J&&(k(e),k(r),k(a)),q(m),q(g),n[15](null),q(y),V.d(),K&&K.d(),P=!1,De(R)}}}function oR(n){let e,t,i,l;return e=new ps({}),i=new di({props:{$$slots:{default:[sR]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,o){const r={};o[0]&2047|o[1]&2&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function rR(n,e,t){let i,l;Qe(n,cn,ee=>t(10,l=ee)),Nn(cn,l="Backups",l);let s,o={},r={},a=!1,u=!1,f="",c=!1,d=!1,m=!1,h=null;g();async function g(){t(4,a=!0);try{const ee=await me.settings.getAll()||{};y(ee)}catch(ee){me.error(ee)}t(4,a=!1)}async function _(){if(!(u||!i)){t(5,u=!0);try{const ee=await me.settings.update(z.filterRedactedProps(r));Wt({}),await T(),y(ee),tn("Successfully saved application settings.")}catch(ee){me.error(ee)}t(5,u=!1)}}function y(ee={}){t(1,r={backups:(ee==null?void 0:ee.backups)||{}}),t(2,c=r.backups.cron!=""),t(0,o=JSON.parse(JSON.stringify(r)))}function S(){t(1,r=JSON.parse(JSON.stringify(o||{backups:{}}))),t(2,c=r.backups.cron!="")}async function T(){return s==null?void 0:s.loadBackups()}function $(ee){ie[ee?"unshift":"push"](()=>{s=ee,t(3,s)})}const E=()=>t(6,d=!d);function M(){c=this.checked,t(2,c)}function L(){r.backups.cron=this.value,t(1,r),t(2,c)}const I=()=>{t(1,r.backups.cron="0 0 * * *",r)},A=()=>{t(1,r.backups.cron="0 0 * * 0",r)},P=()=>{t(1,r.backups.cron="0 0 * * 1,3",r)},R=()=>{t(1,r.backups.cron="0 0 1 * *",r)};function N(){r.backups.cronMaxKeep=St(this.value),t(1,r),t(2,c)}function U(ee){n.$$.not_equal(r.backups.s3,ee)&&(r.backups.s3=ee,t(1,r),t(2,c))}function j(ee){m=ee,t(7,m)}function V(ee){h=ee,t(8,h)}const K=()=>S(),J=()=>_();return n.$$.update=()=>{var ee;n.$$.dirty[0]&1&&t(14,f=JSON.stringify(o)),n.$$.dirty[0]&6&&!c&&(ee=r==null?void 0:r.backups)!=null&&ee.cron&&(fi("backups.cron"),t(1,r.backups.cron="",r)),n.$$.dirty[0]&16386&&t(9,i=f!=JSON.stringify(r))},[o,r,c,s,a,u,d,m,h,i,l,_,S,T,f,$,E,M,L,I,A,P,R,N,U,j,V,K,J]}class aR extends ye{constructor(e){super(),be(this,e,rR,oR,_e,{},null,[-1,-1])}}function I1(n,e,t){const i=n.slice();return i[22]=e[t],i}function uR(n){let e,t,i,l,s,o,r,a=[],u=new Map,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I,A,P,R,N,U;o=new fe({props:{class:"form-field",$$slots:{default:[cR,({uniqueId:K})=>({12:K}),({uniqueId:K})=>K?4096:0]},$$scope:{ctx:n}}});let j=pe(n[0]);const V=K=>K[22].id;for(let K=0;KBelow you'll find your current collections configuration that you could import in + another PocketBase environment.

    `,t=C(),i=b("div"),l=b("div"),s=b("div"),H(o.$$.fragment),r=C();for(let K=0;K({12:o}),({uniqueId:o})=>o?4096:0]},$$scope:{ctx:e}}}),{key:n,first:null,c(){t=b("div"),H(i.$$.fragment),l=C(),p(t,"class","list-item list-item-collection"),this.first=t},m(o,r){v(o,t,r),F(i,t,null),w(t,l),s=!0},p(o,r){e=o;const a={};r&33558531&&(a.$$scope={dirty:r,ctx:e}),i.$set(a)},i(o){s||(O(i.$$.fragment,o),s=!0)},o(o){D(i.$$.fragment,o),s=!1},d(o){o&&k(t),q(i)}}}function pR(n){let e,t,i,l,s,o,r,a,u,f,c,d;const m=[fR,uR],h=[];function g(_,y){return _[4]?0:1}return f=g(n),c=h[f]=m[f](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=C(),s=b("div"),o=Y(n[7]),r=C(),a=b("div"),u=b("div"),c.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","panel"),p(a,"class","wrapper")},m(_,y){v(_,e,y),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),v(_,r,y),v(_,a,y),w(a,u),h[f].m(u,null),d=!0},p(_,y){(!d||y&128)&&ue(o,_[7]);let S=f;f=g(_),f===S?h[f].p(_,y):(re(),D(h[S],1,1,()=>{h[S]=null}),ae(),c=h[f],c?c.p(_,y):(c=h[f]=m[f](_),c.c()),O(c,1),c.m(u,null))},i(_){d||(O(c),d=!0)},o(_){D(c),d=!1},d(_){_&&(k(e),k(r),k(a)),h[f].d()}}}function mR(n){let e,t,i,l;return e=new ps({}),i=new di({props:{$$slots:{default:[pR]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,[o]){const r={};o&33554687&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}function hR(n,e,t){let i,l,s,o;Qe(n,cn,A=>t(7,o=A)),Nn(cn,o="Export collections",o);const r="export_"+z.randomString(5);let a,u=[],f={},c=!1;d();async function d(){var A;t(4,c=!0);try{t(0,u=await me.collections.getFullList({batch:100,$cancelKey:r})),t(0,u=z.sortCollections(u));for(let P of u)delete P.created,delete P.updated,(A=P.oauth2)==null||delete A.providers;y()}catch(P){me.error(P)}t(4,c=!1)}function m(){z.downloadJson(Object.values(f),"pb_schema")}function h(){z.copyToClipboard(i),Ys("The configuration was copied to your clipboard!",3e3)}function g(){s?_():y()}function _(){t(1,f={})}function y(){t(1,f={});for(const A of u)t(1,f[A.id]=A,f)}function S(A){f[A.id]?delete f[A.id]:t(1,f[A.id]=A,f),t(1,f)}const T=()=>g(),$=A=>S(A),E=()=>h();function M(A){ie[A?"unshift":"push"](()=>{a=A,t(3,a)})}const L=A=>{if(A.ctrlKey&&A.code==="KeyA"){A.preventDefault();const P=window.getSelection(),R=document.createRange();R.selectNodeContents(a),P.removeAllRanges(),P.addRange(R)}},I=()=>m();return n.$$.update=()=>{n.$$.dirty&2&&t(6,i=JSON.stringify(Object.values(f),null,4)),n.$$.dirty&2&&t(2,l=Object.keys(f).length),n.$$.dirty&5&&t(5,s=u.length&&l===u.length)},[u,f,l,a,c,s,i,o,m,h,g,S,r,T,$,E,M,L,I]}class _R extends ye{constructor(e){super(),be(this,e,hR,mR,_e,{})}}function A1(n,e,t){const i=n.slice();return i[14]=e[t],i}function P1(n,e,t){const i=n.slice();return i[17]=e[t][0],i[18]=e[t][1],i}function N1(n,e,t){const i=n.slice();return i[14]=e[t],i}function R1(n,e,t){const i=n.slice();return i[17]=e[t][0],i[23]=e[t][1],i}function F1(n,e,t){const i=n.slice();return i[14]=e[t],i}function q1(n,e,t){const i=n.slice();return i[17]=e[t][0],i[18]=e[t][1],i}function H1(n,e,t){const i=n.slice();return i[30]=e[t],i}function gR(n){let e,t,i,l,s=n[1].name+"",o,r=n[10]&&j1(),a=n[0].name!==n[1].name&&z1(n);return{c(){e=b("div"),r&&r.c(),t=C(),a&&a.c(),i=C(),l=b("strong"),o=Y(s),p(l,"class","txt"),p(e,"class","inline-flex fleg-gap-5")},m(u,f){v(u,e,f),r&&r.m(e,null),w(e,t),a&&a.m(e,null),w(e,i),w(e,l),w(l,o)},p(u,f){u[10]?r||(r=j1(),r.c(),r.m(e,t)):r&&(r.d(1),r=null),u[0].name!==u[1].name?a?a.p(u,f):(a=z1(u),a.c(),a.m(e,i)):a&&(a.d(1),a=null),f[0]&2&&s!==(s=u[1].name+"")&&ue(o,s)},d(u){u&&k(e),r&&r.d(),a&&a.d()}}}function bR(n){var o;let e,t,i,l=((o=n[0])==null?void 0:o.name)+"",s;return{c(){e=b("span"),e.textContent="Deleted",t=C(),i=b("strong"),s=Y(l),p(e,"class","label label-danger")},m(r,a){v(r,e,a),v(r,t,a),v(r,i,a),w(i,s)},p(r,a){var u;a[0]&1&&l!==(l=((u=r[0])==null?void 0:u.name)+"")&&ue(s,l)},d(r){r&&(k(e),k(t),k(i))}}}function yR(n){var o;let e,t,i,l=((o=n[1])==null?void 0:o.name)+"",s;return{c(){e=b("span"),e.textContent="Added",t=C(),i=b("strong"),s=Y(l),p(e,"class","label label-success")},m(r,a){v(r,e,a),v(r,t,a),v(r,i,a),w(i,s)},p(r,a){var u;a[0]&2&&l!==(l=((u=r[1])==null?void 0:u.name)+"")&&ue(s,l)},d(r){r&&(k(e),k(t),k(i))}}}function j1(n){let e;return{c(){e=b("span"),e.textContent="Changed",p(e,"class","label label-warning")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function z1(n){let e,t=n[0].name+"",i,l,s;return{c(){e=b("strong"),i=Y(t),l=C(),s=b("i"),p(e,"class","txt-strikethrough txt-hint"),p(s,"class","ri-arrow-right-line txt-sm")},m(o,r){v(o,e,r),w(e,i),v(o,l,r),v(o,s,r)},p(o,r){r[0]&1&&t!==(t=o[0].name+"")&&ue(i,t)},d(o){o&&(k(e),k(l),k(s))}}}function U1(n){var _,y;let e,t,i,l=n[30]+"",s,o,r,a,u=n[12]((_=n[0])==null?void 0:_[n[30]])+"",f,c,d,m,h=n[12]((y=n[1])==null?void 0:y[n[30]])+"",g;return{c(){var S,T,$,E,M,L;e=b("tr"),t=b("td"),i=b("span"),s=Y(l),o=C(),r=b("td"),a=b("pre"),f=Y(u),c=C(),d=b("td"),m=b("pre"),g=Y(h),p(t,"class","min-width svelte-qs0w8h"),p(a,"class","txt diff-value svelte-qs0w8h"),p(r,"class","svelte-qs0w8h"),x(r,"changed-old-col",!n[11]&&An((S=n[0])==null?void 0:S[n[30]],(T=n[1])==null?void 0:T[n[30]])),x(r,"changed-none-col",n[11]),p(m,"class","txt diff-value svelte-qs0w8h"),p(d,"class","svelte-qs0w8h"),x(d,"changed-new-col",!n[5]&&An(($=n[0])==null?void 0:$[n[30]],(E=n[1])==null?void 0:E[n[30]])),x(d,"changed-none-col",n[5]),p(e,"class","svelte-qs0w8h"),x(e,"txt-primary",An((M=n[0])==null?void 0:M[n[30]],(L=n[1])==null?void 0:L[n[30]]))},m(S,T){v(S,e,T),w(e,t),w(t,i),w(i,s),w(e,o),w(e,r),w(r,a),w(a,f),w(e,c),w(e,d),w(d,m),w(m,g)},p(S,T){var $,E,M,L,I,A,P,R;T[0]&512&&l!==(l=S[30]+"")&&ue(s,l),T[0]&513&&u!==(u=S[12](($=S[0])==null?void 0:$[S[30]])+"")&&ue(f,u),T[0]&2563&&x(r,"changed-old-col",!S[11]&&An((E=S[0])==null?void 0:E[S[30]],(M=S[1])==null?void 0:M[S[30]])),T[0]&2048&&x(r,"changed-none-col",S[11]),T[0]&514&&h!==(h=S[12]((L=S[1])==null?void 0:L[S[30]])+"")&&ue(g,h),T[0]&547&&x(d,"changed-new-col",!S[5]&&An((I=S[0])==null?void 0:I[S[30]],(A=S[1])==null?void 0:A[S[30]])),T[0]&32&&x(d,"changed-none-col",S[5]),T[0]&515&&x(e,"txt-primary",An((P=S[0])==null?void 0:P[S[30]],(R=S[1])==null?void 0:R[S[30]]))},d(S){S&&k(e)}}}function V1(n){let e,t=pe(n[6]),i=[];for(let l=0;lProps Old New',s=C(),o=b("tbody");for(let $=0;$!c.find(S=>y.id==S.id))))}function _(y){return typeof y>"u"?"":z.isObject(y)?JSON.stringify(y,null,4):y}return n.$$set=y=>{"collectionA"in y&&t(0,r=y.collectionA),"collectionB"in y&&t(1,a=y.collectionB),"deleteMissing"in y&&t(2,u=y.deleteMissing)},n.$$.update=()=>{n.$$.dirty[0]&2&&t(5,i=!(a!=null&&a.id)&&!(a!=null&&a.name)),n.$$.dirty[0]&33&&t(11,l=!i&&!(r!=null&&r.id)),n.$$.dirty[0]&1&&t(3,f=Array.isArray(r==null?void 0:r.fields)?r==null?void 0:r.fields.concat():[]),n.$$.dirty[0]&7&&(typeof(r==null?void 0:r.fields)<"u"||typeof(a==null?void 0:a.fields)<"u"||typeof u<"u")&&g(),n.$$.dirty[0]&24&&t(6,d=f.filter(y=>!c.find(S=>y.id==S.id))),n.$$.dirty[0]&24&&t(7,m=c.filter(y=>f.find(S=>S.id==y.id))),n.$$.dirty[0]&24&&t(8,h=c.filter(y=>!f.find(S=>S.id==y.id))),n.$$.dirty[0]&7&&t(10,s=z.hasCollectionChanges(r,a,u)),n.$$.dirty[0]&3&&t(9,o=z.mergeUnique(Object.keys(r||{}),Object.keys(a||{})).filter(y=>!["fields","created","updated"].includes(y)))},[r,a,u,f,c,i,d,m,h,o,s,l,_]}class wR extends ye{constructor(e){super(),be(this,e,vR,kR,_e,{collectionA:0,collectionB:1,deleteMissing:2},null,[-1,-1])}}function X1(n,e,t){const i=n.slice();return i[17]=e[t],i}function Q1(n){let e,t;return e=new wR({props:{collectionA:n[17].old,collectionB:n[17].new,deleteMissing:n[3]}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l&4&&(s.collectionA=i[17].old),l&4&&(s.collectionB=i[17].new),l&8&&(s.deleteMissing=i[3]),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function SR(n){let e,t,i=pe(n[2]),l=[];for(let o=0;oD(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;o{h()}):h()}async function h(){if(!u){t(4,u=!0);try{await me.collections.import(o,a),tn("Successfully imported collections configuration."),i("submit")}catch($){me.error($)}t(4,u=!1),c()}}const g=()=>m(),_=()=>!u;function y($){ie[$?"unshift":"push"](()=>{l=$,t(1,l)})}function S($){Pe.call(this,n,$)}function T($){Pe.call(this,n,$)}return n.$$.update=()=>{n.$$.dirty&384&&Array.isArray(s)&&Array.isArray(o)&&d()},[c,l,r,a,u,m,f,s,o,g,_,y,S,T]}class ER extends ye{constructor(e){super(),be(this,e,OR,CR,_e,{show:6,hide:0})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[0]}}function x1(n,e,t){const i=n.slice();return i[33]=e[t],i}function eb(n,e,t){const i=n.slice();return i[36]=e[t],i}function tb(n,e,t){const i=n.slice();return i[33]=e[t],i}function MR(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$,E,M,L,I;a=new fe({props:{class:"form-field "+(n[6]?"":"field-error"),name:"collections",$$slots:{default:[IR,({uniqueId:V})=>({41:V}),({uniqueId:V})=>[0,V?1024:0]]},$$scope:{ctx:n}}});let A=n[1].length&&ib(n),P=!1,R=n[6]&&n[1].length&&!n[7]&&lb(),N=n[6]&&n[1].length&&n[7]&&sb(n),U=n[13].length&&_b(n),j=!!n[0]&&gb(n);return{c(){e=b("input"),t=C(),i=b("div"),l=b("p"),s=Y(`Paste below the collections configuration you want to import or + `),o=b("button"),o.innerHTML='Load from JSON file',r=C(),H(a.$$.fragment),u=C(),A&&A.c(),f=C(),c=C(),R&&R.c(),d=C(),N&&N.c(),m=C(),U&&U.c(),h=C(),g=b("div"),j&&j.c(),_=C(),y=b("div"),S=C(),T=b("button"),$=b("span"),$.textContent="Review",p(e,"type","file"),p(e,"class","hidden"),p(e,"accept",".json"),p(o,"class","btn btn-outline btn-sm m-l-5"),x(o,"btn-loading",n[12]),p(i,"class","content txt-xl m-b-base"),p(y,"class","flex-fill"),p($,"class","txt"),p(T,"type","button"),p(T,"class","btn btn-expanded btn-warning m-l-auto"),T.disabled=E=!n[14],p(g,"class","flex m-t-base")},m(V,K){v(V,e,K),n[21](e),v(V,t,K),v(V,i,K),w(i,l),w(l,s),w(l,o),v(V,r,K),F(a,V,K),v(V,u,K),A&&A.m(V,K),v(V,f,K),v(V,c,K),R&&R.m(V,K),v(V,d,K),N&&N.m(V,K),v(V,m,K),U&&U.m(V,K),v(V,h,K),v(V,g,K),j&&j.m(g,null),w(g,_),w(g,y),w(g,S),w(g,T),w(T,$),M=!0,L||(I=[B(e,"change",n[22]),B(o,"click",n[23]),B(T,"click",n[19])],L=!0)},p(V,K){(!M||K[0]&4096)&&x(o,"btn-loading",V[12]);const J={};K[0]&64&&(J.class="form-field "+(V[6]?"":"field-error")),K[0]&65|K[1]&3072&&(J.$$scope={dirty:K,ctx:V}),a.$set(J),V[1].length?A?(A.p(V,K),K[0]&2&&O(A,1)):(A=ib(V),A.c(),O(A,1),A.m(f.parentNode,f)):A&&(re(),D(A,1,1,()=>{A=null}),ae()),V[6]&&V[1].length&&!V[7]?R||(R=lb(),R.c(),R.m(d.parentNode,d)):R&&(R.d(1),R=null),V[6]&&V[1].length&&V[7]?N?N.p(V,K):(N=sb(V),N.c(),N.m(m.parentNode,m)):N&&(N.d(1),N=null),V[13].length?U?U.p(V,K):(U=_b(V),U.c(),U.m(h.parentNode,h)):U&&(U.d(1),U=null),V[0]?j?j.p(V,K):(j=gb(V),j.c(),j.m(g,_)):j&&(j.d(1),j=null),(!M||K[0]&16384&&E!==(E=!V[14]))&&(T.disabled=E)},i(V){M||(O(a.$$.fragment,V),O(A),O(P),M=!0)},o(V){D(a.$$.fragment,V),D(A),D(P),M=!1},d(V){V&&(k(e),k(t),k(i),k(r),k(u),k(f),k(c),k(d),k(m),k(h),k(g)),n[21](null),q(a,V),A&&A.d(V),R&&R.d(V),N&&N.d(V),U&&U.d(V),j&&j.d(),L=!1,De(I)}}}function DR(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function nb(n){let e;return{c(){e=b("div"),e.textContent="Invalid collections configuration.",p(e,"class","help-block help-block-error")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function IR(n){let e,t,i,l,s,o,r,a,u,f,c=!!n[0]&&!n[6]&&nb();return{c(){e=b("label"),t=Y("Collections"),l=C(),s=b("textarea"),r=C(),c&&c.c(),a=ge(),p(e,"for",i=n[41]),p(e,"class","p-b-10"),p(s,"id",o=n[41]),p(s,"class","code"),p(s,"spellcheck","false"),p(s,"rows","15"),s.required=!0},m(d,m){v(d,e,m),w(e,t),v(d,l,m),v(d,s,m),ce(s,n[0]),v(d,r,m),c&&c.m(d,m),v(d,a,m),u||(f=B(s,"input",n[24]),u=!0)},p(d,m){m[1]&1024&&i!==(i=d[41])&&p(e,"for",i),m[1]&1024&&o!==(o=d[41])&&p(s,"id",o),m[0]&1&&ce(s,d[0]),d[0]&&!d[6]?c||(c=nb(),c.c(),c.m(a.parentNode,a)):c&&(c.d(1),c=null)},d(d){d&&(k(e),k(l),k(s),k(r),k(a)),c&&c.d(d),u=!1,f()}}}function ib(n){let e,t;return e=new fe({props:{class:"form-field form-field-toggle",$$slots:{default:[LR,({uniqueId:i})=>({41:i}),({uniqueId:i})=>[0,i?1024:0]]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,l){const s={};l[0]&96|l[1]&3072&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function LR(n){let e,t,i,l,s,o,r,a,u;return{c(){e=b("input"),l=C(),s=b("label"),o=Y("Merge with the existing collections"),p(e,"type","checkbox"),p(e,"id",t=n[41]),e.disabled=i=!n[6],p(s,"for",r=n[41])},m(f,c){v(f,e,c),e.checked=n[5],v(f,l,c),v(f,s,c),w(s,o),a||(u=B(e,"change",n[25]),a=!0)},p(f,c){c[1]&1024&&t!==(t=f[41])&&p(e,"id",t),c[0]&64&&i!==(i=!f[6])&&(e.disabled=i),c[0]&32&&(e.checked=f[5]),c[1]&1024&&r!==(r=f[41])&&p(s,"for",r)},d(f){f&&(k(e),k(l),k(s)),a=!1,u()}}}function lb(n){let e;return{c(){e=b("div"),e.innerHTML='
    Your collections configuration is already up-to-date!
    ',p(e,"class","alert alert-info")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function sb(n){let e,t,i,l,s,o=n[9].length&&ob(n),r=n[3].length&&ub(n),a=n[8].length&&pb(n);return{c(){e=b("h5"),e.textContent="Detected changes",t=C(),i=b("div"),o&&o.c(),l=C(),r&&r.c(),s=C(),a&&a.c(),p(e,"class","section-title"),p(i,"class","list")},m(u,f){v(u,e,f),v(u,t,f),v(u,i,f),o&&o.m(i,null),w(i,l),r&&r.m(i,null),w(i,s),a&&a.m(i,null)},p(u,f){u[9].length?o?o.p(u,f):(o=ob(u),o.c(),o.m(i,l)):o&&(o.d(1),o=null),u[3].length?r?r.p(u,f):(r=ub(u),r.c(),r.m(i,s)):r&&(r.d(1),r=null),u[8].length?a?a.p(u,f):(a=pb(u),a.c(),a.m(i,null)):a&&(a.d(1),a=null)},d(u){u&&(k(e),k(t),k(i)),o&&o.d(),r&&r.d(),a&&a.d()}}}function ob(n){let e=[],t=new Map,i,l=pe(n[9]);const s=o=>o[33].id;for(let o=0;oo[36].old.id+o[36].new.id;for(let o=0;oo[33].id;for(let o=0;o',i=C(),l=b("div"),l.innerHTML=`Some of the imported collections share the same name and/or fields but are + imported with different IDs. You can replace them in the import if you want + to.`,s=C(),o=b("button"),o.innerHTML='Replace with original ids',p(t,"class","icon"),p(l,"class","content"),p(o,"type","button"),p(o,"class","btn btn-warning btn-sm btn-outline"),p(e,"class","alert alert-warning m-t-base")},m(u,f){v(u,e,f),w(e,t),w(e,i),w(e,l),w(e,s),w(e,o),r||(a=B(o,"click",n[27]),r=!0)},p:te,d(u){u&&k(e),r=!1,a()}}}function gb(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear',p(e,"type","button"),p(e,"class","btn btn-transparent link-hint")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[28]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function AR(n){let e,t,i,l,s,o,r,a,u,f,c,d;const m=[DR,MR],h=[];function g(_,y){return _[4]?0:1}return f=g(n),c=h[f]=m[f](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=C(),s=b("div"),o=Y(n[15]),r=C(),a=b("div"),u=b("div"),c.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","panel"),p(a,"class","wrapper")},m(_,y){v(_,e,y),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),v(_,r,y),v(_,a,y),w(a,u),h[f].m(u,null),d=!0},p(_,y){(!d||y[0]&32768)&&ue(o,_[15]);let S=f;f=g(_),f===S?h[f].p(_,y):(re(),D(h[S],1,1,()=>{h[S]=null}),ae(),c=h[f],c?c.p(_,y):(c=h[f]=m[f](_),c.c()),O(c,1),c.m(u,null))},i(_){d||(O(c),d=!0)},o(_){D(c),d=!1},d(_){_&&(k(e),k(r),k(a)),h[f].d()}}}function PR(n){let e,t,i,l,s,o;e=new ps({}),i=new di({props:{$$slots:{default:[AR]},$$scope:{ctx:n}}});let r={};return s=new ER({props:r}),n[29](s),s.$on("submit",n[18]),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),l=C(),H(s.$$.fragment)},m(a,u){F(e,a,u),v(a,t,u),F(i,a,u),v(a,l,u),F(s,a,u),o=!0},p(a,u){const f={};u[0]&63487|u[1]&2048&&(f.$$scope={dirty:u,ctx:a}),i.$set(f);const c={};s.$set(c)},i(a){o||(O(e.$$.fragment,a),O(i.$$.fragment,a),O(s.$$.fragment,a),o=!0)},o(a){D(e.$$.fragment,a),D(i.$$.fragment,a),D(s.$$.fragment,a),o=!1},d(a){a&&(k(t),k(l)),q(e,a),q(i,a),n[29](null),q(s,a)}}}function NR(n,e,t){let i,l,s,o,r,a,u;Qe(n,cn,oe=>t(15,u=oe)),Nn(cn,u="Import collections",u);let f,c,d="",m=!1,h=[],g=[],_=!0,y=[],S=!1,T=!1;$();async function $(){var oe;t(4,S=!0);try{t(20,g=await me.collections.getFullList(200));for(let Se of g)delete Se.created,delete Se.updated,(oe=Se.oauth2)==null||delete oe.providers}catch(Se){me.error(Se)}t(4,S=!1)}function E(){if(t(3,y=[]),!!i)for(let oe of h){const Se=z.findByKey(g,"id",oe.id);!(Se!=null&&Se.id)||!z.hasCollectionChanges(Se,oe,_)||y.push({new:oe,old:Se})}}function M(){t(1,h=[]);try{t(1,h=JSON.parse(d))}catch{}Array.isArray(h)?t(1,h=z.filterDuplicatesByKey(h)):t(1,h=[]);for(let oe of h)delete oe.created,delete oe.updated,oe.fields=z.filterDuplicatesByKey(oe.fields)}function L(){for(let oe of h){const Se=z.findByKey(g,"name",oe.name)||z.findByKey(g,"id",oe.id);if(!Se)continue;const ke=oe.id,Ce=Se.id;oe.id=Ce;const We=Array.isArray(Se.fields)?Se.fields:[],st=Array.isArray(oe.fields)?oe.fields:[];for(const et of st){const Be=z.findByKey(We,"name",et.name);Be&&Be.id&&(et.id=Be.id)}for(let et of h)if(Array.isArray(et.fields))for(let Be of et.fields)Be.collectionId&&Be.collectionId===ke&&(Be.collectionId=Ce)}t(0,d=JSON.stringify(h,null,4))}function I(oe){t(12,m=!0);const Se=new FileReader;Se.onload=async ke=>{t(12,m=!1),t(10,f.value="",f),t(0,d=ke.target.result),await fn(),h.length||($i("Invalid collections configuration."),A())},Se.onerror=ke=>{console.warn(ke),$i("Failed to load the imported JSON."),t(12,m=!1),t(10,f.value="",f)},Se.readAsText(oe)}function A(){t(0,d=""),t(10,f.value="",f),Wt({})}function P(){const oe=T?z.filterDuplicatesByKey(g.concat(h)):h;c==null||c.show(g,oe,_)}function R(oe){ie[oe?"unshift":"push"](()=>{f=oe,t(10,f)})}const N=()=>{f.files.length&&I(f.files[0])},U=()=>{f.click()};function j(){d=this.value,t(0,d)}function V(){T=this.checked,t(5,T)}function K(){_=this.checked,t(2,_)}const J=()=>L(),ee=()=>A();function X(oe){ie[oe?"unshift":"push"](()=>{c=oe,t(11,c)})}return n.$$.update=()=>{n.$$.dirty[0]&33&&typeof d<"u"&&T!==null&&M(),n.$$.dirty[0]&3&&t(6,i=!!d&&h.length&&h.length===h.filter(oe=>!!oe.id&&!!oe.name).length),n.$$.dirty[0]&1048678&&t(9,l=g.filter(oe=>i&&!T&&_&&!z.findByKey(h,"id",oe.id))),n.$$.dirty[0]&1048642&&t(8,s=h.filter(oe=>i&&!z.findByKey(g,"id",oe.id))),n.$$.dirty[0]&6&&(typeof h<"u"||typeof _<"u")&&E(),n.$$.dirty[0]&777&&t(7,o=!!d&&(l.length||s.length||y.length)),n.$$.dirty[0]&208&&t(14,r=!S&&i&&o),n.$$.dirty[0]&1048578&&t(13,a=h.filter(oe=>{let Se=z.findByKey(g,"name",oe.name)||z.findByKey(g,"id",oe.id);if(!Se)return!1;if(Se.id!=oe.id)return!0;const ke=Array.isArray(Se.fields)?Se.fields:[],Ce=Array.isArray(oe.fields)?oe.fields:[];for(const We of Ce){if(z.findByKey(ke,"id",We.id))continue;const et=z.findByKey(ke,"name",We.name);if(et&&We.id!=et.id)return!0}return!1}))},[d,h,_,y,S,T,i,o,s,l,f,c,m,a,r,u,L,I,A,P,g,R,N,U,j,V,K,J,ee,X]}class RR extends ye{constructor(e){super(),be(this,e,NR,PR,_e,{},null,[-1,-1])}}function FR(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h;i=new fe({props:{class:"form-field required",name:"meta.senderName",$$slots:{default:[HR,({uniqueId:T})=>({33:T}),({uniqueId:T})=>[0,T?4:0]]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field required",name:"meta.senderAddress",$$slots:{default:[jR,({uniqueId:T})=>({33:T}),({uniqueId:T})=>[0,T?4:0]]},$$scope:{ctx:n}}}),a=new fe({props:{class:"form-field form-field-toggle m-b-sm",$$slots:{default:[zR,({uniqueId:T})=>({33:T}),({uniqueId:T})=>[0,T?4:0]]},$$scope:{ctx:n}}});let g=n[0].smtp.enabled&&bb(n);function _(T,$){return T[6]?QR:XR}let y=_(n),S=y(n);return{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),H(a.$$.fragment),u=C(),g&&g.c(),f=C(),c=b("div"),d=b("div"),m=C(),S.c(),p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(e,"class","grid m-b-base"),p(d,"class","flex-fill"),p(c,"class","flex")},m(T,$){v(T,e,$),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),v(T,r,$),F(a,T,$),v(T,u,$),g&&g.m(T,$),v(T,f,$),v(T,c,$),w(c,d),w(c,m),S.m(c,null),h=!0},p(T,$){const E={};$[0]&1|$[1]&12&&(E.$$scope={dirty:$,ctx:T}),i.$set(E);const M={};$[0]&1|$[1]&12&&(M.$$scope={dirty:$,ctx:T}),o.$set(M);const L={};$[0]&1|$[1]&12&&(L.$$scope={dirty:$,ctx:T}),a.$set(L),T[0].smtp.enabled?g?(g.p(T,$),$[0]&1&&O(g,1)):(g=bb(T),g.c(),O(g,1),g.m(f.parentNode,f)):g&&(re(),D(g,1,1,()=>{g=null}),ae()),y===(y=_(T))&&S?S.p(T,$):(S.d(1),S=y(T),S&&(S.c(),S.m(c,null)))},i(T){h||(O(i.$$.fragment,T),O(o.$$.fragment,T),O(a.$$.fragment,T),O(g),h=!0)},o(T){D(i.$$.fragment,T),D(o.$$.fragment,T),D(a.$$.fragment,T),D(g),h=!1},d(T){T&&(k(e),k(r),k(u),k(f),k(c)),q(i),q(o),q(a,T),g&&g.d(T),S.d()}}}function qR(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function HR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Sender name"),l=C(),s=b("input"),p(e,"for",i=n[33]),p(s,"type","text"),p(s,"id",o=n[33]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].meta.senderName),r||(a=B(s,"input",n[14]),r=!0)},p(u,f){f[1]&4&&i!==(i=u[33])&&p(e,"for",i),f[1]&4&&o!==(o=u[33])&&p(s,"id",o),f[0]&1&&s.value!==u[0].meta.senderName&&ce(s,u[0].meta.senderName)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function jR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Sender address"),l=C(),s=b("input"),p(e,"for",i=n[33]),p(s,"type","email"),p(s,"id",o=n[33]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].meta.senderAddress),r||(a=B(s,"input",n[15]),r=!0)},p(u,f){f[1]&4&&i!==(i=u[33])&&p(e,"for",i),f[1]&4&&o!==(o=u[33])&&p(s,"id",o),f[0]&1&&s.value!==u[0].meta.senderAddress&&ce(s,u[0].meta.senderAddress)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function zR(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("input"),i=C(),l=b("label"),s=b("span"),s.innerHTML="Use SMTP mail server (recommended)",o=C(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[33]),e.required=!0,p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[33])},m(c,d){v(c,e,d),e.checked=n[0].smtp.enabled,v(c,i,d),v(c,l,d),w(l,s),w(l,o),w(l,r),u||(f=[B(e,"change",n[16]),Me(He.call(null,r,{text:'By default PocketBase uses the unix "sendmail" command for sending emails. For better emails deliverability it is recommended to use a SMTP mail server.',position:"top"}))],u=!0)},p(c,d){d[1]&4&&t!==(t=c[33])&&p(e,"id",t),d[0]&1&&(e.checked=c[0].smtp.enabled),d[1]&4&&a!==(a=c[33])&&p(l,"for",a)},d(c){c&&(k(e),k(i),k(l)),u=!1,De(f)}}}function bb(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_,y,S,T,$;l=new fe({props:{class:"form-field required",name:"smtp.host",$$slots:{default:[UR,({uniqueId:A})=>({33:A}),({uniqueId:A})=>[0,A?4:0]]},$$scope:{ctx:n}}}),r=new fe({props:{class:"form-field required",name:"smtp.port",$$slots:{default:[VR,({uniqueId:A})=>({33:A}),({uniqueId:A})=>[0,A?4:0]]},$$scope:{ctx:n}}}),f=new fe({props:{class:"form-field",name:"smtp.username",$$slots:{default:[BR,({uniqueId:A})=>({33:A}),({uniqueId:A})=>[0,A?4:0]]},$$scope:{ctx:n}}}),m=new fe({props:{class:"form-field",name:"smtp.password",$$slots:{default:[WR,({uniqueId:A})=>({33:A}),({uniqueId:A})=>[0,A?4:0]]},$$scope:{ctx:n}}});function E(A,P){return A[5]?KR:YR}let M=E(n),L=M(n),I=n[5]&&yb(n);return{c(){e=b("div"),t=b("div"),i=b("div"),H(l.$$.fragment),s=C(),o=b("div"),H(r.$$.fragment),a=C(),u=b("div"),H(f.$$.fragment),c=C(),d=b("div"),H(m.$$.fragment),h=C(),g=b("button"),L.c(),_=C(),I&&I.c(),p(i,"class","col-lg-4"),p(o,"class","col-lg-2"),p(u,"class","col-lg-3"),p(d,"class","col-lg-3"),p(t,"class","grid"),p(g,"type","button"),p(g,"class","btn btn-sm btn-secondary m-t-sm m-b-sm")},m(A,P){v(A,e,P),w(e,t),w(t,i),F(l,i,null),w(t,s),w(t,o),F(r,o,null),w(t,a),w(t,u),F(f,u,null),w(t,c),w(t,d),F(m,d,null),w(e,h),w(e,g),L.m(g,null),w(e,_),I&&I.m(e,null),S=!0,T||($=B(g,"click",tt(n[22])),T=!0)},p(A,P){const R={};P[0]&1|P[1]&12&&(R.$$scope={dirty:P,ctx:A}),l.$set(R);const N={};P[0]&1|P[1]&12&&(N.$$scope={dirty:P,ctx:A}),r.$set(N);const U={};P[0]&1|P[1]&12&&(U.$$scope={dirty:P,ctx:A}),f.$set(U);const j={};P[0]&17|P[1]&12&&(j.$$scope={dirty:P,ctx:A}),m.$set(j),M!==(M=E(A))&&(L.d(1),L=M(A),L&&(L.c(),L.m(g,null))),A[5]?I?(I.p(A,P),P[0]&32&&O(I,1)):(I=yb(A),I.c(),O(I,1),I.m(e,null)):I&&(re(),D(I,1,1,()=>{I=null}),ae())},i(A){S||(O(l.$$.fragment,A),O(r.$$.fragment,A),O(f.$$.fragment,A),O(m.$$.fragment,A),O(I),A&&nt(()=>{S&&(y||(y=ze(e,vt,{duration:150},!0)),y.run(1))}),S=!0)},o(A){D(l.$$.fragment,A),D(r.$$.fragment,A),D(f.$$.fragment,A),D(m.$$.fragment,A),D(I),A&&(y||(y=ze(e,vt,{duration:150},!1)),y.run(0)),S=!1},d(A){A&&k(e),q(l),q(r),q(f),q(m),L.d(),I&&I.d(),A&&y&&y.end(),T=!1,$()}}}function UR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("SMTP server host"),l=C(),s=b("input"),p(e,"for",i=n[33]),p(s,"type","text"),p(s,"id",o=n[33]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].smtp.host),r||(a=B(s,"input",n[17]),r=!0)},p(u,f){f[1]&4&&i!==(i=u[33])&&p(e,"for",i),f[1]&4&&o!==(o=u[33])&&p(s,"id",o),f[0]&1&&s.value!==u[0].smtp.host&&ce(s,u[0].smtp.host)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function VR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Port"),l=C(),s=b("input"),p(e,"for",i=n[33]),p(s,"type","number"),p(s,"id",o=n[33]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].smtp.port),r||(a=B(s,"input",n[18]),r=!0)},p(u,f){f[1]&4&&i!==(i=u[33])&&p(e,"for",i),f[1]&4&&o!==(o=u[33])&&p(s,"id",o),f[0]&1&&St(s.value)!==u[0].smtp.port&&ce(s,u[0].smtp.port)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function BR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Username"),l=C(),s=b("input"),p(e,"for",i=n[33]),p(s,"type","text"),p(s,"id",o=n[33])},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[0].smtp.username),r||(a=B(s,"input",n[19]),r=!0)},p(u,f){f[1]&4&&i!==(i=u[33])&&p(e,"for",i),f[1]&4&&o!==(o=u[33])&&p(s,"id",o),f[0]&1&&s.value!==u[0].smtp.username&&ce(s,u[0].smtp.username)},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function WR(n){let e,t,i,l,s,o,r,a;function u(d){n[20](d)}function f(d){n[21](d)}let c={id:n[33]};return n[4]!==void 0&&(c.mask=n[4]),n[0].smtp.password!==void 0&&(c.value=n[0].smtp.password),s=new xu({props:c}),ie.push(()=>ve(s,"mask",u)),ie.push(()=>ve(s,"value",f)),{c(){e=b("label"),t=Y("Password"),l=C(),H(s.$$.fragment),p(e,"for",i=n[33])},m(d,m){v(d,e,m),w(e,t),v(d,l,m),F(s,d,m),a=!0},p(d,m){(!a||m[1]&4&&i!==(i=d[33]))&&p(e,"for",i);const h={};m[1]&4&&(h.id=d[33]),!o&&m[0]&16&&(o=!0,h.mask=d[4],$e(()=>o=!1)),!r&&m[0]&1&&(r=!0,h.value=d[0].smtp.password,$e(()=>r=!1)),s.$set(h)},i(d){a||(O(s.$$.fragment,d),a=!0)},o(d){D(s.$$.fragment,d),a=!1},d(d){d&&(k(e),k(l)),q(s,d)}}}function YR(n){let e,t,i;return{c(){e=b("span"),e.textContent="Show more options",t=C(),i=b("i"),p(e,"class","txt"),p(i,"class","ri-arrow-down-s-line")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},d(l){l&&(k(e),k(t),k(i))}}}function KR(n){let e,t,i;return{c(){e=b("span"),e.textContent="Hide more options",t=C(),i=b("i"),p(e,"class","txt"),p(i,"class","ri-arrow-up-s-line")},m(l,s){v(l,e,s),v(l,t,s),v(l,i,s)},d(l){l&&(k(e),k(t),k(i))}}}function yb(n){let e,t,i,l,s,o,r,a,u,f,c,d,m;return i=new fe({props:{class:"form-field",name:"smtp.tls",$$slots:{default:[JR,({uniqueId:h})=>({33:h}),({uniqueId:h})=>[0,h?4:0]]},$$scope:{ctx:n}}}),o=new fe({props:{class:"form-field",name:"smtp.authMethod",$$slots:{default:[ZR,({uniqueId:h})=>({33:h}),({uniqueId:h})=>[0,h?4:0]]},$$scope:{ctx:n}}}),u=new fe({props:{class:"form-field",name:"smtp.localName",$$slots:{default:[GR,({uniqueId:h})=>({33:h}),({uniqueId:h})=>[0,h?4:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),H(i.$$.fragment),l=C(),s=b("div"),H(o.$$.fragment),r=C(),a=b("div"),H(u.$$.fragment),f=C(),c=b("div"),p(t,"class","col-lg-3"),p(s,"class","col-lg-3"),p(a,"class","col-lg-6"),p(c,"class","col-lg-12"),p(e,"class","grid")},m(h,g){v(h,e,g),w(e,t),F(i,t,null),w(e,l),w(e,s),F(o,s,null),w(e,r),w(e,a),F(u,a,null),w(e,f),w(e,c),m=!0},p(h,g){const _={};g[0]&1|g[1]&12&&(_.$$scope={dirty:g,ctx:h}),i.$set(_);const y={};g[0]&1|g[1]&12&&(y.$$scope={dirty:g,ctx:h}),o.$set(y);const S={};g[0]&1|g[1]&12&&(S.$$scope={dirty:g,ctx:h}),u.$set(S)},i(h){m||(O(i.$$.fragment,h),O(o.$$.fragment,h),O(u.$$.fragment,h),h&&nt(()=>{m&&(d||(d=ze(e,vt,{duration:150},!0)),d.run(1))}),m=!0)},o(h){D(i.$$.fragment,h),D(o.$$.fragment,h),D(u.$$.fragment,h),h&&(d||(d=ze(e,vt,{duration:150},!1)),d.run(0)),m=!1},d(h){h&&k(e),q(i),q(o),q(u),h&&d&&d.end()}}}function JR(n){let e,t,i,l,s,o,r;function a(f){n[23](f)}let u={id:n[33],items:n[8]};return n[0].smtp.tls!==void 0&&(u.keyOfSelected=n[0].smtp.tls),s=new xn({props:u}),ie.push(()=>ve(s,"keyOfSelected",a)),{c(){e=b("label"),t=Y("TLS encryption"),l=C(),H(s.$$.fragment),p(e,"for",i=n[33])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[1]&4&&i!==(i=f[33]))&&p(e,"for",i);const d={};c[1]&4&&(d.id=f[33]),!o&&c[0]&1&&(o=!0,d.keyOfSelected=f[0].smtp.tls,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function ZR(n){let e,t,i,l,s,o,r;function a(f){n[24](f)}let u={id:n[33],items:n[9]};return n[0].smtp.authMethod!==void 0&&(u.keyOfSelected=n[0].smtp.authMethod),s=new xn({props:u}),ie.push(()=>ve(s,"keyOfSelected",a)),{c(){e=b("label"),t=Y("AUTH method"),l=C(),H(s.$$.fragment),p(e,"for",i=n[33])},m(f,c){v(f,e,c),w(e,t),v(f,l,c),F(s,f,c),r=!0},p(f,c){(!r||c[1]&4&&i!==(i=f[33]))&&p(e,"for",i);const d={};c[1]&4&&(d.id=f[33]),!o&&c[0]&1&&(o=!0,d.keyOfSelected=f[0].smtp.authMethod,$e(()=>o=!1)),s.$set(d)},i(f){r||(O(s.$$.fragment,f),r=!0)},o(f){D(s.$$.fragment,f),r=!1},d(f){f&&(k(e),k(l)),q(s,f)}}}function GR(n){let e,t,i,l,s,o,r,a,u,f;return{c(){e=b("label"),t=b("span"),t.textContent="EHLO/HELO domain",i=C(),l=b("i"),o=C(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[33]),p(r,"type","text"),p(r,"id",a=n[33]),p(r,"placeholder","Default to localhost")},m(c,d){v(c,e,d),w(e,t),w(e,i),w(e,l),v(c,o,d),v(c,r,d),ce(r,n[0].smtp.localName),u||(f=[Me(He.call(null,l,{text:"Some SMTP servers, such as the Gmail SMTP-relay, requires a proper domain name in the inital EHLO/HELO exchange and will reject attempts to use localhost.",position:"top"})),B(r,"input",n[25])],u=!0)},p(c,d){d[1]&4&&s!==(s=c[33])&&p(e,"for",s),d[1]&4&&a!==(a=c[33])&&p(r,"id",a),d[0]&1&&r.value!==c[0].smtp.localName&&ce(r,c[0].smtp.localName)},d(c){c&&(k(e),k(o),k(r)),u=!1,De(f)}}}function XR(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send test email',p(e,"type","button"),p(e,"class","btn btn-expanded btn-outline")},m(l,s){v(l,e,s),t||(i=B(e,"click",n[28]),t=!0)},p:te,d(l){l&&k(e),t=!1,i()}}}function QR(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=C(),l=b("button"),s=b("span"),s.textContent="Save changes",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"class","btn btn-expanded"),l.disabled=o=!n[6]||n[3],x(l,"btn-loading",n[3])},m(u,f){v(u,e,f),w(e,t),v(u,i,f),v(u,l,f),w(l,s),r||(a=[B(e,"click",n[26]),B(l,"click",n[27])],r=!0)},p(u,f){f[0]&8&&(e.disabled=u[3]),f[0]&72&&o!==(o=!u[6]||u[3])&&(l.disabled=o),f[0]&8&&x(l,"btn-loading",u[3])},d(u){u&&(k(e),k(i),k(l)),r=!1,De(a)}}}function xR(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;const y=[qR,FR],S=[];function T($,E){return $[2]?0:1}return d=T(n),m=S[d]=y[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=C(),s=b("div"),o=Y(n[7]),r=C(),a=b("div"),u=b("form"),f=b("div"),f.innerHTML="

    Configure common settings for sending emails.

    ",c=C(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(f,"class","content txt-xl m-b-base"),p(u,"class","panel"),p(u,"autocomplete","off"),p(a,"class","wrapper")},m($,E){v($,e,E),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),v($,r,E),v($,a,E),w(a,u),w(u,f),w(u,c),S[d].m(u,null),h=!0,g||(_=B(u,"submit",tt(n[29])),g=!0)},p($,E){(!h||E[0]&128)&&ue(o,$[7]);let M=d;d=T($),d===M?S[d].p($,E):(re(),D(S[M],1,1,()=>{S[M]=null}),ae(),m=S[d],m?m.p($,E):(m=S[d]=y[d]($),m.c()),O(m,1),m.m(u,null))},i($){h||(O(m),h=!0)},o($){D(m),h=!1},d($){$&&(k(e),k(r),k(a)),S[d].d(),g=!1,_()}}}function eF(n){let e,t,i,l,s,o;e=new ps({}),i=new di({props:{$$slots:{default:[xR]},$$scope:{ctx:n}}});let r={};return s=new tk({props:r}),n[30](s),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment),l=C(),H(s.$$.fragment)},m(a,u){F(e,a,u),v(a,t,u),F(i,a,u),v(a,l,u),F(s,a,u),o=!0},p(a,u){const f={};u[0]&255|u[1]&8&&(f.$$scope={dirty:u,ctx:a}),i.$set(f);const c={};s.$set(c)},i(a){o||(O(e.$$.fragment,a),O(i.$$.fragment,a),O(s.$$.fragment,a),o=!0)},o(a){D(e.$$.fragment,a),D(i.$$.fragment,a),D(s.$$.fragment,a),o=!1},d(a){a&&(k(t),k(l)),q(e,a),q(i,a),n[30](null),q(s,a)}}}function tF(n,e,t){let i,l,s;Qe(n,cn,oe=>t(7,s=oe));const o=[{label:"Auto (StartTLS)",value:!1},{label:"Always",value:!0}],r=[{label:"PLAIN (default)",value:"PLAIN"},{label:"LOGIN",value:"LOGIN"}];Nn(cn,s="Mail settings",s);let a,u={},f={},c=!1,d=!1,m=!1,h=!1;g();async function g(){t(2,c=!0);try{const oe=await me.settings.getAll()||{};y(oe)}catch(oe){me.error(oe)}t(2,c=!1)}async function _(){if(!(d||!l)){t(3,d=!0);try{const oe=await me.settings.update(z.filterRedactedProps(f));y(oe),Wt({}),tn("Successfully saved mail settings.")}catch(oe){me.error(oe)}t(3,d=!1)}}function y(oe={}){t(0,f={meta:(oe==null?void 0:oe.meta)||{},smtp:(oe==null?void 0:oe.smtp)||{}}),f.smtp.authMethod||t(0,f.smtp.authMethod=r[0].value,f),t(12,u=JSON.parse(JSON.stringify(f))),t(4,m=!!f.smtp.username)}function S(){t(0,f=JSON.parse(JSON.stringify(u||{})))}function T(){f.meta.senderName=this.value,t(0,f)}function $(){f.meta.senderAddress=this.value,t(0,f)}function E(){f.smtp.enabled=this.checked,t(0,f)}function M(){f.smtp.host=this.value,t(0,f)}function L(){f.smtp.port=St(this.value),t(0,f)}function I(){f.smtp.username=this.value,t(0,f)}function A(oe){m=oe,t(4,m)}function P(oe){n.$$.not_equal(f.smtp.password,oe)&&(f.smtp.password=oe,t(0,f))}const R=()=>{t(5,h=!h)};function N(oe){n.$$.not_equal(f.smtp.tls,oe)&&(f.smtp.tls=oe,t(0,f))}function U(oe){n.$$.not_equal(f.smtp.authMethod,oe)&&(f.smtp.authMethod=oe,t(0,f))}function j(){f.smtp.localName=this.value,t(0,f)}const V=()=>S(),K=()=>_(),J=()=>a==null?void 0:a.show(),ee=()=>_();function X(oe){ie[oe?"unshift":"push"](()=>{a=oe,t(1,a)})}return n.$$.update=()=>{n.$$.dirty[0]&4096&&t(13,i=JSON.stringify(u)),n.$$.dirty[0]&8193&&t(6,l=i!=JSON.stringify(f))},[f,a,c,d,m,h,l,s,o,r,_,S,u,i,T,$,E,M,L,I,A,P,R,N,U,j,V,K,J,ee,X]}class nF extends ye{constructor(e){super(),be(this,e,tF,eF,_e,{},null,[-1,-1])}}function iF(n){var L;let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;function y(I){n[11](I)}function S(I){n[12](I)}function T(I){n[13](I)}let $={toggleLabel:"Use S3 storage",originalConfig:n[0].s3,$$slots:{default:[sF]},$$scope:{ctx:n}};n[1].s3!==void 0&&($.config=n[1].s3),n[4]!==void 0&&($.isTesting=n[4]),n[5]!==void 0&&($.testError=n[5]),e=new uk({props:$}),ie.push(()=>ve(e,"config",y)),ie.push(()=>ve(e,"isTesting",S)),ie.push(()=>ve(e,"testError",T));let E=((L=n[1].s3)==null?void 0:L.enabled)&&!n[6]&&!n[3]&&vb(n),M=n[6]&&wb(n);return{c(){H(e.$$.fragment),s=C(),o=b("div"),r=b("div"),a=C(),E&&E.c(),u=C(),M&&M.c(),f=C(),c=b("button"),d=b("span"),d.textContent="Save changes",p(r,"class","flex-fill"),p(d,"class","txt"),p(c,"type","submit"),p(c,"class","btn btn-expanded"),c.disabled=m=!n[6]||n[3],x(c,"btn-loading",n[3]),p(o,"class","flex")},m(I,A){F(e,I,A),v(I,s,A),v(I,o,A),w(o,r),w(o,a),E&&E.m(o,null),w(o,u),M&&M.m(o,null),w(o,f),w(o,c),w(c,d),h=!0,g||(_=B(c,"click",n[15]),g=!0)},p(I,A){var R;const P={};A&1&&(P.originalConfig=I[0].s3),A&524291&&(P.$$scope={dirty:A,ctx:I}),!t&&A&2&&(t=!0,P.config=I[1].s3,$e(()=>t=!1)),!i&&A&16&&(i=!0,P.isTesting=I[4],$e(()=>i=!1)),!l&&A&32&&(l=!0,P.testError=I[5],$e(()=>l=!1)),e.$set(P),(R=I[1].s3)!=null&&R.enabled&&!I[6]&&!I[3]?E?E.p(I,A):(E=vb(I),E.c(),E.m(o,u)):E&&(E.d(1),E=null),I[6]?M?M.p(I,A):(M=wb(I),M.c(),M.m(o,f)):M&&(M.d(1),M=null),(!h||A&72&&m!==(m=!I[6]||I[3]))&&(c.disabled=m),(!h||A&8)&&x(c,"btn-loading",I[3])},i(I){h||(O(e.$$.fragment,I),h=!0)},o(I){D(e.$$.fragment,I),h=!1},d(I){I&&(k(s),k(o)),q(e,I),E&&E.d(),M&&M.d(),g=!1,_()}}}function lF(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function kb(n){var A;let e,t,i,l,s,o,r,a=(A=n[0].s3)!=null&&A.enabled?"S3 storage":"local file system",u,f,c,d=n[1].s3.enabled?"S3 storage":"local file system",m,h,g,_,y,S,T,$,E,M,L,I;return{c(){e=b("div"),t=b("div"),i=b("div"),i.innerHTML='',l=C(),s=b("div"),o=Y(`If you have existing uploaded files, you'll have to migrate them manually + from the + `),r=b("strong"),u=Y(a),f=Y(` + to the + `),c=b("strong"),m=Y(d),h=Y(`. + `),g=b("br"),_=Y(` + There are numerous command line tools that can help you, such as: + `),y=b("a"),y.textContent=`rclone + `,S=Y(`, + `),T=b("a"),T.textContent=`s5cmd + `,$=Y(", etc."),E=C(),M=b("div"),p(i,"class","icon"),p(y,"href","https://github.com/rclone/rclone"),p(y,"target","_blank"),p(y,"rel","noopener noreferrer"),p(y,"class","txt-bold"),p(T,"href","https://github.com/peak/s5cmd"),p(T,"target","_blank"),p(T,"rel","noopener noreferrer"),p(T,"class","txt-bold"),p(s,"class","content"),p(t,"class","alert alert-warning m-0"),p(M,"class","clearfix m-t-base")},m(P,R){v(P,e,R),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),w(s,r),w(r,u),w(s,f),w(s,c),w(c,m),w(s,h),w(s,g),w(s,_),w(s,y),w(s,S),w(s,T),w(s,$),w(e,E),w(e,M),I=!0},p(P,R){var N;(!I||R&1)&&a!==(a=(N=P[0].s3)!=null&&N.enabled?"S3 storage":"local file system")&&ue(u,a),(!I||R&2)&&d!==(d=P[1].s3.enabled?"S3 storage":"local file system")&&ue(m,d)},i(P){I||(P&&nt(()=>{I&&(L||(L=ze(e,vt,{duration:150},!0)),L.run(1))}),I=!0)},o(P){P&&(L||(L=ze(e,vt,{duration:150},!1)),L.run(0)),I=!1},d(P){P&&k(e),P&&L&&L.end()}}}function sF(n){var i;let e,t=((i=n[0].s3)==null?void 0:i.enabled)!=n[1].s3.enabled&&kb(n);return{c(){t&&t.c(),e=ge()},m(l,s){t&&t.m(l,s),v(l,e,s)},p(l,s){var o;((o=l[0].s3)==null?void 0:o.enabled)!=l[1].s3.enabled?t?(t.p(l,s),s&3&&O(t,1)):(t=kb(l),t.c(),O(t,1),t.m(e.parentNode,e)):t&&(re(),D(t,1,1,()=>{t=null}),ae())},d(l){l&&k(e),t&&t.d(l)}}}function vb(n){let e;function t(s,o){return s[4]?aF:s[5]?rF:oF}let i=t(n),l=i(n);return{c(){l.c(),e=ge()},m(s,o){l.m(s,o),v(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&k(e),l.d(s)}}}function oF(n){let e;return{c(){e=b("div"),e.innerHTML=' S3 connected successfully',p(e,"class","label label-sm label-success entrance-right")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function rF(n){let e,t,i,l;return{c(){e=b("div"),e.innerHTML=' Failed to establish S3 connection',p(e,"class","label label-sm label-warning entrance-right")},m(s,o){var r;v(s,e,o),i||(l=Me(t=He.call(null,e,(r=n[5].data)==null?void 0:r.message)),i=!0)},p(s,o){var r;t&&Rt(t.update)&&o&32&&t.update.call(null,(r=s[5].data)==null?void 0:r.message)},d(s){s&&k(e),i=!1,l()}}}function aF(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){v(t,e,i)},p:te,d(t){t&&k(e)}}}function wb(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Reset",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[3]},m(s,o){v(s,e,o),w(e,t),i||(l=B(e,"click",n[14]),i=!0)},p(s,o){o&8&&(e.disabled=s[3])},d(s){s&&k(e),i=!1,l()}}}function uF(n){let e,t,i,l,s,o,r,a,u,f,c,d,m,h,g,_;const y=[lF,iF],S=[];function T($,E){return $[2]?0:1}return d=T(n),m=S[d]=y[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=C(),s=b("div"),o=Y(n[7]),r=C(),a=b("div"),u=b("form"),f=b("div"),f.innerHTML="

    By default PocketBase uses the local file system to store uploaded files.

    If you have limited disk space, you could optionally connect to an S3 compatible storage.

    ",c=C(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(f,"class","content txt-xl m-b-base"),p(u,"class","panel"),p(u,"autocomplete","off"),p(a,"class","wrapper")},m($,E){v($,e,E),w(e,t),w(t,i),w(t,l),w(t,s),w(s,o),v($,r,E),v($,a,E),w(a,u),w(u,f),w(u,c),S[d].m(u,null),h=!0,g||(_=B(u,"submit",tt(n[16])),g=!0)},p($,E){(!h||E&128)&&ue(o,$[7]);let M=d;d=T($),d===M?S[d].p($,E):(re(),D(S[M],1,1,()=>{S[M]=null}),ae(),m=S[d],m?m.p($,E):(m=S[d]=y[d]($),m.c()),O(m,1),m.m(u,null))},i($){h||(O(m),h=!0)},o($){D(m),h=!1},d($){$&&(k(e),k(r),k(a)),S[d].d(),g=!1,_()}}}function fF(n){let e,t,i,l;return e=new ps({}),i=new di({props:{$$slots:{default:[uF]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment),t=C(),H(i.$$.fragment)},m(s,o){F(e,s,o),v(s,t,o),F(i,s,o),l=!0},p(s,[o]){const r={};o&524543&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(O(e.$$.fragment,s),O(i.$$.fragment,s),l=!0)},o(s){D(e.$$.fragment,s),D(i.$$.fragment,s),l=!1},d(s){s&&k(t),q(e,s),q(i,s)}}}const cF="s3_test_request";function dF(n,e,t){let i,l,s;Qe(n,cn,M=>t(7,s=M)),Nn(cn,s="Files storage",s);let o={},r={},a=!1,u=!1,f=!1,c=null;d();async function d(){t(2,a=!0);try{const M=await me.settings.getAll()||{};h(M)}catch(M){me.error(M)}t(2,a=!1)}async function m(){if(!(u||!l)){t(3,u=!0);try{me.cancelRequest(cF);const M=await me.settings.update(z.filterRedactedProps(r));Wt({}),await h(M),Ds(),c?lw("Successfully saved but failed to establish S3 connection."):tn("Successfully saved files storage settings.")}catch(M){me.error(M)}t(3,u=!1)}}async function h(M={}){t(1,r={s3:(M==null?void 0:M.s3)||{}}),t(0,o=JSON.parse(JSON.stringify(r)))}async function g(){t(1,r=JSON.parse(JSON.stringify(o||{})))}function _(M){n.$$.not_equal(r.s3,M)&&(r.s3=M,t(1,r))}function y(M){f=M,t(4,f)}function S(M){c=M,t(5,c)}const T=()=>g(),$=()=>m(),E=()=>m();return n.$$.update=()=>{n.$$.dirty&1&&t(10,i=JSON.stringify(o)),n.$$.dirty&1026&&t(6,l=i!=JSON.stringify(r))},[o,r,a,u,f,c,l,s,m,g,i,_,y,S,T,$,E]}class pF extends ye{constructor(e){super(),be(this,e,dF,fF,_e,{})}}function Sb(n){let e,t,i,l,s;return{c(){e=Y("("),t=Y(n[1]),i=Y("/"),l=Y(n[2]),s=Y(")")},m(o,r){v(o,e,r),v(o,t,r),v(o,i,r),v(o,l,r),v(o,s,r)},p(o,r){r&2&&ue(t,o[1]),r&4&&ue(l,o[2])},d(o){o&&(k(e),k(t),k(i),k(l),k(s))}}}function mF(n){let e,t,i,l;const s=[bF,gF],o=[];function r(a,u){return a[4]?1:0}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ge()},m(a,u){o[e].m(a,u),v(a,i,u),l=!0},p(a,u){let f=e;e=r(a),e===f?o[e].p(a,u):(re(),D(o[f],1,1,()=>{o[f]=null}),ae(),t=o[e],t?t.p(a,u):(t=o[e]=s[e](a),t.c()),O(t,1),t.m(i.parentNode,i))},i(a){l||(O(t),l=!0)},o(a){D(t),l=!1},d(a){a&&k(i),o[e].d(a)}}}function hF(n){let e,t,i,l,s,o,r,a=n[2]>1?"Next":"Login",u,f,c,d,m,h;return t=new fe({props:{class:"form-field required",name:"identity",$$slots:{default:[vF,({uniqueId:g})=>({25:g}),({uniqueId:g})=>g?33554432:0]},$$scope:{ctx:n}}}),l=new fe({props:{class:"form-field required",name:"password",$$slots:{default:[wF,({uniqueId:g})=>({25:g}),({uniqueId:g})=>g?33554432:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),H(t.$$.fragment),i=C(),H(l.$$.fragment),s=C(),o=b("button"),r=b("span"),u=Y(a),f=C(),c=b("i"),p(r,"class","txt"),p(c,"class","ri-arrow-right-line"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block btn-next"),x(o,"btn-disabled",n[7]),x(o,"btn-loading",n[7]),p(e,"class","block")},m(g,_){v(g,e,_),F(t,e,null),w(e,i),F(l,e,null),w(e,s),w(e,o),w(o,r),w(r,u),w(o,f),w(o,c),d=!0,m||(h=B(e,"submit",tt(n[13])),m=!0)},p(g,_){const y={};_&100663329&&(y.$$scope={dirty:_,ctx:g}),t.$set(y);const S={};_&100663360&&(S.$$scope={dirty:_,ctx:g}),l.$set(S),(!d||_&4)&&a!==(a=g[2]>1?"Next":"Login")&&ue(u,a),(!d||_&128)&&x(o,"btn-disabled",g[7]),(!d||_&128)&&x(o,"btn-loading",g[7])},i(g){d||(O(t.$$.fragment,g),O(l.$$.fragment,g),d=!0)},o(g){D(t.$$.fragment,g),D(l.$$.fragment,g),d=!1},d(g){g&&k(e),q(t),q(l),m=!1,h()}}}function _F(n){let e;return{c(){e=b("div"),e.innerHTML='',p(e,"class","block txt-center")},m(t,i){v(t,e,i)},p:te,i:te,o:te,d(t){t&&k(e)}}}function gF(n){let e,t,i,l,s,o,r,a,u,f,c,d,m=n[11]&&Tb(n);return i=new fe({props:{class:"form-field required",name:"password",$$slots:{default:[yF,({uniqueId:h})=>({25:h}),({uniqueId:h})=>h?33554432:0]},$$scope:{ctx:n}}}),{c(){m&&m.c(),e=C(),t=b("form"),H(i.$$.fragment),l=C(),s=b("button"),s.innerHTML='Login ',o=C(),r=b("div"),a=b("button"),u=Y("Request another OTP"),p(s,"type","submit"),p(s,"class","btn btn-lg btn-block btn-next"),x(s,"btn-disabled",n[9]),x(s,"btn-loading",n[9]),p(t,"class","block"),p(a,"type","button"),p(a,"class","link-hint"),a.disabled=n[9],p(r,"class","content txt-center m-t-sm")},m(h,g){m&&m.m(h,g),v(h,e,g),v(h,t,g),F(i,t,null),w(t,l),w(t,s),v(h,o,g),v(h,r,g),w(r,a),w(a,u),f=!0,c||(d=[B(t,"submit",tt(n[15])),B(a,"click",n[20])],c=!0)},p(h,g){h[11]?m?m.p(h,g):(m=Tb(h),m.c(),m.m(e.parentNode,e)):m&&(m.d(1),m=null);const _={};g&100667392&&(_.$$scope={dirty:g,ctx:h}),i.$set(_),(!f||g&512)&&x(s,"btn-disabled",h[9]),(!f||g&512)&&x(s,"btn-loading",h[9]),(!f||g&512)&&(a.disabled=h[9])},i(h){f||(O(i.$$.fragment,h),f=!0)},o(h){D(i.$$.fragment,h),f=!1},d(h){h&&(k(e),k(t),k(o),k(r)),m&&m.d(h),q(i),c=!1,De(d)}}}function bF(n){let e,t,i,l,s,o,r;return t=new fe({props:{class:"form-field required",name:"email",$$slots:{default:[kF,({uniqueId:a})=>({25:a}),({uniqueId:a})=>a?33554432:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),H(t.$$.fragment),i=C(),l=b("button"),l.innerHTML=' Send OTP',p(l,"type","submit"),p(l,"class","btn btn-lg btn-block btn-next"),x(l,"btn-disabled",n[8]),x(l,"btn-loading",n[8]),p(e,"class","block")},m(a,u){v(a,e,u),F(t,e,null),w(e,i),w(e,l),s=!0,o||(r=B(e,"submit",tt(n[14])),o=!0)},p(a,u){const f={};u&100665344&&(f.$$scope={dirty:u,ctx:a}),t.$set(f),(!s||u&256)&&x(l,"btn-disabled",a[8]),(!s||u&256)&&x(l,"btn-loading",a[8])},i(a){s||(O(t.$$.fragment,a),s=!0)},o(a){D(t.$$.fragment,a),s=!1},d(a){a&&k(e),q(t),o=!1,r()}}}function Tb(n){let e,t,i,l,s,o;return{c(){e=b("div"),t=b("p"),i=Y("Check your "),l=b("strong"),s=Y(n[11]),o=Y(` inbox and enter in the input below the received + One-time password (OTP).`),p(e,"class","content txt-center m-b-sm")},m(r,a){v(r,e,a),w(e,t),w(t,i),w(t,l),w(l,s),w(t,o)},p(r,a){a&2048&&ue(s,r[11])},d(r){r&&k(e)}}}function yF(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("One-time password"),l=C(),s=b("input"),p(e,"for",i=n[25]),p(s,"type","password"),p(s,"id",o=n[25]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[12]),r||(a=B(s,"input",n[19]),r=!0)},p(u,f){f&33554432&&i!==(i=u[25])&&p(e,"for",i),f&33554432&&o!==(o=u[25])&&p(s,"id",o),f&4096&&s.value!==u[12]&&ce(s,u[12])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function kF(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=Y("Email"),l=C(),s=b("input"),p(e,"for",i=n[25]),p(s,"type","email"),p(s,"id",o=n[25]),s.required=!0},m(u,f){v(u,e,f),w(e,t),v(u,l,f),v(u,s,f),ce(s,n[11]),r||(a=B(s,"input",n[18]),r=!0)},p(u,f){f&33554432&&i!==(i=u[25])&&p(e,"for",i),f&33554432&&o!==(o=u[25])&&p(s,"id",o),f&2048&&s.value!==u[11]&&ce(s,u[11])},d(u){u&&(k(e),k(l),k(s)),r=!1,a()}}}function vF(n){let e,t=z.sentenize(n[0].password.identityFields.join(" or "),!1)+"",i,l,s,o,r,a,u,f;return{c(){e=b("label"),i=Y(t),s=C(),o=b("input"),p(e,"for",l=n[25]),p(o,"id",r=n[25]),p(o,"type",a=n[0].password.identityFields.length==1&&n[0].password.identityFields[0]=="email"?"email":"text"),o.value=n[5],o.required=!0,o.autofocus=!0},m(c,d){v(c,e,d),w(e,i),v(c,s,d),v(c,o,d),o.focus(),u||(f=B(o,"input",n[16]),u=!0)},p(c,d){d&1&&t!==(t=z.sentenize(c[0].password.identityFields.join(" or "),!1)+"")&&ue(i,t),d&33554432&&l!==(l=c[25])&&p(e,"for",l),d&33554432&&r!==(r=c[25])&&p(o,"id",r),d&1&&a!==(a=c[0].password.identityFields.length==1&&c[0].password.identityFields[0]=="email"?"email":"text")&&p(o,"type",a),d&32&&o.value!==c[5]&&(o.value=c[5])},d(c){c&&(k(e),k(s),k(o)),u=!1,f()}}}function wF(n){let e,t,i,l,s,o,r,a,u,f,c;return{c(){e=b("label"),t=Y("Password"),l=C(),s=b("input"),r=C(),a=b("div"),u=b("a"),u.textContent="Forgotten password?",p(e,"for",i=n[25]),p(s,"type","password"),p(s,"id",o=n[25]),s.required=!0,p(u,"href","/request-password-reset"),p(u,"class","link-hint"),p(a,"class","help-block")},m(d,m){v(d,e,m),w(e,t),v(d,l,m),v(d,s,m),ce(s,n[6]),v(d,r,m),v(d,a,m),w(a,u),f||(c=[B(s,"input",n[17]),Me(Un.call(null,u))],f=!0)},p(d,m){m&33554432&&i!==(i=d[25])&&p(e,"for",i),m&33554432&&o!==(o=d[25])&&p(s,"id",o),m&64&&s.value!==d[6]&&ce(s,d[6])},d(d){d&&(k(e),k(l),k(s),k(r),k(a)),f=!1,De(c)}}}function SF(n){let e,t,i,l,s,o,r,a,u=n[2]>1&&Sb(n);const f=[_F,hF,mF],c=[];function d(m,h){return m[10]?0:m[0].password.enabled&&!m[3]?1:m[0].otp.enabled?2:-1}return~(s=d(n))&&(o=c[s]=f[s](n)),{c(){e=b("div"),t=b("h4"),i=Y(`Superuser login + `),u&&u.c(),l=C(),o&&o.c(),r=ge(),p(e,"class","content txt-center m-b-base")},m(m,h){v(m,e,h),w(e,t),w(t,i),u&&u.m(t,null),v(m,l,h),~s&&c[s].m(m,h),v(m,r,h),a=!0},p(m,h){m[2]>1?u?u.p(m,h):(u=Sb(m),u.c(),u.m(t,null)):u&&(u.d(1),u=null);let g=s;s=d(m),s===g?~s&&c[s].p(m,h):(o&&(re(),D(c[g],1,1,()=>{c[g]=null}),ae()),~s?(o=c[s],o?o.p(m,h):(o=c[s]=f[s](m),o.c()),O(o,1),o.m(r.parentNode,r)):o=null)},i(m){a||(O(o),a=!0)},o(m){D(o),a=!1},d(m){m&&(k(e),k(l),k(r)),u&&u.d(),~s&&c[s].d(m)}}}function TF(n){let e,t;return e=new ty({props:{$$slots:{default:[SF]},$$scope:{ctx:n}}}),{c(){H(e.$$.fragment)},m(i,l){F(e,i,l),t=!0},p(i,[l]){const s={};l&67117055&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(O(e.$$.fragment,i),t=!0)},o(i){D(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function $F(n,e,t){let i;Qe(n,Lu,N=>t(22,i=N));const l=new URLSearchParams(i);let s=l.get("demoEmail")||"",o=l.get("demoPassword")||"",r={},a=1,u=1,f=!1,c=!1,d=!1,m=!1,h="",g="",_="",y="",S="";T();async function T(){if(!m){t(10,m=!0);try{t(0,r=await me.collection("_superusers").listAuthMethods())}catch(N){me.error(N)}t(10,m=!1)}}async function $(){var N,U;if(!f){t(7,f=!0);try{await me.collection("_superusers").authWithPassword(s,o),Ds(),Wt({}),Il("/")}catch(j){j.status==401?(t(3,h=j.response.mfaId),((U=(N=r==null?void 0:r.password)==null?void 0:N.identityFields)==null?void 0:U.length)==1&&r.password.identityFields[0]=="email"?(t(11,y=s),await E()):/^[^@\s]+@[^@\s]+$/.test(s)&&t(11,y=s)):j.status!=400?me.error(j):$i("Invalid login credentials.")}t(7,f=!1)}}async function E(){if(!c){t(8,c=!0);try{const N=await me.collection("_superusers").requestOTP(y);t(4,g=N.otpId),_=g,Ds(),Wt({})}catch(N){N.status==429&&t(4,g=_),me.error(N)}t(8,c=!1)}}async function M(){if(!d){t(9,d=!0);try{await me.collection("_superusers").authWithOTP(g,S,{mfaId:h}),Ds(),Wt({}),Il("/")}catch(N){me.error(N)}t(9,d=!1)}}const L=N=>{t(5,s=N.target.value)};function I(){o=this.value,t(6,o)}function A(){y=this.value,t(11,y)}function P(){S=this.value,t(12,S)}const R=()=>{t(4,g="")};return n.$$.update=()=>{var N,U;n.$$.dirty&31&&(t(2,u=1),t(1,a=1),(N=r==null?void 0:r.mfa)!=null&&N.enabled&&t(2,u++,u),(U=r==null?void 0:r.otp)!=null&&U.enabled&&t(2,u++,u),h!=""&&t(1,a++,a),g!=""&&t(1,a++,a))},[r,a,u,h,g,s,o,f,c,d,m,y,S,$,E,M,L,I,A,P,R]}class CF extends ye{constructor(e){super(),be(this,e,$F,TF,_e,{})}}function Qt(n){if(!n)throw Error("Parameter args is required");if(!n.component==!n.asyncComponent)throw Error("One and only one of component and asyncComponent is required");if(n.component&&(n.asyncComponent=()=>Promise.resolve(n.component)),typeof n.asyncComponent!="function")throw Error("Parameter asyncComponent must be a function");if(n.conditions){Array.isArray(n.conditions)||(n.conditions=[n.conditions]);for(let t=0;t{const e=new URLSearchParams(window.location.search);return n.location!=="/"&&e.has("pbinstal")?Il("/"):!0}],OF={"/login":Qt({component:CF,conditions:on.concat([n=>!me.authStore.isValid]),userData:{showAppSidebar:!1}}),"/request-password-reset":Qt({asyncComponent:()=>Ot(()=>import("./PageSuperuserRequestPasswordReset-S4Qbbggx.js"),[],import.meta.url),conditions:on.concat([n=>!me.authStore.isValid]),userData:{showAppSidebar:!1}}),"/confirm-password-reset/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageSuperuserConfirmPasswordReset-jxr0GY89.js"),[],import.meta.url),conditions:on.concat([n=>!me.authStore.isValid]),userData:{showAppSidebar:!1}}),"/collections":Qt({component:vN,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/logs":Qt({component:g5,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings":Qt({component:h7,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/mail":Qt({component:nF,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/storage":Qt({component:pF,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/export-collections":Qt({component:_R,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/import-collections":Qt({component:RR,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/backups":Qt({component:aR,conditions:on.concat([n=>me.authStore.isValid]),userData:{showAppSidebar:!0}}),"/users/confirm-password-reset/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmPasswordReset-BZpXiFAY.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/auth/confirm-password-reset/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmPasswordReset-BZpXiFAY.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/users/confirm-verification/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmVerification-O66HL0pH.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/auth/confirm-verification/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmVerification-O66HL0pH.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/users/confirm-email-change/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmEmailChange-DoQqYIxa.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/auth/confirm-email-change/:token":Qt({asyncComponent:()=>Ot(()=>import("./PageRecordConfirmEmailChange-DoQqYIxa.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/auth/oauth2-redirect-success":Qt({asyncComponent:()=>Ot(()=>import("./PageOAuth2RedirectSuccess-B4oH6pbq.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"/auth/oauth2-redirect-failure":Qt({asyncComponent:()=>Ot(()=>import("./PageOAuth2RedirectFailure-Bj9LYgF_.js"),[],import.meta.url),conditions:on,userData:{showAppSidebar:!1}}),"*":Qt({component:r3,userData:{showAppSidebar:!1}})};function EF(n){let e;return{c(){e=b("link"),p(e,"rel","shortcut icon"),p(e,"type","image/png"),p(e,"href","/images/favicon/favicon_prod.png")},m(t,i){v(t,e,i)},d(t){t&&k(e)}}}function $b(n){let e,t,i,l,s,o,r,a,u,f,c,d,m=z.getInitials(n[0].email)+"",h,g,_,y,S,T,$;return _=new Hn({props:{class:"dropdown dropdown-nowrap dropdown-upside dropdown-left",$$slots:{default:[MF]},$$scope:{ctx:n}}}),{c(){e=b("aside"),t=b("a"),t.innerHTML='PocketBase logo',i=C(),l=b("nav"),s=b("a"),s.innerHTML='',o=C(),r=b("a"),r.innerHTML='',a=C(),u=b("a"),u.innerHTML='',f=C(),c=b("div"),d=b("span"),h=Y(m),g=C(),H(_.$$.fragment),p(t,"href","/"),p(t,"class","logo logo-sm"),p(s,"href","/collections"),p(s,"class","menu-item"),p(s,"aria-label","Collections"),p(r,"href","/logs"),p(r,"class","menu-item"),p(r,"aria-label","Logs"),p(u,"href","/settings"),p(u,"class","menu-item"),p(u,"aria-label","Settings"),p(l,"class","main-menu"),p(d,"class","initials"),p(c,"tabindex","0"),p(c,"role","button"),p(c,"aria-label","Logged superuser menu"),p(c,"class","thumb thumb-circle link-hint"),p(c,"title",y=n[0].email),p(e,"class","app-sidebar")},m(E,M){v(E,e,M),w(e,t),w(e,i),w(e,l),w(l,s),w(l,o),w(l,r),w(l,a),w(l,u),w(e,f),w(e,c),w(c,d),w(d,h),w(c,g),F(_,c,null),S=!0,T||($=[Me(Un.call(null,t)),Me(Un.call(null,s)),Me(Ri.call(null,s,{path:"/collections/?.*",className:"current-route"})),Me(He.call(null,s,{text:"Collections",position:"right"})),Me(Un.call(null,r)),Me(Ri.call(null,r,{path:"/logs/?.*",className:"current-route"})),Me(He.call(null,r,{text:"Logs",position:"right"})),Me(Un.call(null,u)),Me(Ri.call(null,u,{path:"/settings/?.*",className:"current-route"})),Me(He.call(null,u,{text:"Settings",position:"right"}))],T=!0)},p(E,M){(!S||M&1)&&m!==(m=z.getInitials(E[0].email)+"")&&ue(h,m);const L={};M&4097&&(L.$$scope={dirty:M,ctx:E}),_.$set(L),(!S||M&1&&y!==(y=E[0].email))&&p(c,"title",y)},i(E){S||(O(_.$$.fragment,E),S=!0)},o(E){D(_.$$.fragment,E),S=!1},d(E){E&&k(e),q(_),T=!1,De($)}}}function MF(n){let e,t=n[0].email+"",i,l,s,o,r,a,u,f,c,d;return{c(){e=b("div"),i=Y(t),s=C(),o=b("hr"),r=C(),a=b("a"),a.innerHTML=' Manage superusers',u=C(),f=b("button"),f.innerHTML=' Logout',p(e,"class","txt-ellipsis current-superuser svelte-1ahgi3o"),p(e,"title",l=n[0].email),p(a,"href","/collections?collectionId=_pbc_3323866339"),p(a,"class","dropdown-item closable"),p(a,"role","menuitem"),p(f,"type","button"),p(f,"class","dropdown-item closable"),p(f,"role","menuitem")},m(m,h){v(m,e,h),w(e,i),v(m,s,h),v(m,o,h),v(m,r,h),v(m,a,h),v(m,u,h),v(m,f,h),c||(d=[Me(Un.call(null,a)),B(f,"click",n[7])],c=!0)},p(m,h){h&1&&t!==(t=m[0].email+"")&&ue(i,t),h&1&&l!==(l=m[0].email)&&p(e,"title",l)},d(m){m&&(k(e),k(s),k(o),k(r),k(a),k(u),k(f)),c=!1,De(d)}}}function Cb(n){let e,t,i;return t=new Tu({props:{conf:z.defaultEditorOptions()}}),t.$on("init",n[8]),{c(){e=b("div"),H(t.$$.fragment),p(e,"class","tinymce-preloader hidden")},m(l,s){v(l,e,s),F(t,e,null),i=!0},p:te,i(l){i||(O(t.$$.fragment,l),i=!0)},o(l){D(t.$$.fragment,l),i=!1},d(l){l&&k(e),q(t)}}}function DF(n){var S;let e,t,i,l,s,o,r,a,u,f,c,d,m,h;document.title=e=z.joinNonEmpty([n[4],n[3],"PocketBase"]," - ");let g=window.location.protocol=="https:"&&EF(),_=((S=n[0])==null?void 0:S.id)&&n[1]&&$b(n);r=new Fw({props:{routes:OF}}),r.$on("routeLoading",n[5]),r.$on("conditionsFailed",n[6]),u=new cw({}),c=new G2({});let y=n[1]&&!n[2]&&Cb(n);return{c(){g&&g.c(),t=ge(),i=C(),l=b("div"),_&&_.c(),s=C(),o=b("div"),H(r.$$.fragment),a=C(),H(u.$$.fragment),f=C(),H(c.$$.fragment),d=C(),y&&y.c(),m=ge(),p(o,"class","app-body"),p(l,"class","app-layout")},m(T,$){g&&g.m(document.head,null),w(document.head,t),v(T,i,$),v(T,l,$),_&&_.m(l,null),w(l,s),w(l,o),F(r,o,null),w(o,a),F(u,o,null),v(T,f,$),F(c,T,$),v(T,d,$),y&&y.m(T,$),v(T,m,$),h=!0},p(T,[$]){var E;(!h||$&24)&&e!==(e=z.joinNonEmpty([T[4],T[3],"PocketBase"]," - "))&&(document.title=e),(E=T[0])!=null&&E.id&&T[1]?_?(_.p(T,$),$&3&&O(_,1)):(_=$b(T),_.c(),O(_,1),_.m(l,s)):_&&(re(),D(_,1,1,()=>{_=null}),ae()),T[1]&&!T[2]?y?(y.p(T,$),$&6&&O(y,1)):(y=Cb(T),y.c(),O(y,1),y.m(m.parentNode,m)):y&&(re(),D(y,1,1,()=>{y=null}),ae())},i(T){h||(O(_),O(r.$$.fragment,T),O(u.$$.fragment,T),O(c.$$.fragment,T),O(y),h=!0)},o(T){D(_),D(r.$$.fragment,T),D(u.$$.fragment,T),D(c.$$.fragment,T),D(y),h=!1},d(T){T&&(k(i),k(l),k(f),k(d),k(m)),g&&g.d(T),k(t),_&&_.d(),q(r),q(u),q(c,T),y&&y.d(T)}}}function IF(n,e,t){let i,l,s,o;Qe(n,Dl,g=>t(10,i=g)),Qe(n,_r,g=>t(3,l=g)),Qe(n,Fr,g=>t(0,s=g)),Qe(n,cn,g=>t(4,o=g));let r,a=!1,u=!1;function f(g){var _,y,S,T;((_=g==null?void 0:g.detail)==null?void 0:_.location)!==r&&(t(1,a=!!((S=(y=g==null?void 0:g.detail)==null?void 0:y.userData)!=null&&S.showAppSidebar)),r=(T=g==null?void 0:g.detail)==null?void 0:T.location,Nn(cn,o="",o),Wt({}),V0())}function c(){Il("/")}async function d(){var g,_;if(s!=null&&s.id)try{const y=await me.settings.getAll({$cancelKey:"initialAppSettings"});Nn(_r,l=((g=y==null?void 0:y.meta)==null?void 0:g.appName)||"",l),Nn(Dl,i=!!((_=y==null?void 0:y.meta)!=null&&_.hideControls),i)}catch(y){y!=null&&y.isAbort||console.warn("Failed to load app settings.",y)}}function m(){me.logout()}const h=()=>{t(2,u=!0)};return n.$$.update=()=>{n.$$.dirty&1&&s!=null&&s.id&&d()},[s,a,u,l,o,f,c,m,h]}class LF extends ye{constructor(e){super(),be(this,e,IF,DF,_e,{})}}new LF({target:document.getElementById("app")});export{De as A,tn as B,z as C,Il as D,ge as E,ty as F,fo as G,xl as H,Cu as I,Qe as J,Nn as K,Yt as L,cn as M,En as N,_t as O,ie as P,fP as Q,Qu as R,ye as S,pe as T,yt as U,ci as V,zt as W,pt as X,jt as Y,Sk as Z,D as a,C as b,H as c,q as d,b as e,p as f,v as g,w as h,be as i,Me as j,re as k,Un as l,F as m,ae as n,k as o,me as p,fe as q,x as r,_e as s,O as t,B as u,tt as v,Y as w,ue as x,te as y,ce as z}; diff --git a/ui/dist/assets/index-B5ReTu-C.js b/ui/dist/assets/index-B5ReTu-C.js new file mode 100644 index 00000000..1e5e46a8 --- /dev/null +++ b/ui/dist/assets/index-B5ReTu-C.js @@ -0,0 +1,14 @@ +class V{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,e,i){[t,e]=Ue(this,t,e);let n=[];return this.decompose(0,t,n,2),i.length&&i.decompose(0,i.length,n,3),this.decompose(e,this.length,n,1),Gt.from(n,this.length-(e-t)+i.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,e=this.length){[t,e]=Ue(this,t,e);let i=[];return this.decompose(t,e,i,0),Gt.from(i,e-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let e=this.scanIdentical(t,1),i=this.length-this.scanIdentical(t,-1),n=new mi(this),r=new mi(t);for(let o=e,l=e;;){if(n.next(o),r.next(o),o=0,n.lineBreak!=r.lineBreak||n.done!=r.done||n.value!=r.value)return!1;if(l+=n.value.length,n.done||l>=i)return!0}}iter(t=1){return new mi(this,t)}iterRange(t,e=this.length){return new Ol(this,t,e)}iterLines(t,e){let i;if(t==null)i=this.iter();else{e==null&&(e=this.lines+1);let n=this.line(t).from;i=this.iterRange(n,Math.max(n,e==this.lines+1?this.length:e<=1?0:this.line(e-1).to))}return new Tl(i)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(t.length==0)throw new RangeError("A document must have at least one line");return t.length==1&&!t[0]?V.empty:t.length<=32?new _(t):Gt.from(_.split(t,[]))}}class _ extends V{constructor(t,e=Sc(t)){super(),this.text=t,this.length=e}get lines(){return this.text.length}get children(){return null}lineInner(t,e,i,n){for(let r=0;;r++){let o=this.text[r],l=n+o.length;if((e?i:l)>=t)return new kc(n,l,i,o);n=l+1,i++}}decompose(t,e,i,n){let r=t<=0&&e>=this.length?this:new _(jr(this.text,t,e),Math.min(e,this.length)-Math.max(0,t));if(n&1){let o=i.pop(),l=an(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new _(l,o.length+r.length));else{let a=l.length>>1;i.push(new _(l.slice(0,a)),new _(l.slice(a)))}}else i.push(r)}replace(t,e,i){if(!(i instanceof _))return super.replace(t,e,i);[t,e]=Ue(this,t,e);let n=an(this.text,an(i.text,jr(this.text,0,t)),e),r=this.length+i.length-(e-t);return n.length<=32?new _(n,r):Gt.from(_.split(n,[]),r)}sliceString(t,e=this.length,i=` +`){[t,e]=Ue(this,t,e);let n="";for(let r=0,o=0;r<=e&&ot&&o&&(n+=i),tr&&(n+=l.slice(Math.max(0,t-r),e-r)),r=a+1}return n}flatten(t){for(let e of this.text)t.push(e)}scanIdentical(){return 0}static split(t,e){let i=[],n=-1;for(let r of t)i.push(r),n+=r.length+1,i.length==32&&(e.push(new _(i,n)),i=[],n=-1);return n>-1&&e.push(new _(i,n)),e}}class Gt extends V{constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;for(let i of t)this.lines+=i.lines}lineInner(t,e,i,n){for(let r=0;;r++){let o=this.children[r],l=n+o.length,a=i+o.lines-1;if((e?a:l)>=t)return o.lineInner(t,e,i,n);n=l+1,i=a+1}}decompose(t,e,i,n){for(let r=0,o=0;o<=e&&r=o){let c=n&((o<=t?1:0)|(a>=e?2:0));o>=t&&a<=e&&!c?i.push(l):l.decompose(t-o,e-o,i,c)}o=a+1}}replace(t,e,i){if([t,e]=Ue(this,t,e),i.lines=r&&e<=l){let a=o.replace(t-r,e-r,i),c=this.lines-o.lines+a.lines;if(a.lines>4&&a.lines>c>>6){let h=this.children.slice();return h[n]=a,new Gt(h,this.length-(e-t)+i.length)}return super.replace(r,l,a)}r=l+1}return super.replace(t,e,i)}sliceString(t,e=this.length,i=` +`){[t,e]=Ue(this,t,e);let n="";for(let r=0,o=0;rt&&r&&(n+=i),to&&(n+=l.sliceString(t-o,e-o,i)),o=a+1}return n}flatten(t){for(let e of this.children)e.flatten(t)}scanIdentical(t,e){if(!(t instanceof Gt))return 0;let i=0,[n,r,o,l]=e>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;n+=e,r+=e){if(n==o||r==l)return i;let a=this.children[n],c=t.children[r];if(a!=c)return i+a.scanIdentical(c,e);i+=a.length+1}}static from(t,e=t.reduce((i,n)=>i+n.length+1,-1)){let i=0;for(let d of t)i+=d.lines;if(i<32){let d=[];for(let p of t)p.flatten(d);return new _(d,e)}let n=Math.max(32,i>>5),r=n<<1,o=n>>1,l=[],a=0,c=-1,h=[];function f(d){let p;if(d.lines>r&&d instanceof Gt)for(let g of d.children)f(g);else d.lines>o&&(a>o||!a)?(u(),l.push(d)):d instanceof _&&a&&(p=h[h.length-1])instanceof _&&d.lines+p.lines<=32?(a+=d.lines,c+=d.length+1,h[h.length-1]=new _(p.text.concat(d.text),p.length+1+d.length)):(a+d.lines>n&&u(),a+=d.lines,c+=d.length+1,h.push(d))}function u(){a!=0&&(l.push(h.length==1?h[0]:Gt.from(h,c)),c=-1,a=h.length=0)}for(let d of t)f(d);return u(),l.length==1?l[0]:new Gt(l,e)}}V.empty=new _([""],0);function Sc(s){let t=-1;for(let e of s)t+=e.length+1;return t}function an(s,t,e=0,i=1e9){for(let n=0,r=0,o=!0;r=e&&(a>i&&(l=l.slice(0,i-n)),n0?1:(t instanceof _?t.text.length:t.children.length)<<1]}nextInner(t,e){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,n=this.nodes[i],r=this.offsets[i],o=r>>1,l=n instanceof _?n.text.length:n.children.length;if(o==(e>0?l:0)){if(i==0)return this.done=!0,this.value="",this;e>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(e>0?0:1)){if(this.offsets[i]+=e,t==0)return this.lineBreak=!0,this.value=` +`,this;t--}else if(n instanceof _){let a=n.text[o+(e<0?-1:0)];if(this.offsets[i]+=e,a.length>Math.max(0,t))return this.value=t==0?a:e>0?a.slice(t):a.slice(0,a.length-t),this;t-=a.length}else{let a=n.children[o+(e<0?-1:0)];t>a.length?(t-=a.length,this.offsets[i]+=e):(e<0&&this.offsets[i]--,this.nodes.push(a),this.offsets.push(e>0?1:(a instanceof _?a.text.length:a.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class Ol{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new mi(t,e>i?-1:1),this.pos=e>i?t.length:0,this.from=Math.min(e,i),this.to=Math.max(e,i)}nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,e<0?this.pos-this.to:this.from-this.pos);let i=e<0?this.pos-this.from:this.to-this.pos;t>i&&(t=i),i-=t;let{value:n}=this.cursor.next(t);return this.pos+=(n.length+t)*e,this.value=n.length<=i?n:e<0?n.slice(n.length-i):n.slice(0,i),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class Tl{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e&&this.afterBreak?(this.value="",this.afterBreak=!1):e?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=n,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(V.prototype[Symbol.iterator]=function(){return this.iter()},mi.prototype[Symbol.iterator]=Ol.prototype[Symbol.iterator]=Tl.prototype[Symbol.iterator]=function(){return this});class kc{constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}get length(){return this.to-this.from}}function Ue(s,t,e){return t=Math.max(0,Math.min(s.length,t)),[t,Math.max(t,Math.min(s.length,e))]}let We="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(s=>s?parseInt(s,36):1);for(let s=1;ss)return We[t-1]<=s;return!1}function Ur(s){return s>=127462&&s<=127487}const Gr=8205;function ot(s,t,e=!0,i=!0){return(e?Pl:Cc)(s,t,i)}function Pl(s,t,e){if(t==s.length)return t;t&&Bl(s.charCodeAt(t))&&Rl(s.charCodeAt(t-1))&&t--;let i=nt(s,t);for(t+=Rt(i);t=0&&Ur(nt(s,o));)r++,o-=2;if(r%2==0)break;t+=2}else break}return t}function Cc(s,t,e){for(;t>0;){let i=Pl(s,t-2,e);if(i=56320&&s<57344}function Rl(s){return s>=55296&&s<56320}function nt(s,t){let e=s.charCodeAt(t);if(!Rl(e)||t+1==s.length)return e;let i=s.charCodeAt(t+1);return Bl(i)?(e-55296<<10)+(i-56320)+65536:e}function cr(s){return s<=65535?String.fromCharCode(s):(s-=65536,String.fromCharCode((s>>10)+55296,(s&1023)+56320))}function Rt(s){return s<65536?1:2}const gs=/\r\n?|\n/;var ht=function(s){return s[s.Simple=0]="Simple",s[s.TrackDel=1]="TrackDel",s[s.TrackBefore=2]="TrackBefore",s[s.TrackAfter=3]="TrackAfter",s}(ht||(ht={}));class Qt{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;et)return r+(t-n);r+=l}else{if(i!=ht.Simple&&c>=t&&(i==ht.TrackDel&&nt||i==ht.TrackBefore&&nt))return null;if(c>t||c==t&&e<0&&!l)return t==n||e<0?r:r+a;r+=a}n=c}if(t>n)throw new RangeError(`Position ${t} is out of range for changeset of length ${n}`);return r}touchesRange(t,e=t){for(let i=0,n=0;i=0&&n<=e&&l>=t)return ne?"cover":!0;n=l}return!1}toString(){let t="";for(let e=0;e=0?":"+n:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(e=>typeof e!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new Qt(t)}static create(t){return new Qt(t)}}class et extends Qt{constructor(t,e){super(t),this.inserted=e}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return ms(this,(e,i,n,r,o)=>t=t.replace(n,n+(i-e),o),!1),t}mapDesc(t,e=!1){return ys(this,t,e,!0)}invert(t){let e=this.sections.slice(),i=[];for(let n=0,r=0;n=0){e[n]=l,e[n+1]=o;let a=n>>1;for(;i.length0&&he(i,e,r.text),r.forward(h),l+=h}let c=t[o++];for(;l>1].toJSON()))}return t}static of(t,e,i){let n=[],r=[],o=0,l=null;function a(h=!1){if(!h&&!n.length)return;ou||f<0||u>e)throw new RangeError(`Invalid change range ${f} to ${u} (in doc of length ${e})`);let p=d?typeof d=="string"?V.of(d.split(i||gs)):d:V.empty,g=p.length;if(f==u&&g==0)return;fo&&at(n,f-o,-1),at(n,u-f,g),he(r,n,p),o=u}}return c(t),a(!l),l}static empty(t){return new et(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let e=[],i=[];for(let n=0;nl&&typeof o!="string"))throw new RangeError("Invalid JSON representation of ChangeSet");if(r.length==1)e.push(r[0],0);else{for(;i.length=0&&e<=0&&e==s[n+1]?s[n]+=t:t==0&&s[n]==0?s[n+1]+=e:i?(s[n]+=t,s[n+1]+=e):s.push(t,e)}function he(s,t,e){if(e.length==0)return;let i=t.length-2>>1;if(i>1])),!(e||o==s.sections.length||s.sections[o+1]<0);)l=s.sections[o++],a=s.sections[o++];t(n,c,r,h,f),n=c,r=h}}}function ys(s,t,e,i=!1){let n=[],r=i?[]:null,o=new wi(s),l=new wi(t);for(let a=-1;;)if(o.ins==-1&&l.ins==-1){let c=Math.min(o.len,l.len);at(n,c,-1),o.forward(c),l.forward(c)}else if(l.ins>=0&&(o.ins<0||a==o.i||o.off==0&&(l.len=0&&a=0){let c=0,h=o.len;for(;h;)if(l.ins==-1){let f=Math.min(h,l.len);c+=f,h-=f,l.forward(f)}else if(l.ins==0&&l.lena||o.ins>=0&&o.len>a)&&(l||i.length>c),r.forward2(a),o.forward(a)}}}}class wi{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i>1;return e>=t.length?V.empty:t[e]}textBit(t){let{inserted:e}=this.set,i=this.i-2>>1;return i>=e.length&&!t?V.empty:e[i].slice(this.off,t==null?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){this.ins==-1?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class ke{constructor(t,e,i){this.from=t,this.to=e,this.flags=i}get anchor(){return this.flags&32?this.to:this.from}get head(){return this.flags&32?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return this.flags&8?-1:this.flags&16?1:0}get bidiLevel(){let t=this.flags&7;return t==7?null:t}get goalColumn(){let t=this.flags>>6;return t==16777215?void 0:t}map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.mapPos(this.from,1),n=t.mapPos(this.to,-1)),i==this.from&&n==this.to?this:new ke(i,n,this.flags)}extend(t,e=t){if(t<=this.anchor&&e>=this.anchor)return b.range(t,e);let i=Math.abs(t-this.anchor)>Math.abs(e-this.anchor)?t:e;return b.range(this.anchor,i)}eq(t,e=!1){return this.anchor==t.anchor&&this.head==t.head&&(!e||!this.empty||this.assoc==t.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||typeof t.anchor!="number"||typeof t.head!="number")throw new RangeError("Invalid JSON representation for SelectionRange");return b.range(t.anchor,t.head)}static create(t,e,i){return new ke(t,e,i)}}class b{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){return t.empty?this:b.create(this.ranges.map(i=>i.map(t,e)),this.mainIndex)}eq(t,e=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let i=0;it.toJSON()),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||typeof t.main!="number"||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new b(t.ranges.map(e=>ke.fromJSON(e)),t.main)}static single(t,e=t){return new b([b.range(t,e)],0)}static create(t,e=0){if(t.length==0)throw new RangeError("A selection needs at least one range");for(let i=0,n=0;nt?8:0)|r)}static normalized(t,e=0){let i=t[e];t.sort((n,r)=>n.from-r.from),e=t.indexOf(i);for(let n=1;nr.head?b.range(a,l):b.range(l,a))}}return new b(t,e)}}function El(s,t){for(let e of s.ranges)if(e.to>t)throw new RangeError("Selection points outside of document")}let fr=0;class T{constructor(t,e,i,n,r){this.combine=t,this.compareInput=e,this.compare=i,this.isStatic=n,this.id=fr++,this.default=t([]),this.extensions=typeof r=="function"?r(this):r}get reader(){return this}static define(t={}){return new T(t.combine||(e=>e),t.compareInput||((e,i)=>e===i),t.compare||(t.combine?(e,i)=>e===i:ur),!!t.static,t.enables)}of(t){return new hn([],this,0,t)}compute(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new hn(t,this,1,e)}computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new hn(t,this,2,e)}from(t,e){return e||(e=i=>i),this.compute([t],i=>e(i.field(t)))}}function ur(s,t){return s==t||s.length==t.length&&s.every((e,i)=>e===t[i])}class hn{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this.value=n,this.id=fr++}dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,r=this.id,o=t[r]>>1,l=this.type==2,a=!1,c=!1,h=[];for(let f of this.dependencies)f=="doc"?a=!0:f=="selection"?c=!0:((e=t[f.id])!==null&&e!==void 0?e:1)&1||h.push(t[f.id]);return{create(f){return f.values[o]=i(f),1},update(f,u){if(a&&u.docChanged||c&&(u.docChanged||u.selection)||bs(f,h)){let d=i(f);if(l?!Jr(d,f.values[o],n):!n(d,f.values[o]))return f.values[o]=d,1}return 0},reconfigure:(f,u)=>{let d,p=u.config.address[r];if(p!=null){let g=yn(u,p);if(this.dependencies.every(m=>m instanceof T?u.facet(m)===f.facet(m):m instanceof yt?u.field(m,!1)==f.field(m,!1):!0)||(l?Jr(d=i(f),g,n):n(d=i(f),g)))return f.values[o]=g,0}else d=i(f);return f.values[o]=d,1}}}}function Jr(s,t,e){if(s.length!=t.length)return!1;for(let i=0;is[a.id]),n=e.map(a=>a.type),r=i.filter(a=>!(a&1)),o=s[t.id]>>1;function l(a){let c=[];for(let h=0;hi===n),t);return t.provide&&(e.provides=t.provide(e)),e}create(t){let e=t.facet(Yr).find(i=>i.field==this);return((e==null?void 0:e.create)||this.createF)(t)}slot(t){let e=t[this.id]>>1;return{create:i=>(i.values[e]=this.create(i),1),update:(i,n)=>{let r=i.values[e],o=this.updateF(r,n);return this.compareF(r,o)?0:(i.values[e]=o,1)},reconfigure:(i,n)=>n.config.address[this.id]!=null?(i.values[e]=n.field(this),0):(i.values[e]=this.create(i),1)}}init(t){return[this,Yr.of({field:this,create:t})]}get extension(){return this}}const Se={lowest:4,low:3,default:2,high:1,highest:0};function ri(s){return t=>new Il(t,s)}const ye={highest:ri(Se.highest),high:ri(Se.high),default:ri(Se.default),low:ri(Se.low),lowest:ri(Se.lowest)};class Il{constructor(t,e){this.inner=t,this.prec=e}}class Vn{of(t){return new xs(this,t)}reconfigure(t){return Vn.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class xs{constructor(t,e){this.compartment=t,this.inner=e}}class mn{constructor(t,e,i,n,r,o){for(this.base=t,this.compartments=e,this.dynamicSlots=i,this.address=n,this.staticValues=r,this.facets=o,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(t,e,i){let n=[],r=Object.create(null),o=new Map;for(let u of Mc(t,e,o))u instanceof yt?n.push(u):(r[u.facet.id]||(r[u.facet.id]=[])).push(u);let l=Object.create(null),a=[],c=[];for(let u of n)l[u.id]=c.length<<1,c.push(d=>u.slot(d));let h=i==null?void 0:i.config.facets;for(let u in r){let d=r[u],p=d[0].facet,g=h&&h[u]||[];if(d.every(m=>m.type==0))if(l[p.id]=a.length<<1|1,ur(g,d))a.push(i.facet(p));else{let m=p.combine(d.map(y=>y.value));a.push(i&&p.compare(m,i.facet(p))?i.facet(p):m)}else{for(let m of d)m.type==0?(l[m.id]=a.length<<1|1,a.push(m.value)):(l[m.id]=c.length<<1,c.push(y=>m.dynamicSlot(y)));l[p.id]=c.length<<1,c.push(m=>Ac(m,p,d))}}let f=c.map(u=>u(l));return new mn(t,o,f,l,a,r)}}function Mc(s,t,e){let i=[[],[],[],[],[]],n=new Map;function r(o,l){let a=n.get(o);if(a!=null){if(a<=l)return;let c=i[a].indexOf(o);c>-1&&i[a].splice(c,1),o instanceof xs&&e.delete(o.compartment)}if(n.set(o,l),Array.isArray(o))for(let c of o)r(c,l);else if(o instanceof xs){if(e.has(o.compartment))throw new RangeError("Duplicate use of compartment in extensions");let c=t.get(o.compartment)||o.inner;e.set(o.compartment,c),r(c,l)}else if(o instanceof Il)r(o.inner,o.prec);else if(o instanceof yt)i[l].push(o),o.provides&&r(o.provides,l);else if(o instanceof hn)i[l].push(o),o.facet.extensions&&r(o.facet.extensions,Se.default);else{let c=o.extension;if(!c)throw new Error(`Unrecognized extension value in extension set (${o}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(c,l)}}return r(s,Se.default),i.reduce((o,l)=>o.concat(l))}function yi(s,t){if(t&1)return 2;let e=t>>1,i=s.status[e];if(i==4)throw new Error("Cyclic dependency between fields and/or facets");if(i&2)return i;s.status[e]=4;let n=s.computeSlot(s,s.config.dynamicSlots[e]);return s.status[e]=2|n}function yn(s,t){return t&1?s.config.staticValues[t>>1]:s.values[t>>1]}const Nl=T.define(),ws=T.define({combine:s=>s.some(t=>t),static:!0}),Fl=T.define({combine:s=>s.length?s[0]:void 0,static:!0}),Vl=T.define(),Hl=T.define(),Wl=T.define(),zl=T.define({combine:s=>s.length?s[0]:!1});class se{constructor(t,e){this.type=t,this.value=e}static define(){return new Dc}}class Dc{of(t){return new se(this,t)}}class Oc{constructor(t){this.map=t}of(t){return new N(this,t)}}class N{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.type.map(this.value,t);return e===void 0?void 0:e==this.value?this:new N(this.type,e)}is(t){return this.type==t}static define(t={}){return new Oc(t.map||(e=>e))}static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){let r=n.map(e);r&&i.push(r)}return i}}N.reconfigure=N.define();N.appendConfig=N.define();class Z{constructor(t,e,i,n,r,o){this.startState=t,this.changes=e,this.selection=i,this.effects=n,this.annotations=r,this.scrollIntoView=o,this._doc=null,this._state=null,i&&El(i,e.newLength),r.some(l=>l.type==Z.time)||(this.annotations=r.concat(Z.time.of(Date.now())))}static create(t,e,i,n,r,o){return new Z(t,e,i,n,r,o)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let e=this.annotation(Z.userEvent);return!!(e&&(e==t||e.length>t.length&&e.slice(0,t.length)==t&&e[t.length]=="."))}}Z.time=se.define();Z.userEvent=se.define();Z.addToHistory=se.define();Z.remote=se.define();function Tc(s,t){let e=[];for(let i=0,n=0;;){let r,o;if(i=s[i]))r=s[i++],o=s[i++];else if(n=0;n--){let r=i[n](s);r instanceof Z?s=r:Array.isArray(r)&&r.length==1&&r[0]instanceof Z?s=r[0]:s=Kl(t,ze(r),!1)}return s}function Bc(s){let t=s.startState,e=t.facet(Wl),i=s;for(let n=e.length-1;n>=0;n--){let r=e[n](s);r&&Object.keys(r).length&&(i=ql(i,Ss(t,r,s.changes.newLength),!0))}return i==s?s:Z.create(t,s.changes,s.selection,i.effects,i.annotations,i.scrollIntoView)}const Rc=[];function ze(s){return s==null?Rc:Array.isArray(s)?s:[s]}var G=function(s){return s[s.Word=0]="Word",s[s.Space=1]="Space",s[s.Other=2]="Other",s}(G||(G={}));const Lc=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let ks;try{ks=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch{}function Ec(s){if(ks)return ks.test(s);for(let t=0;t"€"&&(e.toUpperCase()!=e.toLowerCase()||Lc.test(e)))return!0}return!1}function Ic(s){return t=>{if(!/\S/.test(t))return G.Space;if(Ec(t))return G.Word;for(let e=0;e-1)return G.Word;return G.Other}}class W{constructor(t,e,i,n,r,o){this.config=t,this.doc=e,this.selection=i,this.values=n,this.status=t.statusTemplate.slice(),this.computeSlot=r,o&&(o._state=this);for(let l=0;ln.set(c,a)),e=null),n.set(l.value.compartment,l.value.extension)):l.is(N.reconfigure)?(e=null,i=l.value):l.is(N.appendConfig)&&(e=null,i=ze(i).concat(l.value));let r;e?r=t.startState.values.slice():(e=mn.resolve(i,n,this),r=new W(e,this.doc,this.selection,e.dynamicSlots.map(()=>null),(a,c)=>c.reconfigure(a,this),null).values);let o=t.startState.facet(ws)?t.newSelection:t.newSelection.asSingle();new W(e,t.newDoc,o,r,(l,a)=>a.update(l,t),t)}replaceSelection(t){return typeof t=="string"&&(t=this.toText(t)),this.changeByRange(e=>({changes:{from:e.from,to:e.to,insert:t},range:b.cursor(e.from+t.length)}))}changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(i.changes),r=[i.range],o=ze(i.effects);for(let l=1;lo.spec.fromJSON(l,a)))}}return W.create({doc:t.doc,selection:b.fromJSON(t.selection),extensions:e.extensions?n.concat([e.extensions]):n})}static create(t={}){let e=mn.resolve(t.extensions||[],new Map),i=t.doc instanceof V?t.doc:V.of((t.doc||"").split(e.staticFacet(W.lineSeparator)||gs)),n=t.selection?t.selection instanceof b?t.selection:b.single(t.selection.anchor,t.selection.head):b.single(0);return El(n,i.length),e.staticFacet(ws)||(n=n.asSingle()),new W(e,i,n,e.dynamicSlots.map(()=>null),(r,o)=>o.create(r),null)}get tabSize(){return this.facet(W.tabSize)}get lineBreak(){return this.facet(W.lineSeparator)||` +`}get readOnly(){return this.facet(zl)}phrase(t,...e){for(let i of this.facet(W.phrases))if(Object.prototype.hasOwnProperty.call(i,t)){t=i[t];break}return e.length&&(t=t.replace(/\$(\$|\d*)/g,(i,n)=>{if(n=="$")return"$";let r=+(n||1);return!r||r>e.length?i:e[r-1]})),t}languageDataAt(t,e,i=-1){let n=[];for(let r of this.facet(Nl))for(let o of r(this,e,i))Object.prototype.hasOwnProperty.call(o,t)&&n.push(o[t]);return n}charCategorizer(t){return Ic(this.languageDataAt("wordChars",t).join(""))}wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),r=this.charCategorizer(t),o=t-i,l=t-i;for(;o>0;){let a=ot(e,o,!1);if(r(e.slice(a,o))!=G.Word)break;o=a}for(;ls.length?s[0]:4});W.lineSeparator=Fl;W.readOnly=zl;W.phrases=T.define({compare(s,t){let e=Object.keys(s),i=Object.keys(t);return e.length==i.length&&e.every(n=>s[n]==t[n])}});W.languageData=Nl;W.changeFilter=Vl;W.transactionFilter=Hl;W.transactionExtender=Wl;Vn.reconfigure=N.define();function Le(s,t,e={}){let i={};for(let n of s)for(let r of Object.keys(n)){let o=n[r],l=i[r];if(l===void 0)i[r]=o;else if(!(l===o||o===void 0))if(Object.hasOwnProperty.call(e,r))i[r]=e[r](l,o);else throw new Error("Config merge conflict for field "+r)}for(let n in t)i[n]===void 0&&(i[n]=t[n]);return i}class Me{eq(t){return this==t}range(t,e=t){return vs.create(t,e,this)}}Me.prototype.startSide=Me.prototype.endSide=0;Me.prototype.point=!1;Me.prototype.mapMode=ht.TrackDel;let vs=class $l{constructor(t,e,i){this.from=t,this.to=e,this.value=i}static create(t,e,i){return new $l(t,e,i)}};function Cs(s,t){return s.from-t.from||s.value.startSide-t.value.startSide}class dr{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}get length(){return this.to[this.to.length-1]}findIndex(t,e,i,n=0){let r=i?this.to:this.from;for(let o=n,l=r.length;;){if(o==l)return o;let a=o+l>>1,c=r[a]-t||(i?this.value[a].endSide:this.value[a].startSide)-e;if(a==o)return c>=0?o:l;c>=0?l=a:o=a+1}}between(t,e,i,n){for(let r=this.findIndex(e,-1e9,!0),o=this.findIndex(i,1e9,!1,r);rd||u==d&&c.startSide>0&&c.endSide<=0)continue;(d-u||c.endSide-c.startSide)<0||(o<0&&(o=u),c.point&&(l=Math.max(l,d-u)),i.push(c),n.push(u-o),r.push(d-o))}return{mapped:i.length?new dr(n,r,i,l):null,pos:o}}}class K{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,this.maxPoint=n}static create(t,e,i,n){return new K(t,e,i,n)}get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chunkEnd(t),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let e of this.chunk)t+=e.value.length;return t}chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:r=this.length}=t,o=t.filter;if(e.length==0&&!o)return this;if(i&&(e=e.slice().sort(Cs)),this.isEmpty)return e.length?K.of(e):this;let l=new jl(this,null,-1).goto(0),a=0,c=[],h=new De;for(;l.value||a=0){let f=e[a++];h.addInner(f.from,f.to,f.value)||c.push(f)}else l.rangeIndex==1&&l.chunkIndexthis.chunkEnd(l.chunkIndex)||rl.to||r=r&&t<=r+o.length&&o.between(r,t-r,e-r,i)===!1)return}this.nextLayer.between(t,e,i)}}iter(t=0){return Si.from([this]).goto(t)}get isEmpty(){return this.nextLayer==this}static iter(t,e=0){return Si.from(t).goto(e)}static compare(t,e,i,n,r=-1){let o=t.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),l=e.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),a=Xr(o,l,i),c=new oi(o,a,r),h=new oi(l,a,r);i.iterGaps((f,u,d)=>_r(c,f,h,u,d,n)),i.empty&&i.length==0&&_r(c,0,h,0,0,n)}static eq(t,e,i=0,n){n==null&&(n=999999999);let r=t.filter(h=>!h.isEmpty&&e.indexOf(h)<0),o=e.filter(h=>!h.isEmpty&&t.indexOf(h)<0);if(r.length!=o.length)return!1;if(!r.length)return!0;let l=Xr(r,o),a=new oi(r,l,0).goto(i),c=new oi(o,l,0).goto(i);for(;;){if(a.to!=c.to||!As(a.active,c.active)||a.point&&(!c.point||!a.point.eq(c.point)))return!1;if(a.to>n)return!0;a.next(),c.next()}}static spans(t,e,i,n,r=-1){let o=new oi(t,null,r).goto(e),l=e,a=o.openStart;for(;;){let c=Math.min(o.to,i);if(o.point){let h=o.activeForPoint(o.to),f=o.pointFroml&&(n.span(l,c,o.active,a),a=o.openEnd(c));if(o.to>i)return a+(o.point&&o.to>i?1:0);l=o.to,o.next()}}static of(t,e=!1){let i=new De;for(let n of t instanceof vs?[t]:e?Nc(t):t)i.add(n.from,n.to,n.value);return i.finish()}static join(t){if(!t.length)return K.empty;let e=t[t.length-1];for(let i=t.length-2;i>=0;i--)for(let n=t[i];n!=K.empty;n=n.nextLayer)e=new K(n.chunkPos,n.chunk,e,Math.max(n.maxPoint,e.maxPoint));return e}}K.empty=new K([],[],null,-1);function Nc(s){if(s.length>1)for(let t=s[0],e=1;e0)return s.slice().sort(Cs);t=i}return s}K.empty.nextLayer=K.empty;class De{finishChunk(t){this.chunks.push(new dr(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,t&&(this.from=[],this.to=[],this.value=[])}constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new De)).add(t,e,i)}addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(n<=0&&(t-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return n<0?!1:(this.from.length==250&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=t),this.from.push(t-this.chunkStart),this.to.push(e-this.chunkStart),this.last=i,this.lastFrom=t,this.lastTo=e,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,e-t)),!0)}addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,e.maxPoint),this.chunks.push(e),this.chunkPos.push(t);let i=e.value.length-1;return this.last=e.value[i],this.lastFrom=e.from[i]+t,this.lastTo=e.to[i]+t,!0}finish(){return this.finishInner(K.empty)}finishInner(t){if(this.from.length&&this.finishChunk(!1),this.chunks.length==0)return t;let e=K.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(t):t,this.setMaxPoint);return this.from=null,e}}function Xr(s,t,e){let i=new Map;for(let r of s)for(let o=0;o=this.minPoint)break}}setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&n.push(new jl(o,e,i,r));return n.length==1?n[0]:new Si(n)}get startSide(){return this.value?this.value.startSide:0}goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let i=this.heap.length>>1;i>=0;i--)Yn(this.heap,i);return this.next(),this}forward(t,e){for(let i of this.heap)i.forward(t,e);for(let i=this.heap.length>>1;i>=0;i--)Yn(this.heap,i);(this.to-t||this.value.endSide-e)<0&&this.next()}next(){if(this.heap.length==0)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let t=this.heap[0];this.from=t.from,this.to=t.to,this.value=t.value,this.rank=t.rank,t.value&&t.next(),Yn(this.heap,0)}}}function Yn(s,t){for(let e=s[t];;){let i=(t<<1)+1;if(i>=s.length)break;let n=s[i];if(i+1=0&&(n=s[i+1],i++),e.compare(n)<0)break;s[i]=e,s[t]=n,t=i}}class oi{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=Si.from(t,e,i)}goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=t,this.endSide=e,this.openStart=-1,this.next(),this}forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t||this.active[this.minActive].endSide-e)<0;)this.removeActive(this.minActive);this.cursor.forward(t,e)}removeActive(t){zi(this.active,t),zi(this.activeTo,t),zi(this.activeRank,t),this.minActive=Qr(this.active,this.activeTo)}addActive(t){let e=0,{value:i,to:n,rank:r}=this.cursor;for(;e0;)e++;qi(this.active,e,i),qi(this.activeTo,e,n),qi(this.activeRank,e,r),t&&qi(t,e,this.cursor.from),this.minActive=Qr(this.active,this.activeTo)}next(){let t=this.to,e=this.point;this.point=null;let i=this.openStart<0?[]:null;for(;;){let n=this.minActive;if(n>-1&&(this.activeTo[n]-this.cursor.from||this.active[n].endSide-this.cursor.startSide)<0){if(this.activeTo[n]>t){this.to=this.activeTo[n],this.endSide=this.active[n].endSide;break}this.removeActive(n),i&&zi(i,n)}else if(this.cursor.value)if(this.cursor.from>t){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}else{let r=this.cursor.value;if(!r.point)this.addActive(i),this.cursor.next();else if(e&&this.cursor.to==this.to&&this.cursor.from=0&&i[n]=0&&!(this.activeRank[i]t||this.activeTo[i]==t&&this.active[i].endSide>=this.point.endSide)&&e.push(this.active[i]);return e.reverse()}openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>t;i--)e++;return e}}function _r(s,t,e,i,n,r){s.goto(t),e.goto(i);let o=i+n,l=i,a=i-t;for(;;){let c=s.to+a-e.to||s.endSide-e.endSide,h=c<0?s.to+a:e.to,f=Math.min(h,o);if(s.point||e.point?s.point&&e.point&&(s.point==e.point||s.point.eq(e.point))&&As(s.activeForPoint(s.to),e.activeForPoint(e.to))||r.comparePoint(l,f,s.point,e.point):f>l&&!As(s.active,e.active)&&r.compareRange(l,f,s.active,e.active),h>o)break;l=h,c<=0&&s.next(),c>=0&&e.next()}}function As(s,t){if(s.length!=t.length)return!1;for(let e=0;e=t;i--)s[i+1]=s[i];s[t]=e}function Qr(s,t){let e=-1,i=1e9;for(let n=0;n=t)return n;if(n==s.length)break;r+=s.charCodeAt(n)==9?e-r%e:1,n=ot(s,n)}return i===!0?-1:s.length}const Ds="ͼ",Zr=typeof Symbol>"u"?"__"+Ds:Symbol.for(Ds),Os=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),to=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{};class de{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(o){return/^@/.test(o)?[o]:o.split(/,\s*/)}function r(o,l,a,c){let h=[],f=/^@(\w+)\b/.exec(o[0]),u=f&&f[1]=="keyframes";if(f&&l==null)return a.push(o[0]+";");for(let d in l){let p=l[d];if(/&/.test(d))r(d.split(/,\s*/).map(g=>o.map(m=>g.replace(/&/,m))).reduce((g,m)=>g.concat(m)),p,a);else if(p&&typeof p=="object"){if(!f)throw new RangeError("The value of a property ("+d+") should be a primitive value.");r(n(d),p,h,u)}else p!=null&&h.push(d.replace(/_.*/,"").replace(/[A-Z]/g,g=>"-"+g.toLowerCase())+": "+p+";")}(h.length||u)&&a.push((i&&!f&&!c?o.map(i):o).join(", ")+" {"+h.join(" ")+"}")}for(let o in t)r(n(o),t[o],this.rules)}getRules(){return this.rules.join(` +`)}static newName(){let t=to[Zr]||1;return to[Zr]=t+1,Ds+t.toString(36)}static mount(t,e,i){let n=t[Os],r=i&&i.nonce;n?r&&n.setNonce(r):n=new Fc(t,r),n.mount(Array.isArray(e)?e:[e],t)}}let eo=new Map;class Fc{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&t.adoptedStyleSheets&&n.CSSStyleSheet){let r=eo.get(i);if(r)return t[Os]=r;this.sheet=new n.CSSStyleSheet,eo.set(i,this)}else this.styleTag=i.createElement("style"),e&&this.styleTag.setAttribute("nonce",e);this.modules=[],t[Os]=this}mount(t,e){let i=this.sheet,n=0,r=0;for(let o=0;o-1&&(this.modules.splice(a,1),r--,a=-1),a==-1){if(this.modules.splice(r++,0,l),i)for(let c=0;c",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},Vc=typeof navigator<"u"&&/Mac/.test(navigator.platform),Hc=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);for(var st=0;st<10;st++)pe[48+st]=pe[96+st]=String(st);for(var st=1;st<=24;st++)pe[st+111]="F"+st;for(var st=65;st<=90;st++)pe[st]=String.fromCharCode(st+32),ki[st]=String.fromCharCode(st);for(var Xn in pe)ki.hasOwnProperty(Xn)||(ki[Xn]=pe[Xn]);function Wc(s){var t=Vc&&s.metaKey&&s.shiftKey&&!s.ctrlKey&&!s.altKey||Hc&&s.shiftKey&&s.key&&s.key.length==1||s.key=="Unidentified",e=!t&&s.key||(s.shiftKey?ki:pe)[s.keyCode]||s.key||"Unidentified";return e=="Esc"&&(e="Escape"),e=="Del"&&(e="Delete"),e=="Left"&&(e="ArrowLeft"),e=="Up"&&(e="ArrowUp"),e=="Right"&&(e="ArrowRight"),e=="Down"&&(e="ArrowDown"),e}function vi(s){let t;return s.nodeType==11?t=s.getSelection?s:s.ownerDocument:t=s,t.getSelection()}function Ts(s,t){return t?s==t||s.contains(t.nodeType!=1?t.parentNode:t):!1}function zc(s){let t=s.activeElement;for(;t&&t.shadowRoot;)t=t.shadowRoot.activeElement;return t}function cn(s,t){if(!t.anchorNode)return!1;try{return Ts(s,t.anchorNode)}catch{return!1}}function Ge(s){return s.nodeType==3?Te(s,0,s.nodeValue.length).getClientRects():s.nodeType==1?s.getClientRects():[]}function bi(s,t,e,i){return e?io(s,t,e,i,-1)||io(s,t,e,i,1):!1}function Oe(s){for(var t=0;;t++)if(s=s.previousSibling,!s)return t}function bn(s){return s.nodeType==1&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(s.nodeName)}function io(s,t,e,i,n){for(;;){if(s==e&&t==i)return!0;if(t==(n<0?0:ie(s))){if(s.nodeName=="DIV")return!1;let r=s.parentNode;if(!r||r.nodeType!=1)return!1;t=Oe(s)+(n<0?0:1),s=r}else if(s.nodeType==1){if(s=s.childNodes[t+(n<0?-1:0)],s.nodeType==1&&s.contentEditable=="false")return!1;t=n<0?ie(s):0}else return!1}}function ie(s){return s.nodeType==3?s.nodeValue.length:s.childNodes.length}function Li(s,t){let e=t?s.left:s.right;return{left:e,right:e,top:s.top,bottom:s.bottom}}function qc(s){let t=s.visualViewport;return t?{left:0,right:t.width,top:0,bottom:t.height}:{left:0,right:s.innerWidth,top:0,bottom:s.innerHeight}}function Ul(s,t){let e=t.width/s.offsetWidth,i=t.height/s.offsetHeight;return(e>.995&&e<1.005||!isFinite(e)||Math.abs(t.width-s.offsetWidth)<1)&&(e=1),(i>.995&&i<1.005||!isFinite(i)||Math.abs(t.height-s.offsetHeight)<1)&&(i=1),{scaleX:e,scaleY:i}}function Kc(s,t,e,i,n,r,o,l){let a=s.ownerDocument,c=a.defaultView||window;for(let h=s,f=!1;h&&!f;)if(h.nodeType==1){let u,d=h==a.body,p=1,g=1;if(d)u=qc(c);else{if(/^(fixed|sticky)$/.test(getComputedStyle(h).position)&&(f=!0),h.scrollHeight<=h.clientHeight&&h.scrollWidth<=h.clientWidth){h=h.assignedSlot||h.parentNode;continue}let x=h.getBoundingClientRect();({scaleX:p,scaleY:g}=Ul(h,x)),u={left:x.left,right:x.left+h.clientWidth*p,top:x.top,bottom:x.top+h.clientHeight*g}}let m=0,y=0;if(n=="nearest")t.top0&&t.bottom>u.bottom+y&&(y=t.bottom-u.bottom+y+o)):t.bottom>u.bottom&&(y=t.bottom-u.bottom+o,e<0&&t.top-y0&&t.right>u.right+m&&(m=t.right-u.right+m+r)):t.right>u.right&&(m=t.right-u.right+r,e<0&&t.leftn.clientHeight&&(i=n),!e&&n.scrollWidth>n.clientWidth&&(e=n),n=n.assignedSlot||n.parentNode;else if(n.nodeType==11)n=n.host;else break;return{x:e,y:i}}class jc{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anchorOffset,e?ie(e):0),i,Math.min(t.focusOffset,i?ie(i):0))}set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=n}}let Ne=null;function Gl(s){if(s.setActive)return s.setActive();if(Ne)return s.focus(Ne);let t=[];for(let e=s;e&&(t.push(e,e.scrollTop,e.scrollLeft),e!=e.ownerDocument);e=e.parentNode);if(s.focus(Ne==null?{get preventScroll(){return Ne={preventScroll:!0},!0}}:void 0),!Ne){Ne=!1;for(let e=0;eMath.max(1,s.scrollHeight-s.clientHeight-4)}function Xl(s,t){for(let e=s,i=t;;){if(e.nodeType==3&&i>0)return{node:e,offset:i};if(e.nodeType==1&&i>0){if(e.contentEditable=="false")return null;e=e.childNodes[i-1],i=ie(e)}else if(e.parentNode&&!bn(e))i=Oe(e),e=e.parentNode;else return null}}function _l(s,t){for(let e=s,i=t;;){if(e.nodeType==3&&ie)return f.domBoundsAround(t,e,c);if(u>=t&&n==-1&&(n=a,r=c),c>e&&f.dom.parentNode==this.dom){o=a,l=h;break}h=u,c=u+f.breakAfter}return{from:r,to:l<0?i+this.length:l,startDOM:(n?this.children[n-1].dom.nextSibling:null)||this.dom.firstChild,endDOM:o=0?this.children[o].dom:null}}markDirty(t=!1){this.flags|=2,this.markParentsDirty(t)}markParentsDirty(t){for(let e=this.parent;e;e=e.parent){if(t&&(e.flags|=2),e.flags&1)return;e.flags|=1,t=!1}}setParent(t){this.parent!=t&&(this.parent=t,this.flags&7&&this.markParentsDirty(!0))}setDOM(t){this.dom!=t&&(this.dom&&(this.dom.cmView=null),this.dom=t,t.cmView=this)}get rootView(){for(let t=this;;){let e=t.parent;if(!e)return t;t=e}}replaceChildren(t,e,i=pr){this.markDirty();for(let n=t;nthis.pos||t==this.pos&&(e>0||this.i==0||this.children[this.i-1].breakAfter))return this.off=t-this.pos,this;let i=this.children[--this.i];this.pos-=i.length+i.breakAfter}}}function Zl(s,t,e,i,n,r,o,l,a){let{children:c}=s,h=c.length?c[t]:null,f=r.length?r[r.length-1]:null,u=f?f.breakAfter:o;if(!(t==i&&h&&!o&&!u&&r.length<2&&h.merge(e,n,r.length?f:null,e==0,l,a))){if(i0&&(!o&&r.length&&h.merge(e,h.length,r[0],!1,l,0)?h.breakAfter=r.shift().breakAfter:(e2);var D={mac:lo||/Mac/.test(wt.platform),windows:/Win/.test(wt.platform),linux:/Linux|X11/.test(wt.platform),ie:Hn,ie_version:ea?Ps.documentMode||6:Rs?+Rs[1]:Bs?+Bs[1]:0,gecko:ro,gecko_version:ro?+(/Firefox\/(\d+)/.exec(wt.userAgent)||[0,0])[1]:0,chrome:!!_n,chrome_version:_n?+_n[1]:0,ios:lo,android:/Android\b/.test(wt.userAgent),webkit:oo,safari:ia,webkit_version:oo?+(/\bAppleWebKit\/(\d+)/.exec(wt.userAgent)||[0,0])[1]:0,tabSize:Ps.documentElement.style.tabSize!=null?"tab-size":"-moz-tab-size"};const Jc=256;class Ht extends ${constructor(t){super(),this.text=t}get length(){return this.text.length}createDOM(t){this.setDOM(t||document.createTextNode(this.text))}sync(t,e){this.dom||this.createDOM(),this.dom.nodeValue!=this.text&&(e&&e.node==this.dom&&(e.written=!0),this.dom.nodeValue=this.text)}reuseDOM(t){t.nodeType==3&&this.createDOM(t)}merge(t,e,i){return this.flags&8||i&&(!(i instanceof Ht)||this.length-(e-t)+i.length>Jc||i.flags&8)?!1:(this.text=this.text.slice(0,t)+(i?i.text:"")+this.text.slice(e),this.markDirty(),!0)}split(t){let e=new Ht(this.text.slice(t));return this.text=this.text.slice(0,t),this.markDirty(),e.flags|=this.flags&8,e}localPosFromDOM(t,e){return t==this.dom?e:e?this.text.length:0}domAtPos(t){return new ct(this.dom,t)}domBoundsAround(t,e,i){return{from:i,to:i+this.length,startDOM:this.dom,endDOM:this.dom.nextSibling}}coordsAt(t,e){return Yc(this.dom,t,e)}}class ne extends ${constructor(t,e=[],i=0){super(),this.mark=t,this.children=e,this.length=i;for(let n of e)n.setParent(this)}setAttrs(t){if(Jl(t),this.mark.class&&(t.className=this.mark.class),this.mark.attrs)for(let e in this.mark.attrs)t.setAttribute(e,this.mark.attrs[e]);return t}canReuseDOM(t){return super.canReuseDOM(t)&&!((this.flags|t.flags)&8)}reuseDOM(t){t.nodeName==this.mark.tagName.toUpperCase()&&(this.setDOM(t),this.flags|=6)}sync(t,e){this.dom?this.flags&4&&this.setAttrs(this.dom):this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))),super.sync(t,e)}merge(t,e,i,n,r,o){return i&&(!(i instanceof ne&&i.mark.eq(this.mark))||t&&r<=0||et&&e.push(i=t&&(n=r),i=a,r++}let o=this.length-t;return this.length=t,n>-1&&(this.children.length=n,this.markDirty()),new ne(this.mark,e,o)}domAtPos(t){return na(this,t)}coordsAt(t,e){return ra(this,t,e)}}function Yc(s,t,e){let i=s.nodeValue.length;t>i&&(t=i);let n=t,r=t,o=0;t==0&&e<0||t==i&&e>=0?D.chrome||D.gecko||(t?(n--,o=1):r=0)?0:l.length-1];return D.safari&&!o&&a.width==0&&(a=Array.prototype.find.call(l,c=>c.width)||a),o?Li(a,o<0):a||null}class ve extends ${static create(t,e,i){return new ve(t,e,i)}constructor(t,e,i){super(),this.widget=t,this.length=e,this.side=i,this.prevWidget=null}split(t){let e=ve.create(this.widget,this.length-t,this.side);return this.length-=t,e}sync(t){(!this.dom||!this.widget.updateDOM(this.dom,t))&&(this.dom&&this.prevWidget&&this.prevWidget.destroy(this.dom),this.prevWidget=null,this.setDOM(this.widget.toDOM(t)),this.widget.editable||(this.dom.contentEditable="false"))}getSide(){return this.side}merge(t,e,i,n,r,o){return i&&(!(i instanceof ve)||!this.widget.compare(i.widget)||t>0&&r<=0||e0)?ct.before(this.dom):ct.after(this.dom,t==this.length)}domBoundsAround(){return null}coordsAt(t,e){let i=this.widget.coordsAt(this.dom,t,e);if(i)return i;let n=this.dom.getClientRects(),r=null;if(!n.length)return null;let o=this.side?this.side<0:t>0;for(let l=o?n.length-1:0;r=n[l],!(t>0?l==0:l==n.length-1||r.top0?ct.before(this.dom):ct.after(this.dom)}localPosFromDOM(){return 0}domBoundsAround(){return null}coordsAt(t){return this.dom.getBoundingClientRect()}get overrideDOMText(){return V.empty}get isHidden(){return!0}}Ht.prototype.children=ve.prototype.children=Je.prototype.children=pr;function na(s,t){let e=s.dom,{children:i}=s,n=0;for(let r=0;nr&&t0;r--){let o=i[r-1];if(o.dom.parentNode==e)return o.domAtPos(o.length)}for(let r=n;r0&&t instanceof ne&&n.length&&(i=n[n.length-1])instanceof ne&&i.mark.eq(t.mark)?sa(i,t.children[0],e-1):(n.push(t),t.setParent(s)),s.length+=t.length}function ra(s,t,e){let i=null,n=-1,r=null,o=-1;function l(c,h){for(let f=0,u=0;f=h&&(d.children.length?l(d,h-u):(!r||r.isHidden&&e>0)&&(p>h||u==p&&d.getSide()>0)?(r=d,o=h-u):(u-1?1:0)!=n.length-(e&&n.indexOf(e)>-1?1:0))return!1;for(let r of i)if(r!=e&&(n.indexOf(r)==-1||s[r]!==t[r]))return!1;return!0}function Es(s,t,e){let i=!1;if(t)for(let n in t)e&&n in e||(i=!0,n=="style"?s.style.cssText="":s.removeAttribute(n));if(e)for(let n in e)t&&t[n]==e[n]||(i=!0,n=="style"?s.style.cssText=e[n]:s.setAttribute(n,e[n]));return i}function _c(s){let t=Object.create(null);for(let e=0;e0?3e8:-4e8:e>0?1e8:-1e8,new ge(t,e,e,i,t.widget||null,!1)}static replace(t){let e=!!t.block,i,n;if(t.isBlockGap)i=-5e8,n=4e8;else{let{start:r,end:o}=oa(t,e);i=(r?e?-3e8:-1:5e8)-1,n=(o?e?2e8:1:-6e8)+1}return new ge(t,i,n,e,t.widget||null,!0)}static line(t){return new Ii(t)}static set(t,e=!1){return K.of(t,e)}hasHeight(){return this.widget?this.widget.estimatedHeight>-1:!1}}P.none=K.empty;class Ei extends P{constructor(t){let{start:e,end:i}=oa(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.class=t.class||"",this.attrs=t.attributes||null}eq(t){var e,i;return this==t||t instanceof Ei&&this.tagName==t.tagName&&(this.class||((e=this.attrs)===null||e===void 0?void 0:e.class))==(t.class||((i=t.attrs)===null||i===void 0?void 0:i.class))&&xn(this.attrs,t.attrs,"class")}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}Ei.prototype.point=!1;class Ii extends P{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof Ii&&this.spec.class==t.spec.class&&xn(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}Ii.prototype.mapMode=ht.TrackBefore;Ii.prototype.point=!0;class ge extends P{constructor(t,e,i,n,r,o){super(e,i,r,t),this.block=n,this.isReplace=o,this.mapMode=n?e<=0?ht.TrackBefore:ht.TrackAfter:ht.TrackDel}get type(){return this.startSide!=this.endSide?Ot.WidgetRange:this.startSide<=0?Ot.WidgetBefore:Ot.WidgetAfter}get heightRelevant(){return this.block||!!this.widget&&(this.widget.estimatedHeight>=5||this.widget.lineBreaks>0)}eq(t){return t instanceof ge&&Qc(this.widget,t.widget)&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}ge.prototype.point=!0;function oa(s,t=!1){let{inclusiveStart:e,inclusiveEnd:i}=s;return e==null&&(e=s.inclusive),i==null&&(i=s.inclusive),{start:e??t,end:i??t}}function Qc(s,t){return s==t||!!(s&&t&&s.compare(t))}function Is(s,t,e,i=0){let n=e.length-1;n>=0&&e[n]+i>=s?e[n]=Math.max(e[n],t):e.push(s,t)}class Q extends ${constructor(){super(...arguments),this.children=[],this.length=0,this.prevAttrs=void 0,this.attrs=null,this.breakAfter=0}merge(t,e,i,n,r,o){if(i){if(!(i instanceof Q))return!1;this.dom||i.transferDOM(this)}return n&&this.setDeco(i?i.attrs:null),ta(this,t,e,i?i.children.slice():[],r,o),!0}split(t){let e=new Q;if(e.breakAfter=this.breakAfter,this.length==0)return e;let{i,off:n}=this.childPos(t);n&&(e.append(this.children[i].split(n),0),this.children[i].merge(n,this.children[i].length,null,!1,0,0),i++);for(let r=i;r0&&this.children[i-1].length==0;)this.children[--i].destroy();return this.children.length=i,this.markDirty(),this.length=t,e}transferDOM(t){this.dom&&(this.markDirty(),t.setDOM(this.dom),t.prevAttrs=this.prevAttrs===void 0?this.attrs:this.prevAttrs,this.prevAttrs=void 0,this.dom=null)}setDeco(t){xn(this.attrs,t)||(this.dom&&(this.prevAttrs=this.attrs,this.markDirty()),this.attrs=t)}append(t,e){sa(this,t,e)}addLineDeco(t){let e=t.spec.attributes,i=t.spec.class;e&&(this.attrs=Ls(e,this.attrs||{})),i&&(this.attrs=Ls({class:i},this.attrs||{}))}domAtPos(t){return na(this,t)}reuseDOM(t){t.nodeName=="DIV"&&(this.setDOM(t),this.flags|=6)}sync(t,e){var i;this.dom?this.flags&4&&(Jl(this.dom),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0):(this.setDOM(document.createElement("div")),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0),this.prevAttrs!==void 0&&(Es(this.dom,this.prevAttrs,this.attrs),this.dom.classList.add("cm-line"),this.prevAttrs=void 0),super.sync(t,e);let n=this.dom.lastChild;for(;n&&$.get(n)instanceof ne;)n=n.lastChild;if(!n||!this.length||n.nodeName!="BR"&&((i=$.get(n))===null||i===void 0?void 0:i.isEditable)==!1&&(!D.ios||!this.children.some(r=>r instanceof Ht))){let r=document.createElement("BR");r.cmIgnore=!0,this.dom.appendChild(r)}}measureTextSize(){if(this.children.length==0||this.length>20)return null;let t=0,e;for(let i of this.children){if(!(i instanceof Ht)||/[^ -~]/.test(i.text))return null;let n=Ge(i.dom);if(n.length!=1)return null;t+=n[0].width,e=n[0].height}return t?{lineHeight:this.dom.getBoundingClientRect().height,charWidth:t/this.length,textHeight:e}:null}coordsAt(t,e){let i=ra(this,t,e);if(!this.children.length&&i&&this.parent){let{heightOracle:n}=this.parent.view.viewState,r=i.bottom-i.top;if(Math.abs(r-n.lineHeight)<2&&n.textHeight=e){if(r instanceof Q)return r;if(o>e)break}n=o+r.breakAfter}return null}}class te extends ${constructor(t,e,i){super(),this.widget=t,this.length=e,this.deco=i,this.breakAfter=0,this.prevWidget=null}merge(t,e,i,n,r,o){return i&&(!(i instanceof te)||!this.widget.compare(i.widget)||t>0&&r<=0||e0}}class Ns extends Ee{constructor(t){super(),this.height=t}toDOM(){let t=document.createElement("div");return t.className="cm-gap",this.updateDOM(t),t}eq(t){return t.height==this.height}updateDOM(t){return t.style.height=this.height+"px",!0}get editable(){return!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}class xi{constructor(t,e,i,n){this.doc=t,this.pos=e,this.end=i,this.disallowBlockEffectsFor=n,this.content=[],this.curLine=null,this.breakAtStart=0,this.pendingBuffer=0,this.bufferMarks=[],this.atCursorPos=!0,this.openStart=-1,this.openEnd=-1,this.text="",this.textOff=0,this.cursor=t.iter(),this.skip=e}posCovered(){if(this.content.length==0)return!this.breakAtStart&&this.doc.lineAt(this.pos).from!=this.pos;let t=this.content[this.content.length-1];return!(t.breakAfter||t instanceof te&&t.deco.endSide<0)}getLine(){return this.curLine||(this.content.push(this.curLine=new Q),this.atCursorPos=!0),this.curLine}flushBuffer(t=this.bufferMarks){this.pendingBuffer&&(this.curLine.append(Ki(new Je(-1),t),t.length),this.pendingBuffer=0)}addBlockWidget(t){this.flushBuffer(),this.curLine=null,this.content.push(t)}finish(t){this.pendingBuffer&&t<=this.bufferMarks.length?this.flushBuffer():this.pendingBuffer=0,!this.posCovered()&&!(t&&this.content.length&&this.content[this.content.length-1]instanceof te)&&this.getLine()}buildText(t,e,i){for(;t>0;){if(this.textOff==this.text.length){let{value:r,lineBreak:o,done:l}=this.cursor.next(this.skip);if(this.skip=0,l)throw new Error("Ran out of text content when drawing inline views");if(o){this.posCovered()||this.getLine(),this.content.length?this.content[this.content.length-1].breakAfter=1:this.breakAtStart=1,this.flushBuffer(),this.curLine=null,this.atCursorPos=!0,t--;continue}else this.text=r,this.textOff=0}let n=Math.min(this.text.length-this.textOff,t,512);this.flushBuffer(e.slice(e.length-i)),this.getLine().append(Ki(new Ht(this.text.slice(this.textOff,this.textOff+n)),e),i),this.atCursorPos=!0,this.textOff+=n,t-=n,i=0}}span(t,e,i,n){this.buildText(e-t,i,n),this.pos=e,this.openStart<0&&(this.openStart=n)}point(t,e,i,n,r,o){if(this.disallowBlockEffectsFor[o]&&i instanceof ge){if(i.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.doc.lineAt(this.pos).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}let l=e-t;if(i instanceof ge)if(i.block)i.startSide>0&&!this.posCovered()&&this.getLine(),this.addBlockWidget(new te(i.widget||Ye.block,l,i));else{let a=ve.create(i.widget||Ye.inline,l,l?0:i.startSide),c=this.atCursorPos&&!a.isEditable&&r<=n.length&&(t0),h=!a.isEditable&&(tn.length||i.startSide<=0),f=this.getLine();this.pendingBuffer==2&&!c&&!a.isEditable&&(this.pendingBuffer=0),this.flushBuffer(n),c&&(f.append(Ki(new Je(1),n),r),r=n.length+Math.max(0,r-n.length)),f.append(Ki(a,n),r),this.atCursorPos=h,this.pendingBuffer=h?tn.length?1:2:0,this.pendingBuffer&&(this.bufferMarks=n.slice())}else this.doc.lineAt(this.pos).from==this.pos&&this.getLine().addLineDeco(i);l&&(this.textOff+l<=this.text.length?this.textOff+=l:(this.skip+=l-(this.text.length-this.textOff),this.text="",this.textOff=0),this.pos=e),this.openStart<0&&(this.openStart=r)}static build(t,e,i,n,r){let o=new xi(t,e,i,r);return o.openEnd=K.spans(n,e,i,o),o.openStart<0&&(o.openStart=o.openEnd),o.finish(o.openEnd),o}}function Ki(s,t){for(let e of t)s=new ne(e,[s],s.length);return s}class Ye extends Ee{constructor(t){super(),this.tag=t}eq(t){return t.tag==this.tag}toDOM(){return document.createElement(this.tag)}updateDOM(t){return t.nodeName.toLowerCase()==this.tag}get isHidden(){return!0}}Ye.inline=new Ye("span");Ye.block=new Ye("div");var X=function(s){return s[s.LTR=0]="LTR",s[s.RTL=1]="RTL",s}(X||(X={}));const Pe=X.LTR,gr=X.RTL;function la(s){let t=[];for(let e=0;e=e){if(l.level==i)return o;(r<0||(n!=0?n<0?l.frome:t[r].level>l.level))&&(r=o)}}if(r<0)throw new RangeError("Index out of range");return r}}function ha(s,t){if(s.length!=t.length)return!1;for(let e=0;e=0;g-=3)if(Kt[g+1]==-d){let m=Kt[g+2],y=m&2?n:m&4?m&1?r:n:0;y&&(q[f]=q[Kt[g]]=y),l=g;break}}else{if(Kt.length==189)break;Kt[l++]=f,Kt[l++]=u,Kt[l++]=a}else if((p=q[f])==2||p==1){let g=p==n;a=g?0:1;for(let m=l-3;m>=0;m-=3){let y=Kt[m+2];if(y&2)break;if(g)Kt[m+2]|=2;else{if(y&4)break;Kt[m+2]|=4}}}}}function rf(s,t,e,i){for(let n=0,r=i;n<=e.length;n++){let o=n?e[n-1].to:s,l=na;)p==m&&(p=e[--g].from,m=g?e[g-1].to:s),q[--p]=d;a=h}else r=c,a++}}}function Vs(s,t,e,i,n,r,o){let l=i%2?2:1;if(i%2==n%2)for(let a=t,c=0;aa&&o.push(new ce(a,g.from,d));let m=g.direction==Pe!=!(d%2);Hs(s,m?i+1:i,n,g.inner,g.from,g.to,o),a=g.to}p=g.to}else{if(p==e||(h?q[p]!=l:q[p]==l))break;p++}u?Vs(s,a,p,i+1,n,u,o):at;){let h=!0,f=!1;if(!c||a>r[c-1].to){let g=q[a-1];g!=l&&(h=!1,f=g==16)}let u=!h&&l==1?[]:null,d=h?i:i+1,p=a;t:for(;;)if(c&&p==r[c-1].to){if(f)break t;let g=r[--c];if(!h)for(let m=g.from,y=c;;){if(m==t)break t;if(y&&r[y-1].to==m)m=r[--y].from;else{if(q[m-1]==l)break t;break}}if(u)u.push(g);else{g.toq.length;)q[q.length]=256;let i=[],n=t==Pe?0:1;return Hs(s,n,n,e,0,s.length,i),i}function ca(s){return[new ce(0,s,0)]}let fa="";function lf(s,t,e,i,n){var r;let o=i.head-s.from,l=ce.find(t,o,(r=i.bidiLevel)!==null&&r!==void 0?r:-1,i.assoc),a=t[l],c=a.side(n,e);if(o==c){let u=l+=n?1:-1;if(u<0||u>=t.length)return null;a=t[l=u],o=a.side(!n,e),c=a.side(n,e)}let h=ot(s.text,o,a.forward(n,e));(ha.to)&&(h=c),fa=s.text.slice(Math.min(o,h),Math.max(o,h));let f=l==(n?t.length-1:0)?null:t[l+(n?1:-1)];return f&&h==c&&f.level+(n?0:1)s.some(t=>t)}),xa=T.define({combine:s=>s.some(t=>t)}),wa=T.define();class Ke{constructor(t,e="nearest",i="nearest",n=5,r=5,o=!1){this.range=t,this.y=e,this.x=i,this.yMargin=n,this.xMargin=r,this.isSnapshot=o}map(t){return t.empty?this:new Ke(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}clip(t){return this.range.to<=t.doc.length?this:new Ke(b.cursor(t.doc.length),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}}const $i=N.define({map:(s,t)=>s.map(t)}),Sa=N.define();function Dt(s,t,e){let i=s.facet(ga);i.length?i[0](t):window.onerror?window.onerror(String(t),e,void 0,void 0,t):e?console.error(e+":",t):console.error(t)}const le=T.define({combine:s=>s.length?s[0]:!0});let hf=0;const fi=T.define();class ut{constructor(t,e,i,n,r){this.id=t,this.create=e,this.domEventHandlers=i,this.domEventObservers=n,this.extension=r(this)}static define(t,e){const{eventHandlers:i,eventObservers:n,provide:r,decorations:o}=e||{};return new ut(hf++,t,i,n,l=>{let a=[fi.of(l)];return o&&a.push(Ci.of(c=>{let h=c.plugin(l);return h?o(h):P.none})),r&&a.push(r(l)),a})}static fromClass(t,e){return ut.define(i=>new t(i),e)}}class Qn{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}update(t){if(this.value){if(this.mustUpdate){let e=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(e)}catch(i){if(Dt(e.state,i,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch{}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.create(t)}catch(e){Dt(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(!((e=this.value)===null||e===void 0)&&e.destroy)try{this.value.destroy()}catch(i){Dt(t.state,i,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const ka=T.define(),br=T.define(),Ci=T.define(),va=T.define(),xr=T.define(),Ca=T.define();function ho(s,t){let e=s.state.facet(Ca);if(!e.length)return e;let i=e.map(r=>r instanceof Function?r(s):r),n=[];return K.spans(i,t.from,t.to,{point(){},span(r,o,l,a){let c=r-t.from,h=o-t.from,f=n;for(let u=l.length-1;u>=0;u--,a--){let d=l[u].spec.bidiIsolate,p;if(d==null&&(d=af(t.text,c,h)),a>0&&f.length&&(p=f[f.length-1]).to==c&&p.direction==d)p.to=h,f=p.inner;else{let g={from:c,to:h,direction:d,inner:[]};f.push(g),f=g.inner}}}}),n}const Aa=T.define();function Ma(s){let t=0,e=0,i=0,n=0;for(let r of s.state.facet(Aa)){let o=r(s);o&&(o.left!=null&&(t=Math.max(t,o.left)),o.right!=null&&(e=Math.max(e,o.right)),o.top!=null&&(i=Math.max(i,o.top)),o.bottom!=null&&(n=Math.max(n,o.bottom)))}return{left:t,right:e,top:i,bottom:n}}const ui=T.define();class Nt{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}join(t){return new Nt(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fromA>i.toA)){if(n.toAh)break;r+=2}if(!a)return i;new Nt(a.fromA,a.toA,a.fromB,a.toB).addToSet(i),o=a.toA,l=a.toB}}}class wn{constructor(t,e,i){this.view=t,this.state=e,this.transactions=i,this.flags=0,this.startState=t.state,this.changes=et.empty(this.startState.doc.length);for(let r of i)this.changes=this.changes.compose(r.changes);let n=[];this.changes.iterChangedRanges((r,o,l,a)=>n.push(new Nt(r,o,l,a))),this.changedRanges=n}static create(t,e,i){return new wn(t,e,i)}get viewportChanged(){return(this.flags&4)>0}get heightChanged(){return(this.flags&2)>0}get geometryChanged(){return this.docChanged||(this.flags&10)>0}get focusChanged(){return(this.flags&1)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(t=>t.selection)}get empty(){return this.flags==0&&this.transactions.length==0}}class co extends ${get length(){return this.view.state.doc.length}constructor(t){super(),this.view=t,this.decorations=[],this.dynamicDecorationMap=[!1],this.domChanged=null,this.hasComposition=null,this.markedForComposition=new Set,this.editContextFormatting=P.none,this.lastCompositionAfterCursor=!1,this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.setDOM(t.contentDOM),this.children=[new Q],this.children[0].setParent(this),this.updateDeco(),this.updateInner([new Nt(0,0,0,t.state.doc.length)],0,null)}update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.every(({fromA:c,toA:h})=>hthis.minWidthTo)?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.updateEditContextFormatting(t);let n=-1;this.view.inputState.composing>=0&&!this.view.observer.editContext&&(!((e=this.domChanged)===null||e===void 0)&&e.newSel?n=this.domChanged.newSel.head:!mf(t.changes,this.hasComposition)&&!t.selectionSet&&(n=t.state.selection.main.head));let r=n>-1?ff(this.view,t.changes,n):null;if(this.domChanged=null,this.hasComposition){this.markedForComposition.clear();let{from:c,to:h}=this.hasComposition;i=new Nt(c,h,t.changes.mapPos(c,-1),t.changes.mapPos(h,1)).addToSet(i.slice())}this.hasComposition=r?{from:r.range.fromB,to:r.range.toB}:null,(D.ie||D.chrome)&&!r&&t&&t.state.doc.lines!=t.startState.doc.lines&&(this.forceSelection=!0);let o=this.decorations,l=this.updateDeco(),a=pf(o,l,t.changes);return i=Nt.extendWithRanges(i,a),!(this.flags&7)&&i.length==0?!1:(this.updateInner(i,t.startState.doc.length,r),t.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(t,e,i){this.view.viewState.mustMeasureContent=!0,this.updateChildren(t,e,i);let{observer:n}=this.view;n.ignore(()=>{this.dom.style.height=this.view.viewState.contentHeight/this.view.scaleY+"px",this.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let o=D.chrome||D.ios?{node:n.selectionRange.focusNode,written:!1}:void 0;this.sync(this.view,o),this.flags&=-8,o&&(o.written||n.selectionRange.focusNode!=o.node)&&(this.forceSelection=!0),this.dom.style.height=""}),this.markedForComposition.forEach(o=>o.flags&=-9);let r=[];if(this.view.viewport.from||this.view.viewport.to=0?n[o]:null;if(!l)break;let{fromA:a,toA:c,fromB:h,toB:f}=l,u,d,p,g;if(i&&i.range.fromBh){let S=xi.build(this.view.state.doc,h,i.range.fromB,this.decorations,this.dynamicDecorationMap),w=xi.build(this.view.state.doc,i.range.toB,f,this.decorations,this.dynamicDecorationMap);d=S.breakAtStart,p=S.openStart,g=w.openEnd;let M=this.compositionView(i);w.breakAtStart?M.breakAfter=1:w.content.length&&M.merge(M.length,M.length,w.content[0],!1,w.openStart,0)&&(M.breakAfter=w.content[0].breakAfter,w.content.shift()),S.content.length&&M.merge(0,0,S.content[S.content.length-1],!0,0,S.openEnd)&&S.content.pop(),u=S.content.concat(M).concat(w.content)}else({content:u,breakAtStart:d,openStart:p,openEnd:g}=xi.build(this.view.state.doc,h,f,this.decorations,this.dynamicDecorationMap));let{i:m,off:y}=r.findPos(c,1),{i:x,off:k}=r.findPos(a,-1);Zl(this,x,k,m,y,u,d,p,g)}i&&this.fixCompositionDOM(i)}updateEditContextFormatting(t){this.editContextFormatting=this.editContextFormatting.map(t.changes);for(let e of t.transactions)for(let i of e.effects)i.is(Sa)&&(this.editContextFormatting=i.value)}compositionView(t){let e=new Ht(t.text.nodeValue);e.flags|=8;for(let{deco:n}of t.marks)e=new ne(n,[e],e.length);let i=new Q;return i.append(e,0),i}fixCompositionDOM(t){let e=(r,o)=>{o.flags|=8|(o.children.some(a=>a.flags&7)?1:0),this.markedForComposition.add(o);let l=$.get(r);l&&l!=o&&(l.dom=null),o.setDOM(r)},i=this.childPos(t.range.fromB,1),n=this.children[i.i];e(t.line,n);for(let r=t.marks.length-1;r>=-1;r--)i=n.childPos(i.off,1),n=n.children[i.i],e(r>=0?t.marks[r].node:t.text,n)}updateSelection(t=!1,e=!1){(t||!this.view.observer.selectionRange.focusNode)&&this.view.observer.readSelectionRange();let i=this.view.root.activeElement,n=i==this.dom,r=!n&&cn(this.dom,this.view.observer.selectionRange)&&!(i&&this.dom.contains(i));if(!(n||e||r))return;let o=this.forceSelection;this.forceSelection=!1;let l=this.view.state.selection.main,a=this.moveToLine(this.domAtPos(l.anchor)),c=l.empty?a:this.moveToLine(this.domAtPos(l.head));if(D.gecko&&l.empty&&!this.hasComposition&&cf(a)){let f=document.createTextNode("");this.view.observer.ignore(()=>a.node.insertBefore(f,a.node.childNodes[a.offset]||null)),a=c=new ct(f,0),o=!0}let h=this.view.observer.selectionRange;(o||!h.focusNode||(!bi(a.node,a.offset,h.anchorNode,h.anchorOffset)||!bi(c.node,c.offset,h.focusNode,h.focusOffset))&&!this.suppressWidgetCursorChange(h,l))&&(this.view.observer.ignore(()=>{D.android&&D.chrome&&this.dom.contains(h.focusNode)&&gf(h.focusNode,this.dom)&&(this.dom.blur(),this.dom.focus({preventScroll:!0}));let f=vi(this.view.root);if(f)if(l.empty){if(D.gecko){let u=uf(a.node,a.offset);if(u&&u!=3){let d=(u==1?Xl:_l)(a.node,a.offset);d&&(a=new ct(d.node,d.offset))}}f.collapse(a.node,a.offset),l.bidiLevel!=null&&f.caretBidiLevel!==void 0&&(f.caretBidiLevel=l.bidiLevel)}else if(f.extend){f.collapse(a.node,a.offset);try{f.extend(c.node,c.offset)}catch{}}else{let u=document.createRange();l.anchor>l.head&&([a,c]=[c,a]),u.setEnd(c.node,c.offset),u.setStart(a.node,a.offset),f.removeAllRanges(),f.addRange(u)}r&&this.view.root.activeElement==this.dom&&(this.dom.blur(),i&&i.focus())}),this.view.observer.setSelectionRange(a,c)),this.impreciseAnchor=a.precise?null:new ct(h.anchorNode,h.anchorOffset),this.impreciseHead=c.precise?null:new ct(h.focusNode,h.focusOffset)}suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&bi(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset)&&this.posFromDOM(t.focusNode,t.focusOffset)==e.head}enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=t.state.selection.main,i=vi(t.root),{anchorNode:n,anchorOffset:r}=t.observer.selectionRange;if(!i||!e.empty||!e.assoc||!i.modify)return;let o=Q.find(this,e.head);if(!o)return;let l=o.posAtStart;if(e.head==l||e.head==l+o.length)return;let a=this.coordsAt(e.head,-1),c=this.coordsAt(e.head,1);if(!a||!c||a.bottom>c.top)return;let h=this.domAtPos(e.head+e.assoc);i.collapse(h.node,h.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let f=t.observer.selectionRange;t.docView.posFromDOM(f.anchorNode,f.anchorOffset)!=e.from&&i.collapse(n,r)}moveToLine(t){let e=this.dom,i;if(t.node!=e)return t;for(let n=t.offset;!i&&n=0;n--){let r=$.get(e.childNodes[n]);r instanceof Q&&(i=r.domAtPos(r.length))}return i?new ct(i.node,i.offset,!0):t}nearest(t){for(let e=t;e;){let i=$.get(e);if(i&&i.rootView==this)return i;e=e.parentNode}return null}posFromDOM(t,e){let i=this.nearest(t);if(!i)throw new RangeError("Trying to find position for a DOM position outside of the document");return i.localPosFromDOM(t,e)+i.posAtStart}domAtPos(t){let{i:e,off:i}=this.childCursor().findPos(t,-1);for(;e=0;o--){let l=this.children[o],a=r-l.breakAfter,c=a-l.length;if(at||l.covers(1))&&(!i||l instanceof Q&&!(i instanceof Q&&e>=0)))i=l,n=c;else if(i&&c==t&&a==t&&l instanceof te&&Math.abs(e)<2){if(l.deco.startSide<0)break;o&&(i=null)}r=c}return i?i.coordsAt(t-n,e):null}coordsForChar(t){let{i:e,off:i}=this.childPos(t,1),n=this.children[e];if(!(n instanceof Q))return null;for(;n.children.length;){let{i:l,off:a}=n.childPos(i,1);for(;;l++){if(l==n.children.length)return null;if((n=n.children[l]).length)break}i=a}if(!(n instanceof Ht))return null;let r=ot(n.text,i);if(r==i)return null;let o=Te(n.dom,i,r).getClientRects();for(let l=0;lMath.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,l=-1,a=this.view.textDirection==X.LTR;for(let c=0,h=0;hn)break;if(c>=i){let d=f.dom.getBoundingClientRect();if(e.push(d.height),o){let p=f.dom.lastChild,g=p?Ge(p):[];if(g.length){let m=g[g.length-1],y=a?m.right-d.left:d.right-m.left;y>l&&(l=y,this.minWidth=r,this.minWidthFrom=c,this.minWidthTo=u)}}}c=u+f.breakAfter}return e}textDirectionAt(t){let{i:e}=this.childPos(t,1);return getComputedStyle(this.children[e].dom).direction=="rtl"?X.RTL:X.LTR}measureTextSize(){for(let r of this.children)if(r instanceof Q){let o=r.measureTextSize();if(o)return o}let t=document.createElement("div"),e,i,n;return t.className="cm-line",t.style.width="99999px",t.style.position="absolute",t.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.dom.appendChild(t);let r=Ge(t.firstChild)[0];e=t.getBoundingClientRect().height,i=r?r.width/27:7,n=r?r.height:e,t.remove()}),{lineHeight:e,charWidth:i,textHeight:n}}childCursor(t=this.length){let e=this.children.length;return e&&(t-=this.children[--e].length),new Ql(this.children,t,e)}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;n++){let r=n==e.viewports.length?null:e.viewports[n],o=r?r.from-1:this.length;if(o>i){let l=(e.lineBlockAt(o).bottom-e.lineBlockAt(i).top)/this.view.scaleY;t.push(P.replace({widget:new Ns(l),block:!0,inclusive:!0,isBlockGap:!0}).range(i,o))}if(!r)break;i=r.to+1}return P.set(t)}updateDeco(){let t=1,e=this.view.state.facet(Ci).map(r=>(this.dynamicDecorationMap[t++]=typeof r=="function")?r(this.view):r),i=!1,n=this.view.state.facet(va).map((r,o)=>{let l=typeof r=="function";return l&&(i=!0),l?r(this.view):r});for(n.length&&(this.dynamicDecorationMap[t++]=i,e.push(K.join(n))),this.decorations=[this.editContextFormatting,...e,this.computeBlockGapDeco(),this.view.viewState.lineGapDeco];te.anchor?-1:1),n;if(!i)return;!e.empty&&(n=this.coordsAt(e.anchor,e.anchor>e.head?-1:1))&&(i={left:Math.min(i.left,n.left),top:Math.min(i.top,n.top),right:Math.max(i.right,n.right),bottom:Math.max(i.bottom,n.bottom)});let r=Ma(this.view),o={left:i.left-r.left,top:i.top-r.top,right:i.right+r.right,bottom:i.bottom+r.bottom},{offsetWidth:l,offsetHeight:a}=this.view.scrollDOM;Kc(this.view.scrollDOM,o,e.head{it.from&&(e=!0)}),e}function yf(s,t,e=1){let i=s.charCategorizer(t),n=s.doc.lineAt(t),r=t-n.from;if(n.length==0)return b.cursor(t);r==0?e=1:r==n.length&&(e=-1);let o=r,l=r;e<0?o=ot(n.text,r,!1):l=ot(n.text,r);let a=i(n.text.slice(o,l));for(;o>0;){let c=ot(n.text,o,!1);if(i(n.text.slice(c,o))!=a)break;o=c}for(;ls?t.left-s:Math.max(0,s-t.right)}function xf(s,t){return t.top>s?t.top-s:Math.max(0,s-t.bottom)}function Zn(s,t){return s.topt.top+1}function fo(s,t){return ts.bottom?{top:s.top,left:s.left,right:s.right,bottom:t}:s}function zs(s,t,e){let i,n,r,o,l=!1,a,c,h,f;for(let p=s.firstChild;p;p=p.nextSibling){let g=Ge(p);for(let m=0;mk||o==k&&r>x){i=p,n=y,r=x,o=k;let S=k?e0?m0)}x==0?e>y.bottom&&(!h||h.bottomy.top)&&(c=p,f=y):h&&Zn(h,y)?h=uo(h,y.bottom):f&&Zn(f,y)&&(f=fo(f,y.top))}}if(h&&h.bottom>=e?(i=a,n=h):f&&f.top<=e&&(i=c,n=f),!i)return{node:s,offset:0};let u=Math.max(n.left,Math.min(n.right,t));if(i.nodeType==3)return po(i,u,e);if(l&&i.contentEditable!="false")return zs(i,u,e);let d=Array.prototype.indexOf.call(s.childNodes,i)+(t>=(n.left+n.right)/2?1:0);return{node:s,offset:d}}function po(s,t,e){let i=s.nodeValue.length,n=-1,r=1e9,o=0;for(let l=0;le?h.top-e:e-h.bottom)-1;if(h.left-1<=t&&h.right+1>=t&&f=(h.left+h.right)/2,d=u;if((D.chrome||D.gecko)&&Te(s,l).getBoundingClientRect().left==h.right&&(d=!u),f<=0)return{node:s,offset:l+(d?1:0)};n=l+(d?1:0),r=f}}}return{node:s,offset:n>-1?n:o>0?s.nodeValue.length:0}}function Oa(s,t,e,i=-1){var n,r;let o=s.contentDOM.getBoundingClientRect(),l=o.top+s.viewState.paddingTop,a,{docHeight:c}=s.viewState,{x:h,y:f}=t,u=f-l;if(u<0)return 0;if(u>c)return s.state.doc.length;for(let S=s.viewState.heightOracle.textHeight/2,w=!1;a=s.elementAtHeight(u),a.type!=Ot.Text;)for(;u=i>0?a.bottom+S:a.top-S,!(u>=0&&u<=c);){if(w)return e?null:0;w=!0,i=-i}f=l+u;let d=a.from;if(ds.viewport.to)return s.viewport.to==s.state.doc.length?s.state.doc.length:e?null:go(s,o,a,h,f);let p=s.dom.ownerDocument,g=s.root.elementFromPoint?s.root:p,m=g.elementFromPoint(h,f);m&&!s.contentDOM.contains(m)&&(m=null),m||(h=Math.max(o.left+1,Math.min(o.right-1,h)),m=g.elementFromPoint(h,f),m&&!s.contentDOM.contains(m)&&(m=null));let y,x=-1;if(m&&((n=s.docView.nearest(m))===null||n===void 0?void 0:n.isEditable)!=!1){if(p.caretPositionFromPoint){let S=p.caretPositionFromPoint(h,f);S&&({offsetNode:y,offset:x}=S)}else if(p.caretRangeFromPoint){let S=p.caretRangeFromPoint(h,f);S&&({startContainer:y,startOffset:x}=S,(!s.contentDOM.contains(y)||D.safari&&wf(y,x,h)||D.chrome&&Sf(y,x,h))&&(y=void 0))}}if(!y||!s.docView.dom.contains(y)){let S=Q.find(s.docView,d);if(!S)return u>a.top+a.height/2?a.to:a.from;({node:y,offset:x}=zs(S.dom,h,f))}let k=s.docView.nearest(y);if(!k)return null;if(k.isWidget&&((r=k.dom)===null||r===void 0?void 0:r.nodeType)==1){let S=k.dom.getBoundingClientRect();return t.ys.defaultLineHeight*1.5){let l=s.viewState.heightOracle.textHeight,a=Math.floor((n-e.top-(s.defaultLineHeight-l)*.5)/l);r+=a*s.viewState.heightOracle.lineLength}let o=s.state.sliceDoc(e.from,e.to);return e.from+Ms(o,r,s.state.tabSize)}function wf(s,t,e){let i;if(s.nodeType!=3||t!=(i=s.nodeValue.length))return!1;for(let n=s.nextSibling;n;n=n.nextSibling)if(n.nodeType!=1||n.nodeName!="BR")return!1;return Te(s,i-1,i).getBoundingClientRect().left>e}function Sf(s,t,e){if(t!=0)return!1;for(let n=s;;){let r=n.parentNode;if(!r||r.nodeType!=1||r.firstChild!=n)return!1;if(r.classList.contains("cm-line"))break;n=r}let i=s.nodeType==1?s.getBoundingClientRect():Te(s,0,Math.max(s.nodeValue.length,1)).getBoundingClientRect();return e-i.left>5}function qs(s,t){let e=s.lineBlockAt(t);if(Array.isArray(e.type)){for(let i of e.type)if(i.to>t||i.to==t&&(i.to==e.to||i.type==Ot.Text))return i}return e}function kf(s,t,e,i){let n=qs(s,t.head),r=!i||n.type!=Ot.Text||!(s.lineWrapping||n.widgetLineBreaks)?null:s.coordsAtPos(t.assoc<0&&t.head>n.from?t.head-1:t.head);if(r){let o=s.dom.getBoundingClientRect(),l=s.textDirectionAt(n.from),a=s.posAtCoords({x:e==(l==X.LTR)?o.right-1:o.left+1,y:(r.top+r.bottom)/2});if(a!=null)return b.cursor(a,e?-1:1)}return b.cursor(e?n.to:n.from,e?-1:1)}function mo(s,t,e,i){let n=s.state.doc.lineAt(t.head),r=s.bidiSpans(n),o=s.textDirectionAt(n.from);for(let l=t,a=null;;){let c=lf(n,r,o,l,e),h=fa;if(!c){if(n.number==(e?s.state.doc.lines:1))return l;h=` +`,n=s.state.doc.line(n.number+(e?1:-1)),r=s.bidiSpans(n),c=s.visualLineSide(n,!e)}if(a){if(!a(h))return l}else{if(!i)return c;a=i(h)}l=c}}function vf(s,t,e){let i=s.state.charCategorizer(t),n=i(e);return r=>{let o=i(r);return n==G.Space&&(n=o),n==o}}function Cf(s,t,e,i){let n=t.head,r=e?1:-1;if(n==(e?s.state.doc.length:0))return b.cursor(n,t.assoc);let o=t.goalColumn,l,a=s.contentDOM.getBoundingClientRect(),c=s.coordsAtPos(n,t.assoc||-1),h=s.documentTop;if(c)o==null&&(o=c.left-a.left),l=r<0?c.top:c.bottom;else{let d=s.viewState.lineBlockAt(n);o==null&&(o=Math.min(a.right-a.left,s.defaultCharacterWidth*(n-d.from))),l=(r<0?d.top:d.bottom)+h}let f=a.left+o,u=i??s.viewState.heightOracle.textHeight>>1;for(let d=0;;d+=10){let p=l+(u+d)*r,g=Oa(s,{x:f,y:p},!1,r);if(pa.bottom||(r<0?gn)){let m=s.docView.coordsForChar(g),y=!m||p{if(t>r&&tn(s)),e.from,t.head>e.from?-1:1);return i==e.from?e:b.cursor(i,ir)&&this.lineBreak(),n=o}return this.findPointBefore(i,e),this}readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&(i.pos=this.text.length+Math.min(i.offset,e.length));for(let i=0,n=this.lineSeparator?null:/\r\n?|\n/g;;){let r=-1,o=1,l;if(this.lineSeparator?(r=e.indexOf(this.lineSeparator,i),o=this.lineSeparator.length):(l=n.exec(e))&&(r=l.index,o=l[0].length),this.append(e.slice(i,r<0?e.length:r)),r<0)break;if(this.lineBreak(),o>1)for(let a of this.points)a.node==t&&a.pos>this.text.length&&(a.pos-=o-1);i=r+o}}readNode(t){if(t.cmIgnore)return;let e=$.get(t),i=e&&e.overrideDOMText;if(i!=null){this.findPointInside(t,i.length);for(let n=i.iter();!n.next().done;)n.lineBreak?this.lineBreak():this.append(n.value)}else t.nodeType==3?this.readTextNode(t):t.nodeName=="BR"?t.nextSibling&&this.lineBreak():t.nodeType==1&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(t.nodeType==3?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+(Mf(t,i.node,i.offset)?e:0))}}function Mf(s,t,e){for(;;){if(!t||e-1;let{impreciseHead:r,impreciseAnchor:o}=t.docView;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=t.docView.domBoundsAround(e,i,0))){let l=r||o?[]:Pf(t),a=new Af(l,t.state);a.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=a.text,this.newSel=Bf(l,this.bounds.from)}else{let l=t.observer.selectionRange,a=r&&r.node==l.focusNode&&r.offset==l.focusOffset||!Ts(t.contentDOM,l.focusNode)?t.state.selection.main.head:t.docView.posFromDOM(l.focusNode,l.focusOffset),c=o&&o.node==l.anchorNode&&o.offset==l.anchorOffset||!Ts(t.contentDOM,l.anchorNode)?t.state.selection.main.anchor:t.docView.posFromDOM(l.anchorNode,l.anchorOffset),h=t.viewport;if((D.ios||D.chrome)&&t.state.selection.main.empty&&a!=c&&(h.from>0||h.toDate.now()-100?s.inputState.lastKeyCode:-1;if(t.bounds){let{from:o,to:l}=t.bounds,a=n.from,c=null;(r===8||D.android&&t.text.length=n.from&&e.to<=n.to&&(e.from!=n.from||e.to!=n.to)&&n.to-n.from-(e.to-e.from)<=4?e={from:n.from,to:n.to,insert:s.state.doc.slice(n.from,e.from).append(e.insert).append(s.state.doc.slice(e.to,n.to))}:(D.mac||D.android)&&e&&e.from==e.to&&e.from==n.head-1&&/^\. ?$/.test(e.insert.toString())&&s.contentDOM.getAttribute("autocorrect")=="off"?(i&&e.insert.length==2&&(i=b.single(i.main.anchor-1,i.main.head-1)),e={from:n.from,to:n.to,insert:V.of([" "])}):D.chrome&&e&&e.from==e.to&&e.from==n.head&&e.insert.toString()==` + `&&s.lineWrapping&&(i&&(i=b.single(i.main.anchor-1,i.main.head-1)),e={from:n.from,to:n.to,insert:V.of([" "])}),e)return wr(s,e,i,r);if(i&&!i.main.eq(n)){let o=!1,l="select";return s.inputState.lastSelectionTime>Date.now()-50&&(s.inputState.lastSelectionOrigin=="select"&&(o=!0),l=s.inputState.lastSelectionOrigin),s.dispatch({selection:i,scrollIntoView:o,userEvent:l}),!0}else return!1}function wr(s,t,e,i=-1){if(D.ios&&s.inputState.flushIOSKey(t))return!0;let n=s.state.selection.main;if(D.android&&(t.to==n.to&&(t.from==n.from||t.from==n.from-1&&s.state.sliceDoc(t.from,n.from)==" ")&&t.insert.length==1&&t.insert.lines==2&&qe(s.contentDOM,"Enter",13)||(t.from==n.from-1&&t.to==n.to&&t.insert.length==0||i==8&&t.insert.lengthn.head)&&qe(s.contentDOM,"Backspace",8)||t.from==n.from&&t.to==n.to+1&&t.insert.length==0&&qe(s.contentDOM,"Delete",46)))return!0;let r=t.insert.toString();s.inputState.composing>=0&&s.inputState.composing++;let o,l=()=>o||(o=Of(s,t,e));return s.state.facet(ma).some(a=>a(s,t.from,t.to,r,l))||s.dispatch(l()),!0}function Of(s,t,e){let i,n=s.state,r=n.selection.main;if(t.from>=r.from&&t.to<=r.to&&t.to-t.from>=(r.to-r.from)/3&&(!e||e.main.empty&&e.main.from==t.from+t.insert.length)&&s.inputState.composing<0){let l=r.fromt.to?n.sliceDoc(t.to,r.to):"";i=n.replaceSelection(s.state.toText(l+t.insert.sliceString(0,void 0,s.state.lineBreak)+a))}else{let l=n.changes(t),a=e&&e.main.to<=l.newLength?e.main:void 0;if(n.selection.ranges.length>1&&s.inputState.composing>=0&&t.to<=r.to&&t.to>=r.to-10){let c=s.state.sliceDoc(t.from,t.to),h,f=e&&Da(s,e.main.head);if(f){let p=t.insert.length-(t.to-t.from);h={from:f.from,to:f.to-p}}else h=s.state.doc.lineAt(r.head);let u=r.to-t.to,d=r.to-r.from;i=n.changeByRange(p=>{if(p.from==r.from&&p.to==r.to)return{changes:l,range:a||p.map(l)};let g=p.to-u,m=g-c.length;if(p.to-p.from!=d||s.state.sliceDoc(m,g)!=c||p.to>=h.from&&p.from<=h.to)return{range:p};let y=n.changes({from:m,to:g,insert:t.insert}),x=p.to-r.to;return{changes:y,range:a?b.range(Math.max(0,a.anchor+x),Math.max(0,a.head+x)):p.map(y)}})}else i={changes:l,selection:a&&n.selection.replaceRange(a)}}let o="input.type";return(s.composing||s.inputState.compositionPendingChange&&s.inputState.compositionEndedAt>Date.now()-50)&&(s.inputState.compositionPendingChange=!1,o+=".compose",s.inputState.compositionFirstChange&&(o+=".start",s.inputState.compositionFirstChange=!1)),n.update(i,{userEvent:o,scrollIntoView:!0})}function Tf(s,t,e,i){let n=Math.min(s.length,t.length),r=0;for(;r0&&l>0&&s.charCodeAt(o-1)==t.charCodeAt(l-1);)o--,l--;if(i=="end"){let a=Math.max(0,r-Math.min(o,l));e-=o+a-r}if(o=o?r-e:0;r-=a,l=r+(l-o),o=r}else if(l=l?r-e:0;r-=a,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}function Pf(s){let t=[];if(s.root.activeElement!=s.contentDOM)return t;let{anchorNode:e,anchorOffset:i,focusNode:n,focusOffset:r}=s.observer.selectionRange;return e&&(t.push(new yo(e,i)),(n!=e||r!=i)&&t.push(new yo(n,r))),t}function Bf(s,t){if(s.length==0)return null;let e=s[0].pos,i=s.length==2?s[1].pos:e;return e>-1&&i>-1?b.single(e+t,i+t):null}class Rf{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}constructor(t){this.view=t,this.lastKeyCode=0,this.lastKeyTime=0,this.lastTouchTime=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.pendingIOSKey=void 0,this.tabFocusMode=-1,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastContextMenu=0,this.scrollHandlers=[],this.handlers=Object.create(null),this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.compositionPendingKey=!1,this.compositionPendingChange=!1,this.mouseSelection=null,this.draggedContent=null,this.handleEvent=this.handleEvent.bind(this),this.notifiedFocused=t.hasFocus,D.safari&&t.contentDOM.addEventListener("input",()=>null),D.gecko&&Jf(t.contentDOM.ownerDocument)}handleEvent(t){!Wf(this.view,t)||this.ignoreDuringComposition(t)||t.type=="keydown"&&this.keydown(t)||this.runHandlers(t.type,t)}runHandlers(t,e){let i=this.handlers[t];if(i){for(let n of i.observers)n(this.view,e);for(let n of i.handlers){if(e.defaultPrevented)break;if(n(this.view,e)){e.preventDefault();break}}}}ensureHandlers(t){let e=Lf(t),i=this.handlers,n=this.view.contentDOM;for(let r in e)if(r!="scroll"){let o=!e[r].handlers.length,l=i[r];l&&o!=!l.handlers.length&&(n.removeEventListener(r,this.handleEvent),l=null),l||n.addEventListener(r,this.handleEvent,{passive:o})}for(let r in i)r!="scroll"&&!e[r]&&n.removeEventListener(r,this.handleEvent);this.handlers=e}keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),t.keyCode==9&&this.tabFocusMode>-1&&(!this.tabFocusMode||Date.now()<=this.tabFocusMode))return!0;if(this.tabFocusMode>0&&t.keyCode!=27&&Ba.indexOf(t.keyCode)<0&&(this.tabFocusMode=-1),D.android&&D.chrome&&!t.synthetic&&(t.keyCode==13||t.keyCode==8))return this.view.observer.delayAndroidKey(t.key,t.keyCode),!0;let e;return D.ios&&!t.synthetic&&!t.altKey&&!t.metaKey&&((e=Pa.find(i=>i.keyCode==t.keyCode))&&!t.ctrlKey||Ef.indexOf(t.key)>-1&&t.ctrlKey&&!t.shiftKey)?(this.pendingIOSKey=e||t,setTimeout(()=>this.flushIOSKey(),250),!0):(t.keyCode!=229&&this.view.observer.forceFlush(),!1)}flushIOSKey(t){let e=this.pendingIOSKey;return!e||e.key=="Enter"&&t&&t.from0?!0:D.safari&&!D.ios&&this.compositionPendingKey&&Date.now()-this.compositionEndedAt<100?(this.compositionPendingKey=!1,!0):!1:!1}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.view.observer.update(t),this.mouseSelection&&this.mouseSelection.update(t),this.draggedContent&&t.docChanged&&(this.draggedContent=this.draggedContent.map(t.changes)),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}function bo(s,t){return(e,i)=>{try{return t.call(s,i,e)}catch(n){Dt(e.state,n)}}}function Lf(s){let t=Object.create(null);function e(i){return t[i]||(t[i]={observers:[],handlers:[]})}for(let i of s){let n=i.spec;if(n&&n.domEventHandlers)for(let r in n.domEventHandlers){let o=n.domEventHandlers[r];o&&e(r).handlers.push(bo(i.value,o))}if(n&&n.domEventObservers)for(let r in n.domEventObservers){let o=n.domEventObservers[r];o&&e(r).observers.push(bo(i.value,o))}}for(let i in Wt)e(i).handlers.push(Wt[i]);for(let i in Ft)e(i).observers.push(Ft[i]);return t}const Pa=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Enter",keyCode:13,inputType:"insertLineBreak"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],Ef="dthko",Ba=[16,17,18,20,91,92,224,225],ji=6;function Ui(s){return Math.max(0,s)*.7+8}function If(s,t){return Math.max(Math.abs(s.clientX-t.clientX),Math.abs(s.clientY-t.clientY))}class Nf{constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i,this.mustSelect=n,this.scrollSpeed={x:0,y:0},this.scrolling=-1,this.lastEvent=e,this.scrollParents=$c(t.contentDOM),this.atoms=t.state.facet(xr).map(o=>o(t));let r=t.contentDOM.ownerDocument;r.addEventListener("mousemove",this.move=this.move.bind(this)),r.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(W.allowMultipleSelections)&&Ff(t,e),this.dragging=Hf(t,e)&&Ea(e)==1?null:!1}start(t){this.dragging===!1&&this.select(t)}move(t){if(t.buttons==0)return this.destroy();if(this.dragging||this.dragging==null&&If(this.startEvent,t)<10)return;this.select(this.lastEvent=t);let e=0,i=0,n=0,r=0,o=this.view.win.innerWidth,l=this.view.win.innerHeight;this.scrollParents.x&&({left:n,right:o}=this.scrollParents.x.getBoundingClientRect()),this.scrollParents.y&&({top:r,bottom:l}=this.scrollParents.y.getBoundingClientRect());let a=Ma(this.view);t.clientX-a.left<=n+ji?e=-Ui(n-t.clientX):t.clientX+a.right>=o-ji&&(e=Ui(t.clientX-o)),t.clientY-a.top<=r+ji?i=-Ui(r-t.clientY):t.clientY+a.bottom>=l-ji&&(i=Ui(t.clientY-l)),this.setScrollSpeed(e,i)}up(t){this.dragging==null&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=this.view.inputState.draggedContent=null}setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&(this.scrolling=setInterval(()=>this.scroll(),50)):this.scrolling>-1&&(clearInterval(this.scrolling),this.scrolling=-1)}scroll(){let{x:t,y:e}=this.scrollSpeed;t&&this.scrollParents.x&&(this.scrollParents.x.scrollLeft+=t,t=0),e&&this.scrollParents.y&&(this.scrollParents.y.scrollTop+=e,e=0),(t||e)&&this.view.win.scrollBy(t,e),this.dragging===!1&&this.select(this.lastEvent)}skipAtoms(t){let e=null;for(let i=0;ie.isUserEvent("input.type"))?this.destroy():this.style.update(t)&&setTimeout(()=>this.select(this.lastEvent),20)}}function Ff(s,t){let e=s.state.facet(ua);return e.length?e[0](t):D.mac?t.metaKey:t.ctrlKey}function Vf(s,t){let e=s.state.facet(da);return e.length?e[0](t):D.mac?!t.altKey:!t.ctrlKey}function Hf(s,t){let{main:e}=s.state.selection;if(e.empty)return!1;let i=vi(s.root);if(!i||i.rangeCount==0)return!0;let n=i.getRangeAt(0).getClientRects();for(let r=0;r=t.clientX&&o.top<=t.clientY&&o.bottom>=t.clientY)return!0}return!1}function Wf(s,t){if(!t.bubbles)return!0;if(t.defaultPrevented)return!1;for(let e=t.target,i;e!=s.contentDOM;e=e.parentNode)if(!e||e.nodeType==11||(i=$.get(e))&&i.ignoreEvent(t))return!1;return!0}const Wt=Object.create(null),Ft=Object.create(null),Ra=D.ie&&D.ie_version<15||D.ios&&D.webkit_version<604;function zf(s){let t=s.dom.parentNode;if(!t)return;let e=t.appendChild(document.createElement("textarea"));e.style.cssText="position: fixed; left: -10000px; top: 10px",e.focus(),setTimeout(()=>{s.focus(),e.remove(),La(s,e.value)},50)}function Wn(s,t,e){for(let i of s.facet(t))e=i(e,s);return e}function La(s,t){t=Wn(s.state,mr,t);let{state:e}=s,i,n=1,r=e.toText(t),o=r.lines==e.selection.ranges.length;if(Ks!=null&&e.selection.ranges.every(a=>a.empty)&&Ks==r.toString()){let a=-1;i=e.changeByRange(c=>{let h=e.doc.lineAt(c.from);if(h.from==a)return{range:c};a=h.from;let f=e.toText((o?r.line(n++).text:t)+e.lineBreak);return{changes:{from:h.from,insert:f},range:b.cursor(c.from+f.length)}})}else o?i=e.changeByRange(a=>{let c=r.line(n++);return{changes:{from:a.from,to:a.to,insert:c.text},range:b.cursor(a.from+c.length)}}):i=e.replaceSelection(r);s.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}Ft.scroll=s=>{s.inputState.lastScrollTop=s.scrollDOM.scrollTop,s.inputState.lastScrollLeft=s.scrollDOM.scrollLeft};Wt.keydown=(s,t)=>(s.inputState.setSelectionOrigin("select"),t.keyCode==27&&s.inputState.tabFocusMode!=0&&(s.inputState.tabFocusMode=Date.now()+2e3),!1);Ft.touchstart=(s,t)=>{s.inputState.lastTouchTime=Date.now(),s.inputState.setSelectionOrigin("select.pointer")};Ft.touchmove=s=>{s.inputState.setSelectionOrigin("select.pointer")};Wt.mousedown=(s,t)=>{if(s.observer.flush(),s.inputState.lastTouchTime>Date.now()-2e3)return!1;let e=null;for(let i of s.state.facet(pa))if(e=i(s,t),e)break;if(!e&&t.button==0&&(e=$f(s,t)),e){let i=!s.hasFocus;s.inputState.startMouseSelection(new Nf(s,t,e,i)),i&&s.observer.ignore(()=>{Gl(s.contentDOM);let r=s.root.activeElement;r&&!r.contains(s.contentDOM)&&r.blur()});let n=s.inputState.mouseSelection;if(n)return n.start(t),n.dragging===!1}return!1};function xo(s,t,e,i){if(i==1)return b.cursor(t,e);if(i==2)return yf(s.state,t,e);{let n=Q.find(s.docView,t),r=s.state.doc.lineAt(n?n.posAtEnd:t),o=n?n.posAtStart:r.from,l=n?n.posAtEnd:r.to;return lt>=e.top&&t<=e.bottom&&s>=e.left&&s<=e.right;function qf(s,t,e,i){let n=Q.find(s.docView,t);if(!n)return 1;let r=t-n.posAtStart;if(r==0)return 1;if(r==n.length)return-1;let o=n.coordsAt(r,-1);if(o&&wo(e,i,o))return-1;let l=n.coordsAt(r,1);return l&&wo(e,i,l)?1:o&&o.bottom>=i?-1:1}function So(s,t){let e=s.posAtCoords({x:t.clientX,y:t.clientY},!1);return{pos:e,bias:qf(s,e,t.clientX,t.clientY)}}const Kf=D.ie&&D.ie_version<=11;let ko=null,vo=0,Co=0;function Ea(s){if(!Kf)return s.detail;let t=ko,e=Co;return ko=s,Co=Date.now(),vo=!t||e>Date.now()-400&&Math.abs(t.clientX-s.clientX)<2&&Math.abs(t.clientY-s.clientY)<2?(vo+1)%3:1}function $f(s,t){let e=So(s,t),i=Ea(t),n=s.state.selection;return{update(r){r.docChanged&&(e.pos=r.changes.mapPos(e.pos),n=n.map(r.changes))},get(r,o,l){let a=So(s,r),c,h=xo(s,a.pos,a.bias,i);if(e.pos!=a.pos&&!o){let f=xo(s,e.pos,e.bias,i),u=Math.min(f.from,h.from),d=Math.max(f.to,h.to);h=u1&&(c=jf(n,a.pos))?c:l?n.addRange(h):b.create([h])}}}function jf(s,t){for(let e=0;e=t)return b.create(s.ranges.slice(0,e).concat(s.ranges.slice(e+1)),s.mainIndex==e?0:s.mainIndex-(s.mainIndex>e?1:0))}return null}Wt.dragstart=(s,t)=>{let{selection:{main:e}}=s.state;if(t.target.draggable){let n=s.docView.nearest(t.target);if(n&&n.isWidget){let r=n.posAtStart,o=r+n.length;(r>=e.to||o<=e.from)&&(e=b.range(r,o))}}let{inputState:i}=s;return i.mouseSelection&&(i.mouseSelection.dragging=!0),i.draggedContent=e,t.dataTransfer&&(t.dataTransfer.setData("Text",Wn(s.state,yr,s.state.sliceDoc(e.from,e.to))),t.dataTransfer.effectAllowed="copyMove"),!1};Wt.dragend=s=>(s.inputState.draggedContent=null,!1);function Ao(s,t,e,i){if(e=Wn(s.state,mr,e),!e)return;let n=s.posAtCoords({x:t.clientX,y:t.clientY},!1),{draggedContent:r}=s.inputState,o=i&&r&&Vf(s,t)?{from:r.from,to:r.to}:null,l={from:n,insert:e},a=s.state.changes(o?[o,l]:l);s.focus(),s.dispatch({changes:a,selection:{anchor:a.mapPos(n,-1),head:a.mapPos(n,1)},userEvent:o?"move.drop":"input.drop"}),s.inputState.draggedContent=null}Wt.drop=(s,t)=>{if(!t.dataTransfer)return!1;if(s.state.readOnly)return!0;let e=t.dataTransfer.files;if(e&&e.length){let i=Array(e.length),n=0,r=()=>{++n==e.length&&Ao(s,t,i.filter(o=>o!=null).join(s.state.lineBreak),!1)};for(let o=0;o{/[\x00-\x08\x0e-\x1f]{2}/.test(l.result)||(i[o]=l.result),r()},l.readAsText(e[o])}return!0}else{let i=t.dataTransfer.getData("Text");if(i)return Ao(s,t,i,!0),!0}return!1};Wt.paste=(s,t)=>{if(s.state.readOnly)return!0;s.observer.flush();let e=Ra?null:t.clipboardData;return e?(La(s,e.getData("text/plain")||e.getData("text/uri-list")),!0):(zf(s),!1)};function Uf(s,t){let e=s.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.value=t,i.focus(),i.selectionEnd=t.length,i.selectionStart=0,setTimeout(()=>{i.remove(),s.focus()},50)}function Gf(s){let t=[],e=[],i=!1;for(let n of s.selection.ranges)n.empty||(t.push(s.sliceDoc(n.from,n.to)),e.push(n));if(!t.length){let n=-1;for(let{from:r}of s.selection.ranges){let o=s.doc.lineAt(r);o.number>n&&(t.push(o.text),e.push({from:o.from,to:Math.min(s.doc.length,o.to+1)})),n=o.number}i=!0}return{text:Wn(s,yr,t.join(s.lineBreak)),ranges:e,linewise:i}}let Ks=null;Wt.copy=Wt.cut=(s,t)=>{let{text:e,ranges:i,linewise:n}=Gf(s.state);if(!e&&!n)return!1;Ks=n?e:null,t.type=="cut"&&!s.state.readOnly&&s.dispatch({changes:i,scrollIntoView:!0,userEvent:"delete.cut"});let r=Ra?null:t.clipboardData;return r?(r.clearData(),r.setData("text/plain",e),!0):(Uf(s,e),!1)};const Ia=se.define();function Na(s,t){let e=[];for(let i of s.facet(ya)){let n=i(s,t);n&&e.push(n)}return e?s.update({effects:e,annotations:Ia.of(!0)}):null}function Fa(s){setTimeout(()=>{let t=s.hasFocus;if(t!=s.inputState.notifiedFocused){let e=Na(s.state,t);e?s.dispatch(e):s.update([])}},10)}Ft.focus=s=>{s.inputState.lastFocusTime=Date.now(),!s.scrollDOM.scrollTop&&(s.inputState.lastScrollTop||s.inputState.lastScrollLeft)&&(s.scrollDOM.scrollTop=s.inputState.lastScrollTop,s.scrollDOM.scrollLeft=s.inputState.lastScrollLeft),Fa(s)};Ft.blur=s=>{s.observer.clearSelectionRange(),Fa(s)};Ft.compositionstart=Ft.compositionupdate=s=>{s.observer.editContext||(s.inputState.compositionFirstChange==null&&(s.inputState.compositionFirstChange=!0),s.inputState.composing<0&&(s.inputState.composing=0))};Ft.compositionend=s=>{s.observer.editContext||(s.inputState.composing=-1,s.inputState.compositionEndedAt=Date.now(),s.inputState.compositionPendingKey=!0,s.inputState.compositionPendingChange=s.observer.pendingRecords().length>0,s.inputState.compositionFirstChange=null,D.chrome&&D.android?s.observer.flushSoon():s.inputState.compositionPendingChange?Promise.resolve().then(()=>s.observer.flush()):setTimeout(()=>{s.inputState.composing<0&&s.docView.hasComposition&&s.update([])},50))};Ft.contextmenu=s=>{s.inputState.lastContextMenu=Date.now()};Wt.beforeinput=(s,t)=>{var e,i;if(t.inputType=="insertReplacementText"&&s.observer.editContext){let r=(e=t.dataTransfer)===null||e===void 0?void 0:e.getData("text/plain"),o=t.getTargetRanges();if(r&&o.length){let l=o[0],a=s.posAtDOM(l.startContainer,l.startOffset),c=s.posAtDOM(l.endContainer,l.endOffset);return wr(s,{from:a,to:c,insert:s.state.toText(r)},null),!0}}let n;if(D.chrome&&D.android&&(n=Pa.find(r=>r.inputType==t.inputType))&&(s.observer.delayAndroidKey(n.key,n.keyCode),n.key=="Backspace"||n.key=="Delete")){let r=((i=window.visualViewport)===null||i===void 0?void 0:i.height)||0;setTimeout(()=>{var o;(((o=window.visualViewport)===null||o===void 0?void 0:o.height)||0)>r+10&&s.hasFocus&&(s.contentDOM.blur(),s.focus())},100)}return D.ios&&t.inputType=="deleteContentForward"&&s.observer.flushSoon(),D.safari&&t.inputType=="insertText"&&s.inputState.composing>=0&&setTimeout(()=>Ft.compositionend(s,t),20),!1};const Mo=new Set;function Jf(s){Mo.has(s)||(Mo.add(s),s.addEventListener("copy",()=>{}),s.addEventListener("cut",()=>{}))}const Do=["pre-wrap","normal","pre-line","break-spaces"];let Xe=!1;function Oo(){Xe=!1}class Yf{constructor(t){this.lineWrapping=t,this.doc=V.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.textHeight=14,this.lineLength=30}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.max(0,Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength))),this.lineHeight*i}heightForLine(t){return this.lineWrapping?(1+Math.max(0,Math.ceil((t-this.lineLength)/(this.lineLength-5))))*this.lineHeight:this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return Do.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,a=Math.round(e)!=Math.round(this.lineHeight)||this.lineWrapping!=l;if(this.lineWrapping=l,this.lineHeight=e,this.charWidth=i,this.textHeight=n,this.lineLength=r,a){this.heightSamples={};for(let c=0;c0}set outdated(t){this.flags=(t?2:0)|this.flags&-3}setHeight(t){this.height!=t&&(Math.abs(this.height-t)>un&&(Xe=!0),this.height=t)}replace(t,e,i){return pt.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,n){let r=this,o=i.doc;for(let l=n.length-1;l>=0;l--){let{fromA:a,toA:c,fromB:h,toB:f}=n[l],u=r.lineAt(a,U.ByPosNoHeight,i.setDoc(e),0,0),d=u.to>=c?u:r.lineAt(c,U.ByPosNoHeight,i,0,0);for(f+=d.to-c,c=d.to;l>0&&u.from<=n[l-1].toA;)a=n[l-1].fromA,h=n[l-1].fromB,l--,ar*2){let l=t[e-1];l.break?t.splice(--e,1,l.left,null,l.right):t.splice(--e,1,l.left,l.right),i+=1+l.break,n-=l.size}else if(r>n*2){let l=t[i];l.break?t.splice(i,1,l.left,null,l.right):t.splice(i,1,l.left,l.right),i+=2+l.break,r-=l.size}else break;else if(n=r&&o(this.blockAt(0,i,n,r))}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setHeight(n.heights[n.index++]),this.outdated=!1,this}toString(){return`block(${this.length})`}}class At extends Va{constructor(t,e){super(t,e,null),this.collapsed=0,this.widgetHeight=0,this.breaks=0}blockAt(t,e,i,n){return new Jt(n,this.length,i,this.height,this.breaks)}replace(t,e,i){let n=i[0];return i.length==1&&(n instanceof At||n instanceof it&&n.flags&4)&&Math.abs(this.length-n.length)<10?(n instanceof it?n=new At(n.length,this.height):n.height=this.height,this.outdated||(n.outdated=!1),n):pt.of(i)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setHeight(n.heights[n.index++]):(i||this.outdated)&&this.setHeight(Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))+this.breaks*t.lineHeight),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class it extends pt{constructor(t){super(t,0)}heightMetrics(t,e){let i=t.doc.lineAt(e).number,n=t.doc.lineAt(e+this.length).number,r=n-i+1,o,l=0;if(t.lineWrapping){let a=Math.min(this.height,t.lineHeight*r);o=a/r,this.length>r+1&&(l=(this.height-a)/(this.length-r-1))}else o=this.height/r;return{firstLine:i,lastLine:n,perLine:o,perChar:l}}blockAt(t,e,i,n){let{firstLine:r,lastLine:o,perLine:l,perChar:a}=this.heightMetrics(e,n);if(e.lineWrapping){let c=n+(t0){let r=i[i.length-1];r instanceof it?i[i.length-1]=new it(r.length+n):i.push(null,new it(n-1))}if(t>0){let r=i[0];r instanceof it?i[0]=new it(t+r.length):i.unshift(new it(t-1),null)}return pt.of(i)}decomposeLeft(t,e){e.push(new it(t-1),null)}decomposeRight(t,e){e.push(null,new it(this.length-t-1))}updateHeight(t,e=0,i=!1,n){let r=e+this.length;if(n&&n.from<=e+this.length&&n.more){let o=[],l=Math.max(e,n.from),a=-1;for(n.from>e&&o.push(new it(n.from-e-1).updateHeight(t,e));l<=r&&n.more;){let h=t.doc.lineAt(l).length;o.length&&o.push(null);let f=n.heights[n.index++];a==-1?a=f:Math.abs(f-a)>=un&&(a=-2);let u=new At(h,f);u.outdated=!1,o.push(u),l+=h+1}l<=r&&o.push(null,new it(r-l).updateHeight(t,l));let c=pt.of(o);return(a<0||Math.abs(c.height-this.height)>=un||Math.abs(a-this.heightMetrics(t,e).perLine)>=un)&&(Xe=!0),Sn(this,c)}else(i||this.outdated)&&(this.setHeight(t.heightForGap(e,e+this.length)),this.outdated=!1);return this}toString(){return`gap(${this.length})`}}class _f extends pt{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return this.flags&1}blockAt(t,e,i,n){let r=i+this.left.height;return tl))return c;let h=e==U.ByPosNoHeight?U.ByPosNoHeight:U.ByPos;return a?c.join(this.right.lineAt(l,h,i,o,l)):this.left.lineAt(l,h,i,n,r).join(c)}forEachLine(t,e,i,n,r,o){let l=n+this.left.height,a=r+this.left.length+this.break;if(this.break)t=a&&this.right.forEachLine(t,e,i,l,a,o);else{let c=this.lineAt(a,U.ByPos,i,n,r);t=t&&c.from<=e&&o(c),e>c.to&&this.right.forEachLine(c.to+1,e,i,l,a,o)}}replace(t,e,i){let n=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-n,e-n,i));let r=[];t>0&&this.decomposeLeft(t,r);let o=r.length;for(let l of i)r.push(l);if(t>0&&To(r,o-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)return this.right.decomposeRight(t-n,e);t2*e.size||e.size>2*t.size?pt.of(this.break?[t,null,e]:[t,e]):(this.left=Sn(this.left,t),this.right=Sn(this.right,e),this.setHeight(t.height+e.height),this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,n){let{left:r,right:o}=this,l=e+r.length+this.break,a=null;return n&&n.from<=e+r.length&&n.more?a=r=r.updateHeight(t,e,i,n):r.updateHeight(t,e,i),n&&n.from<=l+o.length&&n.more?a=o=o.updateHeight(t,l,i,n):o.updateHeight(t,l,i),a?this.balanced(r,o):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function To(s,t){let e,i;s[t]==null&&(e=s[t-1])instanceof it&&(i=s[t+1])instanceof it&&s.splice(t-1,3,new it(e.length+1+i.length))}const Qf=5;class Sr{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let i=Math.min(e,this.lineEnd),n=this.nodes[this.nodes.length-1];n instanceof At?n.length+=i-this.pos:(i>this.pos||!this.isCovered)&&this.nodes.push(new At(i-this.pos,-1)),this.writtenTo=i,e>i&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=Qf)&&this.addLineDeco(n,r,o)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new At(this.pos-t,-1)),this.writtenTo=this.pos}blankContent(t,e){let i=new it(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof At)return t;let e=new At(0,-1);return this.nodes.push(e),e}addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCovered&&this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,e&&e.endSide>0&&(this.covering=t)}addLineDeco(t,e,i){let n=this.ensureLine();n.length+=i,n.collapsed+=i,n.widgetHeight=Math.max(n.widgetHeight,t),n.breaks+=e,this.writtenTo=this.pos=this.pos+i}finish(t){let e=this.nodes.length==0?null:this.nodes[this.nodes.length-1];this.lineStart>-1&&!(e instanceof At)&&!this.isCovered?this.nodes.push(new At(0,-1)):(this.writtenToh.clientHeight||h.scrollWidth>h.clientWidth)&&f.overflow!="visible"){let u=h.getBoundingClientRect();r=Math.max(r,u.left),o=Math.min(o,u.right),l=Math.max(l,u.top),a=Math.min(c==s.parentNode?n.innerHeight:a,u.bottom)}c=f.position=="absolute"||f.position=="fixed"?h.offsetParent:h.parentNode}else if(c.nodeType==11)c=c.host;else break;return{left:r-e.left,right:Math.max(r,o)-e.left,top:l-(e.top+t),bottom:Math.max(l,a)-(e.top+t)}}function iu(s,t){let e=s.getBoundingClientRect();return{left:0,right:e.right-e.left,top:t,bottom:e.bottom-(e.top+t)}}class es{constructor(t,e,i){this.from=t,this.to=e,this.size=i}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;itypeof i!="function"&&i.class=="cm-lineWrapping");this.heightOracle=new Yf(e),this.stateDeco=t.facet(Ci).filter(i=>typeof i!="function"),this.heightMap=pt.empty().applyChanges(this.stateDeco,V.empty,this.heightOracle.setDoc(t.doc),[new Nt(0,0,0,t.doc.length)]);for(let i=0;i<2&&(this.viewport=this.getViewport(0,null),!!this.updateForViewport());i++);this.updateViewportLines(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=P.set(this.lineGaps.map(i=>i.draw(this,!1))),this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let n=i?e.head:e.anchor;if(!t.some(({from:r,to:o})=>n>=r&&n<=o)){let{from:r,to:o}=this.lineBlockAt(n);t.push(new Gi(r,o))}}return this.viewports=t.sort((i,n)=>i.from-n.from),this.updateScaler()}updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.height<=7e6?Bo:new kr(this.heightOracle,this.heightMap,this.viewports),t.eq(this.scaler)?0:2}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.heightOracle.setDoc(this.state.doc),0,0,t=>{this.viewportLines.push(pi(t,this.scaler))})}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=this.state.facet(Ci).filter(h=>typeof h!="function");let n=t.changedRanges,r=Nt.extendWithRanges(n,Zf(i,this.stateDeco,t?t.changes:et.empty(this.state.doc.length))),o=this.heightMap.height,l=this.scrolledToBottom?null:this.scrollAnchorAt(this.scrollTop);Oo(),this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),r),(this.heightMap.height!=o||Xe)&&(t.flags|=2),l?(this.scrollAnchorPos=t.changes.mapPos(l.from,-1),this.scrollAnchorHeight=l.top):(this.scrollAnchorPos=-1,this.scrollAnchorHeight=this.heightMap.height);let a=r.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.heada.to)||!this.viewportIsAppropriate(a))&&(a=this.getViewport(0,e));let c=a.from!=this.viewport.from||a.to!=this.viewport.to;this.viewport=a,t.flags|=this.updateForViewport(),(c||!t.changes.empty||t.flags&2)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&t.selectionSet&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(xa)&&(this.mustEnforceCursorAssoc=!0)}measure(t){let e=t.contentDOM,i=window.getComputedStyle(e),n=this.heightOracle,r=i.whiteSpace;this.defaultTextDirection=i.direction=="rtl"?X.RTL:X.LTR;let o=this.heightOracle.mustRefreshForWrapping(r),l=e.getBoundingClientRect(),a=o||this.mustMeasureContent||this.contentDOMHeight!=l.height;this.contentDOMHeight=l.height,this.mustMeasureContent=!1;let c=0,h=0;if(l.width&&l.height){let{scaleX:S,scaleY:w}=Ul(e,l);(S>.005&&Math.abs(this.scaleX-S)>.005||w>.005&&Math.abs(this.scaleY-w)>.005)&&(this.scaleX=S,this.scaleY=w,c|=8,o=a=!0)}let f=(parseInt(i.paddingTop)||0)*this.scaleY,u=(parseInt(i.paddingBottom)||0)*this.scaleY;(this.paddingTop!=f||this.paddingBottom!=u)&&(this.paddingTop=f,this.paddingBottom=u,c|=10),this.editorWidth!=t.scrollDOM.clientWidth&&(n.lineWrapping&&(a=!0),this.editorWidth=t.scrollDOM.clientWidth,c|=8);let d=t.scrollDOM.scrollTop*this.scaleY;this.scrollTop!=d&&(this.scrollAnchorHeight=-1,this.scrollTop=d),this.scrolledToBottom=Yl(t.scrollDOM);let p=(this.printing?iu:eu)(e,this.paddingTop),g=p.top-this.pixelViewport.top,m=p.bottom-this.pixelViewport.bottom;this.pixelViewport=p;let y=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(y!=this.inView&&(this.inView=y,y&&(a=!0)),!this.inView&&!this.scrollTarget)return 0;let x=l.width;if((this.contentDOMWidth!=x||this.editorHeight!=t.scrollDOM.clientHeight)&&(this.contentDOMWidth=l.width,this.editorHeight=t.scrollDOM.clientHeight,c|=8),a){let S=t.docView.measureVisibleLineHeights(this.viewport);if(n.mustRefreshForHeights(S)&&(o=!0),o||n.lineWrapping&&Math.abs(x-this.contentDOMWidth)>n.charWidth){let{lineHeight:w,charWidth:M,textHeight:A}=t.docView.measureTextSize();o=w>0&&n.refresh(r,w,M,A,x/M,S),o&&(t.docView.minWidth=0,c|=8)}g>0&&m>0?h=Math.max(g,m):g<0&&m<0&&(h=Math.min(g,m)),Oo();for(let w of this.viewports){let M=w.from==this.viewport.from?S:t.docView.measureVisibleLineHeights(w);this.heightMap=(o?pt.empty().applyChanges(this.stateDeco,V.empty,this.heightOracle,[new Nt(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(n,0,o,new Xf(w.from,M))}Xe&&(c|=2)}let k=!this.viewportIsAppropriate(this.viewport,h)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return k&&(c&2&&(c|=this.updateScaler()),this.viewport=this.getViewport(h,this.scrollTarget),c|=this.updateForViewport()),(c&2||k)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(o?[]:this.lineGaps,t)),c|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),c}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.heightMap,r=this.heightOracle,{visibleTop:o,visibleBottom:l}=this,a=new Gi(n.lineAt(o-i*1e3,U.ByHeight,r,0,0).from,n.lineAt(l+(1-i)*1e3,U.ByHeight,r,0,0).to);if(e){let{head:c}=e.range;if(ca.to){let h=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),f=n.lineAt(c,U.ByPos,r,0,0),u;e.y=="center"?u=(f.top+f.bottom)/2-h/2:e.y=="start"||e.y=="nearest"&&c=l+Math.max(10,Math.min(i,250)))&&n>o-2*1e3&&r>1,o=n<<1;if(this.defaultTextDirection!=X.LTR&&!i)return[];let l=[],a=(h,f,u,d)=>{if(f-hh&&yy.from>=u.from&&y.to<=u.to&&Math.abs(y.from-h)y.fromx));if(!m){if(fy.from<=f&&y.to>=f)){let y=e.moveToLineBoundary(b.cursor(f),!1,!0).head;y>h&&(f=y)}m=new es(h,f,this.gapSize(u,h,f,d))}l.push(m)},c=h=>{if(h.lengthh.from&&a(h.from,d,h,f),pe.draw(this,this.heightOracle.lineWrapping))))}computeVisibleRanges(){let t=this.stateDeco;this.lineGaps.length&&(t=t.concat(this.lineGapDeco));let e=[];K.spans(t,this.viewport.from,this.viewport.to,{span(n,r){e.push({from:n,to:r})},point(){}},20);let i=e.length!=this.visibleRanges.length||this.visibleRanges.some((n,r)=>n.from!=e[r].from||n.to!=e[r].to);return this.visibleRanges=e,i?4:0}lineBlockAt(t){return t>=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find(e=>e.from<=t&&e.to>=t)||pi(this.heightMap.lineAt(t,U.ByPos,this.heightOracle,0,0),this.scaler)}lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.viewportLines[this.viewportLines.length-1].bottom&&this.viewportLines.find(e=>e.top<=t&&e.bottom>=t)||pi(this.heightMap.lineAt(this.scaler.fromDOM(t),U.ByHeight,this.heightOracle,0,0),this.scaler)}scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=this.viewport.from||this.viewportLines[0].top-t>200?e:this.viewportLines[0]}elementAtHeight(t){return pi(this.heightMap.blockAt(this.scaler.fromDOM(t),this.heightOracle,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class Gi{constructor(t,e){this.from=t,this.to=e}}function su(s,t,e){let i=[],n=s,r=0;return K.spans(e,s,t,{span(){},point(o,l){o>n&&(i.push({from:n,to:o}),r+=o-n),n=l}},20),n=1)return t[t.length-1].to;let i=Math.floor(s*e);for(let n=0;;n++){let{from:r,to:o}=t[n],l=o-r;if(i<=l)return r+i;i-=l}}function Yi(s,t){let e=0;for(let{from:i,to:n}of s.ranges){if(t<=n){e+=t-i;break}e+=n-i}return e/s.total}function ru(s,t){for(let e of s)if(t(e))return e}const Bo={toDOM(s){return s},fromDOM(s){return s},scale:1,eq(s){return s==this}};class kr{constructor(t,e,i){let n=0,r=0,o=0;this.viewports=i.map(({from:l,to:a})=>{let c=e.lineAt(l,U.ByPos,t,0,0).top,h=e.lineAt(a,U.ByPos,t,0,0).bottom;return n+=h-c,{from:l,to:a,top:c,bottom:h,domTop:0,domBottom:0}}),this.scale=(7e6-n)/(e.height-n);for(let l of this.viewports)l.domTop=o+(l.top-r)*this.scale,o=l.domBottom=l.domTop+(l.bottom-l.top),r=l.bottom}toDOM(t){for(let e=0,i=0,n=0;;e++){let r=ee.from==t.viewports[i].from&&e.to==t.viewports[i].to):!1}}function pi(s,t){if(t.scale==1)return s;let e=t.toDOM(s.top),i=t.toDOM(s.bottom);return new Jt(s.from,s.length,e,i-e,Array.isArray(s._content)?s._content.map(n=>pi(n,t)):s._content)}const Xi=T.define({combine:s=>s.join(" ")}),$s=T.define({combine:s=>s.indexOf(!0)>-1}),js=de.newName(),Ha=de.newName(),Wa=de.newName(),za={"&light":"."+Ha,"&dark":"."+Wa};function Us(s,t,e){return new de(t,{finish(i){return/&/.test(i)?i.replace(/&\w*/,n=>{if(n=="&")return s;if(!e||!e[n])throw new RangeError(`Unsupported selector: ${n}`);return e[n]}):s+" "+i}})}const ou=Us("."+js,{"&":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0,overflowAnchor:"none"},".cm-content":{margin:0,flexGrow:2,flexShrink:0,display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",minHeight:"100%",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 6px"},".cm-layer":{position:"absolute",left:0,top:0,contain:"size style","& > *":{position:"absolute"}},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{pointerEvents:"none"},"&.cm-focused > .cm-scroller > .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#444"},".cm-dropCursor":{position:"absolute"},"&.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor":{display:"block"},".cm-iso":{unicodeBidi:"isolate"},".cm-announced":{position:"fixed",top:"-10000px"},"@media print":{".cm-announced":{display:"none"}},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",insetInlineStart:0,zIndex:200},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",borderRight:"1px solid #ddd"},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0,zIndex:300},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top"},".cm-highlightSpace:before":{content:"attr(data-display)",position:"absolute",pointerEvents:"none",color:"#888"},".cm-highlightTab":{backgroundImage:`url('data:image/svg+xml,')`,backgroundSize:"auto 100%",backgroundPosition:"right 90%",backgroundRepeat:"no-repeat"},".cm-trailingSpace":{backgroundColor:"#ff332255"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},za),lu={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},is=D.ie&&D.ie_version<=11;class au{constructor(t){this.view=t,this.active=!1,this.editContext=null,this.selectionRange=new jc,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resizeScroll=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.printQuery=null,this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver(e=>{for(let i of e)this.queue.push(i);(D.ie&&D.ie_version<=11||D.ios&&t.composing)&&e.some(i=>i.type=="childList"&&i.removedNodes.length||i.type=="characterData"&&i.oldValue.length>i.target.nodeValue.length)?this.flushSoon():this.flush()}),window.EditContext&&t.constructor.EDIT_CONTEXT!==!1&&!(D.chrome&&D.chrome_version<126)&&(this.editContext=new cu(t),t.state.facet(le)&&(t.contentDOM.editContext=this.editContext.editContext)),is&&(this.onCharData=e=>{this.queue.push({target:e.target,type:"characterData",oldValue:e.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),window.matchMedia&&(this.printQuery=window.matchMedia("print")),typeof ResizeObserver=="function"&&(this.resizeScroll=new ResizeObserver(()=>{var e;((e=this.view.docView)===null||e===void 0?void 0:e.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),e.length>0&&e[e.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{threshold:[0,.001]}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(e=>{e.length>0&&e[e.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.view.requestMeasure(this.editContext.measureReq),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(t){(t.type=="change"||!t.type)&&!t.matches||(this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500))}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some((e,i)=>e!=t[i]))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,n=this.selectionRange;if(i.state.facet(le)?i.root.activeElement!=this.dom:!cn(i.dom,n))return;let r=n.anchorNode&&i.docView.nearest(n.anchorNode);if(r&&r.ignoreEvent(t)){e||(this.selectionChanged=!1);return}(D.ie&&D.ie_version<=11||D.android&&D.chrome)&&!i.state.selection.main.empty&&n.focusNode&&bi(n.focusNode,n.focusOffset,n.anchorNode,n.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=vi(t.root);if(!e)return!1;let i=D.safari&&t.root.nodeType==11&&zc(this.dom.ownerDocument)==this.dom&&hu(this.view,e)||e;if(!i||this.selectionRange.eq(i))return!1;let n=cn(this.dom,i);return n&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let r=this.delayedAndroidKey;r&&(this.clearDelayedAndroidKey(),this.view.inputState.lastKeyCode=r.keyCode,this.view.inputState.lastKeyTime=Date.now(),!this.flush()&&r.force&&qe(this.dom,r.key,r.keyCode))};this.flushingAndroidKey=this.view.win.requestAnimationFrame(n)}(!this.delayedAndroidKey||t=="Enter")&&(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()}))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}pendingRecords(){for(let t of this.observer.takeRecords())this.queue.push(t);return this.queue}processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[]);let e=-1,i=-1,n=!1;for(let r of t){let o=this.readMutation(r);o&&(o.typeOver&&(n=!0),e==-1?{from:e,to:i}=o:(e=Math.min(o.from,e),i=Math.max(o.to,i)))}return{from:e,to:i,typeOver:n}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this.selectionChanged&&cn(this.dom,this.selectionRange);if(t<0&&!n)return null;t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1;let r=new Df(this.view,t,e,i);return this.view.docView.domChanged={newSel:r.newSel?r.newSel.main:null},r}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return this.view.requestMeasure(),!1;let i=this.view.state,n=Ta(this.view,e);return this.view.state==i&&(e.domChanged||e.newSel&&!e.newSel.main.eq(this.view.state.selection.main))&&this.view.update([]),n}readMutation(t){let e=this.view.docView.nearest(t.target);if(!e||e.ignoreMutation(t))return null;if(e.markDirty(t.type=="attributes"),t.type=="attributes"&&(e.flags|=4),t.type=="childList"){let i=Ro(e,t.previousSibling||t.target.previousSibling,-1),n=Ro(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:n?e.posBefore(n):e.posAtEnd,typeOver:!1}}else return t.type=="characterData"?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),this.printQuery?this.printQuery.addEventListener?this.printQuery.addEventListener("change",this.onPrint):this.printQuery.addListener(this.onPrint):t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),this.printQuery?this.printQuery.removeEventListener?this.printQuery.removeEventListener("change",this.onPrint):this.printQuery.removeListener(this.onPrint):t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}update(t){this.editContext&&(this.editContext.update(t),t.startState.facet(le)!=t.state.facet(le)&&(t.view.contentDOM.editContext=t.state.facet(le)?this.editContext.editContext:null))}destroy(){var t,e,i;this.stop(),(t=this.intersection)===null||t===void 0||t.disconnect(),(e=this.gapIntersection)===null||e===void 0||e.disconnect(),(i=this.resizeScroll)===null||i===void 0||i.disconnect();for(let n of this.scrollTargets)n.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey),this.editContext&&(this.view.contentDOM.editContext=null,this.editContext.destroy())}}function Ro(s,t,e){for(;t;){let i=$.get(t);if(i&&i.parent==s)return i;let n=t.parentNode;t=n!=s.dom?n:e>0?t.nextSibling:t.previousSibling}return null}function Lo(s,t){let e=t.startContainer,i=t.startOffset,n=t.endContainer,r=t.endOffset,o=s.docView.domAtPos(s.state.selection.main.anchor);return bi(o.node,o.offset,n,r)&&([e,i,n,r]=[n,r,e,i]),{anchorNode:e,anchorOffset:i,focusNode:n,focusOffset:r}}function hu(s,t){if(t.getComposedRanges){let n=t.getComposedRanges(s.root)[0];if(n)return Lo(s,n)}let e=null;function i(n){n.preventDefault(),n.stopImmediatePropagation(),e=n.getTargetRanges()[0]}return s.contentDOM.addEventListener("beforeinput",i,!0),s.dom.ownerDocument.execCommand("indent"),s.contentDOM.removeEventListener("beforeinput",i,!0),e?Lo(s,e):null}class cu{constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,this.handlers=Object.create(null),this.resetRange(t.state);let e=this.editContext=new window.EditContext({text:t.state.doc.sliceString(this.from,this.to),selectionStart:this.toContextPos(Math.max(this.from,Math.min(this.to,t.state.selection.main.anchor))),selectionEnd:this.toContextPos(t.state.selection.main.head)});this.handlers.textupdate=i=>{let{anchor:n}=t.state.selection.main,r={from:this.toEditorPos(i.updateRangeStart),to:this.toEditorPos(i.updateRangeEnd),insert:V.of(i.text.split(` +`))};r.from==this.from&&nthis.to&&(r.to=n),!(r.from==r.to&&!r.insert.length)&&(this.pendingContextChange=r,t.state.readOnly||wr(t,r,b.single(this.toEditorPos(i.selectionStart),this.toEditorPos(i.selectionEnd))),this.pendingContextChange&&(this.revertPending(t.state),this.setSelection(t.state)))},this.handlers.characterboundsupdate=i=>{let n=[],r=null;for(let o=this.toEditorPos(i.rangeStart),l=this.toEditorPos(i.rangeEnd);o{let n=[];for(let r of i.getTextFormats()){let o=r.underlineStyle,l=r.underlineThickness;if(o!="None"&&l!="None"){let a=`text-decoration: underline ${o=="Dashed"?"dashed ":o=="Squiggle"?"wavy ":""}${l=="Thin"?1:2}px`;n.push(P.mark({attributes:{style:a}}).range(this.toEditorPos(r.rangeStart),this.toEditorPos(r.rangeEnd)))}}t.dispatch({effects:Sa.of(P.set(n))})},this.handlers.compositionstart=()=>{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)},this.handlers.compositionend=()=>{t.inputState.composing=-1,t.inputState.compositionFirstChange=null};for(let i in this.handlers)e.addEventListener(i,this.handlers[i]);this.measureReq={read:i=>{this.editContext.updateControlBounds(i.contentDOM.getBoundingClientRect());let n=vi(i.root);n&&n.rangeCount&&this.editContext.updateSelectionBounds(n.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.changes.iterChanges((r,o,l,a,c)=>{if(i)return;let h=c.length-(o-r);if(n&&o>=n.to)if(n.from==r&&n.to==o&&n.insert.eq(c)){n=this.pendingContextChange=null,e+=h,this.to+=h;return}else n=null,this.revertPending(t.state);if(r+=e,o+=e,o<=this.from)this.from+=h,this.to+=h;else if(rthis.to||this.to-this.from+c.length>3e4){i=!0;return}this.editContext.updateText(this.toContextPos(r),this.toContextPos(o),c.toString()),this.to+=h}e+=h}),n&&!i&&this.revertPending(t.state),!i}update(t){let e=this.pendingContextChange;!this.applyEdits(t)||!this.rangeIsValid(t.state)?(this.pendingContextChange=null,this.resetRange(t.state),this.editContext.updateText(0,this.editContext.text.length,t.state.doc.sliceString(this.from,this.to)),this.setSelection(t.state)):(t.docChanged||t.selectionSet||e)&&this.setSelection(t.state),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.from+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),n=this.toContextPos(e.head);(this.editContext.selectionStart!=i||this.editContext.selectionEnd!=n)&&this.editContext.updateSelection(i,n)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to1e4*3)}toEditorPos(t){return t+this.from}toContextPos(t){return t-this.from}destroy(){for(let t in this.handlers)this.editContext.removeEventListener(t,this.handlers[t])}}class O{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return this.inputState.composing>0}get compositionStarted(){return this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.className="cm-announced",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),t.parent&&t.parent.appendChild(this.dom);let{dispatch:i}=t;this.dispatchTransactions=t.dispatchTransactions||i&&(n=>n.forEach(r=>i(r,this)))||(n=>this.update(n)),this.dispatch=this.dispatch.bind(this),this._root=t.root||Uc(t.parent)||document,this.viewState=new Po(t.state||W.create(t)),t.scrollTo&&t.scrollTo.is($i)&&(this.viewState.scrollTarget=t.scrollTo.value.clip(this.viewState.state)),this.plugins=this.state.facet(fi).map(n=>new Qn(n));for(let n of this.plugins)n.update(this);this.observer=new au(this),this.inputState=new Rf(this),this.inputState.ensureHandlers(this.plugins),this.docView=new co(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),!((e=document.fonts)===null||e===void 0)&&e.ready&&document.fonts.ready.then(()=>this.requestMeasure())}dispatch(...t){let e=t.length==1&&t[0]instanceof Z?t:t.length==1&&Array.isArray(t[0])?t[0]:[this.state.update(...t)];this.dispatchTransactions(e,this)}update(t){if(this.updateState!=0)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e=!1,i=!1,n,r=this.state;for(let u of t){if(u.startState!=r)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");r=u.state}if(this.destroyed){this.viewState.state=r;return}let o=this.hasFocus,l=0,a=null;t.some(u=>u.annotation(Ia))?(this.inputState.notifiedFocused=o,l=1):o!=this.inputState.notifiedFocused&&(this.inputState.notifiedFocused=o,a=Na(r,o),a||(l=1));let c=this.observer.delayedAndroidKey,h=null;if(c?(this.observer.clearDelayedAndroidKey(),h=this.observer.readChange(),(h&&!this.state.doc.eq(r.doc)||!this.state.selection.eq(r.selection))&&(h=null)):this.observer.clear(),r.facet(W.phrases)!=this.state.facet(W.phrases))return this.setState(r);n=wn.create(this,r,t),n.flags|=l;let f=this.viewState.scrollTarget;try{this.updateState=2;for(let u of t){if(f&&(f=f.map(u.changes)),u.scrollIntoView){let{main:d}=u.state.selection;f=new Ke(d.empty?d:b.cursor(d.head,d.head>d.anchor?-1:1))}for(let d of u.effects)d.is($i)&&(f=d.value.clip(this.state))}this.viewState.update(n,f),this.bidiCache=kn.update(this.bidiCache,n.changes),n.empty||(this.updatePlugins(n),this.inputState.update(n)),e=this.docView.update(n),this.state.facet(ui)!=this.styleModules&&this.mountStyles(),i=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(e,t.some(u=>u.isUserEvent("select.pointer")))}finally{this.updateState=0}if(n.startState.facet(Xi)!=n.state.facet(Xi)&&(this.viewState.mustMeasureContent=!0),(e||i||f||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),e&&this.docViewUpdate(),!n.empty)for(let u of this.state.facet(Ws))try{u(n)}catch(d){Dt(this.state,d,"update listener")}(a||h)&&Promise.resolve().then(()=>{a&&this.state==a.startState&&this.dispatch(a),h&&!Ta(this,h)&&c.force&&qe(this.contentDOM,c.key,c.keyCode)})}setState(t){if(this.updateState!=0)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed){this.viewState.state=t;return}this.updateState=2;let e=this.hasFocus;try{for(let i of this.plugins)i.destroy(this);this.viewState=new Po(t),this.plugins=t.facet(fi).map(i=>new Qn(i)),this.pluginMap.clear();for(let i of this.plugins)i.update(this);this.docView.destroy(),this.docView=new co(this),this.inputState.ensureHandlers(this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(fi),i=t.state.facet(fi);if(e!=i){let n=[];for(let r of i){let o=e.indexOf(r);if(o<0)n.push(new Qn(r));else{let l=this.plugins[o];l.mustUpdate=t,n.push(l)}}for(let r of this.plugins)r.mustUpdate!=t&&r.destroy(this);this.plugins=n,this.pluginMap.clear()}else for(let n of this.plugins)n.mustUpdate=t;for(let n=0;n-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.observer.delayedAndroidKey){this.measureScheduled=-1,this.requestMeasure();return}this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,i=this.scrollDOM,n=i.scrollTop*this.scaleY,{scrollAnchorPos:r,scrollAnchorHeight:o}=this.viewState;Math.abs(n-this.viewState.scrollTop)>1&&(o=-1),this.viewState.scrollAnchorHeight=-1;try{for(let l=0;;l++){if(o<0)if(Yl(i))r=-1,o=this.viewState.heightMap.height;else{let d=this.viewState.scrollAnchorAt(n);r=d.from,o=d.top}this.updateState=1;let a=this.viewState.measure(this);if(!a&&!this.measureRequests.length&&this.viewState.scrollTarget==null)break;if(l>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let c=[];a&4||([this.measureRequests,c]=[c,this.measureRequests]);let h=c.map(d=>{try{return d.read(this)}catch(p){return Dt(this.state,p),Eo}}),f=wn.create(this,this.state,[]),u=!1;f.flags|=a,e?e.flags|=a:e=f,this.updateState=2,f.empty||(this.updatePlugins(f),this.inputState.update(f),this.updateAttrs(),u=this.docView.update(f),u&&this.docViewUpdate());for(let d=0;d1||p<-1){n=n+p,i.scrollTop=n/this.scaleY,o=-1;continue}}break}}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let l of this.state.facet(Ws))l(e)}get themeClasses(){return js+" "+(this.state.facet($s)?Wa:Ha)+" "+this.state.facet(Xi)}updateAttrs(){let t=Io(this,ka,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",translate:"no",contenteditable:this.state.facet(le)?"true":"false",class:"cm-content",style:`${D.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),Io(this,br,e);let i=this.observer.ignore(()=>{let n=Es(this.contentDOM,this.contentAttrs,e),r=Es(this.dom,this.editorAttrs,t);return n||r});return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let n of i.effects)if(n.is(O.announce)){e&&(this.announceDOM.textContent=""),e=!1;let r=this.announceDOM.appendChild(document.createElement("div"));r.textContent=n.value}}mountStyles(){this.styleModules=this.state.facet(ui);let t=this.state.facet(O.cspNonce);de.mount(this.root,this.styleModules.concat(ou).reverse(),t?{nonce:t}:void 0)}readMeasured(){if(this.updateState==2)throw new Error("Reading the editor layout isn't allowed during an update");this.updateState==0&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame(()=>this.measure())),t){if(this.measureRequests.indexOf(t)>-1)return;if(t.key!=null){for(let e=0;ei.spec==t)||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}get scaleX(){return this.viewState.scaleX}get scaleY(){return this.viewState.scaleY}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return ts(this,t,mo(this,t,e,i))}moveByGroup(t,e){return ts(this,t,mo(this,t,e,i=>vf(this,t.head,i)))}visualLineSide(t,e){let i=this.bidiSpans(t),n=this.textDirectionAt(t.from),r=i[e?i.length-1:0];return b.cursor(r.side(e,n)+t.from,r.forward(!e,n)?1:-1)}moveToLineBoundary(t,e,i=!0){return kf(this,t,e,i)}moveVertically(t,e,i){return ts(this,t,Cf(this,t,e,i))}domAtPos(t){return this.docView.domAtPos(t)}posAtDOM(t,e=0){return this.docView.posFromDOM(t,e)}posAtCoords(t,e=!0){return this.readMeasured(),Oa(this,t,e)}coordsAtPos(t,e=1){this.readMeasured();let i=this.docView.coordsAt(t,e);if(!i||i.left==i.right)return i;let n=this.state.doc.lineAt(t),r=this.bidiSpans(n),o=r[ce.find(r,t-n.from,-1,e)];return Li(i,o.dir==X.LTR==e>0)}coordsForChar(t){return this.readMeasured(),this.docView.coordsForChar(t)}get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}get textDirection(){return this.viewState.defaultTextDirection}textDirectionAt(t){return!this.state.facet(ba)||tthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>fu)return ca(t.length);let e=this.textDirectionAt(t.from),i;for(let r of this.bidiCache)if(r.from==t.from&&r.dir==e&&(r.fresh||ha(r.isolates,i=ho(this,t))))return r.order;i||(i=ho(this,t));let n=of(t.text,e,i);return this.bidiCache.push(new kn(t.from,t.to,e,i,!0,n)),n}get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||D.safari&&((t=this.inputState)===null||t===void 0?void 0:t.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{Gl(this.contentDOM),this.docView.updateSelection()})}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((t.nodeType==9?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.blur();for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.docView.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){return $i.of(new Ke(typeof t=="number"?b.cursor(t):t,e.y,e.x,e.yMargin,e.xMargin))}scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.viewState.scrollAnchorAt(t);return $i.of(new Ke(b.cursor(i.from),"start","start",i.top-t,e,!0))}setTabFocusMode(t){t==null?this.inputState.tabFocusMode=this.inputState.tabFocusMode<0?0:-1:typeof t=="boolean"?this.inputState.tabFocusMode=t?0:-1:this.inputState.tabFocusMode!=0&&(this.inputState.tabFocusMode=Date.now()+t)}static domEventHandlers(t){return ut.define(()=>({}),{eventHandlers:t})}static domEventObservers(t){return ut.define(()=>({}),{eventObservers:t})}static theme(t,e){let i=de.newName(),n=[Xi.of(i),ui.of(Us(`.${i}`,t))];return e&&e.dark&&n.push($s.of(!0)),n}static baseTheme(t){return ye.lowest(ui.of(Us("."+js,t,za)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&$.get(i)||$.get(t);return((e=n==null?void 0:n.rootView)===null||e===void 0?void 0:e.view)||null}}O.styleModule=ui;O.inputHandler=ma;O.clipboardInputFilter=mr;O.clipboardOutputFilter=yr;O.scrollHandler=wa;O.focusChangeEffect=ya;O.perLineTextDirection=ba;O.exceptionSink=ga;O.updateListener=Ws;O.editable=le;O.mouseSelectionStyle=pa;O.dragMovesSelection=da;O.clickAddsSelectionRange=ua;O.decorations=Ci;O.outerDecorations=va;O.atomicRanges=xr;O.bidiIsolatedRanges=Ca;O.scrollMargins=Aa;O.darkTheme=$s;O.cspNonce=T.define({combine:s=>s.length?s[0]:""});O.contentAttributes=br;O.editorAttributes=ka;O.lineWrapping=O.contentAttributes.of({class:"cm-lineWrapping"});O.announce=N.define();const fu=4096,Eo={};class kn{constructor(t,e,i,n,r,o){this.from=t,this.to=e,this.dir=i,this.isolates=n,this.fresh=r,this.order=o}static update(t,e){if(e.empty&&!t.some(r=>r.fresh))return t;let i=[],n=t.length?t[t.length-1].dir:X.LTR;for(let r=Math.max(0,t.length-10);r=0;n--){let r=i[n],o=typeof r=="function"?r(s):r;o&&Ls(o,e)}return e}const uu=D.mac?"mac":D.windows?"win":D.linux?"linux":"key";function du(s,t){const e=s.split(/-(?!$)/);let i=e[e.length-1];i=="Space"&&(i=" ");let n,r,o,l;for(let a=0;ai.concat(n),[]))),e}function gu(s,t,e){return Ka(qa(s.state),t,s,e)}let ae=null;const mu=4e3;function yu(s,t=uu){let e=Object.create(null),i=Object.create(null),n=(o,l)=>{let a=i[o];if(a==null)i[o]=l;else if(a!=l)throw new Error("Key binding "+o+" is used both as a regular binding and as a multi-stroke prefix")},r=(o,l,a,c,h)=>{var f,u;let d=e[o]||(e[o]=Object.create(null)),p=l.split(/ (?!$)/).map(y=>du(y,t));for(let y=1;y{let S=ae={view:k,prefix:x,scope:o};return setTimeout(()=>{ae==S&&(ae=null)},mu),!0}]})}let g=p.join(" ");n(g,!1);let m=d[g]||(d[g]={preventDefault:!1,stopPropagation:!1,run:((u=(f=d._any)===null||f===void 0?void 0:f.run)===null||u===void 0?void 0:u.slice())||[]});a&&m.run.push(a),c&&(m.preventDefault=!0),h&&(m.stopPropagation=!0)};for(let o of s){let l=o.scope?o.scope.split(" "):["editor"];if(o.any)for(let c of l){let h=e[c]||(e[c]=Object.create(null));h._any||(h._any={preventDefault:!1,stopPropagation:!1,run:[]});let{any:f}=o;for(let u in h)h[u].run.push(d=>f(d,Gs))}let a=o[t]||o.key;if(a)for(let c of l)r(c,a,o.run,o.preventDefault,o.stopPropagation),o.shift&&r(c,"Shift-"+a,o.shift,o.preventDefault,o.stopPropagation)}return e}let Gs=null;function Ka(s,t,e,i){Gs=t;let n=Wc(t),r=nt(n,0),o=Rt(r)==n.length&&n!=" ",l="",a=!1,c=!1,h=!1;ae&&ae.view==e&&ae.scope==i&&(l=ae.prefix+" ",Ba.indexOf(t.keyCode)<0&&(c=!0,ae=null));let f=new Set,u=m=>{if(m){for(let y of m.run)if(!f.has(y)&&(f.add(y),y(e)))return m.stopPropagation&&(h=!0),!0;m.preventDefault&&(m.stopPropagation&&(h=!0),c=!0)}return!1},d=s[i],p,g;return d&&(u(d[l+_i(n,t,!o)])?a=!0:o&&(t.altKey||t.metaKey||t.ctrlKey)&&!(D.windows&&t.ctrlKey&&t.altKey)&&(p=pe[t.keyCode])&&p!=n?(u(d[l+_i(p,t,!0)])||t.shiftKey&&(g=ki[t.keyCode])!=n&&g!=p&&u(d[l+_i(g,t,!1)]))&&(a=!0):o&&t.shiftKey&&u(d[l+_i(n,t,!0)])&&(a=!0),!a&&u(d._any)&&(a=!0)),c&&(a=!0),a&&h&&t.stopPropagation(),Gs=null,a}class Ni{constructor(t,e,i,n,r){this.className=t,this.left=e,this.top=i,this.width=n,this.height=r}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}update(t,e){return e.className!=this.className?!1:(this.adjust(t),!0)}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",this.width!=null&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}static forRange(t,e,i){if(i.empty){let n=t.coordsAtPos(i.head,i.assoc||1);if(!n)return[];let r=$a(t);return[new Ni(e,n.left-r.left,n.top-r.top,null,n.bottom-n.top)]}else return bu(t,e,i)}}function $a(s){let t=s.scrollDOM.getBoundingClientRect();return{left:(s.textDirection==X.LTR?t.left:t.right-s.scrollDOM.clientWidth*s.scaleX)-s.scrollDOM.scrollLeft*s.scaleX,top:t.top-s.scrollDOM.scrollTop*s.scaleY}}function Fo(s,t,e,i){let n=s.coordsAtPos(t,e*2);if(!n)return i;let r=s.dom.getBoundingClientRect(),o=(n.top+n.bottom)/2,l=s.posAtCoords({x:r.left+1,y:o}),a=s.posAtCoords({x:r.right-1,y:o});return l==null||a==null?i:{from:Math.max(i.from,Math.min(l,a)),to:Math.min(i.to,Math.max(l,a))}}function bu(s,t,e){if(e.to<=s.viewport.from||e.from>=s.viewport.to)return[];let i=Math.max(e.from,s.viewport.from),n=Math.min(e.to,s.viewport.to),r=s.textDirection==X.LTR,o=s.contentDOM,l=o.getBoundingClientRect(),a=$a(s),c=o.querySelector(".cm-line"),h=c&&window.getComputedStyle(c),f=l.left+(h?parseInt(h.paddingLeft)+Math.min(0,parseInt(h.textIndent)):0),u=l.right-(h?parseInt(h.paddingRight):0),d=qs(s,i),p=qs(s,n),g=d.type==Ot.Text?d:null,m=p.type==Ot.Text?p:null;if(g&&(s.lineWrapping||d.widgetLineBreaks)&&(g=Fo(s,i,1,g)),m&&(s.lineWrapping||p.widgetLineBreaks)&&(m=Fo(s,n,-1,m)),g&&m&&g.from==m.from&&g.to==m.to)return x(k(e.from,e.to,g));{let w=g?k(e.from,null,g):S(d,!1),M=m?k(null,e.to,m):S(p,!0),A=[];return(g||d).to<(m||p).from-(g&&m?1:0)||d.widgetLineBreaks>1&&w.bottom+s.defaultLineHeight/2R&&H.from=bt)break;tt>J&&E(Math.max(Tt,J),w==null&&Tt<=R,Math.min(tt,bt),M==null&&tt>=z,vt.dir)}if(J=xt.to+1,J>=bt)break}return F.length==0&&E(R,w==null,z,M==null,s.textDirection),{top:B,bottom:I,horizontal:F}}function S(w,M){let A=l.top+(M?w.top:w.bottom);return{top:A,bottom:A,horizontal:[]}}}function xu(s,t){return s.constructor==t.constructor&&s.eq(t)}class wu{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,this.scaleY=1,this.measureReq={read:this.measure.bind(this),write:this.draw.bind(this)},this.dom=t.scrollDOM.appendChild(document.createElement("div")),this.dom.classList.add("cm-layer"),e.above&&this.dom.classList.add("cm-layer-above"),e.class&&this.dom.classList.add(e.class),this.scale(),this.dom.setAttribute("aria-hidden","true"),this.setOrder(t.state),t.requestMeasure(this.measureReq),e.mount&&e.mount(this.dom,t)}update(t){t.startState.facet(dn)!=t.state.facet(dn)&&this.setOrder(t.state),(this.layer.update(t,this.dom)||t.geometryChanged)&&(this.scale(),t.view.requestMeasure(this.measureReq))}docViewUpdate(t){this.layer.updateOnDocViewUpdate!==!1&&t.requestMeasure(this.measureReq)}setOrder(t){let e=0,i=t.facet(dn);for(;e!xu(e,this.drawn[i]))){let e=this.dom.firstChild,i=0;for(let n of t)n.update&&e&&n.constructor&&this.drawn[i].constructor&&n.update(e,this.drawn[i])?(e=e.nextSibling,i++):this.dom.insertBefore(n.draw(),e);for(;e;){let n=e.nextSibling;e.remove(),e=n}this.drawn=t}}destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),this.dom.remove()}}const dn=T.define();function ja(s){return[ut.define(t=>new wu(t,s)),dn.of(s)]}const Ua=!D.ios,Ai=T.define({combine(s){return Le(s,{cursorBlinkRate:1200,drawRangeCursor:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})}});function xm(s={}){return[Ai.of(s),Su,ku,vu,xa.of(!0)]}function Ga(s){return s.startState.facet(Ai)!=s.state.facet(Ai)}const Su=ja({above:!0,markers(s){let{state:t}=s,e=t.facet(Ai),i=[];for(let n of t.selection.ranges){let r=n==t.selection.main;if(n.empty?!r||Ua:e.drawRangeCursor){let o=r?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary",l=n.empty?n:b.cursor(n.head,n.head>n.anchor?-1:1);for(let a of Ni.forRange(s,o,l))i.push(a)}}return i},update(s,t){s.transactions.some(i=>i.selection)&&(t.style.animationName=t.style.animationName=="cm-blink"?"cm-blink2":"cm-blink");let e=Ga(s);return e&&Vo(s.state,t),s.docChanged||s.selectionSet||e},mount(s,t){Vo(t.state,s)},class:"cm-cursorLayer"});function Vo(s,t){t.style.animationDuration=s.facet(Ai).cursorBlinkRate+"ms"}const ku=ja({above:!1,markers(s){return s.state.selection.ranges.map(t=>t.empty?[]:Ni.forRange(s,"cm-selectionBackground",t)).reduce((t,e)=>t.concat(e))},update(s,t){return s.docChanged||s.selectionSet||s.viewportChanged||Ga(s)},class:"cm-selectionLayer"}),Js={".cm-line":{"& ::selection, &::selection":{backgroundColor:"transparent !important"}},".cm-content":{"& :focus":{caretColor:"initial !important","&::selection, & ::selection":{backgroundColor:"Highlight !important"}}}};Ua&&(Js[".cm-line"].caretColor=Js[".cm-content"].caretColor="transparent !important");const vu=ye.highest(O.theme(Js)),Ja=N.define({map(s,t){return s==null?null:t.mapPos(s)}}),gi=yt.define({create(){return null},update(s,t){return s!=null&&(s=t.changes.mapPos(s)),t.effects.reduce((e,i)=>i.is(Ja)?i.value:e,s)}}),Cu=ut.fromClass(class{constructor(s){this.view=s,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(s){var t;let e=s.state.field(gi);e==null?this.cursor!=null&&((t=this.cursor)===null||t===void 0||t.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(s.startState.field(gi)!=e||s.docChanged||s.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let{view:s}=this,t=s.state.field(gi),e=t!=null&&s.coordsAtPos(t);if(!e)return null;let i=s.scrollDOM.getBoundingClientRect();return{left:e.left-i.left+s.scrollDOM.scrollLeft*s.scaleX,top:e.top-i.top+s.scrollDOM.scrollTop*s.scaleY,height:e.bottom-e.top}}drawCursor(s){if(this.cursor){let{scaleX:t,scaleY:e}=this.view;s?(this.cursor.style.left=s.left/t+"px",this.cursor.style.top=s.top/e+"px",this.cursor.style.height=s.height/e+"px"):this.cursor.style.left="-100000px"}}destroy(){this.cursor&&this.cursor.remove()}setDropPos(s){this.view.state.field(gi)!=s&&this.view.dispatch({effects:Ja.of(s)})}},{eventObservers:{dragover(s){this.setDropPos(this.view.posAtCoords({x:s.clientX,y:s.clientY}))},dragleave(s){(s.target==this.view.contentDOM||!this.view.contentDOM.contains(s.relatedTarget))&&this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function wm(){return[gi,Cu]}function Ho(s,t,e,i,n){t.lastIndex=0;for(let r=s.iterRange(e,i),o=e,l;!r.next().done;o+=r.value.length)if(!r.lineBreak)for(;l=t.exec(r.value);)n(o+l.index,l)}function Au(s,t){let e=s.visibleRanges;if(e.length==1&&e[0].from==s.viewport.from&&e[0].to==s.viewport.to)return e;let i=[];for(let{from:n,to:r}of e)n=Math.max(s.state.doc.lineAt(n).from,n-t),r=Math.min(s.state.doc.lineAt(r).to,r+t),i.length&&i[i.length-1].to>=n?i[i.length-1].to=r:i.push({from:n,to:r});return i}class Mu{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:r,maxLength:o=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,n)this.addMatch=(l,a,c,h)=>n(h,c,c+l[0].length,l,a);else if(typeof i=="function")this.addMatch=(l,a,c,h)=>{let f=i(l,a,c);f&&h(c,c+l[0].length,f)};else if(i)this.addMatch=(l,a,c,h)=>h(c,c+l[0].length,i);else throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.boundary=r,this.maxLength=o}createDeco(t){let e=new De,i=e.add.bind(e);for(let{from:n,to:r}of Au(t,this.maxLength))Ho(t.state.doc,this.regexp,n,r,(o,l)=>this.addMatch(l,t,o,i));return e.finish()}updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChanges((r,o,l,a)=>{a>t.view.viewport.from&&l1e3?this.createDeco(t.view):n>-1?this.updateRange(t.view,e.map(t.changes),i,n):e}updateRange(t,e,i,n){for(let r of t.visibleRanges){let o=Math.max(r.from,i),l=Math.min(r.to,n);if(l>o){let a=t.state.doc.lineAt(o),c=a.toa.from;o--)if(this.boundary.test(a.text[o-1-a.from])){h=o;break}for(;lu.push(y.range(g,m));if(a==c)for(this.regexp.lastIndex=h-a.from;(d=this.regexp.exec(a.text))&&d.indexthis.addMatch(m,t,g,p));e=e.update({filterFrom:h,filterTo:f,filter:(g,m)=>gf,add:u})}}return e}}const Ys=/x/.unicode!=null?"gu":"g",Du=new RegExp(`[\0-\b +--Ÿ­؜​‎‏\u2028\u2029‭‮⁦⁧⁩\uFEFF-]`,Ys),Ou={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let ns=null;function Tu(){var s;if(ns==null&&typeof document<"u"&&document.body){let t=document.body.style;ns=((s=t.tabSize)!==null&&s!==void 0?s:t.MozTabSize)!=null}return ns||!1}const pn=T.define({combine(s){let t=Le(s,{render:null,specialChars:Du,addSpecialChars:null});return(t.replaceTabs=!Tu())&&(t.specialChars=new RegExp(" |"+t.specialChars.source,Ys)),t.addSpecialChars&&(t.specialChars=new RegExp(t.specialChars.source+"|"+t.addSpecialChars.source,Ys)),t}});function Sm(s={}){return[pn.of(s),Pu()]}let Wo=null;function Pu(){return Wo||(Wo=ut.fromClass(class{constructor(s){this.view=s,this.decorations=P.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(s.state.facet(pn)),this.decorations=this.decorator.createDeco(s)}makeDecorator(s){return new Mu({regexp:s.specialChars,decoration:(t,e,i)=>{let{doc:n}=e.state,r=nt(t[0],0);if(r==9){let o=n.lineAt(i),l=e.state.tabSize,a=ei(o.text,l,i-o.from);return P.replace({widget:new Eu((l-a%l)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[r]||(this.decorationCache[r]=P.replace({widget:new Lu(s,r)}))},boundary:s.replaceTabs?void 0:/[^]/})}update(s){let t=s.state.facet(pn);s.startState.facet(pn)!=t?(this.decorator=this.makeDecorator(t),this.decorations=this.decorator.createDeco(s.view)):this.decorations=this.decorator.updateDeco(s,this.decorations)}},{decorations:s=>s.decorations}))}const Bu="•";function Ru(s){return s>=32?Bu:s==10?"␤":String.fromCharCode(9216+s)}class Lu extends Ee{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=Ru(this.code),i=t.state.phrase("Control character")+" "+(Ou[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let r=document.createElement("span");return r.textContent=e,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}}class Eu extends Ee{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent=" ",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}class Iu extends Ee{constructor(t){super(),this.content=t}toDOM(){let t=document.createElement("span");return t.className="cm-placeholder",t.style.pointerEvents="none",t.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?t.setAttribute("aria-label","placeholder "+this.content):t.setAttribute("aria-hidden","true"),t}coordsAt(t){let e=t.firstChild?Ge(t.firstChild):[];if(!e.length)return null;let i=window.getComputedStyle(t.parentNode),n=Li(e[0],i.direction!="rtl"),r=parseInt(i.lineHeight);return n.bottom-n.top>r*1.5?{left:n.left,right:n.right,top:n.top,bottom:n.top+r}:n}ignoreEvent(){return!1}}function km(s){return ut.fromClass(class{constructor(t){this.view=t,this.placeholder=s?P.set([P.widget({widget:new Iu(s),side:1}).range(0)]):P.none}get decorations(){return this.view.state.doc.length?P.none:this.placeholder}},{decorations:t=>t.decorations})}const Xs=2e3;function Nu(s,t,e){let i=Math.min(t.line,e.line),n=Math.max(t.line,e.line),r=[];if(t.off>Xs||e.off>Xs||t.col<0||e.col<0){let o=Math.min(t.off,e.off),l=Math.max(t.off,e.off);for(let a=i;a<=n;a++){let c=s.doc.line(a);c.length<=l&&r.push(b.range(c.from+o,c.to+l))}}else{let o=Math.min(t.col,e.col),l=Math.max(t.col,e.col);for(let a=i;a<=n;a++){let c=s.doc.line(a),h=Ms(c.text,o,s.tabSize,!0);if(h<0)r.push(b.cursor(c.to));else{let f=Ms(c.text,l,s.tabSize);r.push(b.range(c.from+h,c.from+f))}}}return r}function Fu(s,t){let e=s.coordsAtPos(s.viewport.from);return e?Math.round(Math.abs((e.left-t)/s.defaultCharacterWidth)):-1}function zo(s,t){let e=s.posAtCoords({x:t.clientX,y:t.clientY},!1),i=s.state.doc.lineAt(e),n=e-i.from,r=n>Xs?-1:n==i.length?Fu(s,t.clientX):ei(i.text,s.state.tabSize,e-i.from);return{line:i.number,col:r,off:n}}function Vu(s,t){let e=zo(s,t),i=s.state.selection;return e?{update(n){if(n.docChanged){let r=n.changes.mapPos(n.startState.doc.line(e.line).from),o=n.state.doc.lineAt(r);e={line:o.number,col:e.col,off:Math.min(e.off,o.length)},i=i.map(n.changes)}},get(n,r,o){let l=zo(s,n);if(!l)return i;let a=Nu(s.state,e,l);return a.length?o?b.create(a.concat(i.ranges)):b.create(a):i}}:null}function vm(s){let t=e=>e.altKey&&e.button==0;return O.mouseSelectionStyle.of((e,i)=>t(i)?Vu(e,i):null)}const li="-10000px";class Hu{constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this.removeTooltipView=n,this.input=t.state.facet(e),this.tooltips=this.input.filter(o=>o);let r=null;this.tooltipViews=this.tooltips.map(o=>r=i(o,r))}update(t,e){var i;let n=t.state.facet(this.facet),r=n.filter(a=>a);if(n===this.input){for(let a of this.tooltipViews)a.update&&a.update(t);return!1}let o=[],l=e?[]:null;for(let a=0;ae[c]=a),e.length=l.length),this.input=n,this.tooltips=r,this.tooltipViews=o,!0}}function Wu(s){let{win:t}=s;return{top:0,left:0,bottom:t.innerHeight,right:t.innerWidth}}const ss=T.define({combine:s=>{var t,e,i;return{position:D.ios?"absolute":((t=s.find(n=>n.position))===null||t===void 0?void 0:t.position)||"fixed",parent:((e=s.find(n=>n.parent))===null||e===void 0?void 0:e.parent)||null,tooltipSpace:((i=s.find(n=>n.tooltipSpace))===null||i===void 0?void 0:i.tooltipSpace)||Wu}}}),qo=new WeakMap,Ya=ut.fromClass(class{constructor(s){this.view=s,this.above=[],this.inView=!0,this.madeAbsolute=!1,this.lastTransaction=0,this.measureTimeout=-1;let t=s.state.facet(ss);this.position=t.position,this.parent=t.parent,this.classes=s.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.resizeObserver=typeof ResizeObserver=="function"?new ResizeObserver(()=>this.measureSoon()):null,this.manager=new Hu(s,Xa,(e,i)=>this.createTooltip(e,i),e=>{this.resizeObserver&&this.resizeObserver.unobserve(e.dom),e.dom.remove()}),this.above=this.manager.tooltips.map(e=>!!e.above),this.intersectionObserver=typeof IntersectionObserver=="function"?new IntersectionObserver(e=>{Date.now()>this.lastTransaction-50&&e.length>0&&e[e.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),s.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let s of this.manager.tooltipViews)this.intersectionObserver.observe(s.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(s){s.transactions.length&&(this.lastTransaction=Date.now());let t=this.manager.update(s,this.above);t&&this.observeIntersection();let e=t||s.geometryChanged,i=s.state.facet(ss);if(i.position!=this.position&&!this.madeAbsolute){this.position=i.position;for(let n of this.manager.tooltipViews)n.dom.style.position=this.position;e=!0}if(i.parent!=this.parent){this.parent&&this.container.remove(),this.parent=i.parent,this.createContainer();for(let n of this.manager.tooltipViews)this.container.appendChild(n.dom);e=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);e&&this.maybeMeasure()}createTooltip(s,t){let e=s.create(this.view),i=t?t.dom:null;if(e.dom.classList.add("cm-tooltip"),s.arrow&&!e.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let n=document.createElement("div");n.className="cm-tooltip-arrow",e.dom.appendChild(n)}return e.dom.style.position=this.position,e.dom.style.top=li,e.dom.style.left="0px",this.container.insertBefore(e.dom,i),e.mount&&e.mount(this.view),this.resizeObserver&&this.resizeObserver.observe(e.dom),e}destroy(){var s,t,e;this.view.win.removeEventListener("resize",this.measureSoon);for(let i of this.manager.tooltipViews)i.dom.remove(),(s=i.destroy)===null||s===void 0||s.call(i);this.parent&&this.container.remove(),(t=this.resizeObserver)===null||t===void 0||t.disconnect(),(e=this.intersectionObserver)===null||e===void 0||e.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let s=this.view.dom.getBoundingClientRect(),t=1,e=1,i=!1;if(this.position=="fixed"&&this.manager.tooltipViews.length){let{dom:n}=this.manager.tooltipViews[0];if(D.gecko)i=n.offsetParent!=this.container.ownerDocument.body;else if(n.style.top==li&&n.style.left=="0px"){let r=n.getBoundingClientRect();i=Math.abs(r.top+1e4)>1||Math.abs(r.left)>1}}if(i||this.position=="absolute")if(this.parent){let n=this.parent.getBoundingClientRect();n.width&&n.height&&(t=n.width/this.parent.offsetWidth,e=n.height/this.parent.offsetHeight)}else({scaleX:t,scaleY:e}=this.view.viewState);return{editor:s,parent:this.parent?this.container.getBoundingClientRect():s,pos:this.manager.tooltips.map((n,r)=>{let o=this.manager.tooltipViews[r];return o.getCoords?o.getCoords(n.pos):this.view.coordsAtPos(n.pos)}),size:this.manager.tooltipViews.map(({dom:n})=>n.getBoundingClientRect()),space:this.view.state.facet(ss).tooltipSpace(this.view),scaleX:t,scaleY:e,makeAbsolute:i}}writeMeasure(s){var t;if(s.makeAbsolute){this.madeAbsolute=!0,this.position="absolute";for(let l of this.manager.tooltipViews)l.dom.style.position="absolute"}let{editor:e,space:i,scaleX:n,scaleY:r}=s,o=[];for(let l=0;l=Math.min(e.bottom,i.bottom)||f.rightMath.min(e.right,i.right)+.1){h.style.top=li;continue}let d=a.arrow?c.dom.querySelector(".cm-tooltip-arrow"):null,p=d?7:0,g=u.right-u.left,m=(t=qo.get(c))!==null&&t!==void 0?t:u.bottom-u.top,y=c.offset||qu,x=this.view.textDirection==X.LTR,k=u.width>i.right-i.left?x?i.left:i.right-u.width:x?Math.max(i.left,Math.min(f.left-(d?14:0)+y.x,i.right-g)):Math.min(Math.max(i.left,f.left-g+(d?14:0)-y.x),i.right-g),S=this.above[l];!a.strictSide&&(S?f.top-(u.bottom-u.top)-y.yi.bottom)&&S==i.bottom-f.bottom>f.top-i.top&&(S=this.above[l]=!S);let w=(S?f.top-i.top:i.bottom-f.bottom)-p;if(wk&&B.topM&&(M=S?B.top-m-2-p:B.bottom+p+2);if(this.position=="absolute"?(h.style.top=(M-s.parent.top)/r+"px",h.style.left=(k-s.parent.left)/n+"px"):(h.style.top=M/r+"px",h.style.left=k/n+"px"),d){let B=f.left+(x?y.x:-y.x)-(k+14-7);d.style.left=B/n+"px"}c.overlap!==!0&&o.push({left:k,top:M,right:A,bottom:M+m}),h.classList.toggle("cm-tooltip-above",S),h.classList.toggle("cm-tooltip-below",!S),c.positioned&&c.positioned(s.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let s of this.manager.tooltipViews)s.dom.style.top=li}},{eventObservers:{scroll(){this.maybeMeasure()}}}),zu=O.baseTheme({".cm-tooltip":{zIndex:100,boxSizing:"border-box"},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:`${7*2}px`,position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),qu={x:0,y:0},Xa=T.define({enables:[Ya,zu]});function _a(s,t){let e=s.plugin(Ya);if(!e)return null;let i=e.manager.tooltips.indexOf(t);return i<0?null:e.manager.tooltipViews[i]}const Ko=T.define({combine(s){let t,e;for(let i of s)t=t||i.topContainer,e=e||i.bottomContainer;return{topContainer:t,bottomContainer:e}}});function vn(s,t){let e=s.plugin(Qa),i=e?e.specs.indexOf(t):-1;return i>-1?e.panels[i]:null}const Qa=ut.fromClass(class{constructor(s){this.input=s.state.facet(Cn),this.specs=this.input.filter(e=>e),this.panels=this.specs.map(e=>e(s));let t=s.state.facet(Ko);this.top=new Qi(s,!0,t.topContainer),this.bottom=new Qi(s,!1,t.bottomContainer),this.top.sync(this.panels.filter(e=>e.top)),this.bottom.sync(this.panels.filter(e=>!e.top));for(let e of this.panels)e.dom.classList.add("cm-panel"),e.mount&&e.mount()}update(s){let t=s.state.facet(Ko);this.top.container!=t.topContainer&&(this.top.sync([]),this.top=new Qi(s.view,!0,t.topContainer)),this.bottom.container!=t.bottomContainer&&(this.bottom.sync([]),this.bottom=new Qi(s.view,!1,t.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let e=s.state.facet(Cn);if(e!=this.input){let i=e.filter(a=>a),n=[],r=[],o=[],l=[];for(let a of i){let c=this.specs.indexOf(a),h;c<0?(h=a(s.view),l.push(h)):(h=this.panels[c],h.update&&h.update(s)),n.push(h),(h.top?r:o).push(h)}this.specs=i,this.panels=n,this.top.sync(r),this.bottom.sync(o);for(let a of l)a.dom.classList.add("cm-panel"),a.mount&&a.mount()}else for(let i of this.panels)i.update&&i.update(s)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:s=>O.scrollMargins.of(t=>{let e=t.plugin(s);return e&&{top:e.top.scrollMargin(),bottom:e.bottom.scrollMargin()}})});class Qi{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(this.panels.length==0){this.dom&&(this.dom.remove(),this.dom=void 0);return}if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let e=this.container||this.view.dom;e.insertBefore(this.dom,this.top?e.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=$o(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=$o(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(!(!this.container||this.classes==this.view.themeClasses)){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function $o(s){let t=s.nextSibling;return s.remove(),t}const Cn=T.define({enables:Qa});class Be extends Me{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}Be.prototype.elementClass="";Be.prototype.toDOM=void 0;Be.prototype.mapMode=ht.TrackBefore;Be.prototype.startSide=Be.prototype.endSide=-1;Be.prototype.point=!0;const Ku=T.define(),$u=new class extends Be{constructor(){super(...arguments),this.elementClass="cm-activeLineGutter"}},ju=Ku.compute(["selection"],s=>{let t=[],e=-1;for(let i of s.selection.ranges){let n=s.doc.lineAt(i.head).from;n>e&&(e=n,t.push($u.range(n)))}return K.of(t)});function Cm(){return ju}const Uu=1024;let Gu=0;class Lt{constructor(t,e){this.from=t,this.to=e}}class L{constructor(t={}){this.id=Gu++,this.perNode=!!t.perNode,this.deserialize=t.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")})}add(t){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return typeof t!="function"&&(t=gt.match(t)),e=>{let i=t(e);return i===void 0?null:[this,i]}}}L.closedBy=new L({deserialize:s=>s.split(" ")});L.openedBy=new L({deserialize:s=>s.split(" ")});L.group=new L({deserialize:s=>s.split(" ")});L.isolate=new L({deserialize:s=>{if(s&&s!="rtl"&&s!="ltr"&&s!="auto")throw new RangeError("Invalid value for isolate: "+s);return s||"auto"}});L.contextHash=new L({perNode:!0});L.lookAhead=new L({perNode:!0});L.mounted=new L({perNode:!0});class Mi{constructor(t,e,i){this.tree=t,this.overlay=e,this.parser=i}static get(t){return t&&t.props&&t.props[L.mounted.id]}}const Ju=Object.create(null);class gt{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}static define(t){let e=t.props&&t.props.length?Object.create(null):Ju,i=(t.top?1:0)|(t.skipped?2:0)|(t.error?4:0)|(t.name==null?8:0),n=new gt(t.name||"",e,t.id,i);if(t.props){for(let r of t.props)if(Array.isArray(r)||(r=r(n)),r){if(r[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");e[r[0].id]=r[1]}}return n}prop(t){return this.props[t.id]}get isTop(){return(this.flags&1)>0}get isSkipped(){return(this.flags&2)>0}get isError(){return(this.flags&4)>0}get isAnonymous(){return(this.flags&8)>0}is(t){if(typeof t=="string"){if(this.name==t)return!0;let e=this.prop(L.group);return e?e.indexOf(t)>-1:!1}return this.id==t}static match(t){let e=Object.create(null);for(let i in t)for(let n of i.split(" "))e[n]=t[i];return i=>{for(let n=i.prop(L.group),r=-1;r<(n?n.length:0);r++){let o=e[r<0?i.name:n[r]];if(o)return o}}}}gt.none=new gt("",Object.create(null),0,8);class Cr{constructor(t){this.types=t;for(let e=0;e0;for(let a=this.cursor(o|Y.IncludeAnonymous);;){let c=!1;if(a.from<=r&&a.to>=n&&(!l&&a.type.isAnonymous||e(a)!==!1)){if(a.firstChild())continue;c=!0}for(;c&&i&&(l||!a.type.isAnonymous)&&i(a),!a.nextSibling();){if(!a.parent())return;c=!0}}}prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type.prop(t)}get propValues(){let t=[];if(this.props)for(let e in this.props)t.push([+e,this.props[e]]);return t}balance(t={}){return this.children.length<=8?this:Dr(gt.none,this.children,this.positions,0,this.children.length,0,this.length,(e,i,n)=>new j(this.type,e,i,n,this.propValues),t.makeTree||((e,i,n)=>new j(gt.none,e,i,n)))}static build(t){return Qu(t)}}j.empty=new j(gt.none,[],[],0);class Ar{constructor(t,e){this.buffer=t,this.index=e}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new Ar(this.buffer,this.index)}}class me{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get type(){return gt.none}toString(){let t=[];for(let e=0;e0));a=o[a+3]);return l}slice(t,e,i){let n=this.buffer,r=new Uint16Array(e-t),o=0;for(let l=t,a=0;l=t&&et;case 1:return e<=t&&i>t;case 2:return i>t;case 4:return!0}}function Di(s,t,e,i){for(var n;s.from==s.to||(e<1?s.from>=t:s.from>t)||(e>-1?s.to<=t:s.to0?l.length:-1;t!=c;t+=e){let h=l[t],f=a[t]+o.from;if(Za(n,i,f,f+h.length)){if(h instanceof me){if(r&Y.ExcludeBuffers)continue;let u=h.findChild(0,h.buffer.length,e,i-f,n);if(u>-1)return new Yt(new Yu(o,h,t,f),null,u)}else if(r&Y.IncludeAnonymous||!h.type.isAnonymous||Mr(h)){let u;if(!(r&Y.IgnoreMounts)&&(u=Mi.get(h))&&!u.overlay)return new ft(u.tree,f,t,o);let d=new ft(h,f,t,o);return r&Y.IncludeAnonymous||!d.type.isAnonymous?d:d.nextChild(e<0?h.children.length-1:0,e,i,n)}}}if(r&Y.IncludeAnonymous||!o.type.isAnonymous||(o.index>=0?t=o.index+e:t=e<0?-1:o._parent._tree.children.length,o=o._parent,!o))return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(t){return this.nextChild(0,1,t,2)}childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t,-2)}enter(t,e,i=0){let n;if(!(i&Y.IgnoreOverlays)&&(n=Mi.get(this._tree))&&n.overlay){let r=t-this.from;for(let{from:o,to:l}of n.overlay)if((e>0?o<=r:o=r:l>r))return new ft(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,t,e,i)}nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;)t=t._parent;return t}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}get tree(){return this._tree}toTree(){return this._tree}toString(){return this._tree.toString()}}function Uo(s,t,e,i){let n=s.cursor(),r=[];if(!n.firstChild())return r;if(e!=null){for(let o=!1;!o;)if(o=n.type.is(e),!n.nextSibling())return r}for(;;){if(i!=null&&n.type.is(i))return r;if(n.type.is(t)&&r.push(n.node),!n.nextSibling())return i==null?r:[]}}function _s(s,t,e=t.length-1){for(let i=s.parent;e>=0;i=i.parent){if(!i)return!1;if(!i.type.isAnonymous){if(t[e]&&t[e]!=i.name)return!1;e--}}return!0}class Yu{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.start=n}}class Yt extends th{get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}constructor(t,e,i){super(),this.context=t,this._parent=e,this.index=i,this.type=t.buffer.set.types[t.buffer.buffer[i]]}child(t,e,i){let{buffer:n}=this.context,r=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.context.start,i);return r<0?null:new Yt(this.context,this,r)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(t){return this.child(1,t,2)}childBefore(t){return this.child(-1,t,-2)}enter(t,e,i=0){if(i&Y.ExcludeBuffers)return null;let{buffer:n}=this.context,r=n.findChild(this.index+4,n.buffer[this.index+3],e>0?1:-1,t-this.context.start,e);return r<0?null:new Yt(this.context,this,r)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(t){return this._parent?null:this.context.parent.nextChild(this.context.index+t,t,0,4)}get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];return e<(this._parent?t.buffer[this._parent.index+3]:t.buffer.length)?new Yt(this.context,this._parent,e):this.externalSibling(1)}get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._parent.index+4:0;return this.index==e?this.externalSibling(-1):new Yt(this.context,this._parent,t.findChild(e,this.index,-1,0,4))}get tree(){return null}toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,r=i.buffer[this.index+3];if(r>n){let o=i.buffer[this.index+1];t.push(i.slice(n,r,o)),e.push(0)}return new j(this.type,t,e,this.to-this.from)}toString(){return this.context.buffer.childString(this.index)}}function eh(s){if(!s.length)return null;let t=0,e=s[0];for(let r=1;re.from||o.to=t){let l=new ft(o.tree,o.overlay[0].from+r.from,-1,r);(n||(n=[i])).push(Di(l,t,e,!1))}}return n?eh(n):i}class An{get name(){return this.type.name}constructor(t,e=0){if(this.mode=e,this.buffer=null,this.stack=[],this.index=0,this.bufferNode=null,t instanceof ft)this.yieldNode(t);else{this._tree=t.context.parent,this.buffer=t.context;for(let i=t._parent;i;i=i._parent)this.stack.unshift(i.index);this.bufferNode=t,this.yieldBuf(t.index)}}yieldNode(t){return t?(this._tree=t,this.type=t.type,this.from=t.from,this.to=t.to,!0):!1}yieldBuf(t,e){this.index=t;let{start:i,buffer:n}=this.buffer;return this.type=e||n.set.types[n.buffer[t]],this.from=i+n.buffer[t+1],this.to=i+n.buffer[t+2],!0}yield(t){return t?t instanceof ft?(this.buffer=null,this.yieldNode(t)):(this.buffer=t.context,this.yieldBuf(t.index,t.type)):!1}toString(){return this.buffer?this.buffer.buffer.childString(this.index):this._tree.toString()}enterChild(t,e,i){if(!this.buffer)return this.yield(this._tree.nextChild(t<0?this._tree._tree.children.length-1:0,t,e,i,this.mode));let{buffer:n}=this.buffer,r=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.buffer.start,i);return r<0?!1:(this.stack.push(this.index),this.yieldBuf(r))}firstChild(){return this.enterChild(1,0,4)}lastChild(){return this.enterChild(-1,0,4)}childAfter(t){return this.enterChild(1,t,2)}childBefore(t){return this.enterChild(-1,t,-2)}enter(t,e,i=this.mode){return this.buffer?i&Y.ExcludeBuffers?!1:this.enterChild(1,t,e):this.yield(this._tree.enter(t,e,i))}parent(){if(!this.buffer)return this.yieldNode(this.mode&Y.IncludeAnonymous?this._tree._parent:this._tree.parent);if(this.stack.length)return this.yieldBuf(this.stack.pop());let t=this.mode&Y.IncludeAnonymous?this.buffer.parent:this.buffer.parent.nextSignificantParent();return this.buffer=null,this.yieldNode(t)}sibling(t){if(!this.buffer)return this._tree._parent?this.yield(this._tree.index<0?null:this._tree._parent.nextChild(this._tree.index+t,t,0,4,this.mode)):!1;let{buffer:e}=this.buffer,i=this.stack.length-1;if(t<0){let n=i<0?0:this.stack[i]+4;if(this.index!=n)return this.yieldBuf(e.findChild(n,this.index,-1,0,4))}else{let n=e.buffer[this.index+3];if(n<(i<0?e.buffer.length:e.buffer[this.stack[i]+3]))return this.yieldBuf(n)}return i<0?this.yield(this.buffer.parent.nextChild(this.buffer.index+t,t,0,4,this.mode)):!1}nextSibling(){return this.sibling(1)}prevSibling(){return this.sibling(-1)}atLastNode(t){let e,i,{buffer:n}=this;if(n){if(t>0){if(this.index-1)for(let r=e+t,o=t<0?-1:i._tree.children.length;r!=o;r+=t){let l=i._tree.children[r];if(this.mode&Y.IncludeAnonymous||l instanceof me||!l.type.isAnonymous||Mr(l))return!1}return!0}move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.sibling(t))return!0;if(this.atLastNode(t)||!this.parent())return!1}}next(t=!0){return this.move(1,t)}prev(t=!0){return this.move(-1,t)}moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)||(e>-1?this.to<=t:this.to=0;){for(let o=t;o;o=o._parent)if(o.index==n){if(n==this.index)return o;e=o,i=r+1;break t}n=this.stack[--r]}for(let n=i;n=0;r--){if(r<0)return _s(this.node,t,n);let o=i[e.buffer[this.stack[r]]];if(!o.isAnonymous){if(t[n]&&t[n]!=o.name)return!1;n--}}return!0}}function Mr(s){return s.children.some(t=>t instanceof me||!t.type.isAnonymous||Mr(t))}function Qu(s){var t;let{buffer:e,nodeSet:i,maxBufferLength:n=Uu,reused:r=[],minRepeatType:o=i.types.length}=s,l=Array.isArray(e)?new Ar(e,e.length):e,a=i.types,c=0,h=0;function f(w,M,A,B,I,F){let{id:E,start:R,end:z,size:H}=l,J=h;for(;H<0;)if(l.next(),H==-1){let tt=r[E];A.push(tt),B.push(R-w);return}else if(H==-3){c=E;return}else if(H==-4){h=E;return}else throw new RangeError(`Unrecognized record size: ${H}`);let bt=a[E],xt,vt,Tt=R-w;if(z-R<=n&&(vt=m(l.pos-M,I))){let tt=new Uint16Array(vt.size-vt.skip),Pt=l.pos-vt.size,qt=tt.length;for(;l.pos>Pt;)qt=y(vt.start,tt,qt);xt=new me(tt,z-vt.start,i),Tt=vt.start-w}else{let tt=l.pos-H;l.next();let Pt=[],qt=[],xe=E>=o?E:-1,Ie=0,Wi=z;for(;l.pos>tt;)xe>=0&&l.id==xe&&l.size>=0?(l.end<=Wi-n&&(p(Pt,qt,R,Ie,l.end,Wi,xe,J),Ie=Pt.length,Wi=l.end),l.next()):F>2500?u(R,tt,Pt,qt):f(R,tt,Pt,qt,xe,F+1);if(xe>=0&&Ie>0&&Ie-1&&Ie>0){let $r=d(bt);xt=Dr(bt,Pt,qt,0,Pt.length,0,z-R,$r,$r)}else xt=g(bt,Pt,qt,z-R,J-z)}A.push(xt),B.push(Tt)}function u(w,M,A,B){let I=[],F=0,E=-1;for(;l.pos>M;){let{id:R,start:z,end:H,size:J}=l;if(J>4)l.next();else{if(E>-1&&z=0;H-=3)R[J++]=I[H],R[J++]=I[H+1]-z,R[J++]=I[H+2]-z,R[J++]=J;A.push(new me(R,I[2]-z,i)),B.push(z-w)}}function d(w){return(M,A,B)=>{let I=0,F=M.length-1,E,R;if(F>=0&&(E=M[F])instanceof j){if(!F&&E.type==w&&E.length==B)return E;(R=E.prop(L.lookAhead))&&(I=A[F]+E.length+R)}return g(w,M,A,B,I)}}function p(w,M,A,B,I,F,E,R){let z=[],H=[];for(;w.length>B;)z.push(w.pop()),H.push(M.pop()+A-I);w.push(g(i.types[E],z,H,F-I,R-F)),M.push(I-A)}function g(w,M,A,B,I=0,F){if(c){let E=[L.contextHash,c];F=F?[E].concat(F):[E]}if(I>25){let E=[L.lookAhead,I];F=F?[E].concat(F):[E]}return new j(w,M,A,B,F)}function m(w,M){let A=l.fork(),B=0,I=0,F=0,E=A.end-n,R={size:0,start:0,skip:0};t:for(let z=A.pos-w;A.pos>z;){let H=A.size;if(A.id==M&&H>=0){R.size=B,R.start=I,R.skip=F,F+=4,B+=4,A.next();continue}let J=A.pos-H;if(H<0||J=o?4:0,xt=A.start;for(A.next();A.pos>J;){if(A.size<0)if(A.size==-3)bt+=4;else break t;else A.id>=o&&(bt+=4);A.next()}I=xt,B+=H,F+=bt}return(M<0||B==w)&&(R.size=B,R.start=I,R.skip=F),R.size>4?R:void 0}function y(w,M,A){let{id:B,start:I,end:F,size:E}=l;if(l.next(),E>=0&&B4){let z=l.pos-(E-4);for(;l.pos>z;)A=y(w,M,A)}M[--A]=R,M[--A]=F-w,M[--A]=I-w,M[--A]=B}else E==-3?c=B:E==-4&&(h=B);return A}let x=[],k=[];for(;l.pos>0;)f(s.start||0,s.bufferStart||0,x,k,-1,0);let S=(t=s.length)!==null&&t!==void 0?t:x.length?k[0]+x[0].length:0;return new j(a[s.topID],x.reverse(),k.reverse(),S)}const Go=new WeakMap;function gn(s,t){if(!s.isAnonymous||t instanceof me||t.type!=s)return 1;let e=Go.get(t);if(e==null){e=1;for(let i of t.children){if(i.type!=s||!(i instanceof j)){e=1;break}e+=gn(s,i)}Go.set(t,e)}return e}function Dr(s,t,e,i,n,r,o,l,a){let c=0;for(let p=i;p=h)break;M+=A}if(k==S+1){if(M>h){let A=p[S];d(A.children,A.positions,0,A.children.length,g[S]+x);continue}f.push(p[S])}else{let A=g[k-1]+p[k-1].length-w;f.push(Dr(s,p,g,S,k,w,A,null,a))}u.push(w+x-r)}}return d(t,e,i,n,0),(l||a)(f,u,o)}class Am{constructor(){this.map=new WeakMap}setBuffer(t,e,i){let n=this.map.get(t);n||this.map.set(t,n=new Map),n.set(e,i)}getBuffer(t,e){let i=this.map.get(t);return i&&i.get(e)}set(t,e){t instanceof Yt?this.setBuffer(t.context.buffer,t.index,e):t instanceof ft&&this.map.set(t.tree,e)}get(t){return t instanceof Yt?this.getBuffer(t.context.buffer,t.index):t instanceof ft?this.map.get(t.tree):void 0}cursorSet(t,e){t.buffer?this.setBuffer(t.buffer.buffer,t.index,e):this.map.set(t.tree,e)}cursorGet(t){return t.buffer?this.getBuffer(t.buffer.buffer,t.index):this.map.get(t.tree)}}class ee{constructor(t,e,i,n,r=!1,o=!1){this.from=t,this.to=e,this.tree=i,this.offset=n,this.open=(r?1:0)|(o?2:0)}get openStart(){return(this.open&1)>0}get openEnd(){return(this.open&2)>0}static addTree(t,e=[],i=!1){let n=[new ee(0,t.length,t,0,!1,i)];for(let r of e)r.to>t.length&&n.push(r);return n}static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],r=1,o=t.length?t[0]:null;for(let l=0,a=0,c=0;;l++){let h=l=i)for(;o&&o.from=u.from||f<=u.to||c){let d=Math.max(u.from,a)-c,p=Math.min(u.to,f)-c;u=d>=p?null:new ee(d,p,u.tree,u.offset+c,l>0,!!h)}if(u&&n.push(u),o.to>f)break;o=rnew Lt(n.from,n.to)):[new Lt(0,0)]:[new Lt(0,t.length)],this.createParse(t,e||[],i)}parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let r=n.advance();if(r)return r}}}class Zu{constructor(t){this.string=t}get length(){return this.string.length}chunk(t){return this.string.slice(t)}get lineChunks(){return!1}read(t,e){return this.string.slice(t,e)}}function Mm(s){return(t,e,i,n)=>new ed(t,s,e,i,n)}class Jo{constructor(t,e,i,n,r){this.parser=t,this.parse=e,this.overlay=i,this.target=n,this.from=r}}function Yo(s){if(!s.length||s.some(t=>t.from>=t.to))throw new RangeError("Invalid inner parse ranges given: "+JSON.stringify(s))}class td{constructor(t,e,i,n,r,o,l){this.parser=t,this.predicate=e,this.mounts=i,this.index=n,this.start=r,this.target=o,this.prev=l,this.depth=0,this.ranges=[]}}const Qs=new L({perNode:!0});class ed{constructor(t,e,i,n,r){this.nest=e,this.input=i,this.fragments=n,this.ranges=r,this.inner=[],this.innerDone=0,this.baseTree=null,this.stoppedAt=null,this.baseParse=t}advance(){if(this.baseParse){let i=this.baseParse.advance();if(!i)return null;if(this.baseParse=null,this.baseTree=i,this.startInner(),this.stoppedAt!=null)for(let n of this.inner)n.parse.stopAt(this.stoppedAt)}if(this.innerDone==this.inner.length){let i=this.baseTree;return this.stoppedAt!=null&&(i=new j(i.type,i.children,i.positions,i.length,i.propValues.concat([[Qs,this.stoppedAt]]))),i}let t=this.inner[this.innerDone],e=t.parse.advance();if(e){this.innerDone++;let i=Object.assign(Object.create(null),t.target.props);i[L.mounted.id]=new Mi(e,t.overlay,t.parser),t.target.props=i}return null}get parsedPos(){if(this.baseParse)return 0;let t=this.input.length;for(let e=this.innerDone;e=this.stoppedAt)l=!1;else if(t.hasNode(n)){if(e){let c=e.mounts.find(h=>h.frag.from<=n.from&&h.frag.to>=n.to&&h.mount.overlay);if(c)for(let h of c.mount.overlay){let f=h.from+c.pos,u=h.to+c.pos;f>=n.from&&u<=n.to&&!e.ranges.some(d=>d.fromf)&&e.ranges.push({from:f,to:u})}}l=!1}else if(i&&(o=id(i.ranges,n.from,n.to)))l=o!=2;else if(!n.type.isAnonymous&&(r=this.nest(n,this.input))&&(n.fromnew Lt(f.from-n.from,f.to-n.from)):null,n.tree,h.length?h[0].from:n.from)),r.overlay?h.length&&(i={ranges:h,depth:0,prev:i}):l=!1}}else e&&(a=e.predicate(n))&&(a===!0&&(a=new Lt(n.from,n.to)),a.fromnew Lt(h.from-e.start,h.to-e.start)),e.target,c[0].from))),e=e.prev}i&&!--i.depth&&(i=i.prev)}}}}function id(s,t,e){for(let i of s){if(i.from>=e)break;if(i.to>t)return i.from<=t&&i.to>=e?2:1}return 0}function Xo(s,t,e,i,n,r){if(t=t&&e.enter(i,1,Y.IgnoreOverlays|Y.ExcludeBuffers)||e.next(!1)||(this.done=!0)}hasNode(t){if(this.moveTo(t.from),!this.done&&this.cursor.from+this.offset==t.from&&this.cursor.tree)for(let e=this.cursor.tree;;){if(e==t.tree)return!0;if(e.children.length&&e.positions[0]==0&&e.children[0]instanceof j)e=e.children[0];else break}return!1}}class sd{constructor(t){var e;if(this.fragments=t,this.curTo=0,this.fragI=0,t.length){let i=this.curFrag=t[0];this.curTo=(e=i.tree.prop(Qs))!==null&&e!==void 0?e:i.to,this.inner=new _o(i.tree,-i.offset)}else this.curFrag=this.inner=null}hasNode(t){for(;this.curFrag&&t.from>=this.curTo;)this.nextFrag();return this.curFrag&&this.curFrag.from<=t.from&&this.curTo>=t.to&&this.inner.hasNode(t)}nextFrag(){var t;if(this.fragI++,this.fragI==this.fragments.length)this.curFrag=this.inner=null;else{let e=this.curFrag=this.fragments[this.fragI];this.curTo=(t=e.tree.prop(Qs))!==null&&t!==void 0?t:e.to,this.inner=new _o(e.tree,-e.offset)}}findMounts(t,e){var i;let n=[];if(this.inner){this.inner.cursor.moveTo(t,1);for(let r=this.inner.cursor.node;r;r=r.parent){let o=(i=r.tree)===null||i===void 0?void 0:i.prop(L.mounted);if(o&&o.parser==e)for(let l=this.fragI;l=r.to)break;a.tree==this.curFrag.tree&&n.push({frag:a,pos:r.from-a.offset,mount:o})}}}return n}}function Qo(s,t){let e=null,i=t;for(let n=1,r=0;n=l)break;a.to<=o||(e||(i=e=t.slice()),a.froml&&e.splice(r+1,0,new Lt(l,a.to))):a.to>l?e[r--]=new Lt(l,a.to):e.splice(r--,1))}}return i}function rd(s,t,e,i){let n=0,r=0,o=!1,l=!1,a=-1e9,c=[];for(;;){let h=n==s.length?1e9:o?s[n].to:s[n].from,f=r==t.length?1e9:l?t[r].to:t[r].from;if(o!=l){let u=Math.max(a,e),d=Math.min(h,f,i);unew Lt(u.from+i,u.to+i)),f=rd(t,h,a,c);for(let u=0,d=a;;u++){let p=u==f.length,g=p?c:f[u].from;if(g>d&&e.push(new ee(d,g,n.tree,-o,r.from>=d||r.openStart,r.to<=g||r.openEnd)),p)break;d=f[u].to}}else e.push(new ee(a,c,n.tree,-o,r.from>=o||r.openStart,r.to<=l||r.openEnd))}return e}let od=0;class Bt{constructor(t,e,i,n){this.name=t,this.set=e,this.base=i,this.modified=n,this.id=od++}toString(){let{name:t}=this;for(let e of this.modified)e.name&&(t=`${e.name}(${t})`);return t}static define(t,e){let i=typeof t=="string"?t:"?";if(t instanceof Bt&&(e=t),e!=null&&e.base)throw new Error("Can not derive from a modified tag");let n=new Bt(i,[],null,[]);if(n.set.push(n),e)for(let r of e.set)n.set.push(r);return n}static defineModifier(t){let e=new Mn(t);return i=>i.modified.indexOf(e)>-1?i:Mn.get(i.base||i,i.modified.concat(e).sort((n,r)=>n.id-r.id))}}let ld=0;class Mn{constructor(t){this.name=t,this.instances=[],this.id=ld++}static get(t,e){if(!e.length)return t;let i=e[0].instances.find(l=>l.base==t&&ad(e,l.modified));if(i)return i;let n=[],r=new Bt(t.name,n,t,e);for(let l of e)l.instances.push(r);let o=hd(e);for(let l of t.set)if(!l.modified.length)for(let a of o)n.push(Mn.get(l,a));return r}}function ad(s,t){return s.length==t.length&&s.every((e,i)=>e==t[i])}function hd(s){let t=[[]];for(let e=0;ei.length-e.length)}function cd(s){let t=Object.create(null);for(let e in s){let i=s[e];Array.isArray(i)||(i=[i]);for(let n of e.split(" "))if(n){let r=[],o=2,l=n;for(let f=0;;){if(l=="..."&&f>0&&f+3==n.length){o=1;break}let u=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(l);if(!u)throw new RangeError("Invalid path: "+n);if(r.push(u[0]=="*"?"":u[0][0]=='"'?JSON.parse(u[0]):u[0]),f+=u[0].length,f==n.length)break;let d=n[f++];if(f==n.length&&d=="!"){o=0;break}if(d!="/")throw new RangeError("Invalid path: "+n);l=n.slice(f)}let a=r.length-1,c=r[a];if(!c)throw new RangeError("Invalid path: "+n);let h=new Dn(i,o,a>0?r.slice(0,a):null);t[c]=h.sort(t[c])}}return nh.add(t)}const nh=new L;class Dn{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}get opaque(){return this.mode==0}get inherit(){return this.mode==1}sort(t){return!t||t.depth{let o=n;for(let l of r)for(let a of l.set){let c=e[a.id];if(c){o=o?o+" "+c:c;break}}return o},scope:i}}function fd(s,t){let e=null;for(let i of s){let n=i.style(t);n&&(e=e?e+" "+n:n)}return e}function ud(s,t,e,i=0,n=s.length){let r=new dd(i,Array.isArray(t)?t:[t],e);r.highlightRange(s.cursor(),i,n,"",r.highlighters),r.flush(n)}class dd{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.class=""}startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=e)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,e,i,n,r){let{type:o,from:l,to:a}=t;if(l>=i||a<=e)return;o.isTop&&(r=this.highlighters.filter(d=>!d.scope||d.scope(o)));let c=n,h=pd(t)||Dn.empty,f=fd(r,h.tags);if(f&&(c&&(c+=" "),c+=f,h.mode==1&&(n+=(n?" ":"")+f)),this.startSpan(Math.max(e,l),c),h.opaque)return;let u=t.tree&&t.tree.prop(L.mounted);if(u&&u.overlay){let d=t.node.enter(u.overlay[0].from+l,1),p=this.highlighters.filter(m=>!m.scope||m.scope(u.tree.type)),g=t.firstChild();for(let m=0,y=l;;m++){let x=m=k||!t.nextSibling())););if(!x||k>i)break;y=x.to+l,y>e&&(this.highlightRange(d.cursor(),Math.max(e,x.from+l),Math.min(i,y),"",p),this.startSpan(Math.min(i,y),c))}g&&t.parent()}else if(t.firstChild()){u&&(n="");do if(!(t.to<=e)){if(t.from>=i)break;this.highlightRange(t,e,i,n,r),this.startSpan(Math.min(i,t.to),c)}while(t.nextSibling());t.parent()}}}function pd(s){let t=s.type.prop(nh);for(;t&&t.context&&!s.matchContext(t.context);)t=t.next;return t||null}const v=Bt.define,tn=v(),re=v(),tl=v(re),el=v(re),oe=v(),en=v(oe),rs=v(oe),Ut=v(),we=v(Ut),$t=v(),jt=v(),Zs=v(),ai=v(Zs),nn=v(),C={comment:tn,lineComment:v(tn),blockComment:v(tn),docComment:v(tn),name:re,variableName:v(re),typeName:tl,tagName:v(tl),propertyName:el,attributeName:v(el),className:v(re),labelName:v(re),namespace:v(re),macroName:v(re),literal:oe,string:en,docString:v(en),character:v(en),attributeValue:v(en),number:rs,integer:v(rs),float:v(rs),bool:v(oe),regexp:v(oe),escape:v(oe),color:v(oe),url:v(oe),keyword:$t,self:v($t),null:v($t),atom:v($t),unit:v($t),modifier:v($t),operatorKeyword:v($t),controlKeyword:v($t),definitionKeyword:v($t),moduleKeyword:v($t),operator:jt,derefOperator:v(jt),arithmeticOperator:v(jt),logicOperator:v(jt),bitwiseOperator:v(jt),compareOperator:v(jt),updateOperator:v(jt),definitionOperator:v(jt),typeOperator:v(jt),controlOperator:v(jt),punctuation:Zs,separator:v(Zs),bracket:ai,angleBracket:v(ai),squareBracket:v(ai),paren:v(ai),brace:v(ai),content:Ut,heading:we,heading1:v(we),heading2:v(we),heading3:v(we),heading4:v(we),heading5:v(we),heading6:v(we),contentSeparator:v(Ut),list:v(Ut),quote:v(Ut),emphasis:v(Ut),strong:v(Ut),link:v(Ut),monospace:v(Ut),strikethrough:v(Ut),inserted:v(),deleted:v(),changed:v(),invalid:v(),meta:nn,documentMeta:v(nn),annotation:v(nn),processingInstruction:v(nn),definition:Bt.defineModifier("definition"),constant:Bt.defineModifier("constant"),function:Bt.defineModifier("function"),standard:Bt.defineModifier("standard"),local:Bt.defineModifier("local"),special:Bt.defineModifier("special")};for(let s in C){let t=C[s];t instanceof Bt&&(t.name=s)}sh([{tag:C.link,class:"tok-link"},{tag:C.heading,class:"tok-heading"},{tag:C.emphasis,class:"tok-emphasis"},{tag:C.strong,class:"tok-strong"},{tag:C.keyword,class:"tok-keyword"},{tag:C.atom,class:"tok-atom"},{tag:C.bool,class:"tok-bool"},{tag:C.url,class:"tok-url"},{tag:C.labelName,class:"tok-labelName"},{tag:C.inserted,class:"tok-inserted"},{tag:C.deleted,class:"tok-deleted"},{tag:C.literal,class:"tok-literal"},{tag:C.string,class:"tok-string"},{tag:C.number,class:"tok-number"},{tag:[C.regexp,C.escape,C.special(C.string)],class:"tok-string2"},{tag:C.variableName,class:"tok-variableName"},{tag:C.local(C.variableName),class:"tok-variableName tok-local"},{tag:C.definition(C.variableName),class:"tok-variableName tok-definition"},{tag:C.special(C.variableName),class:"tok-variableName2"},{tag:C.definition(C.propertyName),class:"tok-propertyName tok-definition"},{tag:C.typeName,class:"tok-typeName"},{tag:C.namespace,class:"tok-namespace"},{tag:C.className,class:"tok-className"},{tag:C.macroName,class:"tok-macroName"},{tag:C.propertyName,class:"tok-propertyName"},{tag:C.operator,class:"tok-operator"},{tag:C.comment,class:"tok-comment"},{tag:C.meta,class:"tok-meta"},{tag:C.invalid,class:"tok-invalid"},{tag:C.punctuation,class:"tok-punctuation"}]);var os;const Ce=new L;function rh(s){return T.define({combine:s?t=>t.concat(s):void 0})}const gd=new L;class Et{constructor(t,e,i=[],n=""){this.data=t,this.name=n,W.prototype.hasOwnProperty("tree")||Object.defineProperty(W.prototype,"tree",{get(){return mt(this)}}),this.parser=e,this.extension=[Ze.of(this),W.languageData.of((r,o,l)=>{let a=il(r,o,l),c=a.type.prop(Ce);if(!c)return[];let h=r.facet(c),f=a.type.prop(gd);if(f){let u=a.resolve(o-a.from,l);for(let d of f)if(d.test(u,r)){let p=r.facet(d.facet);return d.type=="replace"?p:p.concat(h)}}return h})].concat(i)}isActiveAt(t,e,i=-1){return il(t,e,i).type.prop(Ce)==this.data}findRegions(t){let e=t.facet(Ze);if((e==null?void 0:e.data)==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let i=[],n=(r,o)=>{if(r.prop(Ce)==this.data){i.push({from:o,to:o+r.length});return}let l=r.prop(L.mounted);if(l){if(l.tree.prop(Ce)==this.data){if(l.overlay)for(let a of l.overlay)i.push({from:a.from+o,to:a.to+o});else i.push({from:o,to:o+r.length});return}else if(l.overlay){let a=i.length;if(n(l.tree,l.overlay[0].from+o),i.length>a)return}}for(let a=0;ai.isTop?e:void 0)]}),t.name)}configure(t,e){return new tr(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function mt(s){let t=s.field(Et.state,!1);return t?t.tree:j.empty}class md{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=t.iter()}get length(){return this.doc.length}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let i=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-i,e-i)}}let hi=null;class _e{constructor(t,e,i=[],n,r,o,l,a){this.parser=t,this.state=e,this.fragments=i,this.tree=n,this.treeLen=r,this.viewport=o,this.skipped=l,this.scheduleOn=a,this.parse=null,this.tempSkipped=[]}static create(t,e,i){return new _e(t,e,[],j.empty,0,i,[],null)}startParse(){return this.parser.startParse(new md(this.state.doc),this.fragments)}work(t,e){return e!=null&&e>=this.state.doc.length&&(e=void 0),this.tree!=j.empty&&this.isDone(e??this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var i;if(typeof t=="number"){let n=Date.now()+t;t=()=>Date.now()>n}for(this.parse||(this.parse=this.startParse()),e!=null&&(this.parse.stoppedAt==null||this.parse.stoppedAt>e)&&e=this.treeLen&&((this.parse.stoppedAt==null||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext(()=>{for(;!(e=this.parse.advance()););}),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(ee.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=hi;hi=this;try{return t()}finally{hi=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=nl(t,e.from,e.to);return t}changes(t,e){let{fragments:i,tree:n,treeLen:r,viewport:o,skipped:l}=this;if(this.takeTree(),!t.empty){let a=[];if(t.iterChangedRanges((c,h,f,u)=>a.push({fromA:c,toA:h,fromB:f,toB:u})),i=ee.applyChanges(i,a),n=j.empty,r=0,o={from:t.mapPos(o.from,-1),to:t.mapPos(o.to,1)},this.skipped.length){l=[];for(let c of this.skipped){let h=t.mapPos(c.from,1),f=t.mapPos(c.to,-1);ht.from&&(this.fragments=nl(this.fragments,n,r),this.skipped.splice(i--,1))}return this.skipped.length>=e?!1:(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends ih{createParse(e,i,n){let r=n[0].from,o=n[n.length-1].to;return{parsedPos:r,advance(){let a=hi;if(a){for(let c of n)a.tempSkipped.push(c);t&&(a.scheduleOn=a.scheduleOn?Promise.all([a.scheduleOn,t]):t)}return this.parsedPos=o,new j(gt.none,[],[],o-r)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&e[0].from==0&&e[0].to>=t}static get(){return hi}}function nl(s,t,e){return ee.applyChanges(s,[{fromA:t,toA:e,fromB:t,toB:e}])}class Qe{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),i=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,i)||e.takeTree(),new Qe(e)}static init(t){let e=Math.min(3e3,t.doc.length),i=_e.create(t.facet(Ze).parser,t,{from:0,to:e});return i.work(20,e)||i.takeTree(),new Qe(i)}}Et.state=yt.define({create:Qe.init,update(s,t){for(let e of t.effects)if(e.is(Et.setState))return e.value;return t.startState.facet(Ze)!=t.state.facet(Ze)?Qe.init(t.state):s.apply(t)}});let oh=s=>{let t=setTimeout(()=>s(),500);return()=>clearTimeout(t)};typeof requestIdleCallback<"u"&&(oh=s=>{let t=-1,e=setTimeout(()=>{t=requestIdleCallback(s,{timeout:400})},100);return()=>t<0?clearTimeout(e):cancelIdleCallback(t)});const ls=typeof navigator<"u"&&(!((os=navigator.scheduling)===null||os===void 0)&&os.isInputPending)?()=>navigator.scheduling.isInputPending():null,yd=ut.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(Et.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),(t.docChanged||t.selectionSet)&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(Et.state);(e.tree!=e.context.tree||!e.context.isDone(t.doc.length))&&(this.working=oh(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndn+1e3,a=r.context.work(()=>ls&&ls()||Date.now()>o,n+(l?0:1e5));this.chunkBudget-=Date.now()-e,(a||this.chunkBudget<=0)&&(r.context.takeTree(),this.view.dispatch({effects:Et.setState.of(new Qe(r.context))})),this.chunkBudget>0&&!(a&&!l)&&this.scheduleWork(),this.checkAsyncSchedule(r.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then(()=>this.scheduleWork()).catch(e=>Dt(this.view.state,e)).then(()=>this.workScheduled--),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Ze=T.define({combine(s){return s.length?s[0]:null},enables:s=>[Et.state,yd,O.contentAttributes.compute([s],t=>{let e=t.facet(s);return e&&e.name?{"data-language":e.name}:{}})]});class Om{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}const lh=T.define(),zn=T.define({combine:s=>{if(!s.length)return" ";let t=s[0];if(!t||/\S/.test(t)||Array.from(t).some(e=>e!=t[0]))throw new Error("Invalid indent unit: "+JSON.stringify(s[0]));return t}});function Re(s){let t=s.facet(zn);return t.charCodeAt(0)==9?s.tabSize*t.length:t.length}function On(s,t){let e="",i=s.tabSize,n=s.facet(zn)[0];if(n==" "){for(;t>=i;)e+=" ",t-=i;n=" "}for(let r=0;r=t?xd(s,e,t):null}class qn{constructor(t,e={}){this.state=t,this.options=e,this.unit=Re(t)}lineAt(t,e=1){let i=this.state.doc.lineAt(t),{simulateBreak:n,simulateDoubleBreak:r}=this.options;return n!=null&&n>=i.from&&n<=i.to?r&&n==t?{text:"",from:t}:(e<0?n-1&&(r+=o-this.countColumn(i,i.search(/\S|$/))),r}countColumn(t,e=t.length){return ei(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:i,from:n}=this.lineAt(t,e),r=this.options.overrideIndentation;if(r){let o=r(n);if(o>-1)return o}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const bd=new L;function xd(s,t,e){let i=t.resolveStack(e),n=i.node.enterUnfinishedNodesBefore(e);if(n!=i.node){let r=[];for(let o=n;o!=i.node;o=o.parent)r.push(o);for(let o=r.length-1;o>=0;o--)i={node:r[o],next:i}}return hh(i,s,e)}function hh(s,t,e){for(let i=s;i;i=i.next){let n=Sd(i.node);if(n)return n(Or.create(t,e,i))}return 0}function wd(s){return s.pos==s.options.simulateBreak&&s.options.simulateDoubleBreak}function Sd(s){let t=s.type.prop(bd);if(t)return t;let e=s.firstChild,i;if(e&&(i=e.type.prop(L.closedBy))){let n=s.lastChild,r=n&&i.indexOf(n.name)>-1;return o=>ch(o,!0,1,void 0,r&&!wd(o)?n.from:void 0)}return s.parent==null?kd:null}function kd(){return 0}class Or extends qn{constructor(t,e,i){super(t.state,t.options),this.base=t,this.pos=e,this.context=i}get node(){return this.context.node}static create(t,e,i){return new Or(t,e,i)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){return this.baseIndentFor(this.node)}baseIndentFor(t){let e=this.state.doc.lineAt(t.from);for(;;){let i=t.resolve(e.from);for(;i.parent&&i.parent.from==i.from;)i=i.parent;if(vd(i,t))break;e=this.state.doc.lineAt(i.from)}return this.lineIndent(e.from)}continue(){return hh(this.context.next,this.base,this.pos)}}function vd(s,t){for(let e=t;e;e=e.parent)if(s==e)return!0;return!1}function Cd(s){let t=s.node,e=t.childAfter(t.from),i=t.lastChild;if(!e)return null;let n=s.options.simulateBreak,r=s.state.doc.lineAt(e.from),o=n==null||n<=r.from?r.to:Math.min(r.to,n);for(let l=e.to;;){let a=t.childAfter(l);if(!a||a==i)return null;if(!a.type.isSkipped)return a.fromch(i,t,e,s)}function ch(s,t,e,i,n){let r=s.textAfter,o=r.match(/^\s*/)[0].length,l=i&&r.slice(o,o+i.length)==i||n==s.pos+o,a=t?Cd(s):null;return a?l?s.column(a.from):s.column(a.to):s.baseIndent+(l?0:s.unit*e)}const Pm=s=>s.baseIndent;function Bm({except:s,units:t=1}={}){return e=>{let i=s&&s.test(e.textAfter);return e.baseIndent+(i?0:t*e.unit)}}const Rm=new L;function Lm(s){let t=s.firstChild,e=s.lastChild;return t&&t.tol.prop(Ce)==o.data:o?l=>l==o:void 0,this.style=sh(t.map(l=>({tag:l.tag,class:l.class||n(Object.assign({},l,{tag:null}))})),{all:r}).style,this.module=i?new de(i):null,this.themeType=e.themeType}static define(t,e){return new Kn(t,e||{})}}const er=T.define(),fh=T.define({combine(s){return s.length?[s[0]]:null}});function as(s){let t=s.facet(er);return t.length?t:s.facet(fh)}function Em(s,t){let e=[Md],i;return s instanceof Kn&&(s.module&&e.push(O.styleModule.of(s.module)),i=s.themeType),t!=null&&t.fallback?e.push(fh.of(s)):i?e.push(er.computeN([O.darkTheme],n=>n.facet(O.darkTheme)==(i=="dark")?[s]:[])):e.push(er.of(s)),e}class Ad{constructor(t){this.markCache=Object.create(null),this.tree=mt(t.state),this.decorations=this.buildDeco(t,as(t.state)),this.decoratedTo=t.viewport.to}update(t){let e=mt(t.state),i=as(t.state),n=i!=as(t.startState),{viewport:r}=t.view,o=t.changes.mapPos(this.decoratedTo,1);e.length=r.to?(this.decorations=this.decorations.map(t.changes),this.decoratedTo=o):(e!=this.tree||t.viewportChanged||n)&&(this.tree=e,this.decorations=this.buildDeco(t.view,i),this.decoratedTo=r.to)}buildDeco(t,e){if(!e||!this.tree.length)return P.none;let i=new De;for(let{from:n,to:r}of t.visibleRanges)ud(this.tree,e,(o,l,a)=>{i.add(o,l,this.markCache[a]||(this.markCache[a]=P.mark({class:a})))},n,r);return i.finish()}}const Md=ye.high(ut.fromClass(Ad,{decorations:s=>s.decorations})),Im=Kn.define([{tag:C.meta,color:"#404740"},{tag:C.link,textDecoration:"underline"},{tag:C.heading,textDecoration:"underline",fontWeight:"bold"},{tag:C.emphasis,fontStyle:"italic"},{tag:C.strong,fontWeight:"bold"},{tag:C.strikethrough,textDecoration:"line-through"},{tag:C.keyword,color:"#708"},{tag:[C.atom,C.bool,C.url,C.contentSeparator,C.labelName],color:"#219"},{tag:[C.literal,C.inserted],color:"#164"},{tag:[C.string,C.deleted],color:"#a11"},{tag:[C.regexp,C.escape,C.special(C.string)],color:"#e40"},{tag:C.definition(C.variableName),color:"#00f"},{tag:C.local(C.variableName),color:"#30a"},{tag:[C.typeName,C.namespace],color:"#085"},{tag:C.className,color:"#167"},{tag:[C.special(C.variableName),C.macroName],color:"#256"},{tag:C.definition(C.propertyName),color:"#00c"},{tag:C.comment,color:"#940"},{tag:C.invalid,color:"#f00"}]),Dd=O.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),uh=1e4,dh="()[]{}",ph=T.define({combine(s){return Le(s,{afterCursor:!0,brackets:dh,maxScanDistance:uh,renderMatch:Pd})}}),Od=P.mark({class:"cm-matchingBracket"}),Td=P.mark({class:"cm-nonmatchingBracket"});function Pd(s){let t=[],e=s.matched?Od:Td;return t.push(e.range(s.start.from,s.start.to)),s.end&&t.push(e.range(s.end.from,s.end.to)),t}const Bd=yt.define({create(){return P.none},update(s,t){if(!t.docChanged&&!t.selection)return s;let e=[],i=t.state.facet(ph);for(let n of t.state.selection.ranges){if(!n.empty)continue;let r=Xt(t.state,n.head,-1,i)||n.head>0&&Xt(t.state,n.head-1,1,i)||i.afterCursor&&(Xt(t.state,n.head,1,i)||n.headO.decorations.from(s)}),Rd=[Bd,Dd];function Nm(s={}){return[ph.of(s),Rd]}const Ld=new L;function ir(s,t,e){let i=s.prop(t<0?L.openedBy:L.closedBy);if(i)return i;if(s.name.length==1){let n=e.indexOf(s.name);if(n>-1&&n%2==(t<0?1:0))return[e[n+t]]}return null}function nr(s){let t=s.type.prop(Ld);return t?t(s.node):s}function Xt(s,t,e,i={}){let n=i.maxScanDistance||uh,r=i.brackets||dh,o=mt(s),l=o.resolveInner(t,e);for(let a=l;a;a=a.parent){let c=ir(a.type,e,r);if(c&&a.from0?t>=h.from&&th.from&&t<=h.to))return Ed(s,t,e,a,h,c,r)}}return Id(s,t,e,o,l.type,n,r)}function Ed(s,t,e,i,n,r,o){let l=i.parent,a={from:n.from,to:n.to},c=0,h=l==null?void 0:l.cursor();if(h&&(e<0?h.childBefore(i.from):h.childAfter(i.to)))do if(e<0?h.to<=i.from:h.from>=i.to){if(c==0&&r.indexOf(h.type.name)>-1&&h.from0)return null;let c={from:e<0?t-1:t,to:e>0?t+1:t},h=s.doc.iterRange(t,e>0?s.doc.length:0),f=0;for(let u=0;!h.next().done&&u<=r;){let d=h.value;e<0&&(u+=d.length);let p=t+u*e;for(let g=e>0?0:d.length-1,m=e>0?d.length:-1;g!=m;g+=e){let y=o.indexOf(d[g]);if(!(y<0||i.resolveInner(p+g,1).type!=n))if(y%2==0==e>0)f++;else{if(f==1)return{start:c,end:{from:p+g,to:p+g+1},matched:y>>1==a>>1};f--}}e>0&&(u+=d.length)}return h.done?{start:c,matched:!1}:null}function sl(s,t,e,i=0,n=0){t==null&&(t=s.search(/[^\s\u00a0]/),t==-1&&(t=s.length));let r=n;for(let o=i;o=this.string.length}sol(){return this.pos==0}peek(){return this.string.charAt(this.pos)||void 0}next(){if(this.pose}eatSpace(){let t=this.pos;for(;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>t}skipToEnd(){this.pos=this.string.length}skipTo(t){let e=this.string.indexOf(t,this.pos);if(e>-1)return this.pos=e,!0}backUp(t){this.pos-=t}column(){return this.lastColumnPosi?o.toLowerCase():o,r=this.string.substr(this.pos,t.length);return n(r)==n(t)?(e!==!1&&(this.pos+=t.length),!0):null}else{let n=this.string.slice(this.pos).match(t);return n&&n.index>0?null:(n&&e!==!1&&(this.pos+=n[0].length),n)}}current(){return this.string.slice(this.start,this.pos)}}function Nd(s){return{name:s.name||"",token:s.token,blankLine:s.blankLine||(()=>{}),startState:s.startState||(()=>!0),copyState:s.copyState||Fd,indent:s.indent||(()=>null),languageData:s.languageData||{},tokenTable:s.tokenTable||Pr}}function Fd(s){if(typeof s!="object")return s;let t={};for(let e in s){let i=s[e];t[e]=i instanceof Array?i.slice():i}return t}const rl=new WeakMap;class mh extends Et{constructor(t){let e=rh(t.languageData),i=Nd(t),n,r=new class extends ih{createParse(o,l,a){return new Hd(n,o,l,a)}};super(e,r,[lh.of((o,l)=>this.getIndent(o,l))],t.name),this.topNode=qd(e),n=this,this.streamParser=i,this.stateAfter=new L({perNode:!0}),this.tokenTable=t.tokenTable?new wh(i.tokenTable):zd}static define(t){return new mh(t)}getIndent(t,e){let i=mt(t.state),n=i.resolve(e);for(;n&&n.type!=this.topNode;)n=n.parent;if(!n)return null;let r,{overrideIndentation:o}=t.options;o&&(r=rl.get(t.state),r!=null&&r1e4)return null;for(;a=i&&e+t.length<=n&&t.prop(s.stateAfter);if(r)return{state:s.streamParser.copyState(r),pos:e+t.length};for(let o=t.children.length-1;o>=0;o--){let l=t.children[o],a=e+t.positions[o],c=l instanceof j&&a=t.length)return t;!n&&t.type==s.topNode&&(n=!0);for(let r=t.children.length-1;r>=0;r--){let o=t.positions[r],l=t.children[r],a;if(oe&&Tr(s,n.tree,0-n.offset,e,o),a;if(l&&(a=yh(s,n.tree,e+n.offset,l.pos+n.offset,!1)))return{state:l.state,tree:a}}return{state:s.streamParser.startState(i?Re(i):4),tree:j.empty}}class Hd{constructor(t,e,i,n){this.lang=t,this.input=e,this.fragments=i,this.ranges=n,this.stoppedAt=null,this.chunks=[],this.chunkPos=[],this.chunk=[],this.chunkReused=void 0,this.rangeIndex=0,this.to=n[n.length-1].to;let r=_e.get(),o=n[0].from,{state:l,tree:a}=Vd(t,i,o,r==null?void 0:r.state);this.state=l,this.parsedPos=this.chunkStart=o+a.length;for(let c=0;c=e?this.finish():t&&this.parsedPos>=t.viewport.to?(t.skipUntilInView(this.parsedPos,e),this.finish()):null}stopAt(t){this.stoppedAt=t}lineAfter(t){let e=this.input.chunk(t);if(this.input.lineChunks)e==` +`&&(e="");else{let i=e.indexOf(` +`);i>-1&&(e=e.slice(0,i))}return t+e.length<=this.to?e:e.slice(0,this.to-t)}nextLine(){let t=this.parsedPos,e=this.lineAfter(t),i=t+e.length;for(let n=this.rangeIndex;;){let r=this.ranges[n].to;if(r>=i||(e=e.slice(0,r-(i-e.length)),n++,n==this.ranges.length))break;let o=this.ranges[n].from,l=this.lineAfter(o);e+=l,i=o+l.length}return{line:e,end:i}}skipGapsTo(t,e,i){for(;;){let n=this.ranges[this.rangeIndex].to,r=t+e;if(i>0?n>r:n>=r)break;let o=this.ranges[++this.rangeIndex].from;e+=o-n}return e}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to1){r=this.skipGapsTo(e,r,1),e+=r;let o=this.chunk.length;r=this.skipGapsTo(i,r,-1),i+=r,n+=this.chunk.length-o}return this.chunk.push(t,e,i,n),r}parseLine(t){let{line:e,end:i}=this.nextLine(),n=0,{streamParser:r}=this.lang,o=new gh(e,t?t.state.tabSize:4,t?Re(t.state):2);if(o.eol())r.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=bh(r.token,o,this.state);if(l&&(n=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,n)),o.start>1e4)break}this.parsedPos=i,this.moveRangeIndex(),this.parsedPost.start)return n}throw new Error("Stream parser failed to advance stream.")}const Pr=Object.create(null),Oi=[gt.none],Wd=new Cr(Oi),ol=[],ll=Object.create(null),xh=Object.create(null);for(let[s,t]of[["variable","variableName"],["variable-2","variableName.special"],["string-2","string.special"],["def","variableName.definition"],["tag","tagName"],["attribute","attributeName"],["type","typeName"],["builtin","variableName.standard"],["qualifier","modifier"],["error","invalid"],["header","heading"],["property","propertyName"]])xh[s]=Sh(Pr,t);class wh{constructor(t){this.extra=t,this.table=Object.assign(Object.create(null),xh)}resolve(t){return t?this.table[t]||(this.table[t]=Sh(this.extra,t)):0}}const zd=new wh(Pr);function hs(s,t){ol.indexOf(s)>-1||(ol.push(s),console.warn(t))}function Sh(s,t){let e=[];for(let l of t.split(" ")){let a=[];for(let c of l.split(".")){let h=s[c]||C[c];h?typeof h=="function"?a.length?a=a.map(h):hs(c,`Modifier ${c} used at start of tag`):a.length?hs(c,`Tag ${c} used as modifier`):a=Array.isArray(h)?h:[h]:hs(c,`Unknown highlighting tag ${c}`)}for(let c of a)e.push(c)}if(!e.length)return 0;let i=t.replace(/ /g,"_"),n=i+" "+e.map(l=>l.id),r=ll[n];if(r)return r.id;let o=ll[n]=gt.define({id:Oi.length,name:i,props:[cd({[i]:e})]});return Oi.push(o),o.id}function qd(s){let t=gt.define({id:Oi.length,name:"Document",props:[Ce.add(()=>s)],top:!0});return Oi.push(t),t}X.RTL,X.LTR;const Kd=s=>{let{state:t}=s,e=t.doc.lineAt(t.selection.main.from),i=Rr(s.state,e.from);return i.line?$d(s):i.block?Ud(s):!1};function Br(s,t){return({state:e,dispatch:i})=>{if(e.readOnly)return!1;let n=s(t,e);return n?(i(e.update(n)),!0):!1}}const $d=Br(Yd,0),jd=Br(kh,0),Ud=Br((s,t)=>kh(s,t,Jd(t)),0);function Rr(s,t){let e=s.languageDataAt("commentTokens",t);return e.length?e[0]:{}}const ci=50;function Gd(s,{open:t,close:e},i,n){let r=s.sliceDoc(i-ci,i),o=s.sliceDoc(n,n+ci),l=/\s*$/.exec(r)[0].length,a=/^\s*/.exec(o)[0].length,c=r.length-l;if(r.slice(c-t.length,c)==t&&o.slice(a,a+e.length)==e)return{open:{pos:i-l,margin:l&&1},close:{pos:n+a,margin:a&&1}};let h,f;n-i<=2*ci?h=f=s.sliceDoc(i,n):(h=s.sliceDoc(i,i+ci),f=s.sliceDoc(n-ci,n));let u=/^\s*/.exec(h)[0].length,d=/\s*$/.exec(f)[0].length,p=f.length-d-e.length;return h.slice(u,u+t.length)==t&&f.slice(p,p+e.length)==e?{open:{pos:i+u+t.length,margin:/\s/.test(h.charAt(u+t.length))?1:0},close:{pos:n-d-e.length,margin:/\s/.test(f.charAt(p-1))?1:0}}:null}function Jd(s){let t=[];for(let e of s.selection.ranges){let i=s.doc.lineAt(e.from),n=e.to<=i.to?i:s.doc.lineAt(e.to),r=t.length-1;r>=0&&t[r].to>i.from?t[r].to=n.to:t.push({from:i.from+/^\s*/.exec(i.text)[0].length,to:n.to})}return t}function kh(s,t,e=t.selection.ranges){let i=e.map(r=>Rr(t,r.from).block);if(!i.every(r=>r))return null;let n=e.map((r,o)=>Gd(t,i[o],r.from,r.to));if(s!=2&&!n.every(r=>r))return{changes:t.changes(e.map((r,o)=>n[o]?[]:[{from:r.from,insert:i[o].open+" "},{from:r.to,insert:" "+i[o].close}]))};if(s!=1&&n.some(r=>r)){let r=[];for(let o=0,l;on&&(r==o||o>f.from)){n=f.from;let u=/^\s*/.exec(f.text)[0].length,d=u==f.length,p=f.text.slice(u,u+c.length)==c?u:-1;ur.comment<0&&(!r.empty||r.single))){let r=[];for(let{line:l,token:a,indent:c,empty:h,single:f}of i)(f||!h)&&r.push({from:l.from+c,insert:a+" "});let o=t.changes(r);return{changes:o,selection:t.selection.map(o,1)}}else if(s!=1&&i.some(r=>r.comment>=0)){let r=[];for(let{line:o,comment:l,token:a}of i)if(l>=0){let c=o.from+l,h=c+a.length;o.text[h-o.from]==" "&&h++,r.push({from:c,to:h})}return{changes:r}}return null}const sr=se.define(),Xd=se.define(),_d=T.define(),vh=T.define({combine(s){return Le(s,{minDepth:100,newGroupDelay:500,joinToEvent:(t,e)=>e},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,e)=>(i,n)=>t(i,n)||e(i,n)})}}),Ch=yt.define({create(){return _t.empty},update(s,t){let e=t.state.facet(vh),i=t.annotation(sr);if(i){let a=kt.fromTransaction(t,i.selection),c=i.side,h=c==0?s.undone:s.done;return a?h=Tn(h,h.length,e.minDepth,a):h=Dh(h,t.startState.selection),new _t(c==0?i.rest:h,c==0?h:i.rest)}let n=t.annotation(Xd);if((n=="full"||n=="before")&&(s=s.isolate()),t.annotation(Z.addToHistory)===!1)return t.changes.empty?s:s.addMapping(t.changes.desc);let r=kt.fromTransaction(t),o=t.annotation(Z.time),l=t.annotation(Z.userEvent);return r?s=s.addChanges(r,o,l,e,t):t.selection&&(s=s.addSelection(t.startState.selection,o,l,e.newGroupDelay)),(n=="full"||n=="after")&&(s=s.isolate()),s},toJSON(s){return{done:s.done.map(t=>t.toJSON()),undone:s.undone.map(t=>t.toJSON())}},fromJSON(s){return new _t(s.done.map(kt.fromJSON),s.undone.map(kt.fromJSON))}});function Fm(s={}){return[Ch,vh.of(s),O.domEventHandlers({beforeinput(t,e){let i=t.inputType=="historyUndo"?Ah:t.inputType=="historyRedo"?rr:null;return i?(t.preventDefault(),i(e)):!1}})]}function $n(s,t){return function({state:e,dispatch:i}){if(!t&&e.readOnly)return!1;let n=e.field(Ch,!1);if(!n)return!1;let r=n.pop(s,e,t);return r?(i(r),!0):!1}}const Ah=$n(0,!1),rr=$n(1,!1),Qd=$n(0,!0),Zd=$n(1,!0);class kt{constructor(t,e,i,n,r){this.changes=t,this.effects=e,this.mapped=i,this.startSelection=n,this.selectionsAfter=r}setSelAfter(t){return new kt(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,e,i;return{changes:(t=this.changes)===null||t===void 0?void 0:t.toJSON(),mapped:(e=this.mapped)===null||e===void 0?void 0:e.toJSON(),startSelection:(i=this.startSelection)===null||i===void 0?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map(n=>n.toJSON())}}static fromJSON(t){return new kt(t.changes&&et.fromJSON(t.changes),[],t.mapped&&Qt.fromJSON(t.mapped),t.startSelection&&b.fromJSON(t.startSelection),t.selectionsAfter.map(b.fromJSON))}static fromTransaction(t,e){let i=It;for(let n of t.startState.facet(_d)){let r=n(t);r.length&&(i=i.concat(r))}return!i.length&&t.changes.empty?null:new kt(t.changes.invert(t.startState.doc),i,void 0,e||t.startState.selection,It)}static selection(t){return new kt(void 0,It,void 0,void 0,t)}}function Tn(s,t,e,i){let n=t+1>e+20?t-e-1:0,r=s.slice(n,t);return r.push(i),r}function tp(s,t){let e=[],i=!1;return s.iterChangedRanges((n,r)=>e.push(n,r)),t.iterChangedRanges((n,r,o,l)=>{for(let a=0;a=c&&o<=h&&(i=!0)}}),i}function ep(s,t){return s.ranges.length==t.ranges.length&&s.ranges.filter((e,i)=>e.empty!=t.ranges[i].empty).length===0}function Mh(s,t){return s.length?t.length?s.concat(t):s:t}const It=[],ip=200;function Dh(s,t){if(s.length){let e=s[s.length-1],i=e.selectionsAfter.slice(Math.max(0,e.selectionsAfter.length-ip));return i.length&&i[i.length-1].eq(t)?s:(i.push(t),Tn(s,s.length-1,1e9,e.setSelAfter(i)))}else return[kt.selection([t])]}function np(s){let t=s[s.length-1],e=s.slice();return e[s.length-1]=t.setSelAfter(t.selectionsAfter.slice(0,t.selectionsAfter.length-1)),e}function cs(s,t){if(!s.length)return s;let e=s.length,i=It;for(;e;){let n=sp(s[e-1],t,i);if(n.changes&&!n.changes.empty||n.effects.length){let r=s.slice(0,e);return r[e-1]=n,r}else t=n.mapped,e--,i=n.selectionsAfter}return i.length?[kt.selection(i)]:It}function sp(s,t,e){let i=Mh(s.selectionsAfter.length?s.selectionsAfter.map(l=>l.map(t)):It,e);if(!s.changes)return kt.selection(i);let n=s.changes.map(t),r=t.mapDesc(s.changes,!0),o=s.mapped?s.mapped.composeDesc(r):r;return new kt(n,N.mapEffects(s.effects,t),o,s.startSelection.map(r),i)}const rp=/^(input\.type|delete)($|\.)/;class _t{constructor(t,e,i=0,n=void 0){this.done=t,this.undone=e,this.prevTime=i,this.prevUserEvent=n}isolate(){return this.prevTime?new _t(this.done,this.undone):this}addChanges(t,e,i,n,r){let o=this.done,l=o[o.length-1];return l&&l.changes&&!l.changes.empty&&t.changes&&(!i||rp.test(i))&&(!l.selectionsAfter.length&&e-this.prevTime0&&e-this.prevTimee.empty?s.moveByChar(e,t):jn(e,t))}function dt(s){return s.textDirectionAt(s.state.selection.main.head)==X.LTR}const Th=s=>Oh(s,!dt(s)),Ph=s=>Oh(s,dt(s));function Bh(s,t){return zt(s,e=>e.empty?s.moveByGroup(e,t):jn(e,t))}const op=s=>Bh(s,!dt(s)),lp=s=>Bh(s,dt(s));function ap(s,t,e){if(t.type.prop(e))return!0;let i=t.to-t.from;return i&&(i>2||/[^\s,.;:]/.test(s.sliceDoc(t.from,t.to)))||t.firstChild}function Un(s,t,e){let i=mt(s).resolveInner(t.head),n=e?L.closedBy:L.openedBy;for(let a=t.head;;){let c=e?i.childAfter(a):i.childBefore(a);if(!c)break;ap(s,c,n)?i=c:a=e?c.to:c.from}let r=i.type.prop(n),o,l;return r&&(o=e?Xt(s,i.from,1):Xt(s,i.to,-1))&&o.matched?l=e?o.end.to:o.end.from:l=e?i.to:i.from,b.cursor(l,e?-1:1)}const hp=s=>zt(s,t=>Un(s.state,t,!dt(s))),cp=s=>zt(s,t=>Un(s.state,t,dt(s)));function Rh(s,t){return zt(s,e=>{if(!e.empty)return jn(e,t);let i=s.moveVertically(e,t);return i.head!=e.head?i:s.moveToLineBoundary(e,t)})}const Lh=s=>Rh(s,!1),Eh=s=>Rh(s,!0);function Ih(s){let t=s.scrollDOM.clientHeighto.empty?s.moveVertically(o,t,e.height):jn(o,t));if(n.eq(i.selection))return!1;let r;if(e.selfScroll){let o=s.coordsAtPos(i.selection.main.head),l=s.scrollDOM.getBoundingClientRect(),a=l.top+e.marginTop,c=l.bottom-e.marginBottom;o&&o.top>a&&o.bottomNh(s,!1),or=s=>Nh(s,!0);function be(s,t,e){let i=s.lineBlockAt(t.head),n=s.moveToLineBoundary(t,e);if(n.head==t.head&&n.head!=(e?i.to:i.from)&&(n=s.moveToLineBoundary(t,e,!1)),!e&&n.head==i.from&&i.length){let r=/^\s*/.exec(s.state.sliceDoc(i.from,Math.min(i.from+100,i.to)))[0].length;r&&t.head!=i.from+r&&(n=b.cursor(i.from+r))}return n}const fp=s=>zt(s,t=>be(s,t,!0)),up=s=>zt(s,t=>be(s,t,!1)),dp=s=>zt(s,t=>be(s,t,!dt(s))),pp=s=>zt(s,t=>be(s,t,dt(s))),gp=s=>zt(s,t=>b.cursor(s.lineBlockAt(t.head).from,1)),mp=s=>zt(s,t=>b.cursor(s.lineBlockAt(t.head).to,-1));function yp(s,t,e){let i=!1,n=ii(s.selection,r=>{let o=Xt(s,r.head,-1)||Xt(s,r.head,1)||r.head>0&&Xt(s,r.head-1,1)||r.headyp(s,t);function Vt(s,t){let e=ii(s.state.selection,i=>{let n=t(i);return b.range(i.anchor,n.head,n.goalColumn,n.bidiLevel||void 0)});return e.eq(s.state.selection)?!1:(s.dispatch(Zt(s.state,e)),!0)}function Fh(s,t){return Vt(s,e=>s.moveByChar(e,t))}const Vh=s=>Fh(s,!dt(s)),Hh=s=>Fh(s,dt(s));function Wh(s,t){return Vt(s,e=>s.moveByGroup(e,t))}const xp=s=>Wh(s,!dt(s)),wp=s=>Wh(s,dt(s)),Sp=s=>Vt(s,t=>Un(s.state,t,!dt(s))),kp=s=>Vt(s,t=>Un(s.state,t,dt(s)));function zh(s,t){return Vt(s,e=>s.moveVertically(e,t))}const qh=s=>zh(s,!1),Kh=s=>zh(s,!0);function $h(s,t){return Vt(s,e=>s.moveVertically(e,t,Ih(s).height))}const hl=s=>$h(s,!1),cl=s=>$h(s,!0),vp=s=>Vt(s,t=>be(s,t,!0)),Cp=s=>Vt(s,t=>be(s,t,!1)),Ap=s=>Vt(s,t=>be(s,t,!dt(s))),Mp=s=>Vt(s,t=>be(s,t,dt(s))),Dp=s=>Vt(s,t=>b.cursor(s.lineBlockAt(t.head).from)),Op=s=>Vt(s,t=>b.cursor(s.lineBlockAt(t.head).to)),fl=({state:s,dispatch:t})=>(t(Zt(s,{anchor:0})),!0),ul=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.doc.length})),!0),dl=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.selection.main.anchor,head:0})),!0),pl=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.selection.main.anchor,head:s.doc.length})),!0),Tp=({state:s,dispatch:t})=>(t(s.update({selection:{anchor:0,head:s.doc.length},userEvent:"select"})),!0),Pp=({state:s,dispatch:t})=>{let e=Gn(s).map(({from:i,to:n})=>b.range(i,Math.min(n+1,s.doc.length)));return t(s.update({selection:b.create(e),userEvent:"select"})),!0},Bp=({state:s,dispatch:t})=>{let e=ii(s.selection,i=>{var n;let r=mt(s).resolveStack(i.from,1);for(let o=r;o;o=o.next){let{node:l}=o;if((l.from=i.to||l.to>i.to&&l.from<=i.from)&&(!((n=l.parent)===null||n===void 0)&&n.parent))return b.range(l.to,l.from)}return i});return t(Zt(s,e)),!0},Rp=({state:s,dispatch:t})=>{let e=s.selection,i=null;return e.ranges.length>1?i=b.create([e.main]):e.main.empty||(i=b.create([b.cursor(e.main.head)])),i?(t(Zt(s,i)),!0):!1};function Fi(s,t){if(s.state.readOnly)return!1;let e="delete.selection",{state:i}=s,n=i.changeByRange(r=>{let{from:o,to:l}=r;if(o==l){let a=t(r);ao&&(e="delete.forward",a=sn(s,a,!0)),o=Math.min(o,a),l=Math.max(l,a)}else o=sn(s,o,!1),l=sn(s,l,!0);return o==l?{range:r}:{changes:{from:o,to:l},range:b.cursor(o,on(s)))i.between(t,t,(n,r)=>{nt&&(t=e?r:n)});return t}const jh=(s,t,e)=>Fi(s,i=>{let n=i.from,{state:r}=s,o=r.doc.lineAt(n),l,a;if(e&&!t&&n>o.from&&njh(s,!1,!0),Uh=s=>jh(s,!0,!1),Gh=(s,t)=>Fi(s,e=>{let i=e.head,{state:n}=s,r=n.doc.lineAt(i),o=n.charCategorizer(i);for(let l=null;;){if(i==(t?r.to:r.from)){i==e.head&&r.number!=(t?n.doc.lines:1)&&(i+=t?1:-1);break}let a=ot(r.text,i-r.from,t)+r.from,c=r.text.slice(Math.min(i,a)-r.from,Math.max(i,a)-r.from),h=o(c);if(l!=null&&h!=l)break;(c!=" "||i!=e.head)&&(l=h),i=a}return i}),Jh=s=>Gh(s,!1),Lp=s=>Gh(s,!0),Ep=s=>Fi(s,t=>{let e=s.lineBlockAt(t.head).to;return t.headFi(s,t=>{let e=s.moveToLineBoundary(t,!1).head;return t.head>e?e:Math.max(0,t.head-1)}),Np=s=>Fi(s,t=>{let e=s.moveToLineBoundary(t,!0).head;return t.head{if(s.readOnly)return!1;let e=s.changeByRange(i=>({changes:{from:i.from,to:i.to,insert:V.of(["",""])},range:b.cursor(i.from)}));return t(s.update(e,{scrollIntoView:!0,userEvent:"input"})),!0},Vp=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let e=s.changeByRange(i=>{if(!i.empty||i.from==0||i.from==s.doc.length)return{range:i};let n=i.from,r=s.doc.lineAt(n),o=n==r.from?n-1:ot(r.text,n-r.from,!1)+r.from,l=n==r.to?n+1:ot(r.text,n-r.from,!0)+r.from;return{changes:{from:o,to:l,insert:s.doc.slice(n,l).append(s.doc.slice(o,n))},range:b.cursor(l)}});return e.changes.empty?!1:(t(s.update(e,{scrollIntoView:!0,userEvent:"move.character"})),!0)};function Gn(s){let t=[],e=-1;for(let i of s.selection.ranges){let n=s.doc.lineAt(i.from),r=s.doc.lineAt(i.to);if(!i.empty&&i.to==r.from&&(r=s.doc.lineAt(i.to-1)),e>=n.number){let o=t[t.length-1];o.to=r.to,o.ranges.push(i)}else t.push({from:n.from,to:r.to,ranges:[i]});e=r.number+1}return t}function Yh(s,t,e){if(s.readOnly)return!1;let i=[],n=[];for(let r of Gn(s)){if(e?r.to==s.doc.length:r.from==0)continue;let o=s.doc.lineAt(e?r.to+1:r.from-1),l=o.length+1;if(e){i.push({from:r.to,to:o.to},{from:r.from,insert:o.text+s.lineBreak});for(let a of r.ranges)n.push(b.range(Math.min(s.doc.length,a.anchor+l),Math.min(s.doc.length,a.head+l)))}else{i.push({from:o.from,to:r.from},{from:r.to,insert:s.lineBreak+o.text});for(let a of r.ranges)n.push(b.range(a.anchor-l,a.head-l))}}return i.length?(t(s.update({changes:i,scrollIntoView:!0,selection:b.create(n,s.selection.mainIndex),userEvent:"move.line"})),!0):!1}const Hp=({state:s,dispatch:t})=>Yh(s,t,!1),Wp=({state:s,dispatch:t})=>Yh(s,t,!0);function Xh(s,t,e){if(s.readOnly)return!1;let i=[];for(let n of Gn(s))e?i.push({from:n.from,insert:s.doc.slice(n.from,n.to)+s.lineBreak}):i.push({from:n.to,insert:s.lineBreak+s.doc.slice(n.from,n.to)});return t(s.update({changes:i,scrollIntoView:!0,userEvent:"input.copyline"})),!0}const zp=({state:s,dispatch:t})=>Xh(s,t,!1),qp=({state:s,dispatch:t})=>Xh(s,t,!0),Kp=s=>{if(s.state.readOnly)return!1;let{state:t}=s,e=t.changes(Gn(t).map(({from:n,to:r})=>(n>0?n--:r{let r;if(s.lineWrapping){let o=s.lineBlockAt(n.head),l=s.coordsAtPos(n.head,n.assoc||1);l&&(r=o.bottom+s.documentTop-l.bottom+s.defaultLineHeight/2)}return s.moveVertically(n,!0,r)}).map(e);return s.dispatch({changes:e,selection:i,scrollIntoView:!0,userEvent:"delete.line"}),!0};function $p(s,t){if(/\(\)|\[\]|\{\}/.test(s.sliceDoc(t-1,t+1)))return{from:t,to:t};let e=mt(s).resolveInner(t),i=e.childBefore(t),n=e.childAfter(t),r;return i&&n&&i.to<=t&&n.from>=t&&(r=i.type.prop(L.closedBy))&&r.indexOf(n.name)>-1&&s.doc.lineAt(i.to).from==s.doc.lineAt(n.from).from&&!/\S/.test(s.sliceDoc(i.to,n.from))?{from:i.to,to:n.from}:null}const jp=_h(!1),Up=_h(!0);function _h(s){return({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=t.changeByRange(n=>{let{from:r,to:o}=n,l=t.doc.lineAt(r),a=!s&&r==o&&$p(t,r);s&&(r=o=(o<=l.to?l:t.doc.lineAt(o)).to);let c=new qn(t,{simulateBreak:r,simulateDoubleBreak:!!a}),h=ah(c,r);for(h==null&&(h=ei(/^\s*/.exec(t.doc.lineAt(r).text)[0],t.tabSize));ol.from&&r{let n=[];for(let o=i.from;o<=i.to;){let l=s.doc.lineAt(o);l.number>e&&(i.empty||i.to>l.from)&&(t(l,n,i),e=l.number),o=l.to+1}let r=s.changes(n);return{changes:n,range:b.range(r.mapPos(i.anchor,1),r.mapPos(i.head,1))}})}const Gp=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let e=Object.create(null),i=new qn(s,{overrideIndentation:r=>{let o=e[r];return o??-1}}),n=Lr(s,(r,o,l)=>{let a=ah(i,r.from);if(a==null)return;/\S/.test(r.text)||(a=0);let c=/^\s*/.exec(r.text)[0],h=On(s,a);(c!=h||l.froms.readOnly?!1:(t(s.update(Lr(s,(e,i)=>{i.push({from:e.from,insert:s.facet(zn)})}),{userEvent:"input.indent"})),!0),Zh=({state:s,dispatch:t})=>s.readOnly?!1:(t(s.update(Lr(s,(e,i)=>{let n=/^\s*/.exec(e.text)[0];if(!n)return;let r=ei(n,s.tabSize),o=0,l=On(s,Math.max(0,r-Re(s)));for(;o(s.setTabFocusMode(),!0),Yp=[{key:"Ctrl-b",run:Th,shift:Vh,preventDefault:!0},{key:"Ctrl-f",run:Ph,shift:Hh},{key:"Ctrl-p",run:Lh,shift:qh},{key:"Ctrl-n",run:Eh,shift:Kh},{key:"Ctrl-a",run:gp,shift:Dp},{key:"Ctrl-e",run:mp,shift:Op},{key:"Ctrl-d",run:Uh},{key:"Ctrl-h",run:lr},{key:"Ctrl-k",run:Ep},{key:"Ctrl-Alt-h",run:Jh},{key:"Ctrl-o",run:Fp},{key:"Ctrl-t",run:Vp},{key:"Ctrl-v",run:or}],Xp=[{key:"ArrowLeft",run:Th,shift:Vh,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:op,shift:xp,preventDefault:!0},{mac:"Cmd-ArrowLeft",run:dp,shift:Ap,preventDefault:!0},{key:"ArrowRight",run:Ph,shift:Hh,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:lp,shift:wp,preventDefault:!0},{mac:"Cmd-ArrowRight",run:pp,shift:Mp,preventDefault:!0},{key:"ArrowUp",run:Lh,shift:qh,preventDefault:!0},{mac:"Cmd-ArrowUp",run:fl,shift:dl},{mac:"Ctrl-ArrowUp",run:al,shift:hl},{key:"ArrowDown",run:Eh,shift:Kh,preventDefault:!0},{mac:"Cmd-ArrowDown",run:ul,shift:pl},{mac:"Ctrl-ArrowDown",run:or,shift:cl},{key:"PageUp",run:al,shift:hl},{key:"PageDown",run:or,shift:cl},{key:"Home",run:up,shift:Cp,preventDefault:!0},{key:"Mod-Home",run:fl,shift:dl},{key:"End",run:fp,shift:vp,preventDefault:!0},{key:"Mod-End",run:ul,shift:pl},{key:"Enter",run:jp},{key:"Mod-a",run:Tp},{key:"Backspace",run:lr,shift:lr},{key:"Delete",run:Uh},{key:"Mod-Backspace",mac:"Alt-Backspace",run:Jh},{key:"Mod-Delete",mac:"Alt-Delete",run:Lp},{mac:"Mod-Backspace",run:Ip},{mac:"Mod-Delete",run:Np}].concat(Yp.map(s=>({mac:s.key,run:s.run,shift:s.shift}))),Hm=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:hp,shift:Sp},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:cp,shift:kp},{key:"Alt-ArrowUp",run:Hp},{key:"Shift-Alt-ArrowUp",run:zp},{key:"Alt-ArrowDown",run:Wp},{key:"Shift-Alt-ArrowDown",run:qp},{key:"Escape",run:Rp},{key:"Mod-Enter",run:Up},{key:"Alt-l",mac:"Ctrl-l",run:Pp},{key:"Mod-i",run:Bp,preventDefault:!0},{key:"Mod-[",run:Zh},{key:"Mod-]",run:Qh},{key:"Mod-Alt-\\",run:Gp},{key:"Shift-Mod-k",run:Kp},{key:"Shift-Mod-\\",run:bp},{key:"Mod-/",run:Kd},{key:"Alt-A",run:jd},{key:"Ctrl-m",mac:"Shift-Alt-m",run:Jp}].concat(Xp),Wm={key:"Tab",run:Qh,shift:Zh};function lt(){var s=arguments[0];typeof s=="string"&&(s=document.createElement(s));var t=1,e=arguments[1];if(e&&typeof e=="object"&&e.nodeType==null&&!Array.isArray(e)){for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var n=e[i];typeof n=="string"?s.setAttribute(i,n):n!=null&&(s[i]=n)}t++}for(;ts.normalize("NFKD"):s=>s;class ti{constructor(t,e,i=0,n=t.length,r,o){this.test=o,this.value={from:0,to:0},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=t.iterRange(i,n),this.bufferStart=i,this.normalize=r?l=>r(gl(l)):gl,this.query=this.normalize(e)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return nt(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let t=this.peek();if(t<0)return this.done=!0,this;let e=cr(t),i=this.bufferStart+this.bufferPos;this.bufferPos+=Rt(t);let n=this.normalize(e);for(let r=0,o=i;;r++){let l=n.charCodeAt(r),a=this.match(l,o,this.bufferPos+this.bufferStart);if(r==n.length-1){if(a)return this.value=a,this;break}o==i&&rthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let t=this.matchPos-this.curLineStart;;){this.re.lastIndex=t;let e=this.matchPos<=this.to&&this.re.exec(this.curLine);if(e){let i=this.curLineStart+e.index,n=i+e[0].length;if(this.matchPos=Pn(this.text,n+(i==n?1:0)),i==this.curLineStart+this.curLine.length&&this.nextLine(),(ithis.value.to)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,match:e},this;t=this.matchPos-this.curLineStart}else if(this.curLineStart+this.curLine.length=i||n.to<=e){let l=new $e(e,t.sliceString(e,i));return fs.set(t,l),l}if(n.from==e&&n.to==i)return n;let{text:r,from:o}=n;return o>e&&(r=t.sliceString(e,o)+r,o=e),n.to=this.to?this.to:this.text.lineAt(t).to}next(){for(;;){let t=this.re.lastIndex=this.matchPos-this.flat.from,e=this.re.exec(this.flat.text);if(e&&!e[0]&&e.index==t&&(this.re.lastIndex=t+1,e=this.re.exec(this.flat.text)),e){let i=this.flat.from+e.index,n=i+e[0].length;if((this.flat.to>=this.to||e.index+e[0].length<=this.flat.text.length-10)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,match:e},this.matchPos=Pn(this.text,n+(i==n?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=$e.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+this.flat.text.length*2))}}}typeof Symbol<"u"&&(ic.prototype[Symbol.iterator]=nc.prototype[Symbol.iterator]=function(){return this});function _p(s){try{return new RegExp(s,Er),!0}catch{return!1}}function Pn(s,t){if(t>=s.length)return t;let e=s.lineAt(t),i;for(;t=56320&&i<57344;)t++;return t}function ar(s){let t=String(s.state.doc.lineAt(s.state.selection.main.head).number),e=lt("input",{class:"cm-textfield",name:"line",value:t}),i=lt("form",{class:"cm-gotoLine",onkeydown:r=>{r.keyCode==27?(r.preventDefault(),s.dispatch({effects:Bn.of(!1)}),s.focus()):r.keyCode==13&&(r.preventDefault(),n())},onsubmit:r=>{r.preventDefault(),n()}},lt("label",s.state.phrase("Go to line"),": ",e)," ",lt("button",{class:"cm-button",type:"submit"},s.state.phrase("go")));function n(){let r=/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(e.value);if(!r)return;let{state:o}=s,l=o.doc.lineAt(o.selection.main.head),[,a,c,h,f]=r,u=h?+h.slice(1):0,d=c?+c:l.number;if(c&&f){let m=d/100;a&&(m=m*(a=="-"?-1:1)+l.number/o.doc.lines),d=Math.round(o.doc.lines*m)}else c&&a&&(d=d*(a=="-"?-1:1)+l.number);let p=o.doc.line(Math.max(1,Math.min(o.doc.lines,d))),g=b.cursor(p.from+Math.max(0,Math.min(u,p.length)));s.dispatch({effects:[Bn.of(!1),O.scrollIntoView(g.from,{y:"center"})],selection:g}),s.focus()}return{dom:i}}const Bn=N.define(),ml=yt.define({create(){return!0},update(s,t){for(let e of t.effects)e.is(Bn)&&(s=e.value);return s},provide:s=>Cn.from(s,t=>t?ar:null)}),Qp=s=>{let t=vn(s,ar);if(!t){let e=[Bn.of(!0)];s.state.field(ml,!1)==null&&e.push(N.appendConfig.of([ml,Zp])),s.dispatch({effects:e}),t=vn(s,ar)}return t&&t.dom.querySelector("input").select(),!0},Zp=O.baseTheme({".cm-panel.cm-gotoLine":{padding:"2px 6px 4px","& label":{fontSize:"80%"}}}),tg={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},eg=T.define({combine(s){return Le(s,tg,{highlightWordAroundCursor:(t,e)=>t||e,minSelectionLength:Math.min,maxMatches:Math.min})}});function zm(s){return[og,rg]}const ig=P.mark({class:"cm-selectionMatch"}),ng=P.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function yl(s,t,e,i){return(e==0||s(t.sliceDoc(e-1,e))!=G.Word)&&(i==t.doc.length||s(t.sliceDoc(i,i+1))!=G.Word)}function sg(s,t,e,i){return s(t.sliceDoc(e,e+1))==G.Word&&s(t.sliceDoc(i-1,i))==G.Word}const rg=ut.fromClass(class{constructor(s){this.decorations=this.getDeco(s)}update(s){(s.selectionSet||s.docChanged||s.viewportChanged)&&(this.decorations=this.getDeco(s.view))}getDeco(s){let t=s.state.facet(eg),{state:e}=s,i=e.selection;if(i.ranges.length>1)return P.none;let n=i.main,r,o=null;if(n.empty){if(!t.highlightWordAroundCursor)return P.none;let a=e.wordAt(n.head);if(!a)return P.none;o=e.charCategorizer(n.head),r=e.sliceDoc(a.from,a.to)}else{let a=n.to-n.from;if(a200)return P.none;if(t.wholeWords){if(r=e.sliceDoc(n.from,n.to),o=e.charCategorizer(n.head),!(yl(o,e,n.from,n.to)&&sg(o,e,n.from,n.to)))return P.none}else if(r=e.sliceDoc(n.from,n.to),!r)return P.none}let l=[];for(let a of s.visibleRanges){let c=new ti(e.doc,r,a.from,a.to);for(;!c.next().done;){let{from:h,to:f}=c.value;if((!o||yl(o,e,h,f))&&(n.empty&&h<=n.from&&f>=n.to?l.push(ng.range(h,f)):(h>=n.to||f<=n.from)&&l.push(ig.range(h,f)),l.length>t.maxMatches))return P.none}}return P.set(l)}},{decorations:s=>s.decorations}),og=O.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}}),lg=({state:s,dispatch:t})=>{let{selection:e}=s,i=b.create(e.ranges.map(n=>s.wordAt(n.head)||b.cursor(n.head)),e.mainIndex);return i.eq(e)?!1:(t(s.update({selection:i})),!0)};function ag(s,t){let{main:e,ranges:i}=s.selection,n=s.wordAt(e.head),r=n&&n.from==e.from&&n.to==e.to;for(let o=!1,l=new ti(s.doc,t,i[i.length-1].to);;)if(l.next(),l.done){if(o)return null;l=new ti(s.doc,t,0,Math.max(0,i[i.length-1].from-1)),o=!0}else{if(o&&i.some(a=>a.from==l.value.from))continue;if(r){let a=s.wordAt(l.value.from);if(!a||a.from!=l.value.from||a.to!=l.value.to)continue}return l.value}}const hg=({state:s,dispatch:t})=>{let{ranges:e}=s.selection;if(e.some(r=>r.from===r.to))return lg({state:s,dispatch:t});let i=s.sliceDoc(e[0].from,e[0].to);if(s.selection.ranges.some(r=>s.sliceDoc(r.from,r.to)!=i))return!1;let n=ag(s,i);return n?(t(s.update({selection:s.selection.addRange(b.range(n.from,n.to),!1),effects:O.scrollIntoView(n.to)})),!0):!1},ni=T.define({combine(s){return Le(s,{top:!1,caseSensitive:!1,literal:!1,regexp:!1,wholeWord:!1,createPanel:t=>new wg(t),scrollToMatch:t=>O.scrollIntoView(t)})}});class sc{constructor(t){this.search=t.search,this.caseSensitive=!!t.caseSensitive,this.literal=!!t.literal,this.regexp=!!t.regexp,this.replace=t.replace||"",this.valid=!!this.search&&(!this.regexp||_p(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!t.wholeWord}unquote(t){return this.literal?t:t.replace(/\\([nrt\\])/g,(e,i)=>i=="n"?` +`:i=="r"?"\r":i=="t"?" ":"\\")}eq(t){return this.search==t.search&&this.replace==t.replace&&this.caseSensitive==t.caseSensitive&&this.regexp==t.regexp&&this.wholeWord==t.wholeWord}create(){return this.regexp?new dg(this):new fg(this)}getCursor(t,e=0,i){let n=t.doc?t:W.create({doc:t});return i==null&&(i=n.doc.length),this.regexp?Ve(this,n,e,i):Fe(this,n,e,i)}}class rc{constructor(t){this.spec=t}}function Fe(s,t,e,i){return new ti(t.doc,s.unquoted,e,i,s.caseSensitive?void 0:n=>n.toLowerCase(),s.wholeWord?cg(t.doc,t.charCategorizer(t.selection.main.head)):void 0)}function cg(s,t){return(e,i,n,r)=>((r>e||r+n.length=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let r=Fe(this.spec,t,Math.max(0,e-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,t.doc.length));for(;!r.next().done;)n(r.value.from,r.value.to)}}function Ve(s,t,e,i){return new ic(t.doc,s.search,{ignoreCase:!s.caseSensitive,test:s.wholeWord?ug(t.charCategorizer(t.selection.main.head)):void 0},e,i)}function Rn(s,t){return s.slice(ot(s,t,!1),t)}function Ln(s,t){return s.slice(t,ot(s,t))}function ug(s){return(t,e,i)=>!i[0].length||(s(Rn(i.input,i.index))!=G.Word||s(Ln(i.input,i.index))!=G.Word)&&(s(Ln(i.input,i.index+i[0].length))!=G.Word||s(Rn(i.input,i.index+i[0].length))!=G.Word)}class dg extends rc{nextMatch(t,e,i){let n=Ve(this.spec,t,i,t.doc.length).next();return n.done&&(n=Ve(this.spec,t,0,e).next()),n.done?null:n.value}prevMatchInRange(t,e,i){for(let n=1;;n++){let r=Math.max(e,i-n*1e4),o=Ve(this.spec,t,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==e||l.from>r+10))return l;if(r==e)return null}}prevMatch(t,e,i){return this.prevMatchInRange(t,0,e)||this.prevMatchInRange(t,i,t.doc.length)}getReplacement(t){return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g,(e,i)=>i=="$"?"$":i=="&"?t.match[0]:i!="0"&&+i=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let r=Ve(this.spec,t,Math.max(0,e-250),Math.min(i+250,t.doc.length));for(;!r.next().done;)n(r.value.from,r.value.to)}}const Ti=N.define(),Ir=N.define(),fe=yt.define({create(s){return new us(hr(s).create(),null)},update(s,t){for(let e of t.effects)e.is(Ti)?s=new us(e.value.create(),s.panel):e.is(Ir)&&(s=new us(s.query,e.value?Nr:null));return s},provide:s=>Cn.from(s,t=>t.panel)});class us{constructor(t,e){this.query=t,this.panel=e}}const pg=P.mark({class:"cm-searchMatch"}),gg=P.mark({class:"cm-searchMatch cm-searchMatch-selected"}),mg=ut.fromClass(class{constructor(s){this.view=s,this.decorations=this.highlight(s.state.field(fe))}update(s){let t=s.state.field(fe);(t!=s.startState.field(fe)||s.docChanged||s.selectionSet||s.viewportChanged)&&(this.decorations=this.highlight(t))}highlight({query:s,panel:t}){if(!t||!s.spec.valid)return P.none;let{view:e}=this,i=new De;for(let n=0,r=e.visibleRanges,o=r.length;nr[n+1].from-2*250;)a=r[++n].to;s.highlight(e.state,l,a,(c,h)=>{let f=e.state.selection.ranges.some(u=>u.from==c&&u.to==h);i.add(c,h,f?gg:pg)})}return i.finish()}},{decorations:s=>s.decorations});function Vi(s){return t=>{let e=t.state.field(fe,!1);return e&&e.query.spec.valid?s(t,e):ac(t)}}const En=Vi((s,{query:t})=>{let{to:e}=s.state.selection.main,i=t.nextMatch(s.state,e,e);if(!i)return!1;let n=b.single(i.from,i.to),r=s.state.facet(ni);return s.dispatch({selection:n,effects:[Fr(s,i),r.scrollToMatch(n.main,s)],userEvent:"select.search"}),lc(s),!0}),In=Vi((s,{query:t})=>{let{state:e}=s,{from:i}=e.selection.main,n=t.prevMatch(e,i,i);if(!n)return!1;let r=b.single(n.from,n.to),o=s.state.facet(ni);return s.dispatch({selection:r,effects:[Fr(s,n),o.scrollToMatch(r.main,s)],userEvent:"select.search"}),lc(s),!0}),yg=Vi((s,{query:t})=>{let e=t.matchAll(s.state,1e3);return!e||!e.length?!1:(s.dispatch({selection:b.create(e.map(i=>b.range(i.from,i.to))),userEvent:"select.search.matches"}),!0)}),bg=({state:s,dispatch:t})=>{let e=s.selection;if(e.ranges.length>1||e.main.empty)return!1;let{from:i,to:n}=e.main,r=[],o=0;for(let l=new ti(s.doc,s.sliceDoc(i,n));!l.next().done;){if(r.length>1e3)return!1;l.value.from==i&&(o=r.length),r.push(b.range(l.value.from,l.value.to))}return t(s.update({selection:b.create(r,o),userEvent:"select.search.matches"})),!0},bl=Vi((s,{query:t})=>{let{state:e}=s,{from:i,to:n}=e.selection.main;if(e.readOnly)return!1;let r=t.nextMatch(e,i,i);if(!r)return!1;let o=[],l,a,c=[];if(r.from==i&&r.to==n&&(a=e.toText(t.getReplacement(r)),o.push({from:r.from,to:r.to,insert:a}),r=t.nextMatch(e,r.from,r.to),c.push(O.announce.of(e.phrase("replaced match on line $",e.doc.lineAt(i).number)+"."))),r){let h=o.length==0||o[0].from>=r.to?0:r.to-r.from-a.length;l=b.single(r.from-h,r.to-h),c.push(Fr(s,r)),c.push(e.facet(ni).scrollToMatch(l.main,s))}return s.dispatch({changes:o,selection:l,effects:c,userEvent:"input.replace"}),!0}),xg=Vi((s,{query:t})=>{if(s.state.readOnly)return!1;let e=t.matchAll(s.state,1e9).map(n=>{let{from:r,to:o}=n;return{from:r,to:o,insert:t.getReplacement(n)}});if(!e.length)return!1;let i=s.state.phrase("replaced $ matches",e.length)+".";return s.dispatch({changes:e,effects:O.announce.of(i),userEvent:"input.replace.all"}),!0});function Nr(s){return s.state.facet(ni).createPanel(s)}function hr(s,t){var e,i,n,r,o;let l=s.selection.main,a=l.empty||l.to>l.from+100?"":s.sliceDoc(l.from,l.to);if(t&&!a)return t;let c=s.facet(ni);return new sc({search:((e=t==null?void 0:t.literal)!==null&&e!==void 0?e:c.literal)?a:a.replace(/\n/g,"\\n"),caseSensitive:(i=t==null?void 0:t.caseSensitive)!==null&&i!==void 0?i:c.caseSensitive,literal:(n=t==null?void 0:t.literal)!==null&&n!==void 0?n:c.literal,regexp:(r=t==null?void 0:t.regexp)!==null&&r!==void 0?r:c.regexp,wholeWord:(o=t==null?void 0:t.wholeWord)!==null&&o!==void 0?o:c.wholeWord})}function oc(s){let t=vn(s,Nr);return t&&t.dom.querySelector("[main-field]")}function lc(s){let t=oc(s);t&&t==s.root.activeElement&&t.select()}const ac=s=>{let t=s.state.field(fe,!1);if(t&&t.panel){let e=oc(s);if(e&&e!=s.root.activeElement){let i=hr(s.state,t.query.spec);i.valid&&s.dispatch({effects:Ti.of(i)}),e.focus(),e.select()}}else s.dispatch({effects:[Ir.of(!0),t?Ti.of(hr(s.state,t.query.spec)):N.appendConfig.of(kg)]});return!0},hc=s=>{let t=s.state.field(fe,!1);if(!t||!t.panel)return!1;let e=vn(s,Nr);return e&&e.dom.contains(s.root.activeElement)&&s.focus(),s.dispatch({effects:Ir.of(!1)}),!0},qm=[{key:"Mod-f",run:ac,scope:"editor search-panel"},{key:"F3",run:En,shift:In,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:En,shift:In,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:hc,scope:"editor search-panel"},{key:"Mod-Shift-l",run:bg},{key:"Mod-Alt-g",run:Qp},{key:"Mod-d",run:hg,preventDefault:!0}];class wg{constructor(t){this.view=t;let e=this.query=t.state.field(fe).query.spec;this.commit=this.commit.bind(this),this.searchField=lt("input",{value:e.search,placeholder:Ct(t,"Find"),"aria-label":Ct(t,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=lt("input",{value:e.replace,placeholder:Ct(t,"Replace"),"aria-label":Ct(t,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=lt("input",{type:"checkbox",name:"case",form:"",checked:e.caseSensitive,onchange:this.commit}),this.reField=lt("input",{type:"checkbox",name:"re",form:"",checked:e.regexp,onchange:this.commit}),this.wordField=lt("input",{type:"checkbox",name:"word",form:"",checked:e.wholeWord,onchange:this.commit});function i(n,r,o){return lt("button",{class:"cm-button",name:n,onclick:r,type:"button"},o)}this.dom=lt("div",{onkeydown:n=>this.keydown(n),class:"cm-search"},[this.searchField,i("next",()=>En(t),[Ct(t,"next")]),i("prev",()=>In(t),[Ct(t,"previous")]),i("select",()=>yg(t),[Ct(t,"all")]),lt("label",null,[this.caseField,Ct(t,"match case")]),lt("label",null,[this.reField,Ct(t,"regexp")]),lt("label",null,[this.wordField,Ct(t,"by word")]),...t.state.readOnly?[]:[lt("br"),this.replaceField,i("replace",()=>bl(t),[Ct(t,"replace")]),i("replaceAll",()=>xg(t),[Ct(t,"replace all")])],lt("button",{name:"close",onclick:()=>hc(t),"aria-label":Ct(t,"close"),type:"button"},["×"])])}commit(){let t=new sc({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});t.eq(this.query)||(this.query=t,this.view.dispatch({effects:Ti.of(t)}))}keydown(t){gu(this.view,t,"search-panel")?t.preventDefault():t.keyCode==13&&t.target==this.searchField?(t.preventDefault(),(t.shiftKey?In:En)(this.view)):t.keyCode==13&&t.target==this.replaceField&&(t.preventDefault(),bl(this.view))}update(t){for(let e of t.transactions)for(let i of e.effects)i.is(Ti)&&!i.value.eq(this.query)&&this.setQuery(i.value)}setQuery(t){this.query=t,this.searchField.value=t.search,this.replaceField.value=t.replace,this.caseField.checked=t.caseSensitive,this.reField.checked=t.regexp,this.wordField.checked=t.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(ni).top}}function Ct(s,t){return s.state.phrase(t)}const rn=30,on=/[\s\.,:;?!]/;function Fr(s,{from:t,to:e}){let i=s.state.doc.lineAt(t),n=s.state.doc.lineAt(e).to,r=Math.max(i.from,t-rn),o=Math.min(n,e+rn),l=s.state.sliceDoc(r,o);if(r!=i.from){for(let a=0;al.length-rn;a--)if(!on.test(l[a-1])&&on.test(l[a])){l=l.slice(0,a);break}}return O.announce.of(`${s.state.phrase("current match")}. ${l} ${s.state.phrase("on line")} ${i.number}.`)}const Sg=O.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),kg=[fe,ye.low(mg),Sg];class cc{constructor(t,e,i,n){this.state=t,this.pos=e,this.explicit=i,this.view=n,this.abortListeners=[],this.abortOnDocChange=!1}tokenBefore(t){let e=mt(this.state).resolveInner(this.pos,-1);for(;e&&t.indexOf(e.name)<0;)e=e.parent;return e?{from:e.from,to:this.pos,text:this.state.sliceDoc(e.from,this.pos),type:e.type}:null}matchBefore(t){let e=this.state.doc.lineAt(this.pos),i=Math.max(e.from,this.pos-250),n=e.text.slice(i-e.from,this.pos-e.from),r=n.search(fc(t,!1));return r<0?null:{from:i+r,to:this.pos,text:n.slice(r)}}get aborted(){return this.abortListeners==null}addEventListener(t,e,i){t=="abort"&&this.abortListeners&&(this.abortListeners.push(e),i&&i.onDocChange&&(this.abortOnDocChange=!0))}}function xl(s){let t=Object.keys(s).join(""),e=/\w/.test(t);return e&&(t=t.replace(/\w/g,"")),`[${e?"\\w":""}${t.replace(/[^\w\s]/g,"\\$&")}]`}function vg(s){let t=Object.create(null),e=Object.create(null);for(let{label:n}of s){t[n[0]]=!0;for(let r=1;rtypeof n=="string"?{label:n}:n),[e,i]=t.every(n=>/^\w+$/.test(n.label))?[/\w*$/,/\w+$/]:vg(t);return n=>{let r=n.matchBefore(i);return r||n.explicit?{from:r?r.from:n.pos,options:t,validFor:e}:null}}function Km(s,t){return e=>{for(let i=mt(e.state).resolveInner(e.pos,-1);i;i=i.parent){if(s.indexOf(i.name)>-1)return null;if(i.type.isTop)break}return t(e)}}class wl{constructor(t,e,i,n){this.completion=t,this.source=e,this.match=i,this.score=n}}function ue(s){return s.selection.main.from}function fc(s,t){var e;let{source:i}=s,n=t&&i[0]!="^",r=i[i.length-1]!="$";return!n&&!r?s:new RegExp(`${n?"^":""}(?:${i})${r?"$":""}`,(e=s.flags)!==null&&e!==void 0?e:s.ignoreCase?"i":"")}const Vr=se.define();function Ag(s,t,e,i){let{main:n}=s.selection,r=e-n.from,o=i-n.from;return Object.assign(Object.assign({},s.changeByRange(l=>l!=n&&e!=i&&s.sliceDoc(l.from+r,l.from+o)!=s.sliceDoc(e,i)?{range:l}:{changes:{from:l.from+r,to:i==n.from?l.to:l.from+o,insert:t},range:b.cursor(l.from+r+t.length)})),{scrollIntoView:!0,userEvent:"input.complete"})}const Sl=new WeakMap;function Mg(s){if(!Array.isArray(s))return s;let t=Sl.get(s);return t||Sl.set(s,t=Cg(s)),t}const Nn=N.define(),Pi=N.define();class Dg{constructor(t){this.pattern=t,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let e=0;e=48&&w<=57||w>=97&&w<=122?2:w>=65&&w<=90?1:0:(M=cr(w))!=M.toLowerCase()?1:M!=M.toUpperCase()?2:0;(!x||A==1&&m||S==0&&A!=0)&&(e[f]==w||i[f]==w&&(u=!0)?o[f++]=x:o.length&&(y=!1)),S=A,x+=Rt(w)}return f==a&&o[0]==0&&y?this.result(-100+(u?-200:0),o,t):d==a&&p==0?this.ret(-200-t.length+(g==t.length?0:-100),[0,g]):l>-1?this.ret(-700-t.length,[l,l+this.pattern.length]):d==a?this.ret(-900-t.length,[p,g]):f==a?this.result(-100+(u?-200:0)+-700+(y?0:-1100),o,t):e.length==2?null:this.result((n[0]?-700:0)+-200+-1100,n,t)}result(t,e,i){let n=[],r=0;for(let o of e){let l=o+(this.astral?Rt(nt(i,o)):1);r&&n[r-1]==o?n[r-1]=l:(n[r++]=o,n[r++]=l)}return this.ret(t-i.length,n)}}class Og{constructor(t){this.pattern=t,this.matched=[],this.score=0,this.folded=t.toLowerCase()}match(t){if(t.length!1,activateOnTypingDelay:100,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,tooltipClass:()=>"",optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],positionInfo:Tg,filterStrict:!1,compareCompletions:(t,e)=>t.label.localeCompare(e.label),interactionDelay:75,updateSyncTime:100},{defaultKeymap:(t,e)=>t&&e,closeOnBlur:(t,e)=>t&&e,icons:(t,e)=>t&&e,tooltipClass:(t,e)=>i=>kl(t(i),e(i)),optionClass:(t,e)=>i=>kl(t(i),e(i)),addToOptions:(t,e)=>t.concat(e),filterStrict:(t,e)=>t||e})}});function kl(s,t){return s?t?s+" "+t:s:t}function Tg(s,t,e,i,n,r){let o=s.textDirection==X.RTL,l=o,a=!1,c="top",h,f,u=t.left-n.left,d=n.right-t.right,p=i.right-i.left,g=i.bottom-i.top;if(l&&u=g||x>t.top?h=e.bottom-t.top:(c="bottom",h=t.bottom-e.top)}let m=(t.bottom-t.top)/r.offsetHeight,y=(t.right-t.left)/r.offsetWidth;return{style:`${c}: ${h/m}px; max-width: ${f/y}px`,class:"cm-completionInfo-"+(a?o?"left-narrow":"right-narrow":l?"left":"right")}}function Pg(s){let t=s.addToOptions.slice();return s.icons&&t.push({render(e){let i=document.createElement("div");return i.classList.add("cm-completionIcon"),e.type&&i.classList.add(...e.type.split(/\s+/g).map(n=>"cm-completionIcon-"+n)),i.setAttribute("aria-hidden","true"),i},position:20}),t.push({render(e,i,n,r){let o=document.createElement("span");o.className="cm-completionLabel";let l=e.displayLabel||e.label,a=0;for(let c=0;ca&&o.appendChild(document.createTextNode(l.slice(a,h)));let u=o.appendChild(document.createElement("span"));u.appendChild(document.createTextNode(l.slice(h,f))),u.className="cm-completionMatchedText",a=f}return ae.position-i.position).map(e=>e.render)}function ds(s,t,e){if(s<=e)return{from:0,to:s};if(t<0&&(t=0),t<=s>>1){let n=Math.floor(t/e);return{from:n*e,to:(n+1)*e}}let i=Math.floor((s-t)/e);return{from:s-(i+1)*e,to:s-i*e}}class Bg{constructor(t,e,i){this.view=t,this.stateField=e,this.applyCompletion=i,this.info=null,this.infoDestroy=null,this.placeInfoReq={read:()=>this.measureInfo(),write:a=>this.placeInfo(a),key:this},this.space=null,this.currentClass="";let n=t.state.field(e),{options:r,selected:o}=n.open,l=t.state.facet(rt);this.optionContent=Pg(l),this.optionClass=l.optionClass,this.tooltipClass=l.tooltipClass,this.range=ds(r.length,o,l.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.updateTooltipClass(t.state),this.dom.addEventListener("mousedown",a=>{let{options:c}=t.state.field(e).open;for(let h=a.target,f;h&&h!=this.dom;h=h.parentNode)if(h.nodeName=="LI"&&(f=/-(\d+)$/.exec(h.id))&&+f[1]{let c=t.state.field(this.stateField,!1);c&&c.tooltip&&t.state.facet(rt).closeOnBlur&&a.relatedTarget!=t.contentDOM&&t.dispatch({effects:Pi.of(null)})}),this.showOptions(r,n.id)}mount(){this.updateSel()}showOptions(t,e){this.list&&this.list.remove(),this.list=this.dom.appendChild(this.createListBox(t,e,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfoReq)})}update(t){var e;let i=t.state.field(this.stateField),n=t.startState.field(this.stateField);if(this.updateTooltipClass(t.state),i!=n){let{options:r,selected:o,disabled:l}=i.open;(!n.open||n.open.options!=r)&&(this.range=ds(r.length,o,t.state.facet(rt).maxRenderedOptions),this.showOptions(r,i.id)),this.updateSel(),l!=((e=n.open)===null||e===void 0?void 0:e.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!l)}}updateTooltipClass(t){let e=this.tooltipClass(t);if(e!=this.currentClass){for(let i of this.currentClass.split(" "))i&&this.dom.classList.remove(i);for(let i of e.split(" "))i&&this.dom.classList.add(i);this.currentClass=e}}positioned(t){this.space=t,this.info&&this.view.requestMeasure(this.placeInfoReq)}updateSel(){let t=this.view.state.field(this.stateField),e=t.open;if((e.selected>-1&&e.selected=this.range.to)&&(this.range=ds(e.options.length,e.selected,this.view.state.facet(rt).maxRenderedOptions),this.showOptions(e.options,t.id)),this.updateSelectedOption(e.selected)){this.destroyInfo();let{completion:i}=e.options[e.selected],{info:n}=i;if(!n)return;let r=typeof n=="string"?document.createTextNode(n):n(i);if(!r)return;"then"in r?r.then(o=>{o&&this.view.state.field(this.stateField,!1)==t&&this.addInfoPane(o,i)}).catch(o=>Dt(this.view.state,o,"completion info")):this.addInfoPane(r,i)}}addInfoPane(t,e){this.destroyInfo();let i=this.info=document.createElement("div");if(i.className="cm-tooltip cm-completionInfo",t.nodeType!=null)i.appendChild(t),this.infoDestroy=null;else{let{dom:n,destroy:r}=t;i.appendChild(n),this.infoDestroy=r||null}this.dom.appendChild(i),this.view.requestMeasure(this.placeInfoReq)}updateSelectedOption(t){let e=null;for(let i=this.list.firstChild,n=this.range.from;i;i=i.nextSibling,n++)i.nodeName!="LI"||!i.id?n--:n==t?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),e=i):i.hasAttribute("aria-selected")&&i.removeAttribute("aria-selected");return e&&Lg(this.list,e),e}measureInfo(){let t=this.dom.querySelector("[aria-selected]");if(!t||!this.info)return null;let e=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),n=t.getBoundingClientRect(),r=this.space;if(!r){let o=this.dom.ownerDocument.defaultView||window;r={left:0,top:0,right:o.innerWidth,bottom:o.innerHeight}}return n.top>Math.min(r.bottom,e.bottom)-10||n.bottomi.from||i.from==0))if(r=u,typeof c!="string"&&c.header)n.appendChild(c.header(c));else{let d=n.appendChild(document.createElement("completion-section"));d.textContent=u}}const h=n.appendChild(document.createElement("li"));h.id=e+"-"+o,h.setAttribute("role","option");let f=this.optionClass(l);f&&(h.className=f);for(let u of this.optionContent){let d=u(l,this.view.state,this.view,a);d&&h.appendChild(d)}}return i.from&&n.classList.add("cm-completionListIncompleteTop"),i.tonew Bg(e,s,t)}function Lg(s,t){let e=s.getBoundingClientRect(),i=t.getBoundingClientRect(),n=e.height/s.offsetHeight;i.tope.bottom&&(s.scrollTop+=(i.bottom-e.bottom)/n)}function vl(s){return(s.boost||0)*100+(s.apply?10:0)+(s.info?5:0)+(s.type?1:0)}function Eg(s,t){let e=[],i=null,n=c=>{e.push(c);let{section:h}=c.completion;if(h){i||(i=[]);let f=typeof h=="string"?h:h.name;i.some(u=>u.name==f)||i.push(typeof h=="string"?{name:f}:h)}},r=t.facet(rt);for(let c of s)if(c.hasResult()){let h=c.result.getMatch;if(c.result.filter===!1)for(let f of c.result.options)n(new wl(f,c.source,h?h(f):[],1e9-e.length));else{let f=t.sliceDoc(c.from,c.to),u,d=r.filterStrict?new Og(f):new Dg(f);for(let p of c.result.options)if(u=d.match(p.label)){let g=p.displayLabel?h?h(p,u.matched):[]:u.matched;n(new wl(p,c.source,g,u.score+(p.boost||0)))}}}if(i){let c=Object.create(null),h=0,f=(u,d)=>{var p,g;return((p=u.rank)!==null&&p!==void 0?p:1e9)-((g=d.rank)!==null&&g!==void 0?g:1e9)||(u.namef.score-h.score||a(h.completion,f.completion))){let h=c.completion;!l||l.label!=h.label||l.detail!=h.detail||l.type!=null&&h.type!=null&&l.type!=h.type||l.apply!=h.apply||l.boost!=h.boost?o.push(c):vl(c.completion)>vl(l)&&(o[o.length-1]=c),l=c.completion}return o}class He{constructor(t,e,i,n,r,o){this.options=t,this.attrs=e,this.tooltip=i,this.timestamp=n,this.selected=r,this.disabled=o}setSelected(t,e){return t==this.selected||t>=this.options.length?this:new He(this.options,Cl(e,t),this.tooltip,this.timestamp,t,this.disabled)}static build(t,e,i,n,r){let o=Eg(t,e);if(!o.length)return n&&t.some(a=>a.state==1)?new He(n.options,n.attrs,n.tooltip,n.timestamp,n.selected,!0):null;let l=e.facet(rt).selectOnOpen?0:-1;if(n&&n.selected!=l&&n.selected!=-1){let a=n.options[n.selected].completion;for(let c=0;cc.hasResult()?Math.min(a,c.from):a,1e8),create:Wg,above:r.aboveCursor},n?n.timestamp:Date.now(),l,!1)}map(t){return new He(this.options,this.attrs,Object.assign(Object.assign({},this.tooltip),{pos:t.mapPos(this.tooltip.pos)}),this.timestamp,this.selected,this.disabled)}}class Fn{constructor(t,e,i){this.active=t,this.id=e,this.open=i}static start(){return new Fn(Vg,"cm-ac-"+Math.floor(Math.random()*2e6).toString(36),null)}update(t){let{state:e}=t,i=e.facet(rt),r=(i.override||e.languageDataAt("autocomplete",ue(e)).map(Mg)).map(l=>(this.active.find(c=>c.source==l)||new Mt(l,this.active.some(c=>c.state!=0)?1:0)).update(t,i));r.length==this.active.length&&r.every((l,a)=>l==this.active[a])&&(r=this.active);let o=this.open;o&&t.docChanged&&(o=o.map(t.changes)),t.selection||r.some(l=>l.hasResult()&&t.changes.touchesRange(l.from,l.to))||!Ig(r,this.active)?o=He.build(r,e,this.id,o,i):o&&o.disabled&&!r.some(l=>l.state==1)&&(o=null),!o&&r.every(l=>l.state!=1)&&r.some(l=>l.hasResult())&&(r=r.map(l=>l.hasResult()?new Mt(l.source,0):l));for(let l of t.effects)l.is(pc)&&(o=o&&o.setSelected(l.value,this.id));return r==this.active&&o==this.open?this:new Fn(r,this.id,o)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:this.active.length?Ng:Fg}}function Ig(s,t){if(s==t)return!0;for(let e=0,i=0;;){for(;e-1&&(e["aria-activedescendant"]=s+"-"+t),e}const Vg=[];function uc(s,t){if(s.isUserEvent("input.complete")){let i=s.annotation(Vr);if(i&&t.activateOnCompletion(i))return 12}let e=s.isUserEvent("input.type");return e&&t.activateOnTyping?5:e?1:s.isUserEvent("delete.backward")?2:s.selection?8:s.docChanged?16:0}class Mt{constructor(t,e,i=-1){this.source=t,this.state=e,this.explicitPos=i}hasResult(){return!1}update(t,e){let i=uc(t,e),n=this;(i&8||i&16&&this.touches(t))&&(n=new Mt(n.source,0)),i&4&&n.state==0&&(n=new Mt(this.source,1)),n=n.updateFor(t,i);for(let r of t.effects)if(r.is(Nn))n=new Mt(n.source,1,r.value?ue(t.state):-1);else if(r.is(Pi))n=new Mt(n.source,0);else if(r.is(dc))for(let o of r.value)o.source==n.source&&(n=o);return n}updateFor(t,e){return this.map(t.changes)}map(t){return t.empty||this.explicitPos<0?this:new Mt(this.source,this.state,t.mapPos(this.explicitPos))}touches(t){return t.changes.touchesRange(ue(t.state))}}class je extends Mt{constructor(t,e,i,n,r){super(t,2,e),this.result=i,this.from=n,this.to=r}hasResult(){return!0}updateFor(t,e){var i;if(!(e&3))return this.map(t.changes);let n=this.result;n.map&&!t.changes.empty&&(n=n.map(n,t.changes));let r=t.changes.mapPos(this.from),o=t.changes.mapPos(this.to,1),l=ue(t.state);if((this.explicitPos<0?l<=r:lo||!n||e&2&&ue(t.startState)==this.from)return new Mt(this.source,e&4?1:0);let a=this.explicitPos<0?-1:t.changes.mapPos(this.explicitPos);return Hg(n.validFor,t.state,r,o)?new je(this.source,a,n,r,o):n.update&&(n=n.update(n,r,o,new cc(t.state,l,a>=0)))?new je(this.source,a,n,n.from,(i=n.to)!==null&&i!==void 0?i:ue(t.state)):new Mt(this.source,1,a)}map(t){return t.empty?this:(this.result.map?this.result.map(this.result,t):this.result)?new je(this.source,this.explicitPos<0?-1:t.mapPos(this.explicitPos),this.result,t.mapPos(this.from),t.mapPos(this.to,1)):new Mt(this.source,0)}touches(t){return t.changes.touchesRange(this.from,this.to)}}function Hg(s,t,e,i){if(!s)return!1;let n=t.sliceDoc(e,i);return typeof s=="function"?s(n,e,i,t):fc(s,!0).test(n)}const dc=N.define({map(s,t){return s.map(e=>e.map(t))}}),pc=N.define(),St=yt.define({create(){return Fn.start()},update(s,t){return s.update(t)},provide:s=>[Xa.from(s,t=>t.tooltip),O.contentAttributes.from(s,t=>t.attrs)]});function Hr(s,t){const e=t.completion.apply||t.completion.label;let i=s.state.field(St).active.find(n=>n.source==t.source);return i instanceof je?(typeof e=="string"?s.dispatch(Object.assign(Object.assign({},Ag(s.state,e,i.from,i.to)),{annotations:Vr.of(t.completion)})):e(s,t.completion,i.from,i.to),!0):!1}const Wg=Rg(St,Hr);function ln(s,t="option"){return e=>{let i=e.state.field(St,!1);if(!i||!i.open||i.open.disabled||Date.now()-i.open.timestamp-1?i.open.selected+n*(s?1:-1):s?0:o-1;return l<0?l=t=="page"?0:o-1:l>=o&&(l=t=="page"?o-1:0),e.dispatch({effects:pc.of(l)}),!0}}const zg=s=>{let t=s.state.field(St,!1);return s.state.readOnly||!t||!t.open||t.open.selected<0||t.open.disabled||Date.now()-t.open.timestamps.state.field(St,!1)?(s.dispatch({effects:Nn.of(!0)}),!0):!1,Kg=s=>{let t=s.state.field(St,!1);return!t||!t.active.some(e=>e.state!=0)?!1:(s.dispatch({effects:Pi.of(null)}),!0)};class $g{constructor(t,e){this.active=t,this.context=e,this.time=Date.now(),this.updates=[],this.done=void 0}}const jg=50,Ug=1e3,Gg=ut.fromClass(class{constructor(s){this.view=s,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.pendingStart=!1,this.composing=0;for(let t of s.state.field(St).active)t.state==1&&this.startQuery(t)}update(s){let t=s.state.field(St),e=s.state.facet(rt);if(!s.selectionSet&&!s.docChanged&&s.startState.field(St)==t)return;let i=s.transactions.some(r=>{let o=uc(r,e);return o&8||(r.selection||r.docChanged)&&!(o&3)});for(let r=0;rjg&&Date.now()-o.time>Ug){for(let l of o.context.abortListeners)try{l()}catch(a){Dt(this.view.state,a)}o.context.abortListeners=null,this.running.splice(r--,1)}else o.updates.push(...s.transactions)}this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),s.transactions.some(r=>r.effects.some(o=>o.is(Nn)))&&(this.pendingStart=!0);let n=this.pendingStart?50:e.activateOnTypingDelay;if(this.debounceUpdate=t.active.some(r=>r.state==1&&!this.running.some(o=>o.active.source==r.source))?setTimeout(()=>this.startUpdate(),n):-1,this.composing!=0)for(let r of s.transactions)r.isUserEvent("input.type")?this.composing=2:this.composing==2&&r.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1,this.pendingStart=!1;let{state:s}=this.view,t=s.field(St);for(let e of t.active)e.state==1&&!this.running.some(i=>i.active.source==e.source)&&this.startQuery(e)}startQuery(s){let{state:t}=this.view,e=ue(t),i=new cc(t,e,s.explicitPos==e,this.view),n=new $g(s,i);this.running.push(n),Promise.resolve(s.source(i)).then(r=>{n.context.aborted||(n.done=r||null,this.scheduleAccept())},r=>{this.view.dispatch({effects:Pi.of(null)}),Dt(this.view.state,r)})}scheduleAccept(){this.running.every(s=>s.done!==void 0)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(rt).updateSyncTime))}accept(){var s;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let t=[],e=this.view.state.facet(rt);for(let i=0;io.source==n.active.source);if(r&&r.state==1)if(n.done==null){let o=new Mt(n.active.source,0);for(let l of n.updates)o=o.update(l,e);o.state!=1&&t.push(o)}else this.startQuery(r)}t.length&&this.view.dispatch({effects:dc.of(t)})}},{eventHandlers:{blur(s){let t=this.view.state.field(St,!1);if(t&&t.tooltip&&this.view.state.facet(rt).closeOnBlur){let e=t.open&&_a(this.view,t.open.tooltip);(!e||!e.dom.contains(s.relatedTarget))&&setTimeout(()=>this.view.dispatch({effects:Pi.of(null)}),10)}},compositionstart(){this.composing=1},compositionend(){this.composing==3&&setTimeout(()=>this.view.dispatch({effects:Nn.of(!1)}),20),this.composing=0}}}),Jg=typeof navigator=="object"&&/Win/.test(navigator.platform),Yg=ye.highest(O.domEventHandlers({keydown(s,t){let e=t.state.field(St,!1);if(!e||!e.open||e.open.disabled||e.open.selected<0||s.key.length>1||s.ctrlKey&&!(Jg&&s.altKey)||s.metaKey)return!1;let i=e.open.options[e.open.selected],n=e.active.find(o=>o.source==i.source),r=i.completion.commitCharacters||n.result.commitCharacters;return r&&r.indexOf(s.key)>-1&&Hr(t,i),!1}})),gc=O.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",height:"100%",listStyle:"none",margin:0,padding:0,"& > li, & > completion-section":{padding:"1px 3px",lineHeight:1.2},"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer"},"& > completion-section":{display:"list-item",borderBottom:"1px solid silver",paddingLeft:"0.5em",opacity:.7}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"···"',opacity:.5,display:"block",textAlign:"center"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box",whiteSpace:"pre-line"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6",boxSizing:"content-box"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'ƒ'"}},".cm-completionIcon-class":{"&:after":{content:"'○'"}},".cm-completionIcon-interface":{"&:after":{content:"'◌'"}},".cm-completionIcon-variable":{"&:after":{content:"'𝑥'"}},".cm-completionIcon-constant":{"&:after":{content:"'𝐶'"}},".cm-completionIcon-type":{"&:after":{content:"'𝑡'"}},".cm-completionIcon-enum":{"&:after":{content:"'∪'"}},".cm-completionIcon-property":{"&:after":{content:"'□'"}},".cm-completionIcon-keyword":{"&:after":{content:"'🔑︎'"}},".cm-completionIcon-namespace":{"&:after":{content:"'▢'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}});class Xg{constructor(t,e,i,n){this.field=t,this.line=e,this.from=i,this.to=n}}class Wr{constructor(t,e,i){this.field=t,this.from=e,this.to=i}map(t){let e=t.mapPos(this.from,-1,ht.TrackDel),i=t.mapPos(this.to,1,ht.TrackDel);return e==null||i==null?null:new Wr(this.field,e,i)}}class zr{constructor(t,e){this.lines=t,this.fieldPositions=e}instantiate(t,e){let i=[],n=[e],r=t.doc.lineAt(e),o=/^\s*/.exec(r.text)[0];for(let a of this.lines){if(i.length){let c=o,h=/^\t*/.exec(a)[0].length;for(let f=0;fnew Wr(a.field,n[a.line]+a.from,n[a.line]+a.to));return{text:i,ranges:l}}static parse(t){let e=[],i=[],n=[],r;for(let o of t.split(/\r\n?|\n/)){for(;r=/[#$]\{(?:(\d+)(?::([^}]*))?|((?:\\[{}]|[^}])*))\}/.exec(o);){let l=r[1]?+r[1]:null,a=r[2]||r[3]||"",c=-1,h=a.replace(/\\[{}]/g,f=>f[1]);for(let f=0;f=c&&u.field++}n.push(new Xg(c,i.length,r.index,r.index+h.length)),o=o.slice(0,r.index)+a+o.slice(r.index+r[0].length)}o=o.replace(/\\([{}])/g,(l,a,c)=>{for(let h of n)h.line==i.length&&h.from>c&&(h.from--,h.to--);return a}),i.push(o)}return new zr(i,n)}}let _g=P.widget({widget:new class extends Ee{toDOM(){let s=document.createElement("span");return s.className="cm-snippetFieldPosition",s}ignoreEvent(){return!1}}}),Qg=P.mark({class:"cm-snippetField"});class si{constructor(t,e){this.ranges=t,this.active=e,this.deco=P.set(t.map(i=>(i.from==i.to?_g:Qg).range(i.from,i.to)))}map(t){let e=[];for(let i of this.ranges){let n=i.map(t);if(!n)return null;e.push(n)}return new si(e,this.active)}selectionInsideField(t){return t.ranges.every(e=>this.ranges.some(i=>i.field==this.active&&i.from<=e.from&&i.to>=e.to))}}const Hi=N.define({map(s,t){return s&&s.map(t)}}),Zg=N.define(),Bi=yt.define({create(){return null},update(s,t){for(let e of t.effects){if(e.is(Hi))return e.value;if(e.is(Zg)&&s)return new si(s.ranges,e.value)}return s&&t.docChanged&&(s=s.map(t.changes)),s&&t.selection&&!s.selectionInsideField(t.selection)&&(s=null),s},provide:s=>O.decorations.from(s,t=>t?t.deco:P.none)});function qr(s,t){return b.create(s.filter(e=>e.field==t).map(e=>b.range(e.from,e.to)))}function tm(s){let t=zr.parse(s);return(e,i,n,r)=>{let{text:o,ranges:l}=t.instantiate(e.state,n),a={changes:{from:n,to:r,insert:V.of(o)},scrollIntoView:!0,annotations:i?[Vr.of(i),Z.userEvent.of("input.complete")]:void 0};if(l.length&&(a.selection=qr(l,0)),l.some(c=>c.field>0)){let c=new si(l,0),h=a.effects=[Hi.of(c)];e.state.field(Bi,!1)===void 0&&h.push(N.appendConfig.of([Bi,rm,om,gc]))}e.dispatch(e.state.update(a))}}function mc(s){return({state:t,dispatch:e})=>{let i=t.field(Bi,!1);if(!i||s<0&&i.active==0)return!1;let n=i.active+s,r=s>0&&!i.ranges.some(o=>o.field==n+s);return e(t.update({selection:qr(i.ranges,n),effects:Hi.of(r?null:new si(i.ranges,n)),scrollIntoView:!0})),!0}}const em=({state:s,dispatch:t})=>s.field(Bi,!1)?(t(s.update({effects:Hi.of(null)})),!0):!1,im=mc(1),nm=mc(-1),sm=[{key:"Tab",run:im,shift:nm},{key:"Escape",run:em}],Al=T.define({combine(s){return s.length?s[0]:sm}}),rm=ye.highest(vr.compute([Al],s=>s.facet(Al)));function $m(s,t){return Object.assign(Object.assign({},t),{apply:tm(s)})}const om=O.domEventHandlers({mousedown(s,t){let e=t.state.field(Bi,!1),i;if(!e||(i=t.posAtCoords({x:s.clientX,y:s.clientY}))==null)return!1;let n=e.ranges.find(r=>r.from<=i&&r.to>=i);return!n||n.field==e.active?!1:(t.dispatch({selection:qr(e.ranges,n.field),effects:Hi.of(e.ranges.some(r=>r.field>n.field)?new si(e.ranges,n.field):null),scrollIntoView:!0}),!0)}}),Ri={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},Ae=N.define({map(s,t){let e=t.mapPos(s,-1,ht.TrackAfter);return e??void 0}}),Kr=new class extends Me{};Kr.startSide=1;Kr.endSide=-1;const yc=yt.define({create(){return K.empty},update(s,t){if(s=s.map(t.changes),t.selection){let e=t.state.doc.lineAt(t.selection.main.head);s=s.update({filter:i=>i>=e.from&&i<=e.to})}for(let e of t.effects)e.is(Ae)&&(s=s.update({add:[Kr.range(e.value,e.value+1)]}));return s}});function jm(){return[am,yc]}const ps="()[]{}<>";function bc(s){for(let t=0;t{if((lm?s.composing:s.compositionStarted)||s.state.readOnly)return!1;let n=s.state.selection.main;if(i.length>2||i.length==2&&Rt(nt(i,0))==1||t!=n.from||e!=n.to)return!1;let r=cm(s.state,i);return r?(s.dispatch(r),!0):!1}),hm=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let i=xc(s,s.selection.main.head).brackets||Ri.brackets,n=null,r=s.changeByRange(o=>{if(o.empty){let l=fm(s.doc,o.head);for(let a of i)if(a==l&&Jn(s.doc,o.head)==bc(nt(a,0)))return{changes:{from:o.head-a.length,to:o.head+a.length},range:b.cursor(o.head-a.length)}}return{range:n=o}});return n||t(s.update(r,{scrollIntoView:!0,userEvent:"delete.backward"})),!n},Um=[{key:"Backspace",run:hm}];function cm(s,t){let e=xc(s,s.selection.main.head),i=e.brackets||Ri.brackets;for(let n of i){let r=bc(nt(n,0));if(t==n)return r==n?pm(s,n,i.indexOf(n+n+n)>-1,e):um(s,n,r,e.before||Ri.before);if(t==r&&wc(s,s.selection.main.from))return dm(s,n,r)}return null}function wc(s,t){let e=!1;return s.field(yc).between(0,s.doc.length,i=>{i==t&&(e=!0)}),e}function Jn(s,t){let e=s.sliceString(t,t+2);return e.slice(0,Rt(nt(e,0)))}function fm(s,t){let e=s.sliceString(t-2,t);return Rt(nt(e,0))==e.length?e:e.slice(1)}function um(s,t,e,i){let n=null,r=s.changeByRange(o=>{if(!o.empty)return{changes:[{insert:t,from:o.from},{insert:e,from:o.to}],effects:Ae.of(o.to+t.length),range:b.range(o.anchor+t.length,o.head+t.length)};let l=Jn(s.doc,o.head);return!l||/\s/.test(l)||i.indexOf(l)>-1?{changes:{insert:t+e,from:o.head},effects:Ae.of(o.head+t.length),range:b.cursor(o.head+t.length)}:{range:n=o}});return n?null:s.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function dm(s,t,e){let i=null,n=s.changeByRange(r=>r.empty&&Jn(s.doc,r.head)==e?{changes:{from:r.head,to:r.head+e.length,insert:e},range:b.cursor(r.head+e.length)}:i={range:r});return i?null:s.update(n,{scrollIntoView:!0,userEvent:"input.type"})}function pm(s,t,e,i){let n=i.stringPrefixes||Ri.stringPrefixes,r=null,o=s.changeByRange(l=>{if(!l.empty)return{changes:[{insert:t,from:l.from},{insert:t,from:l.to}],effects:Ae.of(l.to+t.length),range:b.range(l.anchor+t.length,l.head+t.length)};let a=l.head,c=Jn(s.doc,a),h;if(c==t){if(Ml(s,a))return{changes:{insert:t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)};if(wc(s,a)){let u=e&&s.sliceDoc(a,a+t.length*3)==t+t+t?t+t+t:t;return{changes:{from:a,to:a+u.length,insert:u},range:b.cursor(a+u.length)}}}else{if(e&&s.sliceDoc(a-2*t.length,a)==t+t&&(h=Dl(s,a-2*t.length,n))>-1&&Ml(s,h))return{changes:{insert:t+t+t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)};if(s.charCategorizer(a)(c)!=G.Word&&Dl(s,a,n)>-1&&!gm(s,a,t,n))return{changes:{insert:t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)}}return{range:r=l}});return r?null:s.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function Ml(s,t){let e=mt(s).resolveInner(t+1);return e.parent&&e.from==t}function gm(s,t,e,i){let n=mt(s).resolveInner(t,-1),r=i.reduce((o,l)=>Math.max(o,l.length),0);for(let o=0;o<5;o++){let l=s.sliceDoc(n.from,Math.min(n.to,n.from+e.length+r)),a=l.indexOf(e);if(!a||a>-1&&i.indexOf(l.slice(0,a))>-1){let h=n.firstChild;for(;h&&h.from==n.from&&h.to-h.from>e.length+a;){if(s.sliceDoc(h.to-e.length,h.to)==e)return!1;h=h.firstChild}return!0}let c=n.to==t&&n.parent;if(!c)break;n=c}return!1}function Dl(s,t,e){let i=s.charCategorizer(t);if(i(s.sliceDoc(t-1,t))!=G.Word)return t;for(let n of e){let r=t-n.length;if(s.sliceDoc(r,t)==n&&i(s.sliceDoc(r-1,r))!=G.Word)return r}return-1}function Gm(s={}){return[Yg,St,rt.of(s),Gg,ym,gc]}const mm=[{key:"Ctrl-Space",run:qg},{key:"Escape",run:Kg},{key:"ArrowDown",run:ln(!0)},{key:"ArrowUp",run:ln(!1)},{key:"PageDown",run:ln(!0,"page")},{key:"PageUp",run:ln(!1,"page")},{key:"Enter",run:zg}],ym=ye.highest(vr.computeN([rt],s=>s.facet(rt).defaultKeymap?[mm]:[]));export{Mm as A,bd as B,Vn as C,Uu as D,O as E,Bm as F,Rm as G,Lm as H,Y as I,Om as J,Am as K,tr as L,Pm as M,Cr as N,Tm as O,ih as P,gd as Q,Km as R,mh as S,j as T,Cg as U,b as V,$m as W,rh as X,Ld as Y,mm as a,W as b,Um as c,Hm as d,Cm as e,Sm as f,Fm as g,Vm as h,xm as i,wm as j,Em as k,Nm as l,jm as m,zm as n,vr as o,Gm as p,km as q,vm as r,qm as s,Wm as t,Im as u,mt as v,gt as w,L as x,cd as y,C as z}; diff --git a/ui/dist/assets/index-Bp3jGQ0J.js b/ui/dist/assets/index-Bp3jGQ0J.js deleted file mode 100644 index 6a0da7ca..00000000 --- a/ui/dist/assets/index-Bp3jGQ0J.js +++ /dev/null @@ -1,152 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./FilterAutocompleteInput-l9cXyHQU.js","./index-BztyTJOx.js","./CodeEditor-CZ0EgQcM.js","./ListApiDocs-DX-LwRkY.js","./SdkTabs-DxNNd6Sw.js","./SdkTabs-lBWmLVyw.css","./FieldsQueryParam-zDO3HzQv.js","./ListApiDocs-DhdAtA7Y.css","./ViewApiDocs-D09kZD3M.js","./CreateApiDocs-n2O_YbPr.js","./UpdateApiDocs-CYknfZa_.js","./DeleteApiDocs-DninUosh.js","./RealtimeApiDocs-Bz63T_FK.js","./AuthWithPasswordDocs-B1auplF0.js","./AuthWithOAuth2Docs-CtVYpHU-.js","./AuthRefreshDocs-1UxU_c6D.js","./RequestVerificationDocs-CmHx_pVy.js","./ConfirmVerificationDocs-CzG7odGM.js","./RequestPasswordResetDocs-Ux0BhdtA.js","./ConfirmPasswordResetDocs-DZJDH7s9.js","./RequestEmailChangeDocs-OulvgXBH.js","./ConfirmEmailChangeDocs-DBFq8TK_.js","./AuthMethodsDocs-Dsno-hdt.js","./ListExternalAuthsDocs-DQacf2gi.js","./UnlinkExternalAuthDocs-BcuOuUMj.js"])))=>i.map(i=>d[i]); -var f0=Object.defineProperty;var u0=(n,e,t)=>e in n?f0(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var Ze=(n,e,t)=>u0(n,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))i(l);new MutationObserver(l=>{for(const s of l)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function t(l){const s={};return l.integrity&&(s.integrity=l.integrity),l.referrerPolicy&&(s.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?s.credentials="include":l.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function i(l){if(l.ep)return;l.ep=!0;const s=t(l);fetch(l.href,s)}})();function Q(){}const gs=n=>n;function Ie(n,e){for(const t in e)n[t]=e[t];return n}function c0(n){return!!n&&(typeof n=="object"||typeof n=="function")&&typeof n.then=="function"}function Tg(n){return n()}function Za(){return Object.create(null)}function $e(n){n.forEach(Tg)}function Ct(n){return typeof n=="function"}function me(n,e){return n!=n?e==e:n!==e||n&&typeof n=="object"||typeof n=="function"}let Ds;function en(n,e){return n===e?!0:(Ds||(Ds=document.createElement("a")),Ds.href=e,n===Ds.href)}function d0(n){return Object.keys(n).length===0}function oa(n,...e){if(n==null){for(const i of e)i(void 0);return Q}const t=n.subscribe(...e);return t.unsubscribe?()=>t.unsubscribe():t}function Cg(n){let e;return oa(n,t=>e=t)(),e}function Ue(n,e,t){n.$$.on_destroy.push(oa(e,t))}function wt(n,e,t,i){if(n){const l=Og(n,e,t,i);return n[0](l)}}function Og(n,e,t,i){return n[1]&&i?Ie(t.ctx.slice(),n[1](i(e))):t.ctx}function St(n,e,t,i){if(n[2]&&i){const l=n[2](i(t));if(e.dirty===void 0)return l;if(typeof l=="object"){const s=[],o=Math.max(e.dirty.length,l.length);for(let r=0;r32){const e=[],t=n.ctx.length/32;for(let i=0;iwindow.performance.now():()=>Date.now(),ra=Mg?n=>requestAnimationFrame(n):Q;const bl=new Set;function Dg(n){bl.forEach(e=>{e.c(n)||(bl.delete(e),e.f())}),bl.size!==0&&ra(Dg)}function Ro(n){let e;return bl.size===0&&ra(Dg),{promise:new Promise(t=>{bl.add(e={c:n,f:t})}),abort(){bl.delete(e)}}}function k(n,e){n.appendChild(e)}function Eg(n){if(!n)return document;const e=n.getRootNode?n.getRootNode():n.ownerDocument;return e&&e.host?e:n.ownerDocument}function p0(n){const e=b("style");return e.textContent="/* empty */",m0(Eg(n),e),e.sheet}function m0(n,e){return k(n.head||n,e),e.sheet}function w(n,e,t){n.insertBefore(e,t||null)}function v(n){n.parentNode&&n.parentNode.removeChild(n)}function ot(n,e){for(let t=0;tn.removeEventListener(e,t,i)}function Be(n){return function(e){return e.preventDefault(),n.call(this,e)}}function Tn(n){return function(e){return e.stopPropagation(),n.call(this,e)}}function p(n,e,t){t==null?n.removeAttribute(e):n.getAttribute(e)!==t&&n.setAttribute(e,t)}const h0=["width","height"];function ni(n,e){const t=Object.getOwnPropertyDescriptors(n.__proto__);for(const i in e)e[i]==null?n.removeAttribute(i):i==="style"?n.style.cssText=e[i]:i==="__value"?n.value=n[i]=e[i]:t[i]&&t[i].set&&h0.indexOf(i)===-1?n[i]=e[i]:p(n,i,e[i])}function _0(n){let e;return{p(...t){e=t,e.forEach(i=>n.push(i))},r(){e.forEach(t=>n.splice(n.indexOf(t),1))}}}function it(n){return n===""?null:+n}function g0(n){return Array.from(n.childNodes)}function oe(n,e){e=""+e,n.data!==e&&(n.data=e)}function re(n,e){n.value=e??""}function b0(n,e,t,i){t==null?n.style.removeProperty(e):n.style.setProperty(e,t,"")}function x(n,e,t){n.classList.toggle(e,!!t)}function Ig(n,e,{bubbles:t=!1,cancelable:i=!1}={}){return new CustomEvent(n,{detail:e,bubbles:t,cancelable:i})}function Dt(n,e){return new n(e)}const bo=new Map;let ko=0;function k0(n){let e=5381,t=n.length;for(;t--;)e=(e<<5)-e^n.charCodeAt(t);return e>>>0}function y0(n,e){const t={stylesheet:p0(e),rules:{}};return bo.set(n,t),t}function is(n,e,t,i,l,s,o,r=0){const a=16.666/i;let f=`{ -`;for(let g=0;g<=1;g+=a){const y=e+(t-e)*s(g);f+=g*100+`%{${o(y,1-y)}} -`}const u=f+`100% {${o(t,1-t)}} -}`,c=`__svelte_${k0(u)}_${r}`,d=Eg(n),{stylesheet:m,rules:h}=bo.get(d)||y0(d,n);h[c]||(h[c]=!0,m.insertRule(`@keyframes ${c} ${u}`,m.cssRules.length));const _=n.style.animation||"";return n.style.animation=`${_?`${_}, `:""}${c} ${i}ms linear ${l}ms 1 both`,ko+=1,c}function ls(n,e){const t=(n.style.animation||"").split(", "),i=t.filter(e?s=>s.indexOf(e)<0:s=>s.indexOf("__svelte")===-1),l=t.length-i.length;l&&(n.style.animation=i.join(", "),ko-=l,ko||v0())}function v0(){ra(()=>{ko||(bo.forEach(n=>{const{ownerNode:e}=n.stylesheet;e&&v(e)}),bo.clear())})}function w0(n,e,t,i){if(!e)return Q;const l=n.getBoundingClientRect();if(e.left===l.left&&e.right===l.right&&e.top===l.top&&e.bottom===l.bottom)return Q;const{delay:s=0,duration:o=300,easing:r=gs,start:a=Fo()+s,end:f=a+o,tick:u=Q,css:c}=t(n,{from:e,to:l},i);let d=!0,m=!1,h;function _(){c&&(h=is(n,0,1,o,s,r,c)),s||(m=!0)}function g(){c&&ls(n,h),d=!1}return Ro(y=>{if(!m&&y>=a&&(m=!0),m&&y>=f&&(u(1,0),g()),!d)return!1;if(m){const S=y-a,T=0+1*r(S/o);u(T,1-T)}return!0}),_(),u(0,1),g}function S0(n){const e=getComputedStyle(n);if(e.position!=="absolute"&&e.position!=="fixed"){const{width:t,height:i}=e,l=n.getBoundingClientRect();n.style.position="absolute",n.style.width=t,n.style.height=i,Ag(n,l)}}function Ag(n,e){const t=n.getBoundingClientRect();if(e.left!==t.left||e.top!==t.top){const i=getComputedStyle(n),l=i.transform==="none"?"":i.transform;n.style.transform=`${l} translate(${e.left-t.left}px, ${e.top-t.top}px)`}}let ss;function di(n){ss=n}function bs(){if(!ss)throw new Error("Function called outside component initialization");return ss}function Ht(n){bs().$$.on_mount.push(n)}function $0(n){bs().$$.after_update.push(n)}function ks(n){bs().$$.on_destroy.push(n)}function lt(){const n=bs();return(e,t,{cancelable:i=!1}={})=>{const l=n.$$.callbacks[e];if(l){const s=Ig(e,t,{cancelable:i});return l.slice().forEach(o=>{o.call(n,s)}),!s.defaultPrevented}return!0}}function Ce(n,e){const t=n.$$.callbacks[e.type];t&&t.slice().forEach(i=>i.call(this,e))}const _l=[],ee=[];let kl=[];const Fr=[],Lg=Promise.resolve();let Rr=!1;function Ng(){Rr||(Rr=!0,Lg.then(aa))}function Qt(){return Ng(),Lg}function Ke(n){kl.push(n)}function ke(n){Fr.push(n)}const tr=new Set;let cl=0;function aa(){if(cl!==0)return;const n=ss;do{try{for(;cl<_l.length;){const e=_l[cl];cl++,di(e),T0(e.$$)}}catch(e){throw _l.length=0,cl=0,e}for(di(null),_l.length=0,cl=0;ee.length;)ee.pop()();for(let e=0;en.indexOf(i)===-1?e.push(i):t.push(i)),t.forEach(i=>i()),kl=e}let Rl;function fa(){return Rl||(Rl=Promise.resolve(),Rl.then(()=>{Rl=null})),Rl}function Ki(n,e,t){n.dispatchEvent(Ig(`${e?"intro":"outro"}${t}`))}const io=new Set;let xn;function le(){xn={r:0,c:[],p:xn}}function se(){xn.r||$e(xn.c),xn=xn.p}function E(n,e){n&&n.i&&(io.delete(n),n.i(e))}function A(n,e,t,i){if(n&&n.o){if(io.has(n))return;io.add(n),xn.c.push(()=>{io.delete(n),i&&(t&&n.d(1),i())}),n.o(e)}else i&&i()}const ua={duration:0};function Pg(n,e,t){const i={direction:"in"};let l=e(n,t,i),s=!1,o,r,a=0;function f(){o&&ls(n,o)}function u(){const{delay:d=0,duration:m=300,easing:h=gs,tick:_=Q,css:g}=l||ua;g&&(o=is(n,0,1,m,d,h,g,a++)),_(0,1);const y=Fo()+d,S=y+m;r&&r.abort(),s=!0,Ke(()=>Ki(n,!0,"start")),r=Ro(T=>{if(s){if(T>=S)return _(1,0),Ki(n,!0,"end"),f(),s=!1;if(T>=y){const $=h((T-y)/m);_($,1-$)}}return s})}let c=!1;return{start(){c||(c=!0,ls(n),Ct(l)?(l=l(i),fa().then(u)):u())},invalidate(){c=!1},end(){s&&(f(),s=!1)}}}function ca(n,e,t){const i={direction:"out"};let l=e(n,t,i),s=!0,o;const r=xn;r.r+=1;let a;function f(){const{delay:u=0,duration:c=300,easing:d=gs,tick:m=Q,css:h}=l||ua;h&&(o=is(n,1,0,c,u,d,h));const _=Fo()+u,g=_+c;Ke(()=>Ki(n,!1,"start")),"inert"in n&&(a=n.inert,n.inert=!0),Ro(y=>{if(s){if(y>=g)return m(0,1),Ki(n,!1,"end"),--r.r||$e(r.c),!1;if(y>=_){const S=d((y-_)/c);m(1-S,S)}}return s})}return Ct(l)?fa().then(()=>{l=l(i),f()}):f(),{end(u){u&&"inert"in n&&(n.inert=a),u&&l.tick&&l.tick(1,0),s&&(o&&ls(n,o),s=!1)}}}function Fe(n,e,t,i){let s=e(n,t,{direction:"both"}),o=i?0:1,r=null,a=null,f=null,u;function c(){f&&ls(n,f)}function d(h,_){const g=h.b-o;return _*=Math.abs(g),{a:o,b:h.b,d:g,duration:_,start:h.start,end:h.start+_,group:h.group}}function m(h){const{delay:_=0,duration:g=300,easing:y=gs,tick:S=Q,css:T}=s||ua,$={start:Fo()+_,b:h};h||($.group=xn,xn.r+=1),"inert"in n&&(h?u!==void 0&&(n.inert=u):(u=n.inert,n.inert=!0)),r||a?a=$:(T&&(c(),f=is(n,o,h,g,_,y,T)),h&&S(0,1),r=d($,g),Ke(()=>Ki(n,h,"start")),Ro(C=>{if(a&&C>a.start&&(r=d(a,g),a=null,Ki(n,r.b,"start"),T&&(c(),f=is(n,o,r.b,r.duration,0,y,s.css))),r){if(C>=r.end)S(o=r.b,1-o),Ki(n,r.b,"end"),a||(r.b?c():--r.group.r||$e(r.group.c)),r=null;else if(C>=r.start){const O=C-r.start;o=r.a+r.d*y(O/r.duration),S(o,1-o)}}return!!(r||a)}))}return{run(h){Ct(s)?fa().then(()=>{s=s({direction:h?"in":"out"}),m(h)}):m(h)},end(){c(),r=a=null}}}function Xa(n,e){const t=e.token={};function i(l,s,o,r){if(e.token!==t)return;e.resolved=r;let a=e.ctx;o!==void 0&&(a=a.slice(),a[o]=r);const f=l&&(e.current=l)(a);let u=!1;e.block&&(e.blocks?e.blocks.forEach((c,d)=>{d!==s&&c&&(le(),A(c,1,1,()=>{e.blocks[d]===c&&(e.blocks[d]=null)}),se())}):e.block.d(1),f.c(),E(f,1),f.m(e.mount(),e.anchor),u=!0),e.block=f,e.blocks&&(e.blocks[s]=f),u&&aa()}if(c0(n)){const l=bs();if(n.then(s=>{di(l),i(e.then,1,e.value,s),di(null)},s=>{if(di(l),i(e.catch,2,e.error,s),di(null),!e.hasCatch)throw s}),e.current!==e.pending)return i(e.pending,0),!0}else{if(e.current!==e.then)return i(e.then,1,e.value,n),!0;e.resolved=n}}function O0(n,e,t){const i=e.slice(),{resolved:l}=n;n.current===n.then&&(i[n.value]=l),n.current===n.catch&&(i[n.error]=l),n.block.p(i,t)}function ue(n){return(n==null?void 0:n.length)!==void 0?n:Array.from(n)}function Ii(n,e){n.d(1),e.delete(n.key)}function Et(n,e){A(n,1,1,()=>{e.delete(n.key)})}function M0(n,e){n.f(),Et(n,e)}function at(n,e,t,i,l,s,o,r,a,f,u,c){let d=n.length,m=s.length,h=d;const _={};for(;h--;)_[n[h].key]=h;const g=[],y=new Map,S=new Map,T=[];for(h=m;h--;){const D=c(l,s,h),I=t(D);let L=o.get(I);L?T.push(()=>L.p(D,e)):(L=f(I,D),L.c()),y.set(I,g[h]=L),I in _&&S.set(I,Math.abs(h-_[I]))}const $=new Set,C=new Set;function O(D){E(D,1),D.m(r,u),o.set(D.key,D),u=D.first,m--}for(;d&&m;){const D=g[m-1],I=n[d-1],L=D.key,R=I.key;D===I?(u=D.first,d--,m--):y.has(R)?!o.has(L)||$.has(L)?O(D):C.has(R)?d--:S.get(L)>S.get(R)?(C.add(L),O(D)):($.add(R),d--):(a(I,o),d--)}for(;d--;){const D=n[d];y.has(D.key)||a(D,o)}for(;m;)O(g[m-1]);return $e(T),g}function pt(n,e){const t={},i={},l={$$scope:1};let s=n.length;for(;s--;){const o=n[s],r=e[s];if(r){for(const a in o)a in r||(i[a]=1);for(const a in r)l[a]||(t[a]=r[a],l[a]=1);n[s]=r}else for(const a in o)l[a]=1}for(const o in i)o in t||(t[o]=void 0);return t}function Ot(n){return typeof n=="object"&&n!==null?n:{}}function be(n,e,t){const i=n.$$.props[e];i!==void 0&&(n.$$.bound[i]=t,t(n.$$.ctx[i]))}function B(n){n&&n.c()}function z(n,e,t){const{fragment:i,after_update:l}=n.$$;i&&i.m(e,t),Ke(()=>{const s=n.$$.on_mount.map(Tg).filter(Ct);n.$$.on_destroy?n.$$.on_destroy.push(...s):$e(s),n.$$.on_mount=[]}),l.forEach(Ke)}function V(n,e){const t=n.$$;t.fragment!==null&&(C0(t.after_update),$e(t.on_destroy),t.fragment&&t.fragment.d(e),t.on_destroy=t.fragment=null,t.ctx=[])}function D0(n,e){n.$$.dirty[0]===-1&&(_l.push(n),Ng(),n.$$.dirty.fill(0)),n.$$.dirty[e/31|0]|=1<{const h=m.length?m[0]:d;return f.ctx&&l(f.ctx[c],f.ctx[c]=h)&&(!f.skip_bound&&f.bound[c]&&f.bound[c](h),u&&D0(n,c)),d}):[],f.update(),u=!0,$e(f.before_update),f.fragment=i?i(f.ctx):!1,e.target){if(e.hydrate){const c=g0(e.target);f.fragment&&f.fragment.l(c),c.forEach(v)}else f.fragment&&f.fragment.c();e.intro&&E(n.$$.fragment),z(n,e.target,e.anchor),aa()}di(a)}class ge{constructor(){Ze(this,"$$");Ze(this,"$$set")}$destroy(){V(this,1),this.$destroy=Q}$on(e,t){if(!Ct(t))return Q;const i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(t),()=>{const l=i.indexOf(t);l!==-1&&i.splice(l,1)}}$set(e){this.$$set&&!d0(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}const E0="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(E0);const dl=[];function Fg(n,e){return{subscribe:Cn(n,e).subscribe}}function Cn(n,e=Q){let t;const i=new Set;function l(r){if(me(n,r)&&(n=r,t)){const a=!dl.length;for(const f of i)f[1](),dl.push(f,n);if(a){for(let f=0;f{i.delete(f),i.size===0&&t&&(t(),t=null)}}return{set:l,update:s,subscribe:o}}function Rg(n,e,t){const i=!Array.isArray(n),l=i?[n]:n;if(!l.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const s=e.length<2;return Fg(t,(o,r)=>{let a=!1;const f=[];let u=0,c=Q;const d=()=>{if(u)return;c();const h=e(i?f[0]:f,o,r);s?o(h):c=Ct(h)?h:Q},m=l.map((h,_)=>oa(h,g=>{f[_]=g,u&=~(1<<_),a&&d()},()=>{u|=1<<_}));return a=!0,d(),function(){$e(m),c(),a=!1}})}function qg(n,e){if(n instanceof RegExp)return{keys:!1,pattern:n};var t,i,l,s,o=[],r="",a=n.split("/");for(a[0]||a.shift();l=a.shift();)t=l[0],t==="*"?(o.push("wild"),r+="/(.*)"):t===":"?(i=l.indexOf("?",1),s=l.indexOf(".",1),o.push(l.substring(1,~i?i:~s?s:l.length)),r+=~i&&!~s?"(?:/([^/]+?))?":"/([^/]+?)",~s&&(r+=(~i?"?":"")+"\\"+l.substring(s))):r+="/"+l;return{keys:o,pattern:new RegExp("^"+r+"/?$","i")}}function I0(n){let e,t,i;const l=[n[2]];var s=n[0];function o(r,a){let f={};for(let u=0;u{V(f,1)}),se()}s?(e=Dt(s,o(r,a)),e.$on("routeEvent",r[7]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,t.parentNode,t)):e=null}else if(s){const f=a&4?pt(l,[Ot(r[2])]):{};e.$set(f)}},i(r){i||(e&&E(e.$$.fragment,r),i=!0)},o(r){e&&A(e.$$.fragment,r),i=!1},d(r){r&&v(t),e&&V(e,r)}}}function A0(n){let e,t,i;const l=[{params:n[1]},n[2]];var s=n[0];function o(r,a){let f={};for(let u=0;u{V(f,1)}),se()}s?(e=Dt(s,o(r,a)),e.$on("routeEvent",r[6]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,t.parentNode,t)):e=null}else if(s){const f=a&6?pt(l,[a&2&&{params:r[1]},a&4&&Ot(r[2])]):{};e.$set(f)}},i(r){i||(e&&E(e.$$.fragment,r),i=!0)},o(r){e&&A(e.$$.fragment,r),i=!1},d(r){r&&v(t),e&&V(e,r)}}}function L0(n){let e,t,i,l;const s=[A0,I0],o=[];function r(a,f){return a[1]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,[f]){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function Qa(){const n=window.location.href.indexOf("#/");let e=n>-1?window.location.href.substr(n+1):"/";const t=e.indexOf("?");let i="";return t>-1&&(i=e.substr(t+1),e=e.substr(0,t)),{location:e,querystring:i}}const qo=Fg(null,function(e){e(Qa());const t=()=>{e(Qa())};return window.addEventListener("hashchange",t,!1),function(){window.removeEventListener("hashchange",t,!1)}});Rg(qo,n=>n.location);const jo=Rg(qo,n=>n.querystring),xa=Cn(void 0);async function tl(n){if(!n||n.length<1||n.charAt(0)!="/"&&n.indexOf("#/")!==0)throw Error("Invalid parameter location");await Qt();const e=(n.charAt(0)=="#"?"":"#")+n;try{const t={...history.state};delete t.__svelte_spa_router_scrollX,delete t.__svelte_spa_router_scrollY,window.history.replaceState(t,void 0,e)}catch{console.warn("Caught exception while replacing the current page. If you're running this in the Svelte REPL, please note that the `replace` method might not work in this environment.")}window.dispatchEvent(new Event("hashchange"))}function nn(n,e){if(e=tf(e),!n||!n.tagName||n.tagName.toLowerCase()!="a")throw Error('Action "link" can only be used with tags');return ef(n,e),{update(t){t=tf(t),ef(n,t)}}}function N0(n){n?window.scrollTo(n.__svelte_spa_router_scrollX,n.__svelte_spa_router_scrollY):window.scrollTo(0,0)}function ef(n,e){let t=e.href||n.getAttribute("href");if(t&&t.charAt(0)=="/")t="#"+t;else if(!t||t.length<2||t.slice(0,2)!="#/")throw Error('Invalid value for "href" attribute: '+t);n.setAttribute("href",t),n.addEventListener("click",i=>{i.preventDefault(),e.disabled||P0(i.currentTarget.getAttribute("href"))})}function tf(n){return n&&typeof n=="string"?{href:n}:n||{}}function P0(n){history.replaceState({...history.state,__svelte_spa_router_scrollX:window.scrollX,__svelte_spa_router_scrollY:window.scrollY},void 0),window.location.hash=n}function F0(n,e,t){let{routes:i={}}=e,{prefix:l=""}=e,{restoreScrollState:s=!1}=e;class o{constructor(C,O){if(!O||typeof O!="function"&&(typeof O!="object"||O._sveltesparouter!==!0))throw Error("Invalid component object");if(!C||typeof C=="string"&&(C.length<1||C.charAt(0)!="/"&&C.charAt(0)!="*")||typeof C=="object"&&!(C instanceof RegExp))throw Error('Invalid value for "path" argument - strings must start with / or *');const{pattern:D,keys:I}=qg(C);this.path=C,typeof O=="object"&&O._sveltesparouter===!0?(this.component=O.component,this.conditions=O.conditions||[],this.userData=O.userData,this.props=O.props||{}):(this.component=()=>Promise.resolve(O),this.conditions=[],this.props={}),this._pattern=D,this._keys=I}match(C){if(l){if(typeof l=="string")if(C.startsWith(l))C=C.substr(l.length)||"/";else return null;else if(l instanceof RegExp){const L=C.match(l);if(L&&L[0])C=C.substr(L[0].length)||"/";else return null}}const O=this._pattern.exec(C);if(O===null)return null;if(this._keys===!1)return O;const D={};let I=0;for(;I{r.push(new o(C,$))}):Object.keys(i).forEach($=>{r.push(new o($,i[$]))});let a=null,f=null,u={};const c=lt();async function d($,C){await Qt(),c($,C)}let m=null,h=null;s&&(h=$=>{$.state&&($.state.__svelte_spa_router_scrollY||$.state.__svelte_spa_router_scrollX)?m=$.state:m=null},window.addEventListener("popstate",h),$0(()=>{N0(m)}));let _=null,g=null;const y=qo.subscribe(async $=>{_=$;let C=0;for(;C{xa.set(f)});return}t(0,a=null),g=null,xa.set(void 0)});ks(()=>{y(),h&&window.removeEventListener("popstate",h)});function S($){Ce.call(this,n,$)}function T($){Ce.call(this,n,$)}return n.$$set=$=>{"routes"in $&&t(3,i=$.routes),"prefix"in $&&t(4,l=$.prefix),"restoreScrollState"in $&&t(5,s=$.restoreScrollState)},n.$$.update=()=>{n.$$.dirty&32&&(history.scrollRestoration=s?"manual":"auto")},[a,f,u,i,l,s,S,T]}class R0 extends ge{constructor(e){super(),_e(this,e,F0,L0,me,{routes:3,prefix:4,restoreScrollState:5})}}const lo=[];let jg;function Hg(n){const e=n.pattern.test(jg);nf(n,n.className,e),nf(n,n.inactiveClassName,!e)}function nf(n,e,t){(e||"").split(" ").forEach(i=>{i&&(n.node.classList.remove(i),t&&n.node.classList.add(i))})}qo.subscribe(n=>{jg=n.location+(n.querystring?"?"+n.querystring:""),lo.map(Hg)});function Ln(n,e){if(e&&(typeof e=="string"||typeof e=="object"&&e instanceof RegExp)?e={path:e}:e=e||{},!e.path&&n.hasAttribute("href")&&(e.path=n.getAttribute("href"),e.path&&e.path.length>1&&e.path.charAt(0)=="#"&&(e.path=e.path.substring(1))),e.className||(e.className="active"),!e.path||typeof e.path=="string"&&(e.path.length<1||e.path.charAt(0)!="/"&&e.path.charAt(0)!="*"))throw Error('Invalid value for "path" argument');const{pattern:t}=typeof e.path=="string"?qg(e.path):{pattern:e.path},i={node:n,className:e.className,inactiveClassName:e.inactiveClassName,pattern:t};return lo.push(i),Hg(i),{destroy(){lo.splice(lo.indexOf(i),1)}}}const q0="modulepreload",j0=function(n,e){return new URL(n,e).href},lf={},tt=function(e,t,i){let l=Promise.resolve();if(t&&t.length>0){const s=document.getElementsByTagName("link"),o=document.querySelector("meta[property=csp-nonce]"),r=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));l=Promise.all(t.map(a=>{if(a=j0(a,i),a in lf)return;lf[a]=!0;const f=a.endsWith(".css"),u=f?'[rel="stylesheet"]':"";if(!!i)for(let m=s.length-1;m>=0;m--){const h=s[m];if(h.href===a&&(!f||h.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${a}"]${u}`))return;const d=document.createElement("link");if(d.rel=f?"stylesheet":q0,f||(d.as="script",d.crossOrigin=""),d.href=a,r&&d.setAttribute("nonce",r),document.head.appendChild(d),f)return new Promise((m,h)=>{d.addEventListener("load",m),d.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${a}`)))})}))}return l.then(()=>e()).catch(s=>{const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=s,window.dispatchEvent(o),!o.defaultPrevented)throw s})};function At(n){if(!n)throw Error("Parameter args is required");if(!n.component==!n.asyncComponent)throw Error("One and only one of component and asyncComponent is required");if(n.component&&(n.asyncComponent=()=>Promise.resolve(n.component)),typeof n.asyncComponent!="function")throw Error("Parameter asyncComponent must be a function");if(n.conditions){Array.isArray(n.conditions)||(n.conditions=[n.conditions]);for(let t=0;t0&&(!t.exp||t.exp-e>Date.now()/1e3))}zg=typeof atob=="function"?atob:n=>{let e=String(n).replace(/=+$/,"");if(e.length%4==1)throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");for(var t,i,l=0,s=0,o="";i=e.charAt(s++);~i&&(t=l%4?64*t+i:i,l++%4)?o+=String.fromCharCode(255&t>>(-2*l&6)):0)i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(i);return o};const of="pb_auth";class B0{constructor(){this.baseToken="",this.baseModel=null,this._onChangeCallbacks=[]}get token(){return this.baseToken}get model(){return this.baseModel}get isValid(){return!da(this.token)}get isAdmin(){return so(this.token).type==="admin"}get isAuthRecord(){return so(this.token).type==="authRecord"}save(e,t){this.baseToken=e||"",this.baseModel=t||null,this.triggerChange()}clear(){this.baseToken="",this.baseModel=null,this.triggerChange()}loadFromCookie(e,t=of){const i=H0(e||"")[t]||"";let l={};try{l=JSON.parse(i),(typeof l===null||typeof l!="object"||Array.isArray(l))&&(l={})}catch{}this.save(l.token||"",l.model||null)}exportToCookie(e,t=of){var a,f;const i={secure:!0,sameSite:!0,httpOnly:!0,path:"/"},l=so(this.token);i.expires=l!=null&&l.exp?new Date(1e3*l.exp):new Date("1970-01-01"),e=Object.assign({},i,e);const s={token:this.token,model:this.model?JSON.parse(JSON.stringify(this.model)):null};let o=sf(t,JSON.stringify(s),e);const r=typeof Blob<"u"?new Blob([o]).size:o.length;if(s.model&&r>4096){s.model={id:(a=s==null?void 0:s.model)==null?void 0:a.id,email:(f=s==null?void 0:s.model)==null?void 0:f.email};const u=["collectionId","username","verified"];for(const c in this.model)u.includes(c)&&(s.model[c]=this.model[c]);o=sf(t,JSON.stringify(s),e)}return o}onChange(e,t=!1){return this._onChangeCallbacks.push(e),t&&e(this.token,this.model),()=>{for(let i=this._onChangeCallbacks.length-1;i>=0;i--)if(this._onChangeCallbacks[i]==e)return delete this._onChangeCallbacks[i],void this._onChangeCallbacks.splice(i,1)}}triggerChange(){for(const e of this._onChangeCallbacks)e&&e(this.token,this.model)}}class Vg extends B0{constructor(e="pocketbase_auth"){super(),this.storageFallback={},this.storageKey=e,this._bindStorageEvent()}get token(){return(this._storageGet(this.storageKey)||{}).token||""}get model(){return(this._storageGet(this.storageKey)||{}).model||null}save(e,t){this._storageSet(this.storageKey,{token:e,model:t}),super.save(e,t)}clear(){this._storageRemove(this.storageKey),super.clear()}_storageGet(e){if(typeof window<"u"&&(window!=null&&window.localStorage)){const t=window.localStorage.getItem(e)||"";try{return JSON.parse(t)}catch{return t}}return this.storageFallback[e]}_storageSet(e,t){if(typeof window<"u"&&(window!=null&&window.localStorage)){let i=t;typeof t!="string"&&(i=JSON.stringify(t)),window.localStorage.setItem(e,i)}else this.storageFallback[e]=t}_storageRemove(e){var t;typeof window<"u"&&(window!=null&&window.localStorage)&&((t=window.localStorage)==null||t.removeItem(e)),delete this.storageFallback[e]}_bindStorageEvent(){typeof window<"u"&&(window!=null&&window.localStorage)&&window.addEventListener&&window.addEventListener("storage",e=>{if(e.key!=this.storageKey)return;const t=this._storageGet(this.storageKey)||{};super.save(t.token||"",t.model||null)})}}class nl{constructor(e){this.client=e}}class U0 extends nl{async getAll(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/settings",e)}async update(e,t){return t=Object.assign({method:"PATCH",body:e},t),this.client.send("/api/settings",t)}async testS3(e="storage",t){return t=Object.assign({method:"POST",body:{filesystem:e}},t),this.client.send("/api/settings/test/s3",t).then(()=>!0)}async testEmail(e,t,i){return i=Object.assign({method:"POST",body:{email:e,template:t}},i),this.client.send("/api/settings/test/email",i).then(()=>!0)}async generateAppleClientSecret(e,t,i,l,s,o){return o=Object.assign({method:"POST",body:{clientId:e,teamId:t,keyId:i,privateKey:l,duration:s}},o),this.client.send("/api/settings/apple/generate-client-secret",o)}}class pa extends nl{decode(e){return e}async getFullList(e,t){if(typeof e=="number")return this._getFullList(e,t);let i=500;return(t=Object.assign({},e,t)).batch&&(i=t.batch,delete t.batch),this._getFullList(i,t)}async getList(e=1,t=30,i){return(i=Object.assign({method:"GET"},i)).query=Object.assign({page:e,perPage:t},i.query),this.client.send(this.baseCrudPath,i).then(l=>{var s;return l.items=((s=l.items)==null?void 0:s.map(o=>this.decode(o)))||[],l})}async getFirstListItem(e,t){return(t=Object.assign({requestKey:"one_by_filter_"+this.baseCrudPath+"_"+e},t)).query=Object.assign({filter:e,skipTotal:1},t.query),this.getList(1,1,t).then(i=>{var l;if(!((l=i==null?void 0:i.items)!=null&&l.length))throw new gn({status:404,response:{code:404,message:"The requested resource wasn't found.",data:{}}});return i.items[0]})}async getOne(e,t){if(!e)throw new gn({url:this.client.buildUrl(this.baseCrudPath+"/"),status:404,response:{code:404,message:"Missing required record id.",data:{}}});return t=Object.assign({method:"GET"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),t).then(i=>this.decode(i))}async create(e,t){return t=Object.assign({method:"POST",body:e},t),this.client.send(this.baseCrudPath,t).then(i=>this.decode(i))}async update(e,t,i){return i=Object.assign({method:"PATCH",body:t},i),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),i).then(l=>this.decode(l))}async delete(e,t){return t=Object.assign({method:"DELETE"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e),t).then(()=>!0)}_getFullList(e=500,t){(t=t||{}).query=Object.assign({skipTotal:1},t.query);let i=[],l=async s=>this.getList(s,e||500,t).then(o=>{const r=o.items;return i=i.concat(r),r.length==o.perPage?l(s+1):i});return l(1)}}function wn(n,e,t,i){const l=i!==void 0;return l||t!==void 0?l?(console.warn(n),e.body=Object.assign({},e.body,t),e.query=Object.assign({},e.query,i),e):Object.assign(e,t):e}function nr(n){var e;(e=n._resetAutoRefresh)==null||e.call(n)}class W0 extends pa{get baseCrudPath(){return"/api/admins"}async update(e,t,i){return super.update(e,t,i).then(l=>{var s,o;return((s=this.client.authStore.model)==null?void 0:s.id)===l.id&&((o=this.client.authStore.model)==null?void 0:o.collectionId)===void 0&&this.client.authStore.save(this.client.authStore.token,l),l})}async delete(e,t){return super.delete(e,t).then(i=>{var l,s;return i&&((l=this.client.authStore.model)==null?void 0:l.id)===e&&((s=this.client.authStore.model)==null?void 0:s.collectionId)===void 0&&this.client.authStore.clear(),i})}authResponse(e){const t=this.decode((e==null?void 0:e.admin)||{});return e!=null&&e.token&&(e!=null&&e.admin)&&this.client.authStore.save(e.token,t),Object.assign({},e,{token:(e==null?void 0:e.token)||"",admin:t})}async authWithPassword(e,t,i,l){let s={method:"POST",body:{identity:e,password:t}};s=wn("This form of authWithPassword(email, pass, body?, query?) is deprecated. Consider replacing it with authWithPassword(email, pass, options?).",s,i,l);const o=s.autoRefreshThreshold;delete s.autoRefreshThreshold,s.autoRefresh||nr(this.client);let r=await this.client.send(this.baseCrudPath+"/auth-with-password",s);return r=this.authResponse(r),o&&function(f,u,c,d){nr(f);const m=f.beforeSend,h=f.authStore.model,_=f.authStore.onChange((g,y)=>{(!g||(y==null?void 0:y.id)!=(h==null?void 0:h.id)||(y!=null&&y.collectionId||h!=null&&h.collectionId)&&(y==null?void 0:y.collectionId)!=(h==null?void 0:h.collectionId))&&nr(f)});f._resetAutoRefresh=function(){_(),f.beforeSend=m,delete f._resetAutoRefresh},f.beforeSend=async(g,y)=>{var C;const S=f.authStore.token;if((C=y.query)!=null&&C.autoRefresh)return m?m(g,y):{url:g,sendOptions:y};let T=f.authStore.isValid;if(T&&da(f.authStore.token,u))try{await c()}catch{T=!1}T||await d();const $=y.headers||{};for(let O in $)if(O.toLowerCase()=="authorization"&&S==$[O]&&f.authStore.token){$[O]=f.authStore.token;break}return y.headers=$,m?m(g,y):{url:g,sendOptions:y}}}(this.client,o,()=>this.authRefresh({autoRefresh:!0}),()=>this.authWithPassword(e,t,Object.assign({autoRefresh:!0},s))),r}async authRefresh(e,t){let i={method:"POST"};return i=wn("This form of authRefresh(body?, query?) is deprecated. Consider replacing it with authRefresh(options?).",i,e,t),this.client.send(this.baseCrudPath+"/auth-refresh",i).then(this.authResponse.bind(this))}async requestPasswordReset(e,t,i){let l={method:"POST",body:{email:e}};return l=wn("This form of requestPasswordReset(email, body?, query?) is deprecated. Consider replacing it with requestPasswordReset(email, options?).",l,t,i),this.client.send(this.baseCrudPath+"/request-password-reset",l).then(()=>!0)}async confirmPasswordReset(e,t,i,l,s){let o={method:"POST",body:{token:e,password:t,passwordConfirm:i}};return o=wn("This form of confirmPasswordReset(resetToken, password, passwordConfirm, body?, query?) is deprecated. Consider replacing it with confirmPasswordReset(resetToken, password, passwordConfirm, options?).",o,l,s),this.client.send(this.baseCrudPath+"/confirm-password-reset",o).then(()=>!0)}}const Y0=["requestKey","$cancelKey","$autoCancel","fetch","headers","body","query","params","cache","credentials","headers","integrity","keepalive","method","mode","redirect","referrer","referrerPolicy","signal","window"];function Bg(n){if(n){n.query=n.query||{};for(let e in n)Y0.includes(e)||(n.query[e]=n[e],delete n[e])}}class Ug extends nl{constructor(){super(...arguments),this.clientId="",this.eventSource=null,this.subscriptions={},this.lastSentSubscriptions=[],this.maxConnectTimeout=15e3,this.reconnectAttempts=0,this.maxReconnectAttempts=1/0,this.predefinedReconnectIntervals=[200,300,500,1e3,1200,1500,2e3],this.pendingConnects=[]}get isConnected(){return!!this.eventSource&&!!this.clientId&&!this.pendingConnects.length}async subscribe(e,t,i){var o;if(!e)throw new Error("topic must be set.");let l=e;if(i){Bg(i);const r="options="+encodeURIComponent(JSON.stringify({query:i.query,headers:i.headers}));l+=(l.includes("?")?"&":"?")+r}const s=function(r){const a=r;let f;try{f=JSON.parse(a==null?void 0:a.data)}catch{}t(f||{})};return this.subscriptions[l]||(this.subscriptions[l]=[]),this.subscriptions[l].push(s),this.isConnected?this.subscriptions[l].length===1?await this.submitSubscriptions():(o=this.eventSource)==null||o.addEventListener(l,s):await this.connect(),async()=>this.unsubscribeByTopicAndListener(e,s)}async unsubscribe(e){var i;let t=!1;if(e){const l=this.getSubscriptionsByTopic(e);for(let s in l)if(this.hasSubscriptionListeners(s)){for(let o of this.subscriptions[s])(i=this.eventSource)==null||i.removeEventListener(s,o);delete this.subscriptions[s],t||(t=!0)}}else this.subscriptions={};this.hasSubscriptionListeners()?t&&await this.submitSubscriptions():this.disconnect()}async unsubscribeByPrefix(e){var i;let t=!1;for(let l in this.subscriptions)if((l+"?").startsWith(e)){t=!0;for(let s of this.subscriptions[l])(i=this.eventSource)==null||i.removeEventListener(l,s);delete this.subscriptions[l]}t&&(this.hasSubscriptionListeners()?await this.submitSubscriptions():this.disconnect())}async unsubscribeByTopicAndListener(e,t){var s;let i=!1;const l=this.getSubscriptionsByTopic(e);for(let o in l){if(!Array.isArray(this.subscriptions[o])||!this.subscriptions[o].length)continue;let r=!1;for(let a=this.subscriptions[o].length-1;a>=0;a--)this.subscriptions[o][a]===t&&(r=!0,delete this.subscriptions[o][a],this.subscriptions[o].splice(a,1),(s=this.eventSource)==null||s.removeEventListener(o,t));r&&(this.subscriptions[o].length||delete this.subscriptions[o],i||this.hasSubscriptionListeners(o)||(i=!0))}this.hasSubscriptionListeners()?i&&await this.submitSubscriptions():this.disconnect()}hasSubscriptionListeners(e){var t,i;if(this.subscriptions=this.subscriptions||{},e)return!!((t=this.subscriptions[e])!=null&&t.length);for(let l in this.subscriptions)if((i=this.subscriptions[l])!=null&&i.length)return!0;return!1}async submitSubscriptions(){if(this.clientId)return this.addAllSubscriptionListeners(),this.lastSentSubscriptions=this.getNonEmptySubscriptionKeys(),this.client.send("/api/realtime",{method:"POST",body:{clientId:this.clientId,subscriptions:this.lastSentSubscriptions},requestKey:this.getSubscriptionsCancelKey()}).catch(e=>{if(!(e!=null&&e.isAbort))throw e})}getSubscriptionsCancelKey(){return"realtime_"+this.clientId}getSubscriptionsByTopic(e){const t={};e=e.includes("?")?e:e+"?";for(let i in this.subscriptions)(i+"?").startsWith(e)&&(t[i]=this.subscriptions[i]);return t}getNonEmptySubscriptionKeys(){const e=[];for(let t in this.subscriptions)this.subscriptions[t].length&&e.push(t);return e}addAllSubscriptionListeners(){if(this.eventSource){this.removeAllSubscriptionListeners();for(let e in this.subscriptions)for(let t of this.subscriptions[e])this.eventSource.addEventListener(e,t)}}removeAllSubscriptionListeners(){if(this.eventSource)for(let e in this.subscriptions)for(let t of this.subscriptions[e])this.eventSource.removeEventListener(e,t)}async connect(){if(!(this.reconnectAttempts>0))return new Promise((e,t)=>{this.pendingConnects.push({resolve:e,reject:t}),this.pendingConnects.length>1||this.initConnect()})}initConnect(){this.disconnect(!0),clearTimeout(this.connectTimeoutId),this.connectTimeoutId=setTimeout(()=>{this.connectErrorHandler(new Error("EventSource connect took too long."))},this.maxConnectTimeout),this.eventSource=new EventSource(this.client.buildUrl("/api/realtime")),this.eventSource.onerror=e=>{this.connectErrorHandler(new Error("Failed to establish realtime connection."))},this.eventSource.addEventListener("PB_CONNECT",e=>{const t=e;this.clientId=t==null?void 0:t.lastEventId,this.submitSubscriptions().then(async()=>{let i=3;for(;this.hasUnsentSubscriptions()&&i>0;)i--,await this.submitSubscriptions()}).then(()=>{for(let l of this.pendingConnects)l.resolve();this.pendingConnects=[],this.reconnectAttempts=0,clearTimeout(this.reconnectTimeoutId),clearTimeout(this.connectTimeoutId);const i=this.getSubscriptionsByTopic("PB_CONNECT");for(let l in i)for(let s of i[l])s(e)}).catch(i=>{this.clientId="",this.connectErrorHandler(i)})})}hasUnsentSubscriptions(){const e=this.getNonEmptySubscriptionKeys();if(e.length!=this.lastSentSubscriptions.length)return!0;for(const t of e)if(!this.lastSentSubscriptions.includes(t))return!0;return!1}connectErrorHandler(e){if(clearTimeout(this.connectTimeoutId),clearTimeout(this.reconnectTimeoutId),!this.clientId&&!this.reconnectAttempts||this.reconnectAttempts>this.maxReconnectAttempts){for(let i of this.pendingConnects)i.reject(new gn(e));return this.pendingConnects=[],void this.disconnect()}this.disconnect(!0);const t=this.predefinedReconnectIntervals[this.reconnectAttempts]||this.predefinedReconnectIntervals[this.predefinedReconnectIntervals.length-1];this.reconnectAttempts++,this.reconnectTimeoutId=setTimeout(()=>{this.initConnect()},t)}disconnect(e=!1){var t;if(clearTimeout(this.connectTimeoutId),clearTimeout(this.reconnectTimeoutId),this.removeAllSubscriptionListeners(),this.client.cancelRequest(this.getSubscriptionsCancelKey()),(t=this.eventSource)==null||t.close(),this.eventSource=null,this.clientId="",!e){this.reconnectAttempts=0;for(let i of this.pendingConnects)i.resolve();this.pendingConnects=[]}}}class K0 extends pa{constructor(e,t){super(e),this.collectionIdOrName=t}get baseCrudPath(){return this.baseCollectionPath+"/records"}get baseCollectionPath(){return"/api/collections/"+encodeURIComponent(this.collectionIdOrName)}async subscribe(e,t,i){if(!e)throw new Error("Missing topic.");if(!t)throw new Error("Missing subscription callback.");return this.client.realtime.subscribe(this.collectionIdOrName+"/"+e,t,i)}async unsubscribe(e){return e?this.client.realtime.unsubscribe(this.collectionIdOrName+"/"+e):this.client.realtime.unsubscribeByPrefix(this.collectionIdOrName)}async getFullList(e,t){if(typeof e=="number")return super.getFullList(e,t);const i=Object.assign({},e,t);return super.getFullList(i)}async getList(e=1,t=30,i){return super.getList(e,t,i)}async getFirstListItem(e,t){return super.getFirstListItem(e,t)}async getOne(e,t){return super.getOne(e,t)}async create(e,t){return super.create(e,t)}async update(e,t,i){return super.update(e,t,i).then(l=>{var s,o,r;return((s=this.client.authStore.model)==null?void 0:s.id)!==(l==null?void 0:l.id)||((o=this.client.authStore.model)==null?void 0:o.collectionId)!==this.collectionIdOrName&&((r=this.client.authStore.model)==null?void 0:r.collectionName)!==this.collectionIdOrName||this.client.authStore.save(this.client.authStore.token,l),l})}async delete(e,t){return super.delete(e,t).then(i=>{var l,s,o;return!i||((l=this.client.authStore.model)==null?void 0:l.id)!==e||((s=this.client.authStore.model)==null?void 0:s.collectionId)!==this.collectionIdOrName&&((o=this.client.authStore.model)==null?void 0:o.collectionName)!==this.collectionIdOrName||this.client.authStore.clear(),i})}authResponse(e){const t=this.decode((e==null?void 0:e.record)||{});return this.client.authStore.save(e==null?void 0:e.token,t),Object.assign({},e,{token:(e==null?void 0:e.token)||"",record:t})}async listAuthMethods(e){return e=Object.assign({method:"GET"},e),this.client.send(this.baseCollectionPath+"/auth-methods",e).then(t=>Object.assign({},t,{usernamePassword:!!(t!=null&&t.usernamePassword),emailPassword:!!(t!=null&&t.emailPassword),authProviders:Array.isArray(t==null?void 0:t.authProviders)?t==null?void 0:t.authProviders:[]}))}async authWithPassword(e,t,i,l){let s={method:"POST",body:{identity:e,password:t}};return s=wn("This form of authWithPassword(usernameOrEmail, pass, body?, query?) is deprecated. Consider replacing it with authWithPassword(usernameOrEmail, pass, options?).",s,i,l),this.client.send(this.baseCollectionPath+"/auth-with-password",s).then(o=>this.authResponse(o))}async authWithOAuth2Code(e,t,i,l,s,o,r){let a={method:"POST",body:{provider:e,code:t,codeVerifier:i,redirectUrl:l,createData:s}};return a=wn("This form of authWithOAuth2Code(provider, code, codeVerifier, redirectUrl, createData?, body?, query?) is deprecated. Consider replacing it with authWithOAuth2Code(provider, code, codeVerifier, redirectUrl, createData?, options?).",a,o,r),this.client.send(this.baseCollectionPath+"/auth-with-oauth2",a).then(f=>this.authResponse(f))}async authWithOAuth2(...e){if(e.length>1||typeof(e==null?void 0:e[0])=="string")return console.warn("PocketBase: This form of authWithOAuth2() is deprecated and may get removed in the future. Please replace with authWithOAuth2Code() OR use the authWithOAuth2() realtime form as shown in https://pocketbase.io/docs/authentication/#oauth2-integration."),this.authWithOAuth2Code((e==null?void 0:e[0])||"",(e==null?void 0:e[1])||"",(e==null?void 0:e[2])||"",(e==null?void 0:e[3])||"",(e==null?void 0:e[4])||{},(e==null?void 0:e[5])||{},(e==null?void 0:e[6])||{});const t=(e==null?void 0:e[0])||{},i=(await this.listAuthMethods()).authProviders.find(a=>a.name===t.provider);if(!i)throw new gn(new Error(`Missing or invalid provider "${t.provider}".`));const l=this.client.buildUrl("/api/oauth2-redirect"),s=new Ug(this.client);let o=null;function r(){o==null||o.close(),s.unsubscribe()}return t.urlCallback||(o=rf(void 0)),new Promise(async(a,f)=>{var u;try{await s.subscribe("@oauth2",async h=>{const _=s.clientId;try{if(!h.state||_!==h.state)throw new Error("State parameters don't match.");const g=Object.assign({},t);delete g.provider,delete g.scopes,delete g.createData,delete g.urlCallback;const y=await this.authWithOAuth2Code(i.name,h.code,i.codeVerifier,l,t.createData,g);a(y)}catch(g){f(new gn(g))}r()});const c={state:s.clientId};(u=t.scopes)!=null&&u.length&&(c.scope=t.scopes.join(" "));const d=this._replaceQueryParams(i.authUrl+l,c);await(t.urlCallback||function(h){o?o.location.href=h:o=rf(h)})(d)}catch(c){r(),f(new gn(c))}})}async authRefresh(e,t){let i={method:"POST"};return i=wn("This form of authRefresh(body?, query?) is deprecated. Consider replacing it with authRefresh(options?).",i,e,t),this.client.send(this.baseCollectionPath+"/auth-refresh",i).then(l=>this.authResponse(l))}async requestPasswordReset(e,t,i){let l={method:"POST",body:{email:e}};return l=wn("This form of requestPasswordReset(email, body?, query?) is deprecated. Consider replacing it with requestPasswordReset(email, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-password-reset",l).then(()=>!0)}async confirmPasswordReset(e,t,i,l,s){let o={method:"POST",body:{token:e,password:t,passwordConfirm:i}};return o=wn("This form of confirmPasswordReset(token, password, passwordConfirm, body?, query?) is deprecated. Consider replacing it with confirmPasswordReset(token, password, passwordConfirm, options?).",o,l,s),this.client.send(this.baseCollectionPath+"/confirm-password-reset",o).then(()=>!0)}async requestVerification(e,t,i){let l={method:"POST",body:{email:e}};return l=wn("This form of requestVerification(email, body?, query?) is deprecated. Consider replacing it with requestVerification(email, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-verification",l).then(()=>!0)}async confirmVerification(e,t,i){let l={method:"POST",body:{token:e}};return l=wn("This form of confirmVerification(token, body?, query?) is deprecated. Consider replacing it with confirmVerification(token, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/confirm-verification",l).then(()=>!0)}async requestEmailChange(e,t,i){let l={method:"POST",body:{newEmail:e}};return l=wn("This form of requestEmailChange(newEmail, body?, query?) is deprecated. Consider replacing it with requestEmailChange(newEmail, options?).",l,t,i),this.client.send(this.baseCollectionPath+"/request-email-change",l).then(()=>!0)}async confirmEmailChange(e,t,i,l){let s={method:"POST",body:{token:e,password:t}};return s=wn("This form of confirmEmailChange(token, password, body?, query?) is deprecated. Consider replacing it with confirmEmailChange(token, password, options?).",s,i,l),this.client.send(this.baseCollectionPath+"/confirm-email-change",s).then(()=>!0)}async listExternalAuths(e,t){return t=Object.assign({method:"GET"},t),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e)+"/external-auths",t)}async unlinkExternalAuth(e,t,i){return i=Object.assign({method:"DELETE"},i),this.client.send(this.baseCrudPath+"/"+encodeURIComponent(e)+"/external-auths/"+encodeURIComponent(t),i).then(()=>!0)}_replaceQueryParams(e,t={}){let i=e,l="";e.indexOf("?")>=0&&(i=e.substring(0,e.indexOf("?")),l=e.substring(e.indexOf("?")+1));const s={},o=l.split("&");for(const r of o){if(r=="")continue;const a=r.split("=");s[decodeURIComponent(a[0].replace(/\+/g," "))]=decodeURIComponent((a[1]||"").replace(/\+/g," "))}for(let r in t)t.hasOwnProperty(r)&&(t[r]==null?delete s[r]:s[r]=t[r]);l="";for(let r in s)s.hasOwnProperty(r)&&(l!=""&&(l+="&"),l+=encodeURIComponent(r.replace(/%20/g,"+"))+"="+encodeURIComponent(s[r].replace(/%20/g,"+")));return l!=""?i+"?"+l:i}}function rf(n){if(typeof window>"u"||!(window!=null&&window.open))throw new gn(new Error("Not in a browser context - please pass a custom urlCallback function."));let e=1024,t=768,i=window.innerWidth,l=window.innerHeight;e=e>i?i:e,t=t>l?l:t;let s=i/2-e/2,o=l/2-t/2;return window.open(n,"popup_window","width="+e+",height="+t+",top="+o+",left="+s+",resizable,menubar=no")}class J0 extends pa{get baseCrudPath(){return"/api/collections"}async import(e,t=!1,i){return i=Object.assign({method:"PUT",body:{collections:e,deleteMissing:t}},i),this.client.send(this.baseCrudPath+"/import",i).then(()=>!0)}}class Z0 extends nl{async getList(e=1,t=30,i){return(i=Object.assign({method:"GET"},i)).query=Object.assign({page:e,perPage:t},i.query),this.client.send("/api/logs",i)}async getOne(e,t){if(!e)throw new gn({url:this.client.buildUrl("/api/logs/"),status:404,response:{code:404,message:"Missing required log id.",data:{}}});return t=Object.assign({method:"GET"},t),this.client.send("/api/logs/"+encodeURIComponent(e),t)}async getStats(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/logs/stats",e)}}class G0 extends nl{async check(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/health",e)}}class X0 extends nl{getUrl(e,t,i={}){if(!t||!(e!=null&&e.id)||!(e!=null&&e.collectionId)&&!(e!=null&&e.collectionName))return"";const l=[];l.push("api"),l.push("files"),l.push(encodeURIComponent(e.collectionId||e.collectionName)),l.push(encodeURIComponent(e.id)),l.push(encodeURIComponent(t));let s=this.client.buildUrl(l.join("/"));if(Object.keys(i).length){i.download===!1&&delete i.download;const o=new URLSearchParams(i);s+=(s.includes("?")?"&":"?")+o}return s}async getToken(e){return e=Object.assign({method:"POST"},e),this.client.send("/api/files/token",e).then(t=>(t==null?void 0:t.token)||"")}}class Q0 extends nl{async getFullList(e){return e=Object.assign({method:"GET"},e),this.client.send("/api/backups",e)}async create(e,t){return t=Object.assign({method:"POST",body:{name:e}},t),this.client.send("/api/backups",t).then(()=>!0)}async upload(e,t){return t=Object.assign({method:"POST",body:e},t),this.client.send("/api/backups/upload",t).then(()=>!0)}async delete(e,t){return t=Object.assign({method:"DELETE"},t),this.client.send(`/api/backups/${encodeURIComponent(e)}`,t).then(()=>!0)}async restore(e,t){return t=Object.assign({method:"POST"},t),this.client.send(`/api/backups/${encodeURIComponent(e)}/restore`,t).then(()=>!0)}getDownloadUrl(e,t){return this.client.buildUrl(`/api/backups/${encodeURIComponent(t)}?token=${encodeURIComponent(e)}`)}}class Ho{constructor(e="/",t,i="en-US"){this.cancelControllers={},this.recordServices={},this.enableAutoCancellation=!0,this.baseUrl=e,this.lang=i,this.authStore=t||new Vg,this.admins=new W0(this),this.collections=new J0(this),this.files=new X0(this),this.logs=new Z0(this),this.settings=new U0(this),this.realtime=new Ug(this),this.health=new G0(this),this.backups=new Q0(this)}collection(e){return this.recordServices[e]||(this.recordServices[e]=new K0(this,e)),this.recordServices[e]}autoCancellation(e){return this.enableAutoCancellation=!!e,this}cancelRequest(e){return this.cancelControllers[e]&&(this.cancelControllers[e].abort(),delete this.cancelControllers[e]),this}cancelAllRequests(){for(let e in this.cancelControllers)this.cancelControllers[e].abort();return this.cancelControllers={},this}filter(e,t){if(!t)return e;for(let i in t){let l=t[i];switch(typeof l){case"boolean":case"number":l=""+l;break;case"string":l="'"+l.replace(/'/g,"\\'")+"'";break;default:l=l===null?"null":l instanceof Date?"'"+l.toISOString().replace("T"," ")+"'":"'"+JSON.stringify(l).replace(/'/g,"\\'")+"'"}e=e.replaceAll("{:"+i+"}",l)}return e}getFileUrl(e,t,i={}){return this.files.getUrl(e,t,i)}buildUrl(e){var i;let t=this.baseUrl;return typeof window>"u"||!window.location||t.startsWith("https://")||t.startsWith("http://")||(t=(i=window.location.origin)!=null&&i.endsWith("/")?window.location.origin.substring(0,window.location.origin.length-1):window.location.origin||"",this.baseUrl.startsWith("/")||(t+=window.location.pathname||"/",t+=t.endsWith("/")?"":"/"),t+=this.baseUrl),e&&(t+=t.endsWith("/")?"":"/",t+=e.startsWith("/")?e.substring(1):e),t}async send(e,t){t=this.initSendOptions(e,t);let i=this.buildUrl(e);if(this.beforeSend){const l=Object.assign({},await this.beforeSend(i,t));l.url!==void 0||l.options!==void 0?(i=l.url||i,t=l.options||t):Object.keys(l).length&&(t=l,console!=null&&console.warn&&console.warn("Deprecated format of beforeSend return: please use `return { url, options }`, instead of `return options`."))}if(t.query!==void 0){const l=this.serializeQueryParams(t.query);l&&(i+=(i.includes("?")?"&":"?")+l),delete t.query}return this.getHeader(t.headers,"Content-Type")=="application/json"&&t.body&&typeof t.body!="string"&&(t.body=JSON.stringify(t.body)),(t.fetch||fetch)(i,t).then(async l=>{let s={};try{s=await l.json()}catch{}if(this.afterSend&&(s=await this.afterSend(l,s)),l.status>=400)throw new gn({url:l.url,status:l.status,data:s});return s}).catch(l=>{throw new gn(l)})}initSendOptions(e,t){if((t=Object.assign({method:"GET"},t)).body=this.convertToFormDataIfNeeded(t.body),Bg(t),t.query=Object.assign({},t.params,t.query),t.requestKey===void 0&&(t.$autoCancel===!1||t.query.$autoCancel===!1?t.requestKey=null:(t.$cancelKey||t.query.$cancelKey)&&(t.requestKey=t.$cancelKey||t.query.$cancelKey)),delete t.$autoCancel,delete t.query.$autoCancel,delete t.$cancelKey,delete t.query.$cancelKey,this.getHeader(t.headers,"Content-Type")!==null||this.isFormData(t.body)||(t.headers=Object.assign({},t.headers,{"Content-Type":"application/json"})),this.getHeader(t.headers,"Accept-Language")===null&&(t.headers=Object.assign({},t.headers,{"Accept-Language":this.lang})),this.authStore.token&&this.getHeader(t.headers,"Authorization")===null&&(t.headers=Object.assign({},t.headers,{Authorization:this.authStore.token})),this.enableAutoCancellation&&t.requestKey!==null){const i=t.requestKey||(t.method||"GET")+e;delete t.requestKey,this.cancelRequest(i);const l=new AbortController;this.cancelControllers[i]=l,t.signal=l.signal}return t}convertToFormDataIfNeeded(e){if(typeof FormData>"u"||e===void 0||typeof e!="object"||e===null||this.isFormData(e)||!this.hasBlobField(e))return e;const t=new FormData;for(let i in e){const l=this.normalizeFormDataValue(e[i]),s=Array.isArray(l)?l:[l];if(s.length)for(const o of s)t.append(i,o);else t.append(i,"")}return t}normalizeFormDataValue(e){return e===null||typeof e!="object"||e instanceof Date||this.hasBlobField({data:e})||Array.isArray(e)&&!e.filter(t=>typeof t!="string").length?e:JSON.stringify(e)}hasBlobField(e){for(let t in e){const i=Array.isArray(e[t])?e[t]:[e[t]];for(let l of i)if(typeof Blob<"u"&&l instanceof Blob||typeof File<"u"&&l instanceof File)return!0}return!1}getHeader(e,t){e=e||{},t=t.toLowerCase();for(let i in e)if(i.toLowerCase()==t)return e[i];return null}isFormData(e){return e&&(e.constructor.name==="FormData"||typeof FormData<"u"&&e instanceof FormData)}serializeQueryParams(e){const t=[];for(const i in e){if(e[i]===null)continue;const l=e[i],s=encodeURIComponent(i);if(Array.isArray(l))for(const o of l)t.push(s+"="+encodeURIComponent(o));else l instanceof Date?t.push(s+"="+encodeURIComponent(l.toISOString())):typeof l!==null&&typeof l=="object"?t.push(s+"="+encodeURIComponent(JSON.stringify(l))):t.push(s+"="+encodeURIComponent(l))}return t.join("&")}}class il extends Error{}class x0 extends il{constructor(e){super(`Invalid DateTime: ${e.toMessage()}`)}}class ek extends il{constructor(e){super(`Invalid Interval: ${e.toMessage()}`)}}class tk extends il{constructor(e){super(`Invalid Duration: ${e.toMessage()}`)}}class gl extends il{}class Wg extends il{constructor(e){super(`Invalid unit ${e}`)}}class mn extends il{}class ki extends il{constructor(){super("Zone is an abstract class")}}const Me="numeric",Wn="short",$n="long",yo={year:Me,month:Me,day:Me},Yg={year:Me,month:Wn,day:Me},nk={year:Me,month:Wn,day:Me,weekday:Wn},Kg={year:Me,month:$n,day:Me},Jg={year:Me,month:$n,day:Me,weekday:$n},Zg={hour:Me,minute:Me},Gg={hour:Me,minute:Me,second:Me},Xg={hour:Me,minute:Me,second:Me,timeZoneName:Wn},Qg={hour:Me,minute:Me,second:Me,timeZoneName:$n},xg={hour:Me,minute:Me,hourCycle:"h23"},e1={hour:Me,minute:Me,second:Me,hourCycle:"h23"},t1={hour:Me,minute:Me,second:Me,hourCycle:"h23",timeZoneName:Wn},n1={hour:Me,minute:Me,second:Me,hourCycle:"h23",timeZoneName:$n},i1={year:Me,month:Me,day:Me,hour:Me,minute:Me},l1={year:Me,month:Me,day:Me,hour:Me,minute:Me,second:Me},s1={year:Me,month:Wn,day:Me,hour:Me,minute:Me},o1={year:Me,month:Wn,day:Me,hour:Me,minute:Me,second:Me},ik={year:Me,month:Wn,day:Me,weekday:Wn,hour:Me,minute:Me},r1={year:Me,month:$n,day:Me,hour:Me,minute:Me,timeZoneName:Wn},a1={year:Me,month:$n,day:Me,hour:Me,minute:Me,second:Me,timeZoneName:Wn},f1={year:Me,month:$n,day:Me,weekday:$n,hour:Me,minute:Me,timeZoneName:$n},u1={year:Me,month:$n,day:Me,weekday:$n,hour:Me,minute:Me,second:Me,timeZoneName:$n};class ys{get type(){throw new ki}get name(){throw new ki}get ianaName(){return this.name}get isUniversal(){throw new ki}offsetName(e,t){throw new ki}formatOffset(e,t){throw new ki}offset(e){throw new ki}equals(e){throw new ki}get isValid(){throw new ki}}let ir=null;class zo extends ys{static get instance(){return ir===null&&(ir=new zo),ir}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(e,{format:t,locale:i}){return k1(e,t,i)}formatOffset(e,t){return Zl(this.offset(e),t)}offset(e){return-new Date(e).getTimezoneOffset()}equals(e){return e.type==="system"}get isValid(){return!0}}let oo={};function lk(n){return oo[n]||(oo[n]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:n,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),oo[n]}const sk={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function ok(n,e){const t=n.format(e).replace(/\u200E/g,""),i=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(t),[,l,s,o,r,a,f,u]=i;return[o,l,s,r,a,f,u]}function rk(n,e){const t=n.formatToParts(e),i=[];for(let l=0;l=0?h:1e3+h,(d-m)/(60*1e3)}equals(e){return e.type==="iana"&&e.name===this.name}get isValid(){return this.valid}}let af={};function ak(n,e={}){const t=JSON.stringify([n,e]);let i=af[t];return i||(i=new Intl.ListFormat(n,e),af[t]=i),i}let qr={};function jr(n,e={}){const t=JSON.stringify([n,e]);let i=qr[t];return i||(i=new Intl.DateTimeFormat(n,e),qr[t]=i),i}let Hr={};function fk(n,e={}){const t=JSON.stringify([n,e]);let i=Hr[t];return i||(i=new Intl.NumberFormat(n,e),Hr[t]=i),i}let zr={};function uk(n,e={}){const{base:t,...i}=e,l=JSON.stringify([n,i]);let s=zr[l];return s||(s=new Intl.RelativeTimeFormat(n,e),zr[l]=s),s}let Wl=null;function ck(){return Wl||(Wl=new Intl.DateTimeFormat().resolvedOptions().locale,Wl)}let ff={};function dk(n){let e=ff[n];if(!e){const t=new Intl.Locale(n);e="getWeekInfo"in t?t.getWeekInfo():t.weekInfo,ff[n]=e}return e}function pk(n){const e=n.indexOf("-x-");e!==-1&&(n=n.substring(0,e));const t=n.indexOf("-u-");if(t===-1)return[n];{let i,l;try{i=jr(n).resolvedOptions(),l=n}catch{const a=n.substring(0,t);i=jr(a).resolvedOptions(),l=a}const{numberingSystem:s,calendar:o}=i;return[l,s,o]}}function mk(n,e,t){return(t||e)&&(n.includes("-u-")||(n+="-u"),t&&(n+=`-ca-${t}`),e&&(n+=`-nu-${e}`)),n}function hk(n){const e=[];for(let t=1;t<=12;t++){const i=je.utc(2009,t,1);e.push(n(i))}return e}function _k(n){const e=[];for(let t=1;t<=7;t++){const i=je.utc(2016,11,13+t);e.push(n(i))}return e}function As(n,e,t,i){const l=n.listingMode();return l==="error"?null:l==="en"?t(e):i(e)}function gk(n){return n.numberingSystem&&n.numberingSystem!=="latn"?!1:n.numberingSystem==="latn"||!n.locale||n.locale.startsWith("en")||new Intl.DateTimeFormat(n.intl).resolvedOptions().numberingSystem==="latn"}class bk{constructor(e,t,i){this.padTo=i.padTo||0,this.floor=i.floor||!1;const{padTo:l,floor:s,...o}=i;if(!t||Object.keys(o).length>0){const r={useGrouping:!1,...i};i.padTo>0&&(r.minimumIntegerDigits=i.padTo),this.inf=fk(e,r)}}format(e){if(this.inf){const t=this.floor?Math.floor(e):e;return this.inf.format(t)}else{const t=this.floor?Math.floor(e):ga(e,3);return Vt(t,this.padTo)}}}class kk{constructor(e,t,i){this.opts=i,this.originalZone=void 0;let l;if(this.opts.timeZone)this.dt=e;else if(e.zone.type==="fixed"){const o=-1*(e.offset/60),r=o>=0?`Etc/GMT+${o}`:`Etc/GMT${o}`;e.offset!==0&&pi.create(r).valid?(l=r,this.dt=e):(l="UTC",this.dt=e.offset===0?e:e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone)}else e.zone.type==="system"?this.dt=e:e.zone.type==="iana"?(this.dt=e,l=e.zone.name):(l="UTC",this.dt=e.setZone("UTC").plus({minutes:e.offset}),this.originalZone=e.zone);const s={...this.opts};s.timeZone=s.timeZone||l,this.dtf=jr(t,s)}format(){return this.originalZone?this.formatToParts().map(({value:e})=>e).join(""):this.dtf.format(this.dt.toJSDate())}formatToParts(){const e=this.dtf.formatToParts(this.dt.toJSDate());return this.originalZone?e.map(t=>{if(t.type==="timeZoneName"){const i=this.originalZone.offsetName(this.dt.ts,{locale:this.dt.locale,format:this.opts.timeZoneName});return{...t,value:i}}else return t}):e}resolvedOptions(){return this.dtf.resolvedOptions()}}class yk{constructor(e,t,i){this.opts={style:"long",...i},!t&&g1()&&(this.rtf=uk(e,i))}format(e,t){return this.rtf?this.rtf.format(e,t):Hk(t,e,this.opts.numeric,this.opts.style!=="long")}formatToParts(e,t){return this.rtf?this.rtf.formatToParts(e,t):[]}}const vk={firstDay:1,minimalDays:4,weekend:[6,7]};class kt{static fromOpts(e){return kt.create(e.locale,e.numberingSystem,e.outputCalendar,e.weekSettings,e.defaultToEN)}static create(e,t,i,l,s=!1){const o=e||qt.defaultLocale,r=o||(s?"en-US":ck()),a=t||qt.defaultNumberingSystem,f=i||qt.defaultOutputCalendar,u=Vr(l)||qt.defaultWeekSettings;return new kt(r,a,f,u,o)}static resetCache(){Wl=null,qr={},Hr={},zr={}}static fromObject({locale:e,numberingSystem:t,outputCalendar:i,weekSettings:l}={}){return kt.create(e,t,i,l)}constructor(e,t,i,l,s){const[o,r,a]=pk(e);this.locale=o,this.numberingSystem=t||r||null,this.outputCalendar=i||a||null,this.weekSettings=l,this.intl=mk(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=s,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=gk(this)),this.fastNumbersCached}listingMode(){const e=this.isEnglish(),t=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return e&&t?"en":"intl"}clone(e){return!e||Object.getOwnPropertyNames(e).length===0?this:kt.create(e.locale||this.specifiedLocale,e.numberingSystem||this.numberingSystem,e.outputCalendar||this.outputCalendar,Vr(e.weekSettings)||this.weekSettings,e.defaultToEN||!1)}redefaultToEN(e={}){return this.clone({...e,defaultToEN:!0})}redefaultToSystem(e={}){return this.clone({...e,defaultToEN:!1})}months(e,t=!1){return As(this,e,w1,()=>{const i=t?{month:e,day:"numeric"}:{month:e},l=t?"format":"standalone";return this.monthsCache[l][e]||(this.monthsCache[l][e]=hk(s=>this.extract(s,i,"month"))),this.monthsCache[l][e]})}weekdays(e,t=!1){return As(this,e,T1,()=>{const i=t?{weekday:e,year:"numeric",month:"long",day:"numeric"}:{weekday:e},l=t?"format":"standalone";return this.weekdaysCache[l][e]||(this.weekdaysCache[l][e]=_k(s=>this.extract(s,i,"weekday"))),this.weekdaysCache[l][e]})}meridiems(){return As(this,void 0,()=>C1,()=>{if(!this.meridiemCache){const e={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[je.utc(2016,11,13,9),je.utc(2016,11,13,19)].map(t=>this.extract(t,e,"dayperiod"))}return this.meridiemCache})}eras(e){return As(this,e,O1,()=>{const t={era:e};return this.eraCache[e]||(this.eraCache[e]=[je.utc(-40,1,1),je.utc(2017,1,1)].map(i=>this.extract(i,t,"era"))),this.eraCache[e]})}extract(e,t,i){const l=this.dtFormatter(e,t),s=l.formatToParts(),o=s.find(r=>r.type.toLowerCase()===i);return o?o.value:null}numberFormatter(e={}){return new bk(this.intl,e.forceSimple||this.fastNumbers,e)}dtFormatter(e,t={}){return new kk(e,this.intl,t)}relFormatter(e={}){return new yk(this.intl,this.isEnglish(),e)}listFormatter(e={}){return ak(this.intl,e)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")}getWeekSettings(){return this.weekSettings?this.weekSettings:b1()?dk(this.locale):vk}getStartOfWeek(){return this.getWeekSettings().firstDay}getMinDaysInFirstWeek(){return this.getWeekSettings().minimalDays}getWeekendDays(){return this.getWeekSettings().weekend}equals(e){return this.locale===e.locale&&this.numberingSystem===e.numberingSystem&&this.outputCalendar===e.outputCalendar}}let lr=null;class un extends ys{static get utcInstance(){return lr===null&&(lr=new un(0)),lr}static instance(e){return e===0?un.utcInstance:new un(e)}static parseSpecifier(e){if(e){const t=e.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(t)return new un(Uo(t[1],t[2]))}return null}constructor(e){super(),this.fixed=e}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${Zl(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${Zl(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(e,t){return Zl(this.fixed,t)}get isUniversal(){return!0}offset(){return this.fixed}equals(e){return e.type==="fixed"&&e.fixed===this.fixed}get isValid(){return!0}}class wk extends ys{constructor(e){super(),this.zoneName=e}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}}function Si(n,e){if(We(n)||n===null)return e;if(n instanceof ys)return n;if(Tk(n)){const t=n.toLowerCase();return t==="default"?e:t==="local"||t==="system"?zo.instance:t==="utc"||t==="gmt"?un.utcInstance:un.parseSpecifier(t)||pi.create(n)}else return Ji(n)?un.instance(n):typeof n=="object"&&"offset"in n&&typeof n.offset=="function"?n:new wk(n)}let uf=()=>Date.now(),cf="system",df=null,pf=null,mf=null,hf=60,_f,gf=null;class qt{static get now(){return uf}static set now(e){uf=e}static set defaultZone(e){cf=e}static get defaultZone(){return Si(cf,zo.instance)}static get defaultLocale(){return df}static set defaultLocale(e){df=e}static get defaultNumberingSystem(){return pf}static set defaultNumberingSystem(e){pf=e}static get defaultOutputCalendar(){return mf}static set defaultOutputCalendar(e){mf=e}static get defaultWeekSettings(){return gf}static set defaultWeekSettings(e){gf=Vr(e)}static get twoDigitCutoffYear(){return hf}static set twoDigitCutoffYear(e){hf=e%100}static get throwOnInvalid(){return _f}static set throwOnInvalid(e){_f=e}static resetCaches(){kt.resetCache(),pi.resetCache()}}class zn{constructor(e,t){this.reason=e,this.explanation=t}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}}const c1=[0,31,59,90,120,151,181,212,243,273,304,334],d1=[0,31,60,91,121,152,182,213,244,274,305,335];function Nn(n,e){return new zn("unit out of range",`you specified ${e} (of type ${typeof e}) as a ${n}, which is invalid`)}function ma(n,e,t){const i=new Date(Date.UTC(n,e-1,t));n<100&&n>=0&&i.setUTCFullYear(i.getUTCFullYear()-1900);const l=i.getUTCDay();return l===0?7:l}function p1(n,e,t){return t+(vs(n)?d1:c1)[e-1]}function m1(n,e){const t=vs(n)?d1:c1,i=t.findIndex(s=>sos(i,e,t)?(f=i+1,a=1):f=i,{weekYear:f,weekNumber:a,weekday:r,...Wo(n)}}function bf(n,e=4,t=1){const{weekYear:i,weekNumber:l,weekday:s}=n,o=ha(ma(i,1,e),t),r=yl(i);let a=l*7+s-o-7+e,f;a<1?(f=i-1,a+=yl(f)):a>r?(f=i+1,a-=yl(i)):f=i;const{month:u,day:c}=m1(f,a);return{year:f,month:u,day:c,...Wo(n)}}function sr(n){const{year:e,month:t,day:i}=n,l=p1(e,t,i);return{year:e,ordinal:l,...Wo(n)}}function kf(n){const{year:e,ordinal:t}=n,{month:i,day:l}=m1(e,t);return{year:e,month:i,day:l,...Wo(n)}}function yf(n,e){if(!We(n.localWeekday)||!We(n.localWeekNumber)||!We(n.localWeekYear)){if(!We(n.weekday)||!We(n.weekNumber)||!We(n.weekYear))throw new gl("Cannot mix locale-based week fields with ISO-based week fields");return We(n.localWeekday)||(n.weekday=n.localWeekday),We(n.localWeekNumber)||(n.weekNumber=n.localWeekNumber),We(n.localWeekYear)||(n.weekYear=n.localWeekYear),delete n.localWeekday,delete n.localWeekNumber,delete n.localWeekYear,{minDaysInFirstWeek:e.getMinDaysInFirstWeek(),startOfWeek:e.getStartOfWeek()}}else return{minDaysInFirstWeek:4,startOfWeek:1}}function Sk(n,e=4,t=1){const i=Vo(n.weekYear),l=Pn(n.weekNumber,1,os(n.weekYear,e,t)),s=Pn(n.weekday,1,7);return i?l?s?!1:Nn("weekday",n.weekday):Nn("week",n.weekNumber):Nn("weekYear",n.weekYear)}function $k(n){const e=Vo(n.year),t=Pn(n.ordinal,1,yl(n.year));return e?t?!1:Nn("ordinal",n.ordinal):Nn("year",n.year)}function h1(n){const e=Vo(n.year),t=Pn(n.month,1,12),i=Pn(n.day,1,wo(n.year,n.month));return e?t?i?!1:Nn("day",n.day):Nn("month",n.month):Nn("year",n.year)}function _1(n){const{hour:e,minute:t,second:i,millisecond:l}=n,s=Pn(e,0,23)||e===24&&t===0&&i===0&&l===0,o=Pn(t,0,59),r=Pn(i,0,59),a=Pn(l,0,999);return s?o?r?a?!1:Nn("millisecond",l):Nn("second",i):Nn("minute",t):Nn("hour",e)}function We(n){return typeof n>"u"}function Ji(n){return typeof n=="number"}function Vo(n){return typeof n=="number"&&n%1===0}function Tk(n){return typeof n=="string"}function Ck(n){return Object.prototype.toString.call(n)==="[object Date]"}function g1(){try{return typeof Intl<"u"&&!!Intl.RelativeTimeFormat}catch{return!1}}function b1(){try{return typeof Intl<"u"&&!!Intl.Locale&&("weekInfo"in Intl.Locale.prototype||"getWeekInfo"in Intl.Locale.prototype)}catch{return!1}}function Ok(n){return Array.isArray(n)?n:[n]}function vf(n,e,t){if(n.length!==0)return n.reduce((i,l)=>{const s=[e(l),l];return i&&t(i[0],s[0])===i[0]?i:s},null)[1]}function Mk(n,e){return e.reduce((t,i)=>(t[i]=n[i],t),{})}function Tl(n,e){return Object.prototype.hasOwnProperty.call(n,e)}function Vr(n){if(n==null)return null;if(typeof n!="object")throw new mn("Week settings must be an object");if(!Pn(n.firstDay,1,7)||!Pn(n.minimalDays,1,7)||!Array.isArray(n.weekend)||n.weekend.some(e=>!Pn(e,1,7)))throw new mn("Invalid week settings");return{firstDay:n.firstDay,minimalDays:n.minimalDays,weekend:Array.from(n.weekend)}}function Pn(n,e,t){return Vo(n)&&n>=e&&n<=t}function Dk(n,e){return n-e*Math.floor(n/e)}function Vt(n,e=2){const t=n<0;let i;return t?i="-"+(""+-n).padStart(e,"0"):i=(""+n).padStart(e,"0"),i}function vi(n){if(!(We(n)||n===null||n===""))return parseInt(n,10)}function Ni(n){if(!(We(n)||n===null||n===""))return parseFloat(n)}function _a(n){if(!(We(n)||n===null||n==="")){const e=parseFloat("0."+n)*1e3;return Math.floor(e)}}function ga(n,e,t=!1){const i=10**e;return(t?Math.trunc:Math.round)(n*i)/i}function vs(n){return n%4===0&&(n%100!==0||n%400===0)}function yl(n){return vs(n)?366:365}function wo(n,e){const t=Dk(e-1,12)+1,i=n+(e-t)/12;return t===2?vs(i)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][t-1]}function Bo(n){let e=Date.UTC(n.year,n.month-1,n.day,n.hour,n.minute,n.second,n.millisecond);return n.year<100&&n.year>=0&&(e=new Date(e),e.setUTCFullYear(n.year,n.month-1,n.day)),+e}function wf(n,e,t){return-ha(ma(n,1,e),t)+e-1}function os(n,e=4,t=1){const i=wf(n,e,t),l=wf(n+1,e,t);return(yl(n)-i+l)/7}function Br(n){return n>99?n:n>qt.twoDigitCutoffYear?1900+n:2e3+n}function k1(n,e,t,i=null){const l=new Date(n),s={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};i&&(s.timeZone=i);const o={timeZoneName:e,...s},r=new Intl.DateTimeFormat(t,o).formatToParts(l).find(a=>a.type.toLowerCase()==="timezonename");return r?r.value:null}function Uo(n,e){let t=parseInt(n,10);Number.isNaN(t)&&(t=0);const i=parseInt(e,10)||0,l=t<0||Object.is(t,-0)?-i:i;return t*60+l}function y1(n){const e=Number(n);if(typeof n=="boolean"||n===""||Number.isNaN(e))throw new mn(`Invalid unit value ${n}`);return e}function So(n,e){const t={};for(const i in n)if(Tl(n,i)){const l=n[i];if(l==null)continue;t[e(i)]=y1(l)}return t}function Zl(n,e){const t=Math.trunc(Math.abs(n/60)),i=Math.trunc(Math.abs(n%60)),l=n>=0?"+":"-";switch(e){case"short":return`${l}${Vt(t,2)}:${Vt(i,2)}`;case"narrow":return`${l}${t}${i>0?`:${i}`:""}`;case"techie":return`${l}${Vt(t,2)}${Vt(i,2)}`;default:throw new RangeError(`Value format ${e} is out of range for property format`)}}function Wo(n){return Mk(n,["hour","minute","second","millisecond"])}const Ek=["January","February","March","April","May","June","July","August","September","October","November","December"],v1=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Ik=["J","F","M","A","M","J","J","A","S","O","N","D"];function w1(n){switch(n){case"narrow":return[...Ik];case"short":return[...v1];case"long":return[...Ek];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}const S1=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],$1=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],Ak=["M","T","W","T","F","S","S"];function T1(n){switch(n){case"narrow":return[...Ak];case"short":return[...$1];case"long":return[...S1];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}const C1=["AM","PM"],Lk=["Before Christ","Anno Domini"],Nk=["BC","AD"],Pk=["B","A"];function O1(n){switch(n){case"narrow":return[...Pk];case"short":return[...Nk];case"long":return[...Lk];default:return null}}function Fk(n){return C1[n.hour<12?0:1]}function Rk(n,e){return T1(e)[n.weekday-1]}function qk(n,e){return w1(e)[n.month-1]}function jk(n,e){return O1(e)[n.year<0?0:1]}function Hk(n,e,t="always",i=!1){const l={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},s=["hours","minutes","seconds"].indexOf(n)===-1;if(t==="auto"&&s){const c=n==="days";switch(e){case 1:return c?"tomorrow":`next ${l[n][0]}`;case-1:return c?"yesterday":`last ${l[n][0]}`;case 0:return c?"today":`this ${l[n][0]}`}}const o=Object.is(e,-0)||e<0,r=Math.abs(e),a=r===1,f=l[n],u=i?a?f[1]:f[2]||f[1]:a?l[n][0]:n;return o?`${r} ${u} ago`:`in ${r} ${u}`}function Sf(n,e){let t="";for(const i of n)i.literal?t+=i.val:t+=e(i.val);return t}const zk={D:yo,DD:Yg,DDD:Kg,DDDD:Jg,t:Zg,tt:Gg,ttt:Xg,tttt:Qg,T:xg,TT:e1,TTT:t1,TTTT:n1,f:i1,ff:s1,fff:r1,ffff:f1,F:l1,FF:o1,FFF:a1,FFFF:u1};class ln{static create(e,t={}){return new ln(e,t)}static parseFormat(e){let t=null,i="",l=!1;const s=[];for(let o=0;o0&&s.push({literal:l||/^\s+$/.test(i),val:i}),t=null,i="",l=!l):l||r===t?i+=r:(i.length>0&&s.push({literal:/^\s+$/.test(i),val:i}),i=r,t=r)}return i.length>0&&s.push({literal:l||/^\s+$/.test(i),val:i}),s}static macroTokenToFormatOpts(e){return zk[e]}constructor(e,t){this.opts=t,this.loc=e,this.systemLoc=null}formatWithSystemDefault(e,t){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(e,{...this.opts,...t}).format()}dtFormatter(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t})}formatDateTime(e,t){return this.dtFormatter(e,t).format()}formatDateTimeParts(e,t){return this.dtFormatter(e,t).formatToParts()}formatInterval(e,t){return this.dtFormatter(e.start,t).dtf.formatRange(e.start.toJSDate(),e.end.toJSDate())}resolvedOptions(e,t){return this.dtFormatter(e,t).resolvedOptions()}num(e,t=0){if(this.opts.forceSimple)return Vt(e,t);const i={...this.opts};return t>0&&(i.padTo=t),this.loc.numberFormatter(i).format(e)}formatDateTimeFromString(e,t){const i=this.loc.listingMode()==="en",l=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",s=(m,h)=>this.loc.extract(e,m,h),o=m=>e.isOffsetFixed&&e.offset===0&&m.allowZ?"Z":e.isValid?e.zone.formatOffset(e.ts,m.format):"",r=()=>i?Fk(e):s({hour:"numeric",hourCycle:"h12"},"dayperiod"),a=(m,h)=>i?qk(e,m):s(h?{month:m}:{month:m,day:"numeric"},"month"),f=(m,h)=>i?Rk(e,m):s(h?{weekday:m}:{weekday:m,month:"long",day:"numeric"},"weekday"),u=m=>{const h=ln.macroTokenToFormatOpts(m);return h?this.formatWithSystemDefault(e,h):m},c=m=>i?jk(e,m):s({era:m},"era"),d=m=>{switch(m){case"S":return this.num(e.millisecond);case"u":case"SSS":return this.num(e.millisecond,3);case"s":return this.num(e.second);case"ss":return this.num(e.second,2);case"uu":return this.num(Math.floor(e.millisecond/10),2);case"uuu":return this.num(Math.floor(e.millisecond/100));case"m":return this.num(e.minute);case"mm":return this.num(e.minute,2);case"h":return this.num(e.hour%12===0?12:e.hour%12);case"hh":return this.num(e.hour%12===0?12:e.hour%12,2);case"H":return this.num(e.hour);case"HH":return this.num(e.hour,2);case"Z":return o({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return o({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return o({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return e.zone.offsetName(e.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return e.zone.offsetName(e.ts,{format:"long",locale:this.loc.locale});case"z":return e.zoneName;case"a":return r();case"d":return l?s({day:"numeric"},"day"):this.num(e.day);case"dd":return l?s({day:"2-digit"},"day"):this.num(e.day,2);case"c":return this.num(e.weekday);case"ccc":return f("short",!0);case"cccc":return f("long",!0);case"ccccc":return f("narrow",!0);case"E":return this.num(e.weekday);case"EEE":return f("short",!1);case"EEEE":return f("long",!1);case"EEEEE":return f("narrow",!1);case"L":return l?s({month:"numeric",day:"numeric"},"month"):this.num(e.month);case"LL":return l?s({month:"2-digit",day:"numeric"},"month"):this.num(e.month,2);case"LLL":return a("short",!0);case"LLLL":return a("long",!0);case"LLLLL":return a("narrow",!0);case"M":return l?s({month:"numeric"},"month"):this.num(e.month);case"MM":return l?s({month:"2-digit"},"month"):this.num(e.month,2);case"MMM":return a("short",!1);case"MMMM":return a("long",!1);case"MMMMM":return a("narrow",!1);case"y":return l?s({year:"numeric"},"year"):this.num(e.year);case"yy":return l?s({year:"2-digit"},"year"):this.num(e.year.toString().slice(-2),2);case"yyyy":return l?s({year:"numeric"},"year"):this.num(e.year,4);case"yyyyyy":return l?s({year:"numeric"},"year"):this.num(e.year,6);case"G":return c("short");case"GG":return c("long");case"GGGGG":return c("narrow");case"kk":return this.num(e.weekYear.toString().slice(-2),2);case"kkkk":return this.num(e.weekYear,4);case"W":return this.num(e.weekNumber);case"WW":return this.num(e.weekNumber,2);case"n":return this.num(e.localWeekNumber);case"nn":return this.num(e.localWeekNumber,2);case"ii":return this.num(e.localWeekYear.toString().slice(-2),2);case"iiii":return this.num(e.localWeekYear,4);case"o":return this.num(e.ordinal);case"ooo":return this.num(e.ordinal,3);case"q":return this.num(e.quarter);case"qq":return this.num(e.quarter,2);case"X":return this.num(Math.floor(e.ts/1e3));case"x":return this.num(e.ts);default:return u(m)}};return Sf(ln.parseFormat(t),d)}formatDurationFromString(e,t){const i=a=>{switch(a[0]){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":return"hour";case"d":return"day";case"w":return"week";case"M":return"month";case"y":return"year";default:return null}},l=a=>f=>{const u=i(f);return u?this.num(a.get(u),f.length):f},s=ln.parseFormat(t),o=s.reduce((a,{literal:f,val:u})=>f?a:a.concat(u),[]),r=e.shiftTo(...o.map(i).filter(a=>a));return Sf(s,l(r))}}const M1=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;function El(...n){const e=n.reduce((t,i)=>t+i.source,"");return RegExp(`^${e}$`)}function Il(...n){return e=>n.reduce(([t,i,l],s)=>{const[o,r,a]=s(e,l);return[{...t,...o},r||i,a]},[{},null,1]).slice(0,2)}function Al(n,...e){if(n==null)return[null,null];for(const[t,i]of e){const l=t.exec(n);if(l)return i(l)}return[null,null]}function D1(...n){return(e,t)=>{const i={};let l;for(l=0;lm!==void 0&&(h||m&&u)?-m:m;return[{years:d(Ni(t)),months:d(Ni(i)),weeks:d(Ni(l)),days:d(Ni(s)),hours:d(Ni(o)),minutes:d(Ni(r)),seconds:d(Ni(a),a==="-0"),milliseconds:d(_a(f),c)}]}const ey={GMT:0,EDT:-4*60,EST:-5*60,CDT:-5*60,CST:-6*60,MDT:-6*60,MST:-7*60,PDT:-7*60,PST:-8*60};function ya(n,e,t,i,l,s,o){const r={year:e.length===2?Br(vi(e)):vi(e),month:v1.indexOf(t)+1,day:vi(i),hour:vi(l),minute:vi(s)};return o&&(r.second=vi(o)),n&&(r.weekday=n.length>3?S1.indexOf(n)+1:$1.indexOf(n)+1),r}const ty=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function ny(n){const[,e,t,i,l,s,o,r,a,f,u,c]=n,d=ya(e,l,i,t,s,o,r);let m;return a?m=ey[a]:f?m=0:m=Uo(u,c),[d,new un(m)]}function iy(n){return n.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}const ly=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,sy=/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,oy=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function $f(n){const[,e,t,i,l,s,o,r]=n;return[ya(e,l,i,t,s,o,r),un.utcInstance]}function ry(n){const[,e,t,i,l,s,o,r]=n;return[ya(e,r,t,i,l,s,o),un.utcInstance]}const ay=El(Bk,ka),fy=El(Uk,ka),uy=El(Wk,ka),cy=El(I1),L1=Il(Gk,Ll,ws,Ss),dy=Il(Yk,Ll,ws,Ss),py=Il(Kk,Ll,ws,Ss),my=Il(Ll,ws,Ss);function hy(n){return Al(n,[ay,L1],[fy,dy],[uy,py],[cy,my])}function _y(n){return Al(iy(n),[ty,ny])}function gy(n){return Al(n,[ly,$f],[sy,$f],[oy,ry])}function by(n){return Al(n,[Qk,xk])}const ky=Il(Ll);function yy(n){return Al(n,[Xk,ky])}const vy=El(Jk,Zk),wy=El(A1),Sy=Il(Ll,ws,Ss);function $y(n){return Al(n,[vy,L1],[wy,Sy])}const Tf="Invalid Duration",N1={weeks:{days:7,hours:7*24,minutes:7*24*60,seconds:7*24*60*60,milliseconds:7*24*60*60*1e3},days:{hours:24,minutes:24*60,seconds:24*60*60,milliseconds:24*60*60*1e3},hours:{minutes:60,seconds:60*60,milliseconds:60*60*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},Ty={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:91*24,minutes:91*24*60,seconds:91*24*60*60,milliseconds:91*24*60*60*1e3},months:{weeks:4,days:30,hours:30*24,minutes:30*24*60,seconds:30*24*60*60,milliseconds:30*24*60*60*1e3},...N1},Dn=146097/400,pl=146097/4800,Cy={years:{quarters:4,months:12,weeks:Dn/7,days:Dn,hours:Dn*24,minutes:Dn*24*60,seconds:Dn*24*60*60,milliseconds:Dn*24*60*60*1e3},quarters:{months:3,weeks:Dn/28,days:Dn/4,hours:Dn*24/4,minutes:Dn*24*60/4,seconds:Dn*24*60*60/4,milliseconds:Dn*24*60*60*1e3/4},months:{weeks:pl/7,days:pl,hours:pl*24,minutes:pl*24*60,seconds:pl*24*60*60,milliseconds:pl*24*60*60*1e3},...N1},Ui=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],Oy=Ui.slice(0).reverse();function yi(n,e,t=!1){const i={values:t?e.values:{...n.values,...e.values||{}},loc:n.loc.clone(e.loc),conversionAccuracy:e.conversionAccuracy||n.conversionAccuracy,matrix:e.matrix||n.matrix};return new st(i)}function P1(n,e){let t=e.milliseconds??0;for(const i of Oy.slice(1))e[i]&&(t+=e[i]*n[i].milliseconds);return t}function Cf(n,e){const t=P1(n,e)<0?-1:1;Ui.reduceRight((i,l)=>{if(We(e[l]))return i;if(i){const s=e[i]*t,o=n[l][i],r=Math.floor(s/o);e[l]+=r*t,e[i]-=r*o*t}return l},null),Ui.reduce((i,l)=>{if(We(e[l]))return i;if(i){const s=e[i]%1;e[i]-=s,e[l]+=s*n[i][l]}return l},null)}function My(n){const e={};for(const[t,i]of Object.entries(n))i!==0&&(e[t]=i);return e}class st{constructor(e){const t=e.conversionAccuracy==="longterm"||!1;let i=t?Cy:Ty;e.matrix&&(i=e.matrix),this.values=e.values,this.loc=e.loc||kt.create(),this.conversionAccuracy=t?"longterm":"casual",this.invalid=e.invalid||null,this.matrix=i,this.isLuxonDuration=!0}static fromMillis(e,t){return st.fromObject({milliseconds:e},t)}static fromObject(e,t={}){if(e==null||typeof e!="object")throw new mn(`Duration.fromObject: argument expected to be an object, got ${e===null?"null":typeof e}`);return new st({values:So(e,st.normalizeUnit),loc:kt.fromObject(t),conversionAccuracy:t.conversionAccuracy,matrix:t.matrix})}static fromDurationLike(e){if(Ji(e))return st.fromMillis(e);if(st.isDuration(e))return e;if(typeof e=="object")return st.fromObject(e);throw new mn(`Unknown duration argument ${e} of type ${typeof e}`)}static fromISO(e,t){const[i]=by(e);return i?st.fromObject(i,t):st.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static fromISOTime(e,t){const[i]=yy(e);return i?st.fromObject(i,t):st.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static invalid(e,t=null){if(!e)throw new mn("need to specify a reason the Duration is invalid");const i=e instanceof zn?e:new zn(e,t);if(qt.throwOnInvalid)throw new tk(i);return new st({invalid:i})}static normalizeUnit(e){const t={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[e&&e.toLowerCase()];if(!t)throw new Wg(e);return t}static isDuration(e){return e&&e.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(e,t={}){const i={...t,floor:t.round!==!1&&t.floor!==!1};return this.isValid?ln.create(this.loc,i).formatDurationFromString(this,e):Tf}toHuman(e={}){if(!this.isValid)return Tf;const t=Ui.map(i=>{const l=this.values[i];return We(l)?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...e,unit:i.slice(0,-1)}).format(l)}).filter(i=>i);return this.loc.listFormatter({type:"conjunction",style:e.listStyle||"narrow",...e}).format(t)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let e="P";return this.years!==0&&(e+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(e+=this.months+this.quarters*3+"M"),this.weeks!==0&&(e+=this.weeks+"W"),this.days!==0&&(e+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(e+="T"),this.hours!==0&&(e+=this.hours+"H"),this.minutes!==0&&(e+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(e+=ga(this.seconds+this.milliseconds/1e3,3)+"S"),e==="P"&&(e+="T0S"),e}toISOTime(e={}){if(!this.isValid)return null;const t=this.toMillis();return t<0||t>=864e5?null:(e={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...e,includeOffset:!1},je.fromMillis(t,{zone:"UTC"}).toISOTime(e))}toJSON(){return this.toISO()}toString(){return this.toISO()}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Duration { values: ${JSON.stringify(this.values)} }`:`Duration { Invalid, reason: ${this.invalidReason} }`}toMillis(){return this.isValid?P1(this.matrix,this.values):NaN}valueOf(){return this.toMillis()}plus(e){if(!this.isValid)return this;const t=st.fromDurationLike(e),i={};for(const l of Ui)(Tl(t.values,l)||Tl(this.values,l))&&(i[l]=t.get(l)+this.get(l));return yi(this,{values:i},!0)}minus(e){if(!this.isValid)return this;const t=st.fromDurationLike(e);return this.plus(t.negate())}mapUnits(e){if(!this.isValid)return this;const t={};for(const i of Object.keys(this.values))t[i]=y1(e(this.values[i],i));return yi(this,{values:t},!0)}get(e){return this[st.normalizeUnit(e)]}set(e){if(!this.isValid)return this;const t={...this.values,...So(e,st.normalizeUnit)};return yi(this,{values:t})}reconfigure({locale:e,numberingSystem:t,conversionAccuracy:i,matrix:l}={}){const o={loc:this.loc.clone({locale:e,numberingSystem:t}),matrix:l,conversionAccuracy:i};return yi(this,o)}as(e){return this.isValid?this.shiftTo(e).get(e):NaN}normalize(){if(!this.isValid)return this;const e=this.toObject();return Cf(this.matrix,e),yi(this,{values:e},!0)}rescale(){if(!this.isValid)return this;const e=My(this.normalize().shiftToAll().toObject());return yi(this,{values:e},!0)}shiftTo(...e){if(!this.isValid)return this;if(e.length===0)return this;e=e.map(o=>st.normalizeUnit(o));const t={},i={},l=this.toObject();let s;for(const o of Ui)if(e.indexOf(o)>=0){s=o;let r=0;for(const f in i)r+=this.matrix[f][o]*i[f],i[f]=0;Ji(l[o])&&(r+=l[o]);const a=Math.trunc(r);t[o]=a,i[o]=(r*1e3-a*1e3)/1e3}else Ji(l[o])&&(i[o]=l[o]);for(const o in i)i[o]!==0&&(t[s]+=o===s?i[o]:i[o]/this.matrix[s][o]);return Cf(this.matrix,t),yi(this,{values:t},!0)}shiftToAll(){return this.isValid?this.shiftTo("years","months","weeks","days","hours","minutes","seconds","milliseconds"):this}negate(){if(!this.isValid)return this;const e={};for(const t of Object.keys(this.values))e[t]=this.values[t]===0?0:-this.values[t];return yi(this,{values:e},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(e){if(!this.isValid||!e.isValid||!this.loc.equals(e.loc))return!1;function t(i,l){return i===void 0||i===0?l===void 0||l===0:i===l}for(const i of Ui)if(!t(this.values[i],e.values[i]))return!1;return!0}}const ml="Invalid Interval";function Dy(n,e){return!n||!n.isValid?Ft.invalid("missing or invalid start"):!e||!e.isValid?Ft.invalid("missing or invalid end"):ee:!1}isBefore(e){return this.isValid?this.e<=e:!1}contains(e){return this.isValid?this.s<=e&&this.e>e:!1}set({start:e,end:t}={}){return this.isValid?Ft.fromDateTimes(e||this.s,t||this.e):this}splitAt(...e){if(!this.isValid)return[];const t=e.map(jl).filter(o=>this.contains(o)).sort((o,r)=>o.toMillis()-r.toMillis()),i=[];let{s:l}=this,s=0;for(;l+this.e?this.e:o;i.push(Ft.fromDateTimes(l,r)),l=r,s+=1}return i}splitBy(e){const t=st.fromDurationLike(e);if(!this.isValid||!t.isValid||t.as("milliseconds")===0)return[];let{s:i}=this,l=1,s;const o=[];for(;ia*l));s=+r>+this.e?this.e:r,o.push(Ft.fromDateTimes(i,s)),i=s,l+=1}return o}divideEqually(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]}overlaps(e){return this.e>e.s&&this.s=e.e:!1}equals(e){return!this.isValid||!e.isValid?!1:this.s.equals(e.s)&&this.e.equals(e.e)}intersection(e){if(!this.isValid)return this;const t=this.s>e.s?this.s:e.s,i=this.e=i?null:Ft.fromDateTimes(t,i)}union(e){if(!this.isValid)return this;const t=this.se.e?this.e:e.e;return Ft.fromDateTimes(t,i)}static merge(e){const[t,i]=e.sort((l,s)=>l.s-s.s).reduce(([l,s],o)=>s?s.overlaps(o)||s.abutsStart(o)?[l,s.union(o)]:[l.concat([s]),o]:[l,o],[[],null]);return i&&t.push(i),t}static xor(e){let t=null,i=0;const l=[],s=e.map(a=>[{time:a.s,type:"s"},{time:a.e,type:"e"}]),o=Array.prototype.concat(...s),r=o.sort((a,f)=>a.time-f.time);for(const a of r)i+=a.type==="s"?1:-1,i===1?t=a.time:(t&&+t!=+a.time&&l.push(Ft.fromDateTimes(t,a.time)),t=null);return Ft.merge(l)}difference(...e){return Ft.xor([this].concat(e)).map(t=>this.intersection(t)).filter(t=>t&&!t.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} – ${this.e.toISO()})`:ml}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`:`Interval { Invalid, reason: ${this.invalidReason} }`}toLocaleString(e=yo,t={}){return this.isValid?ln.create(this.s.loc.clone(t),e).formatInterval(this):ml}toISO(e){return this.isValid?`${this.s.toISO(e)}/${this.e.toISO(e)}`:ml}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:ml}toISOTime(e){return this.isValid?`${this.s.toISOTime(e)}/${this.e.toISOTime(e)}`:ml}toFormat(e,{separator:t=" – "}={}){return this.isValid?`${this.s.toFormat(e)}${t}${this.e.toFormat(e)}`:ml}toDuration(e,t){return this.isValid?this.e.diff(this.s,e,t):st.invalid(this.invalidReason)}mapEndpoints(e){return Ft.fromDateTimes(e(this.s),e(this.e))}}class Ls{static hasDST(e=qt.defaultZone){const t=je.now().setZone(e).set({month:12});return!e.isUniversal&&t.offset!==t.set({month:6}).offset}static isValidIANAZone(e){return pi.isValidZone(e)}static normalizeZone(e){return Si(e,qt.defaultZone)}static getStartOfWeek({locale:e=null,locObj:t=null}={}){return(t||kt.create(e)).getStartOfWeek()}static getMinimumDaysInFirstWeek({locale:e=null,locObj:t=null}={}){return(t||kt.create(e)).getMinDaysInFirstWeek()}static getWeekendWeekdays({locale:e=null,locObj:t=null}={}){return(t||kt.create(e)).getWeekendDays().slice()}static months(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null,outputCalendar:s="gregory"}={}){return(l||kt.create(t,i,s)).months(e)}static monthsFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null,outputCalendar:s="gregory"}={}){return(l||kt.create(t,i,s)).months(e,!0)}static weekdays(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null}={}){return(l||kt.create(t,i,null)).weekdays(e)}static weekdaysFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:l=null}={}){return(l||kt.create(t,i,null)).weekdays(e,!0)}static meridiems({locale:e=null}={}){return kt.create(e).meridiems()}static eras(e="short",{locale:t=null}={}){return kt.create(t,null,"gregory").eras(e)}static features(){return{relative:g1(),localeWeek:b1()}}}function Of(n,e){const t=l=>l.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),i=t(e)-t(n);return Math.floor(st.fromMillis(i).as("days"))}function Ey(n,e,t){const i=[["years",(a,f)=>f.year-a.year],["quarters",(a,f)=>f.quarter-a.quarter+(f.year-a.year)*4],["months",(a,f)=>f.month-a.month+(f.year-a.year)*12],["weeks",(a,f)=>{const u=Of(a,f);return(u-u%7)/7}],["days",Of]],l={},s=n;let o,r;for(const[a,f]of i)t.indexOf(a)>=0&&(o=a,l[a]=f(n,e),r=s.plus(l),r>e?(l[a]--,n=s.plus(l),n>e&&(r=n,l[a]--,n=s.plus(l))):n=r);return[n,l,r,o]}function Iy(n,e,t,i){let[l,s,o,r]=Ey(n,e,t);const a=e-l,f=t.filter(c=>["hours","minutes","seconds","milliseconds"].indexOf(c)>=0);f.length===0&&(o0?st.fromMillis(a,i).shiftTo(...f).plus(u):u}const va={arab:"[٠-٩]",arabext:"[۰-۹]",bali:"[᭐-᭙]",beng:"[০-৯]",deva:"[०-९]",fullwide:"[0-9]",gujr:"[૦-૯]",hanidec:"[〇|一|二|三|四|五|六|七|八|九]",khmr:"[០-៩]",knda:"[೦-೯]",laoo:"[໐-໙]",limb:"[᥆-᥏]",mlym:"[൦-൯]",mong:"[᠐-᠙]",mymr:"[၀-၉]",orya:"[୦-୯]",tamldec:"[௦-௯]",telu:"[౦-౯]",thai:"[๐-๙]",tibt:"[༠-༩]",latn:"\\d"},Mf={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},Ay=va.hanidec.replace(/[\[|\]]/g,"").split("");function Ly(n){let e=parseInt(n,10);if(isNaN(e)){e="";for(let t=0;t=s&&i<=o&&(e+=i-s)}}return parseInt(e,10)}else return e}function jn({numberingSystem:n},e=""){return new RegExp(`${va[n||"latn"]}${e}`)}const Ny="missing Intl.DateTimeFormat.formatToParts support";function ct(n,e=t=>t){return{regex:n,deser:([t])=>e(Ly(t))}}const Py=" ",F1=`[ ${Py}]`,R1=new RegExp(F1,"g");function Fy(n){return n.replace(/\./g,"\\.?").replace(R1,F1)}function Df(n){return n.replace(/\./g,"").replace(R1," ").toLowerCase()}function Hn(n,e){return n===null?null:{regex:RegExp(n.map(Fy).join("|")),deser:([t])=>n.findIndex(i=>Df(t)===Df(i))+e}}function Ef(n,e){return{regex:n,deser:([,t,i])=>Uo(t,i),groups:e}}function Ns(n){return{regex:n,deser:([e])=>e}}function Ry(n){return n.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function qy(n,e){const t=jn(e),i=jn(e,"{2}"),l=jn(e,"{3}"),s=jn(e,"{4}"),o=jn(e,"{6}"),r=jn(e,"{1,2}"),a=jn(e,"{1,3}"),f=jn(e,"{1,6}"),u=jn(e,"{1,9}"),c=jn(e,"{2,4}"),d=jn(e,"{4,6}"),m=g=>({regex:RegExp(Ry(g.val)),deser:([y])=>y,literal:!0}),_=(g=>{if(n.literal)return m(g);switch(g.val){case"G":return Hn(e.eras("short"),0);case"GG":return Hn(e.eras("long"),0);case"y":return ct(f);case"yy":return ct(c,Br);case"yyyy":return ct(s);case"yyyyy":return ct(d);case"yyyyyy":return ct(o);case"M":return ct(r);case"MM":return ct(i);case"MMM":return Hn(e.months("short",!0),1);case"MMMM":return Hn(e.months("long",!0),1);case"L":return ct(r);case"LL":return ct(i);case"LLL":return Hn(e.months("short",!1),1);case"LLLL":return Hn(e.months("long",!1),1);case"d":return ct(r);case"dd":return ct(i);case"o":return ct(a);case"ooo":return ct(l);case"HH":return ct(i);case"H":return ct(r);case"hh":return ct(i);case"h":return ct(r);case"mm":return ct(i);case"m":return ct(r);case"q":return ct(r);case"qq":return ct(i);case"s":return ct(r);case"ss":return ct(i);case"S":return ct(a);case"SSS":return ct(l);case"u":return Ns(u);case"uu":return Ns(r);case"uuu":return ct(t);case"a":return Hn(e.meridiems(),0);case"kkkk":return ct(s);case"kk":return ct(c,Br);case"W":return ct(r);case"WW":return ct(i);case"E":case"c":return ct(t);case"EEE":return Hn(e.weekdays("short",!1),1);case"EEEE":return Hn(e.weekdays("long",!1),1);case"ccc":return Hn(e.weekdays("short",!0),1);case"cccc":return Hn(e.weekdays("long",!0),1);case"Z":case"ZZ":return Ef(new RegExp(`([+-]${r.source})(?::(${i.source}))?`),2);case"ZZZ":return Ef(new RegExp(`([+-]${r.source})(${i.source})?`),2);case"z":return Ns(/[a-z_+-/]{1,256}?/i);case" ":return Ns(/[^\S\n\r]/);default:return m(g)}})(n)||{invalidReason:Ny};return _.token=n,_}const jy={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour12:{numeric:"h","2-digit":"hh"},hour24:{numeric:"H","2-digit":"HH"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"},timeZoneName:{long:"ZZZZZ",short:"ZZZ"}};function Hy(n,e,t){const{type:i,value:l}=n;if(i==="literal"){const a=/^\s+$/.test(l);return{literal:!a,val:a?" ":l}}const s=e[i];let o=i;i==="hour"&&(e.hour12!=null?o=e.hour12?"hour12":"hour24":e.hourCycle!=null?e.hourCycle==="h11"||e.hourCycle==="h12"?o="hour12":o="hour24":o=t.hour12?"hour12":"hour24");let r=jy[o];if(typeof r=="object"&&(r=r[s]),r)return{literal:!1,val:r}}function zy(n){return[`^${n.map(t=>t.regex).reduce((t,i)=>`${t}(${i.source})`,"")}$`,n]}function Vy(n,e,t){const i=n.match(e);if(i){const l={};let s=1;for(const o in t)if(Tl(t,o)){const r=t[o],a=r.groups?r.groups+1:1;!r.literal&&r.token&&(l[r.token.val[0]]=r.deser(i.slice(s,s+a))),s+=a}return[i,l]}else return[i,{}]}function By(n){const e=s=>{switch(s){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}};let t=null,i;return We(n.z)||(t=pi.create(n.z)),We(n.Z)||(t||(t=new un(n.Z)),i=n.Z),We(n.q)||(n.M=(n.q-1)*3+1),We(n.h)||(n.h<12&&n.a===1?n.h+=12:n.h===12&&n.a===0&&(n.h=0)),n.G===0&&n.y&&(n.y=-n.y),We(n.u)||(n.S=_a(n.u)),[Object.keys(n).reduce((s,o)=>{const r=e(o);return r&&(s[r]=n[o]),s},{}),t,i]}let or=null;function Uy(){return or||(or=je.fromMillis(1555555555555)),or}function Wy(n,e){if(n.literal)return n;const t=ln.macroTokenToFormatOpts(n.val),i=H1(t,e);return i==null||i.includes(void 0)?n:i}function q1(n,e){return Array.prototype.concat(...n.map(t=>Wy(t,e)))}function j1(n,e,t){const i=q1(ln.parseFormat(t),n),l=i.map(o=>qy(o,n)),s=l.find(o=>o.invalidReason);if(s)return{input:e,tokens:i,invalidReason:s.invalidReason};{const[o,r]=zy(l),a=RegExp(o,"i"),[f,u]=Vy(e,a,r),[c,d,m]=u?By(u):[null,null,void 0];if(Tl(u,"a")&&Tl(u,"H"))throw new gl("Can't include meridiem when specifying 24-hour format");return{input:e,tokens:i,regex:a,rawMatches:f,matches:u,result:c,zone:d,specificOffset:m}}}function Yy(n,e,t){const{result:i,zone:l,specificOffset:s,invalidReason:o}=j1(n,e,t);return[i,l,s,o]}function H1(n,e){if(!n)return null;const i=ln.create(e,n).dtFormatter(Uy()),l=i.formatToParts(),s=i.resolvedOptions();return l.map(o=>Hy(o,n,s))}const rr="Invalid DateTime",If=864e13;function Ps(n){return new zn("unsupported zone",`the zone "${n.name}" is not supported`)}function ar(n){return n.weekData===null&&(n.weekData=vo(n.c)),n.weekData}function fr(n){return n.localWeekData===null&&(n.localWeekData=vo(n.c,n.loc.getMinDaysInFirstWeek(),n.loc.getStartOfWeek())),n.localWeekData}function Pi(n,e){const t={ts:n.ts,zone:n.zone,c:n.c,o:n.o,loc:n.loc,invalid:n.invalid};return new je({...t,...e,old:t})}function z1(n,e,t){let i=n-e*60*1e3;const l=t.offset(i);if(e===l)return[i,e];i-=(l-e)*60*1e3;const s=t.offset(i);return l===s?[i,l]:[n-Math.min(l,s)*60*1e3,Math.max(l,s)]}function Fs(n,e){n+=e*60*1e3;const t=new Date(n);return{year:t.getUTCFullYear(),month:t.getUTCMonth()+1,day:t.getUTCDate(),hour:t.getUTCHours(),minute:t.getUTCMinutes(),second:t.getUTCSeconds(),millisecond:t.getUTCMilliseconds()}}function ro(n,e,t){return z1(Bo(n),e,t)}function Af(n,e){const t=n.o,i=n.c.year+Math.trunc(e.years),l=n.c.month+Math.trunc(e.months)+Math.trunc(e.quarters)*3,s={...n.c,year:i,month:l,day:Math.min(n.c.day,wo(i,l))+Math.trunc(e.days)+Math.trunc(e.weeks)*7},o=st.fromObject({years:e.years-Math.trunc(e.years),quarters:e.quarters-Math.trunc(e.quarters),months:e.months-Math.trunc(e.months),weeks:e.weeks-Math.trunc(e.weeks),days:e.days-Math.trunc(e.days),hours:e.hours,minutes:e.minutes,seconds:e.seconds,milliseconds:e.milliseconds}).as("milliseconds"),r=Bo(s);let[a,f]=z1(r,t,n.zone);return o!==0&&(a+=o,f=n.zone.offset(a)),{ts:a,o:f}}function ql(n,e,t,i,l,s){const{setZone:o,zone:r}=t;if(n&&Object.keys(n).length!==0||e){const a=e||r,f=je.fromObject(n,{...t,zone:a,specificOffset:s});return o?f:f.setZone(r)}else return je.invalid(new zn("unparsable",`the input "${l}" can't be parsed as ${i}`))}function Rs(n,e,t=!0){return n.isValid?ln.create(kt.create("en-US"),{allowZ:t,forceSimple:!0}).formatDateTimeFromString(n,e):null}function ur(n,e){const t=n.c.year>9999||n.c.year<0;let i="";return t&&n.c.year>=0&&(i+="+"),i+=Vt(n.c.year,t?6:4),e?(i+="-",i+=Vt(n.c.month),i+="-",i+=Vt(n.c.day)):(i+=Vt(n.c.month),i+=Vt(n.c.day)),i}function Lf(n,e,t,i,l,s){let o=Vt(n.c.hour);return e?(o+=":",o+=Vt(n.c.minute),(n.c.millisecond!==0||n.c.second!==0||!t)&&(o+=":")):o+=Vt(n.c.minute),(n.c.millisecond!==0||n.c.second!==0||!t)&&(o+=Vt(n.c.second),(n.c.millisecond!==0||!i)&&(o+=".",o+=Vt(n.c.millisecond,3))),l&&(n.isOffsetFixed&&n.offset===0&&!s?o+="Z":n.o<0?(o+="-",o+=Vt(Math.trunc(-n.o/60)),o+=":",o+=Vt(Math.trunc(-n.o%60))):(o+="+",o+=Vt(Math.trunc(n.o/60)),o+=":",o+=Vt(Math.trunc(n.o%60)))),s&&(o+="["+n.zone.ianaName+"]"),o}const V1={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},Ky={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},Jy={ordinal:1,hour:0,minute:0,second:0,millisecond:0},B1=["year","month","day","hour","minute","second","millisecond"],Zy=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],Gy=["year","ordinal","hour","minute","second","millisecond"];function Xy(n){const e={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[n.toLowerCase()];if(!e)throw new Wg(n);return e}function Nf(n){switch(n.toLowerCase()){case"localweekday":case"localweekdays":return"localWeekday";case"localweeknumber":case"localweeknumbers":return"localWeekNumber";case"localweekyear":case"localweekyears":return"localWeekYear";default:return Xy(n)}}function Pf(n,e){const t=Si(e.zone,qt.defaultZone),i=kt.fromObject(e),l=qt.now();let s,o;if(We(n.year))s=l;else{for(const f of B1)We(n[f])&&(n[f]=V1[f]);const r=h1(n)||_1(n);if(r)return je.invalid(r);const a=t.offset(l);[s,o]=ro(n,a,t)}return new je({ts:s,zone:t,loc:i,o})}function Ff(n,e,t){const i=We(t.round)?!0:t.round,l=(o,r)=>(o=ga(o,i||t.calendary?0:2,!0),e.loc.clone(t).relFormatter(t).format(o,r)),s=o=>t.calendary?e.hasSame(n,o)?0:e.startOf(o).diff(n.startOf(o),o).get(o):e.diff(n,o).get(o);if(t.unit)return l(s(t.unit),t.unit);for(const o of t.units){const r=s(o);if(Math.abs(r)>=1)return l(r,o)}return l(n>e?-0:0,t.units[t.units.length-1])}function Rf(n){let e={},t;return n.length>0&&typeof n[n.length-1]=="object"?(e=n[n.length-1],t=Array.from(n).slice(0,n.length-1)):t=Array.from(n),[e,t]}class je{constructor(e){const t=e.zone||qt.defaultZone;let i=e.invalid||(Number.isNaN(e.ts)?new zn("invalid input"):null)||(t.isValid?null:Ps(t));this.ts=We(e.ts)?qt.now():e.ts;let l=null,s=null;if(!i)if(e.old&&e.old.ts===this.ts&&e.old.zone.equals(t))[l,s]=[e.old.c,e.old.o];else{const r=t.offset(this.ts);l=Fs(this.ts,r),i=Number.isNaN(l.year)?new zn("invalid input"):null,l=i?null:l,s=i?null:r}this._zone=t,this.loc=e.loc||kt.create(),this.invalid=i,this.weekData=null,this.localWeekData=null,this.c=l,this.o=s,this.isLuxonDateTime=!0}static now(){return new je({})}static local(){const[e,t]=Rf(arguments),[i,l,s,o,r,a,f]=t;return Pf({year:i,month:l,day:s,hour:o,minute:r,second:a,millisecond:f},e)}static utc(){const[e,t]=Rf(arguments),[i,l,s,o,r,a,f]=t;return e.zone=un.utcInstance,Pf({year:i,month:l,day:s,hour:o,minute:r,second:a,millisecond:f},e)}static fromJSDate(e,t={}){const i=Ck(e)?e.valueOf():NaN;if(Number.isNaN(i))return je.invalid("invalid input");const l=Si(t.zone,qt.defaultZone);return l.isValid?new je({ts:i,zone:l,loc:kt.fromObject(t)}):je.invalid(Ps(l))}static fromMillis(e,t={}){if(Ji(e))return e<-If||e>If?je.invalid("Timestamp out of range"):new je({ts:e,zone:Si(t.zone,qt.defaultZone),loc:kt.fromObject(t)});throw new mn(`fromMillis requires a numerical input, but received a ${typeof e} with value ${e}`)}static fromSeconds(e,t={}){if(Ji(e))return new je({ts:e*1e3,zone:Si(t.zone,qt.defaultZone),loc:kt.fromObject(t)});throw new mn("fromSeconds requires a numerical input")}static fromObject(e,t={}){e=e||{};const i=Si(t.zone,qt.defaultZone);if(!i.isValid)return je.invalid(Ps(i));const l=kt.fromObject(t),s=So(e,Nf),{minDaysInFirstWeek:o,startOfWeek:r}=yf(s,l),a=qt.now(),f=We(t.specificOffset)?i.offset(a):t.specificOffset,u=!We(s.ordinal),c=!We(s.year),d=!We(s.month)||!We(s.day),m=c||d,h=s.weekYear||s.weekNumber;if((m||u)&&h)throw new gl("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(d&&u)throw new gl("Can't mix ordinal dates with month/day");const _=h||s.weekday&&!m;let g,y,S=Fs(a,f);_?(g=Zy,y=Ky,S=vo(S,o,r)):u?(g=Gy,y=Jy,S=sr(S)):(g=B1,y=V1);let T=!1;for(const R of g){const F=s[R];We(F)?T?s[R]=y[R]:s[R]=S[R]:T=!0}const $=_?Sk(s,o,r):u?$k(s):h1(s),C=$||_1(s);if(C)return je.invalid(C);const O=_?bf(s,o,r):u?kf(s):s,[D,I]=ro(O,f,i),L=new je({ts:D,zone:i,o:I,loc:l});return s.weekday&&m&&e.weekday!==L.weekday?je.invalid("mismatched weekday",`you can't specify both a weekday of ${s.weekday} and a date of ${L.toISO()}`):L}static fromISO(e,t={}){const[i,l]=hy(e);return ql(i,l,t,"ISO 8601",e)}static fromRFC2822(e,t={}){const[i,l]=_y(e);return ql(i,l,t,"RFC 2822",e)}static fromHTTP(e,t={}){const[i,l]=gy(e);return ql(i,l,t,"HTTP",t)}static fromFormat(e,t,i={}){if(We(e)||We(t))throw new mn("fromFormat requires an input string and a format");const{locale:l=null,numberingSystem:s=null}=i,o=kt.fromOpts({locale:l,numberingSystem:s,defaultToEN:!0}),[r,a,f,u]=Yy(o,e,t);return u?je.invalid(u):ql(r,a,i,`format ${t}`,e,f)}static fromString(e,t,i={}){return je.fromFormat(e,t,i)}static fromSQL(e,t={}){const[i,l]=$y(e);return ql(i,l,t,"SQL",e)}static invalid(e,t=null){if(!e)throw new mn("need to specify a reason the DateTime is invalid");const i=e instanceof zn?e:new zn(e,t);if(qt.throwOnInvalid)throw new x0(i);return new je({invalid:i})}static isDateTime(e){return e&&e.isLuxonDateTime||!1}static parseFormatForOpts(e,t={}){const i=H1(e,kt.fromObject(t));return i?i.map(l=>l?l.val:null).join(""):null}static expandFormat(e,t={}){return q1(ln.parseFormat(e),kt.fromObject(t)).map(l=>l.val).join("")}get(e){return this[e]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?ar(this).weekYear:NaN}get weekNumber(){return this.isValid?ar(this).weekNumber:NaN}get weekday(){return this.isValid?ar(this).weekday:NaN}get isWeekend(){return this.isValid&&this.loc.getWeekendDays().includes(this.weekday)}get localWeekday(){return this.isValid?fr(this).weekday:NaN}get localWeekNumber(){return this.isValid?fr(this).weekNumber:NaN}get localWeekYear(){return this.isValid?fr(this).weekYear:NaN}get ordinal(){return this.isValid?sr(this.c).ordinal:NaN}get monthShort(){return this.isValid?Ls.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?Ls.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?Ls.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?Ls.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}getPossibleOffsets(){if(!this.isValid||this.isOffsetFixed)return[this];const e=864e5,t=6e4,i=Bo(this.c),l=this.zone.offset(i-e),s=this.zone.offset(i+e),o=this.zone.offset(i-l*t),r=this.zone.offset(i-s*t);if(o===r)return[this];const a=i-o*t,f=i-r*t,u=Fs(a,o),c=Fs(f,r);return u.hour===c.hour&&u.minute===c.minute&&u.second===c.second&&u.millisecond===c.millisecond?[Pi(this,{ts:a}),Pi(this,{ts:f})]:[this]}get isInLeapYear(){return vs(this.year)}get daysInMonth(){return wo(this.year,this.month)}get daysInYear(){return this.isValid?yl(this.year):NaN}get weeksInWeekYear(){return this.isValid?os(this.weekYear):NaN}get weeksInLocalWeekYear(){return this.isValid?os(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}resolvedLocaleOptions(e={}){const{locale:t,numberingSystem:i,calendar:l}=ln.create(this.loc.clone(e),e).resolvedOptions(this);return{locale:t,numberingSystem:i,outputCalendar:l}}toUTC(e=0,t={}){return this.setZone(un.instance(e),t)}toLocal(){return this.setZone(qt.defaultZone)}setZone(e,{keepLocalTime:t=!1,keepCalendarTime:i=!1}={}){if(e=Si(e,qt.defaultZone),e.equals(this.zone))return this;if(e.isValid){let l=this.ts;if(t||i){const s=e.offset(this.ts),o=this.toObject();[l]=ro(o,s,e)}return Pi(this,{ts:l,zone:e})}else return je.invalid(Ps(e))}reconfigure({locale:e,numberingSystem:t,outputCalendar:i}={}){const l=this.loc.clone({locale:e,numberingSystem:t,outputCalendar:i});return Pi(this,{loc:l})}setLocale(e){return this.reconfigure({locale:e})}set(e){if(!this.isValid)return this;const t=So(e,Nf),{minDaysInFirstWeek:i,startOfWeek:l}=yf(t,this.loc),s=!We(t.weekYear)||!We(t.weekNumber)||!We(t.weekday),o=!We(t.ordinal),r=!We(t.year),a=!We(t.month)||!We(t.day),f=r||a,u=t.weekYear||t.weekNumber;if((f||o)&&u)throw new gl("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(a&&o)throw new gl("Can't mix ordinal dates with month/day");let c;s?c=bf({...vo(this.c,i,l),...t},i,l):We(t.ordinal)?(c={...this.toObject(),...t},We(t.day)&&(c.day=Math.min(wo(c.year,c.month),c.day))):c=kf({...sr(this.c),...t});const[d,m]=ro(c,this.o,this.zone);return Pi(this,{ts:d,o:m})}plus(e){if(!this.isValid)return this;const t=st.fromDurationLike(e);return Pi(this,Af(this,t))}minus(e){if(!this.isValid)return this;const t=st.fromDurationLike(e).negate();return Pi(this,Af(this,t))}startOf(e,{useLocaleWeeks:t=!1}={}){if(!this.isValid)return this;const i={},l=st.normalizeUnit(e);switch(l){case"years":i.month=1;case"quarters":case"months":i.day=1;case"weeks":case"days":i.hour=0;case"hours":i.minute=0;case"minutes":i.second=0;case"seconds":i.millisecond=0;break}if(l==="weeks")if(t){const s=this.loc.getStartOfWeek(),{weekday:o}=this;othis.valueOf(),r=o?this:e,a=o?e:this,f=Iy(r,a,s,l);return o?f.negate():f}diffNow(e="milliseconds",t={}){return this.diff(je.now(),e,t)}until(e){return this.isValid?Ft.fromDateTimes(this,e):this}hasSame(e,t,i){if(!this.isValid)return!1;const l=e.valueOf(),s=this.setZone(e.zone,{keepLocalTime:!0});return s.startOf(t,i)<=l&&l<=s.endOf(t,i)}equals(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)}toRelative(e={}){if(!this.isValid)return null;const t=e.base||je.fromObject({},{zone:this.zone}),i=e.padding?thist.valueOf(),Math.min)}static max(...e){if(!e.every(je.isDateTime))throw new mn("max requires all arguments be DateTimes");return vf(e,t=>t.valueOf(),Math.max)}static fromFormatExplain(e,t,i={}){const{locale:l=null,numberingSystem:s=null}=i,o=kt.fromOpts({locale:l,numberingSystem:s,defaultToEN:!0});return j1(o,e,t)}static fromStringExplain(e,t,i={}){return je.fromFormatExplain(e,t,i)}static get DATE_SHORT(){return yo}static get DATE_MED(){return Yg}static get DATE_MED_WITH_WEEKDAY(){return nk}static get DATE_FULL(){return Kg}static get DATE_HUGE(){return Jg}static get TIME_SIMPLE(){return Zg}static get TIME_WITH_SECONDS(){return Gg}static get TIME_WITH_SHORT_OFFSET(){return Xg}static get TIME_WITH_LONG_OFFSET(){return Qg}static get TIME_24_SIMPLE(){return xg}static get TIME_24_WITH_SECONDS(){return e1}static get TIME_24_WITH_SHORT_OFFSET(){return t1}static get TIME_24_WITH_LONG_OFFSET(){return n1}static get DATETIME_SHORT(){return i1}static get DATETIME_SHORT_WITH_SECONDS(){return l1}static get DATETIME_MED(){return s1}static get DATETIME_MED_WITH_SECONDS(){return o1}static get DATETIME_MED_WITH_WEEKDAY(){return ik}static get DATETIME_FULL(){return r1}static get DATETIME_FULL_WITH_SECONDS(){return a1}static get DATETIME_HUGE(){return f1}static get DATETIME_HUGE_WITH_SECONDS(){return u1}}function jl(n){if(je.isDateTime(n))return n;if(n&&n.valueOf&&Ji(n.valueOf()))return je.fromJSDate(n);if(n&&typeof n=="object")return je.fromObject(n);throw new mn(`Unknown datetime argument: ${n}, of type ${typeof n}`)}const Qy=[".jpg",".jpeg",".png",".svg",".gif",".jfif",".webp",".avif"],xy=[".mp4",".avi",".mov",".3gp",".wmv"],ev=[".aa",".aac",".m4v",".mp3",".ogg",".oga",".mogg",".amr"],tv=[".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".odp",".odt",".ods",".txt"],U1=[{level:-4,label:"DEBUG",class:""},{level:0,label:"INFO",class:"label-success"},{level:4,label:"WARN",class:"label-warning"},{level:8,label:"ERROR",class:"label-danger"}];class j{static isObject(e){return e!==null&&typeof e=="object"&&e.constructor===Object}static clone(e){return typeof structuredClone<"u"?structuredClone(e):JSON.parse(JSON.stringify(e))}static zeroValue(e){switch(typeof e){case"string":return"";case"number":return 0;case"boolean":return!1;case"object":return e===null?null:Array.isArray(e)?[]:{};case"undefined":return;default:return null}}static isEmpty(e){return e===""||e===null||typeof e>"u"||Array.isArray(e)&&e.length===0||j.isObject(e)&&Object.keys(e).length===0}static isInput(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return t==="input"||t==="select"||t==="textarea"||(e==null?void 0:e.isContentEditable)}static isFocusable(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return j.isInput(e)||t==="button"||t==="a"||t==="details"||(e==null?void 0:e.tabIndex)>=0}static hasNonEmptyProps(e){for(let t in e)if(!j.isEmpty(e[t]))return!0;return!1}static toArray(e,t=!1){return Array.isArray(e)?e.slice():(t||!j.isEmpty(e))&&typeof e<"u"?[e]:[]}static inArray(e,t){e=Array.isArray(e)?e:[];for(let i=e.length-1;i>=0;i--)if(e[i]==t)return!0;return!1}static removeByValue(e,t){e=Array.isArray(e)?e:[];for(let i=e.length-1;i>=0;i--)if(e[i]==t){e.splice(i,1);break}}static pushUnique(e,t){j.inArray(e,t)||e.push(t)}static findByKey(e,t,i){e=Array.isArray(e)?e:[];for(let l in e)if(e[l][t]==i)return e[l];return null}static groupByKey(e,t){e=Array.isArray(e)?e:[];const i={};for(let l in e)i[e[l][t]]=i[e[l][t]]||[],i[e[l][t]].push(e[l]);return i}static removeByKey(e,t,i){for(let l in e)if(e[l][t]==i){e.splice(l,1);break}}static pushOrReplaceByKey(e,t,i="id"){for(let l=e.length-1;l>=0;l--)if(e[l][i]==t[i]){e[l]=t;return}e.push(t)}static filterDuplicatesByKey(e,t="id"){e=Array.isArray(e)?e:[];const i={};for(const l of e)i[l[t]]=l;return Object.values(i)}static filterRedactedProps(e,t="******"){const i=JSON.parse(JSON.stringify(e||{}));for(let l in i)typeof i[l]=="object"&&i[l]!==null?i[l]=j.filterRedactedProps(i[l],t):i[l]===t&&delete i[l];return i}static getNestedVal(e,t,i=null,l="."){let s=e||{},o=(t||"").split(l);for(const r of o){if(!j.isObject(s)&&!Array.isArray(s)||typeof s[r]>"u")return i;s=s[r]}return s}static setByPath(e,t,i,l="."){if(e===null||typeof e!="object"){console.warn("setByPath: data not an object or array.");return}let s=e,o=t.split(l),r=o.pop();for(const a of o)(!j.isObject(s)&&!Array.isArray(s)||!j.isObject(s[a])&&!Array.isArray(s[a]))&&(s[a]={}),s=s[a];s[r]=i}static deleteByPath(e,t,i="."){let l=e||{},s=(t||"").split(i),o=s.pop();for(const r of s)(!j.isObject(l)&&!Array.isArray(l)||!j.isObject(l[r])&&!Array.isArray(l[r]))&&(l[r]={}),l=l[r];Array.isArray(l)?l.splice(o,1):j.isObject(l)&&delete l[o],s.length>0&&(Array.isArray(l)&&!l.length||j.isObject(l)&&!Object.keys(l).length)&&(Array.isArray(e)&&e.length>0||j.isObject(e)&&Object.keys(e).length>0)&&j.deleteByPath(e,s.join(i),i)}static randomString(e=10){let t="",i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let l=0;l"u")return j.randomString(e);const t=new Uint8Array(e);crypto.getRandomValues(t);const i="-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";let l="";for(let s=0;ss.replaceAll("{_PB_ESCAPED_}",t));for(let s of l)s=s.trim(),j.isEmpty(s)||i.push(s);return i}static joinNonEmpty(e,t=", "){e=e||[];const i=[],l=t.length>1?t.trim():t;for(let s of e)s=typeof s=="string"?s.trim():"",j.isEmpty(s)||i.push(s.replaceAll(l,"\\"+l));return i.join(t)}static getInitials(e){if(e=(e||"").split("@")[0].trim(),e.length<=2)return e.toUpperCase();const t=e.split(/[\.\_\-\ ]/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e[0].toUpperCase()}static formattedFileSize(e){const t=e?Math.floor(Math.log(e)/Math.log(1024)):0;return(e/Math.pow(1024,t)).toFixed(2)*1+" "+["B","KB","MB","GB","TB"][t]}static getDateTime(e){if(typeof e=="string"){const t={19:"yyyy-MM-dd HH:mm:ss",23:"yyyy-MM-dd HH:mm:ss.SSS",20:"yyyy-MM-dd HH:mm:ss'Z'",24:"yyyy-MM-dd HH:mm:ss.SSS'Z'"},i=t[e.length]||t[19];return je.fromFormat(e,i,{zone:"UTC"})}return je.fromJSDate(e)}static formatToUTCDate(e,t="yyyy-MM-dd HH:mm:ss"){return j.getDateTime(e).toUTC().toFormat(t)}static formatToLocalDate(e,t="yyyy-MM-dd HH:mm:ss"){return j.getDateTime(e).toLocal().toFormat(t)}static async copyToClipboard(e){var t;if(e=""+e,!(!e.length||!((t=window==null?void 0:window.navigator)!=null&&t.clipboard)))return window.navigator.clipboard.writeText(e).catch(i=>{console.warn("Failed to copy.",i)})}static download(e,t){const i=document.createElement("a");i.setAttribute("href",e),i.setAttribute("download",t),i.setAttribute("target","_blank"),i.click(),i.remove()}static downloadJson(e,t){t=t.endsWith(".json")?t:t+".json";const i=new Blob([JSON.stringify(e,null,2)],{type:"application/json"}),l=window.URL.createObjectURL(i);j.download(l,t)}static getJWTPayload(e){const t=(e||"").split(".")[1]||"";if(t==="")return{};try{const i=decodeURIComponent(atob(t));return JSON.parse(i)||{}}catch(i){console.warn("Failed to parse JWT payload data.",i)}return{}}static hasImageExtension(e){return e=e||"",!!Qy.find(t=>e.toLowerCase().endsWith(t))}static hasVideoExtension(e){return e=e||"",!!xy.find(t=>e.toLowerCase().endsWith(t))}static hasAudioExtension(e){return e=e||"",!!ev.find(t=>e.toLowerCase().endsWith(t))}static hasDocumentExtension(e){return e=e||"",!!tv.find(t=>e.toLowerCase().endsWith(t))}static getFileType(e){return j.hasImageExtension(e)?"image":j.hasDocumentExtension(e)?"document":j.hasVideoExtension(e)?"video":j.hasAudioExtension(e)?"audio":"file"}static generateThumb(e,t=100,i=100){return new Promise(l=>{let s=new FileReader;s.onload=function(o){let r=new Image;r.onload=function(){let a=document.createElement("canvas"),f=a.getContext("2d"),u=r.width,c=r.height;return a.width=t,a.height=i,f.drawImage(r,u>c?(u-c)/2:0,0,u>c?c:u,u>c?c:u,0,0,t,i),l(a.toDataURL(e.type))},r.src=o.target.result},s.readAsDataURL(e)})}static addValueToFormData(e,t,i){if(!(typeof i>"u"))if(j.isEmpty(i))e.append(t,"");else if(Array.isArray(i))for(const l of i)j.addValueToFormData(e,t,l);else i instanceof File?e.append(t,i):i instanceof Date?e.append(t,i.toISOString()):j.isObject(i)?e.append(t,JSON.stringify(i)):e.append(t,""+i)}static dummyCollectionRecord(e){var a,f,u,c,d,m,h;const t=(e==null?void 0:e.schema)||[],i=(e==null?void 0:e.type)==="auth",l=(e==null?void 0:e.type)==="view",s={id:"RECORD_ID",collectionId:e==null?void 0:e.id,collectionName:e==null?void 0:e.name};i&&(s.username="username123",s.verified=!1,s.emailVisibility=!0,s.email="test@example.com"),(!l||j.extractColumnsFromQuery((a=e==null?void 0:e.options)==null?void 0:a.query).includes("created"))&&(s.created="2022-01-01 01:00:00.123Z"),(!l||j.extractColumnsFromQuery((f=e==null?void 0:e.options)==null?void 0:f.query).includes("updated"))&&(s.updated="2022-01-01 23:59:59.456Z");for(const _ of t){let g=null;_.type==="number"?g=123:_.type==="date"?g="2022-01-01 10:00:00.123Z":_.type==="bool"?g=!0:_.type==="email"?g="test@example.com":_.type==="url"?g="https://example.com":_.type==="json"?g="JSON":_.type==="file"?(g="filename.jpg",((u=_.options)==null?void 0:u.maxSelect)!==1&&(g=[g])):_.type==="select"?(g=(d=(c=_.options)==null?void 0:c.values)==null?void 0:d[0],((m=_.options)==null?void 0:m.maxSelect)!==1&&(g=[g])):_.type==="relation"?(g="RELATION_RECORD_ID",((h=_.options)==null?void 0:h.maxSelect)!==1&&(g=[g])):g="test",s[_.name]=g}return s}static dummyCollectionSchemaData(e){var l,s,o,r;const t=(e==null?void 0:e.schema)||[],i={};for(const a of t){let f=null;if(a.type==="number")f=123;else if(a.type==="date")f="2022-01-01 10:00:00.123Z";else if(a.type==="bool")f=!0;else if(a.type==="email")f="test@example.com";else if(a.type==="url")f="https://example.com";else if(a.type==="json")f="JSON";else{if(a.type==="file")continue;a.type==="select"?(f=(s=(l=a.options)==null?void 0:l.values)==null?void 0:s[0],((o=a.options)==null?void 0:o.maxSelect)!==1&&(f=[f])):a.type==="relation"?(f="RELATION_RECORD_ID",((r=a.options)==null?void 0:r.maxSelect)!==1&&(f=[f])):f="test"}i[a.name]=f}return i}static getCollectionTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"auth":return"ri-group-line";case"view":return"ri-table-line";default:return"ri-folder-2-line"}}static getFieldTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"primary":return"ri-key-line";case"text":return"ri-text";case"number":return"ri-hashtag";case"date":return"ri-calendar-line";case"bool":return"ri-toggle-line";case"email":return"ri-mail-line";case"url":return"ri-link";case"editor":return"ri-edit-2-line";case"select":return"ri-list-check";case"json":return"ri-braces-line";case"file":return"ri-image-line";case"relation":return"ri-mind-map";case"user":return"ri-user-line";default:return"ri-star-s-line"}}static getFieldValueType(e){var t;switch(e==null?void 0:e.type){case"bool":return"Boolean";case"number":return"Number";case"file":return"File";case"select":case"relation":return((t=e==null?void 0:e.options)==null?void 0:t.maxSelect)===1?"String":"Array";default:return"String"}}static zeroDefaultStr(e){var t;return(e==null?void 0:e.type)==="number"?"0":(e==null?void 0:e.type)==="bool"?"false":(e==null?void 0:e.type)==="json"?'null, "", [], {}':["select","relation","file"].includes(e==null?void 0:e.type)&&((t=e==null?void 0:e.options)==null?void 0:t.maxSelect)!=1?"[]":'""'}static getApiExampleUrl(e){return(window.location.href.substring(0,window.location.href.indexOf("/_"))||e||"/").replace("//localhost","//127.0.0.1")}static hasCollectionChanges(e,t,i=!1){if(e=e||{},t=t||{},e.id!=t.id)return!0;for(let f in e)if(f!=="schema"&&JSON.stringify(e[f])!==JSON.stringify(t[f]))return!0;const l=Array.isArray(e.schema)?e.schema:[],s=Array.isArray(t.schema)?t.schema:[],o=l.filter(f=>(f==null?void 0:f.id)&&!j.findByKey(s,"id",f.id)),r=s.filter(f=>(f==null?void 0:f.id)&&!j.findByKey(l,"id",f.id)),a=s.filter(f=>{const u=j.isObject(f)&&j.findByKey(l,"id",f.id);if(!u)return!1;for(let c in u)if(JSON.stringify(f[c])!=JSON.stringify(u[c]))return!0;return!1});return!!(r.length||a.length||i&&o.length)}static sortCollections(e=[]){const t=[],i=[],l=[];for(const o of e)o.type==="auth"?t.push(o):o.type==="base"?i.push(o):l.push(o);function s(o,r){return o.name>r.name?1:o.name{setTimeout(e,0)})}static defaultFlatpickrOptions(){return{dateFormat:"Y-m-d H:i:S",disableMobile:!0,allowInput:!0,enableTime:!0,time_24hr:!0,locale:{firstDayOfWeek:1}}}static defaultEditorOptions(){const e=["DIV","P","A","EM","B","STRONG","H1","H2","H3","H4","H5","H6","TABLE","TR","TD","TH","TBODY","THEAD","TFOOT","BR","HR","Q","SUP","SUB","DEL","IMG","OL","UL","LI","CODE"];function t(l){let s=l.parentNode;for(;l.firstChild;)s.insertBefore(l.firstChild,l);s.removeChild(l)}function i(l){if(l){for(const s of l.children)i(s);e.includes(l.tagName)?(l.removeAttribute("style"),l.removeAttribute("class")):t(l)}}return{branding:!1,promotion:!1,menubar:!1,min_height:270,height:270,max_height:700,autoresize_bottom_margin:30,convert_unsafe_embeds:!0,skin:"pocketbase",content_style:"body { font-size: 14px }",plugins:["autoresize","autolink","lists","link","image","searchreplace","fullscreen","media","table","code","codesample","directionality"],codesample_global_prismjs:!0,codesample_languages:[{text:"HTML/XML",value:"markup"},{text:"CSS",value:"css"},{text:"SQL",value:"sql"},{text:"JavaScript",value:"javascript"},{text:"Go",value:"go"},{text:"Dart",value:"dart"},{text:"Zig",value:"zig"},{text:"Rust",value:"rust"},{text:"Lua",value:"lua"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"},{text:"Markdown",value:"markdown"},{text:"Swift",value:"swift"},{text:"Kotlin",value:"kotlin"},{text:"Elixir",value:"elixir"},{text:"Scala",value:"scala"},{text:"Julia",value:"julia"},{text:"Haskell",value:"haskell"}],toolbar:"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link image_picker table codesample direction | code fullscreen",paste_postprocess:(l,s)=>{i(s.node)},file_picker_types:"image",file_picker_callback:(l,s,o)=>{const r=document.createElement("input");r.setAttribute("type","file"),r.setAttribute("accept","image/*"),r.addEventListener("change",a=>{const f=a.target.files[0],u=new FileReader;u.addEventListener("load",()=>{if(!tinymce)return;const c="blobid"+new Date().getTime(),d=tinymce.activeEditor.editorUpload.blobCache,m=u.result.split(",")[1],h=d.create(c,f,m);d.add(h),l(h.blobUri(),{title:f.name})}),u.readAsDataURL(f)}),r.click()},setup:l=>{l.on("keydown",o=>{(o.ctrlKey||o.metaKey)&&o.code=="KeyS"&&l.formElement&&(o.preventDefault(),o.stopPropagation(),l.formElement.dispatchEvent(new KeyboardEvent("keydown",o)))});const s="tinymce_last_direction";l.on("init",()=>{var r;const o=(r=window==null?void 0:window.localStorage)==null?void 0:r.getItem(s);!l.isDirty()&&l.getContent()==""&&o=="rtl"&&l.execCommand("mceDirectionRTL")}),l.ui.registry.addMenuButton("direction",{icon:"visualchars",fetch:o=>{o([{type:"menuitem",text:"LTR content",icon:"ltr",onAction:()=>{var a;(a=window==null?void 0:window.localStorage)==null||a.setItem(s,"ltr"),l.execCommand("mceDirectionLTR")}},{type:"menuitem",text:"RTL content",icon:"rtl",onAction:()=>{var a;(a=window==null?void 0:window.localStorage)==null||a.setItem(s,"rtl"),l.execCommand("mceDirectionRTL")}}])}}),l.ui.registry.addMenuButton("image_picker",{icon:"image",fetch:o=>{o([{type:"menuitem",text:"From collection",icon:"gallery",onAction:()=>{l.dispatch("collections_file_picker",{})}},{type:"menuitem",text:"Inline",icon:"browse",onAction:()=>{l.execCommand("mceImage")}}])}})}}}static displayValue(e,t,i="N/A"){e=e||{},t=t||[];let l=[];for(const o of t){let r=e[o];typeof r>"u"||(r=j.stringifyValue(r,i),l.push(r))}if(l.length>0)return l.join(", ");const s=["title","name","slug","email","username","nickname","label","heading","message","key","identifier","id"];for(const o of s){let r=j.stringifyValue(e[o],"");if(r)return r}return i}static stringifyValue(e,t="N/A",i=150){if(j.isEmpty(e))return t;if(typeof e=="number")return""+e;if(typeof e=="boolean")return e?"True":"False";if(typeof e=="string")return e=e.indexOf("<")>=0?j.plainText(e):e,j.truncate(e,i)||t;if(Array.isArray(e)&&typeof e[0]!="object")return j.truncate(e.join(","),i);if(typeof e=="object")try{return j.truncate(JSON.stringify(e),i)||t}catch{return t}return e}static extractColumnsFromQuery(e){var o;const t="__GROUP__";e=(e||"").replace(/\([\s\S]+?\)/gm,t).replace(/[\t\r\n]|(?:\s\s)+/g," ");const i=e.match(/select\s+([\s\S]+)\s+from/),l=((o=i==null?void 0:i[1])==null?void 0:o.split(","))||[],s=[];for(let r of l){const a=r.trim().split(" ").pop();a!=""&&a!=t&&s.push(a.replace(/[\'\"\`\[\]\s]/g,""))}return s}static getAllCollectionIdentifiers(e,t=""){if(!e)return[];let i=[t+"id"];if(e.type==="view")for(let s of j.extractColumnsFromQuery(e.options.query))j.pushUnique(i,t+s);else e.type==="auth"?(i.push(t+"username"),i.push(t+"email"),i.push(t+"emailVisibility"),i.push(t+"verified"),i.push(t+"created"),i.push(t+"updated")):(i.push(t+"created"),i.push(t+"updated"));const l=e.schema||[];for(const s of l)j.pushUnique(i,t+s.name);return i}static getCollectionAutocompleteKeys(e,t,i="",l=0){var r,a,f;let s=e.find(u=>u.name==t||u.id==t);if(!s||l>=4)return[];s.schema=s.schema||[];let o=j.getAllCollectionIdentifiers(s,i);for(const u of s.schema){const c=i+u.name;if(u.type=="relation"&&((r=u.options)!=null&&r.collectionId)){const d=j.getCollectionAutocompleteKeys(e,u.options.collectionId,c+".",l+1);d.length&&(o=o.concat(d))}((a=u.options)==null?void 0:a.maxSelect)!=1&&["select","file","relation"].includes(u.type)&&(o.push(c+":each"),o.push(c+":length"))}for(const u of e){u.schema=u.schema||[];for(const c of u.schema)if(c.type=="relation"&&((f=c.options)==null?void 0:f.collectionId)==s.id){const d=i+u.name+"_via_"+c.name,m=j.getCollectionAutocompleteKeys(e,u.id,d+".",l+2);m.length&&(o=o.concat(m))}}return o}static getCollectionJoinAutocompleteKeys(e){const t=[];for(const i of e){const l="@collection."+i.name+".",s=j.getCollectionAutocompleteKeys(e,i.name,l);for(const o of s)t.push(o)}return t}static getRequestAutocompleteKeys(e,t){const i=[];i.push("@request.context"),i.push("@request.method"),i.push("@request.query."),i.push("@request.data."),i.push("@request.headers."),i.push("@request.auth.id"),i.push("@request.auth.collectionId"),i.push("@request.auth.collectionName"),i.push("@request.auth.verified"),i.push("@request.auth.username"),i.push("@request.auth.email"),i.push("@request.auth.emailVisibility"),i.push("@request.auth.created"),i.push("@request.auth.updated");const l=e.filter(s=>s.type==="auth");for(const s of l){const o=j.getCollectionAutocompleteKeys(e,s.id,"@request.auth.");for(const r of o)j.pushUnique(i,r)}if(t){const s=["created","updated"],o=j.getCollectionAutocompleteKeys(e,t,"@request.data.");for(const r of o){i.push(r);const a=r.split(".");a.length===3&&a[2].indexOf(":")===-1&&!s.includes(a[2])&&i.push(r+":isset")}}return i}static parseIndex(e){var a,f,u,c,d;const t={unique:!1,optional:!1,schemaName:"",indexName:"",tableName:"",columns:[],where:""},l=/create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?/gmi.exec((e||"").trim());if((l==null?void 0:l.length)!=7)return t;const s=/^[\"\'\`\[\{}]|[\"\'\`\]\}]$/gm;t.unique=((a=l[1])==null?void 0:a.trim().toLowerCase())==="unique",t.optional=!j.isEmpty((f=l[2])==null?void 0:f.trim());const o=(l[3]||"").split(".");o.length==2?(t.schemaName=o[0].replace(s,""),t.indexName=o[1].replace(s,"")):(t.schemaName="",t.indexName=o[0].replace(s,"")),t.tableName=(l[4]||"").replace(s,"");const r=(l[5]||"").replace(/,(?=[^\(]*\))/gmi,"{PB_TEMP}").split(",");for(let m of r){m=m.trim().replaceAll("{PB_TEMP}",",");const _=/^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$/gmi.exec(m);if((_==null?void 0:_.length)!=4)continue;const g=(c=(u=_[1])==null?void 0:u.trim())==null?void 0:c.replace(s,"");g&&t.columns.push({name:g,collate:_[2]||"",sort:((d=_[3])==null?void 0:d.toUpperCase())||""})}return t.where=l[6]||"",t}static buildIndex(e){let t="CREATE ";e.unique&&(t+="UNIQUE "),t+="INDEX ",e.optional&&(t+="IF NOT EXISTS "),e.schemaName&&(t+=`\`${e.schemaName}\`.`),t+=`\`${e.indexName||"idx_"+j.randomString(7)}\` `,t+=`ON \`${e.tableName}\` (`;const i=e.columns.filter(l=>!!(l!=null&&l.name));return i.length>1&&(t+=` - `),t+=i.map(l=>{let s="";return l.name.includes("(")||l.name.includes(" ")?s+=l.name:s+="`"+l.name+"`",l.collate&&(s+=" COLLATE "+l.collate),l.sort&&(s+=" "+l.sort.toUpperCase()),s}).join(`, - `),i.length>1&&(t+=` -`),t+=")",e.where&&(t+=` WHERE ${e.where}`),t}static replaceIndexTableName(e,t){const i=j.parseIndex(e);return i.tableName=t,j.buildIndex(i)}static replaceIndexColumn(e,t,i){if(t===i)return e;const l=j.parseIndex(e);let s=!1;for(let o of l.columns)o.name===t&&(o.name=i,s=!0);return s?j.buildIndex(l):e}static normalizeSearchFilter(e,t){if(e=(e||"").trim(),!e||!t.length)return e;const i=["=","!=","~","!~",">",">=","<","<="];for(const l of i)if(e.includes(l))return e;return e=isNaN(e)&&e!="true"&&e!="false"?`"${e.replace(/^[\"\'\`]|[\"\'\`]$/gm,"")}"`:e,t.map(l=>`${l}~${e}`).join("||")}static normalizeLogsFilter(e,t=[]){return j.normalizeSearchFilter(e,["level","message","data"].concat(t))}static initCollection(e){return Object.assign({id:"",created:"",updated:"",name:"",type:"base",system:!1,listRule:null,viewRule:null,createRule:null,updateRule:null,deleteRule:null,schema:[],indexes:[],options:{}},e)}static initSchemaField(e){return Object.assign({id:"",name:"",type:"text",system:!1,required:!1,options:{}},e)}static triggerResize(){window.dispatchEvent(new Event("resize"))}static getHashQueryParams(){let e="";const t=window.location.hash.indexOf("?");return t>-1&&(e=window.location.hash.substring(t+1)),Object.fromEntries(new URLSearchParams(e))}static replaceHashQueryParams(e){e=e||{};let t="",i=window.location.hash;const l=i.indexOf("?");l>-1&&(t=i.substring(l+1),i=i.substring(0,l));const s=new URLSearchParams(t);for(let a in e){const f=e[a];f===null?s.delete(a):s.set(a,f)}t=s.toString(),t!=""&&(i+="?"+t);let o=window.location.href;const r=o.indexOf("#");r>-1&&(o=o.substring(0,r)),window.location.replace(o+i)}}const Yo=Cn([]);function $o(n,e=4e3){return Ko(n,"info",e)}function Lt(n,e=3e3){return Ko(n,"success",e)}function ii(n,e=4500){return Ko(n,"error",e)}function nv(n,e=4500){return Ko(n,"warning",e)}function Ko(n,e,t){t=t||4e3;const i={message:n,type:e,duration:t,timeout:setTimeout(()=>{W1(i)},t)};Yo.update(l=>(Sa(l,i.message),j.pushOrReplaceByKey(l,i,"message"),l))}function W1(n){Yo.update(e=>(Sa(e,n),e))}function wa(){Yo.update(n=>{for(let e of n)Sa(n,e);return[]})}function Sa(n,e){let t;typeof e=="string"?t=j.findByKey(n,"message",e):t=e,t&&(clearTimeout(t.timeout),j.removeByKey(n,"message",t.message))}const mi=Cn({});function Jt(n){mi.set(n||{})}function li(n){mi.update(e=>(j.deleteByPath(e,n),e))}const $a=Cn({});function Ur(n){$a.set(n||{})}const Rn=Cn([]),Yn=Cn({}),To=Cn(!1),Y1=Cn({});let Gl;typeof BroadcastChannel<"u"&&(Gl=new BroadcastChannel("collections"),Gl.onmessage=()=>{var n;J1((n=Cg(Yn))==null?void 0:n.id)});function K1(){Gl==null||Gl.postMessage("reload")}function iv(n){Rn.update(e=>{const t=j.findByKey(e,"id",n);return t?Yn.set(t):e.length&&Yn.set(e[0]),e})}function lv(n){Yn.update(e=>j.isEmpty(e==null?void 0:e.id)||e.id===n.id?n:e),Rn.update(e=>(j.pushOrReplaceByKey(e,n,"id"),Ta(),K1(),j.sortCollections(e)))}function sv(n){Rn.update(e=>(j.removeByKey(e,"id",n.id),Yn.update(t=>t.id===n.id?e[0]:t),Ta(),K1(),e))}async function J1(n=null){To.set(!0);try{let e=await ae.collections.getFullList(200,{sort:"+name"});e=j.sortCollections(e),Rn.set(e);const t=n&&j.findByKey(e,"id",n);t?Yn.set(t):e.length&&Yn.set(e[0]),Ta()}catch(e){ae.error(e)}To.set(!1)}function Ta(){Y1.update(n=>(Rn.update(e=>{var t;for(let i of e)n[i.id]=!!((t=i.schema)!=null&&t.find(l=>{var s;return l.type=="file"&&((s=l.options)==null?void 0:s.protected)}));return e}),n))}const cr="pb_admin_file_token";Ho.prototype.logout=function(n=!0){this.authStore.clear(),n&&tl("/login")};Ho.prototype.error=function(n,e=!0,t=""){if(!n||!(n instanceof Error)||n.isAbort)return;const i=(n==null?void 0:n.status)<<0||400,l=(n==null?void 0:n.data)||{},s=l.message||n.message||t;if(e&&s&&ii(s),j.isEmpty(l.data)||Jt(l.data),i===401)return this.cancelAllRequests(),this.logout();if(i===403)return this.cancelAllRequests(),tl("/")};Ho.prototype.getAdminFileToken=async function(n=""){let e=!0;if(n){const i=Cg(Y1);e=typeof i[n]<"u"?i[n]:!0}if(!e)return"";let t=localStorage.getItem(cr)||"";return(!t||da(t,10))&&(t&&localStorage.removeItem(cr),this._adminFileTokenRequest||(this._adminFileTokenRequest=this.files.getToken()),t=await this._adminFileTokenRequest,localStorage.setItem(cr,t),this._adminFileTokenRequest=null),t};class ov extends Vg{save(e,t){super.save(e,t),t&&!t.collectionId&&Ur(t)}clear(){super.clear(),Ur(null)}}const ao=new Ho("../",new ov("pb_admin_auth"));ao.authStore.model&&!ao.authStore.model.collectionId&&Ur(ao.authStore.model);const ae=ao,rv=n=>({}),qf=n=>({});function av(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;const h=n[3].default,_=wt(h,n,n[2],null),g=n[3].footer,y=wt(g,n,n[2],qf);return{c(){e=b("div"),t=b("main"),_&&_.c(),i=M(),l=b("footer"),y&&y.c(),s=M(),o=b("a"),o.innerHTML=' Docs',r=M(),a=b("span"),a.textContent="|",f=M(),u=b("a"),c=b("span"),c.textContent="PocketBase v0.22.21",p(t,"class","page-content"),p(o,"href","https://pocketbase.io/docs/"),p(o,"target","_blank"),p(o,"rel","noopener noreferrer"),p(a,"class","delimiter"),p(c,"class","txt"),p(u,"href","https://github.com/pocketbase/pocketbase/releases"),p(u,"target","_blank"),p(u,"rel","noopener noreferrer"),p(u,"title","Releases"),p(l,"class","page-footer"),p(e,"class",d="page-wrapper "+n[1]),x(e,"center-content",n[0])},m(S,T){w(S,e,T),k(e,t),_&&_.m(t,null),k(e,i),k(e,l),y&&y.m(l,null),k(l,s),k(l,o),k(l,r),k(l,a),k(l,f),k(l,u),k(u,c),m=!0},p(S,[T]){_&&_.p&&(!m||T&4)&&$t(_,h,S,S[2],m?St(h,S[2],T,null):Tt(S[2]),null),y&&y.p&&(!m||T&4)&&$t(y,g,S,S[2],m?St(g,S[2],T,rv):Tt(S[2]),qf),(!m||T&2&&d!==(d="page-wrapper "+S[1]))&&p(e,"class",d),(!m||T&3)&&x(e,"center-content",S[0])},i(S){m||(E(_,S),E(y,S),m=!0)},o(S){A(_,S),A(y,S),m=!1},d(S){S&&v(e),_&&_.d(S),y&&y.d(S)}}}function fv(n,e,t){let{$$slots:i={},$$scope:l}=e,{center:s=!1}=e,{class:o=""}=e;return n.$$set=r=>{"center"in r&&t(0,s=r.center),"class"in r&&t(1,o=r.class),"$$scope"in r&&t(2,l=r.$$scope)},[s,o,l,i]}class bn extends ge{constructor(e){super(),_e(this,e,fv,av,me,{center:0,class:1})}}function jf(n){let e,t,i;return{c(){e=b("div"),e.innerHTML='',t=M(),i=b("div"),p(e,"class","block txt-center m-b-lg"),p(i,"class","clearfix")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},d(l){l&&(v(e),v(t),v(i))}}}function uv(n){let e,t,i,l=!n[0]&&jf();const s=n[1].default,o=wt(s,n,n[2],null);return{c(){e=b("div"),l&&l.c(),t=M(),o&&o.c(),p(e,"class","wrapper wrapper-sm m-b-xl panel-wrapper svelte-lxxzfu")},m(r,a){w(r,e,a),l&&l.m(e,null),k(e,t),o&&o.m(e,null),i=!0},p(r,a){r[0]?l&&(l.d(1),l=null):l||(l=jf(),l.c(),l.m(e,t)),o&&o.p&&(!i||a&4)&&$t(o,s,r,r[2],i?St(s,r[2],a,null):Tt(r[2]),null)},i(r){i||(E(o,r),i=!0)},o(r){A(o,r),i=!1},d(r){r&&v(e),l&&l.d(),o&&o.d(r)}}}function cv(n){let e,t;return e=new bn({props:{class:"full-page",center:!0,$$slots:{default:[uv]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&5&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function dv(n,e,t){let{$$slots:i={},$$scope:l}=e,{nobranding:s=!1}=e;return n.$$set=o=>{"nobranding"in o&&t(0,s=o.nobranding),"$$scope"in o&&t(2,l=o.$$scope)},[s,i,l]}class Z1 extends ge{constructor(e){super(),_e(this,e,dv,cv,me,{nobranding:0})}}function Jo(n){const e=n-1;return e*e*e+1}function rs(n,{delay:e=0,duration:t=400,easing:i=gs}={}){const l=+getComputedStyle(n).opacity;return{delay:e,duration:t,easing:i,css:s=>`opacity: ${s*l}`}}function Fn(n,{delay:e=0,duration:t=400,easing:i=Jo,x:l=0,y:s=0,opacity:o=0}={}){const r=getComputedStyle(n),a=+r.opacity,f=r.transform==="none"?"":r.transform,u=a*(1-o),[c,d]=Ga(l),[m,h]=Ga(s);return{delay:e,duration:t,easing:i,css:(_,g)=>` - transform: ${f} translate(${(1-_)*c}${d}, ${(1-_)*m}${h}); - opacity: ${a-u*g}`}}function et(n,{delay:e=0,duration:t=400,easing:i=Jo,axis:l="y"}={}){const s=getComputedStyle(n),o=+s.opacity,r=l==="y"?"height":"width",a=parseFloat(s[r]),f=l==="y"?["top","bottom"]:["left","right"],u=f.map(y=>`${y[0].toUpperCase()}${y.slice(1)}`),c=parseFloat(s[`padding${u[0]}`]),d=parseFloat(s[`padding${u[1]}`]),m=parseFloat(s[`margin${u[0]}`]),h=parseFloat(s[`margin${u[1]}`]),_=parseFloat(s[`border${u[0]}Width`]),g=parseFloat(s[`border${u[1]}Width`]);return{delay:e,duration:t,easing:i,css:y=>`overflow: hidden;opacity: ${Math.min(y*20,1)*o};${r}: ${y*a}px;padding-${f[0]}: ${y*c}px;padding-${f[1]}: ${y*d}px;margin-${f[0]}: ${y*m}px;margin-${f[1]}: ${y*h}px;border-${f[0]}-width: ${y*_}px;border-${f[1]}-width: ${y*g}px;`}}function Wt(n,{delay:e=0,duration:t=400,easing:i=Jo,start:l=0,opacity:s=0}={}){const o=getComputedStyle(n),r=+o.opacity,a=o.transform==="none"?"":o.transform,f=1-l,u=r*(1-s);return{delay:e,duration:t,easing:i,css:(c,d)=>` - transform: ${a} scale(${1-f*d}); - opacity: ${r-u*d} - `}}let Wr,Fi;const Yr="app-tooltip";function Hf(n){return typeof n=="string"?{text:n,position:"bottom",hideOnClick:null}:n||{}}function Oi(){return Fi=Fi||document.querySelector("."+Yr),Fi||(Fi=document.createElement("div"),Fi.classList.add(Yr),document.body.appendChild(Fi)),Fi}function G1(n,e){let t=Oi();if(!t.classList.contains("active")||!(e!=null&&e.text)){Kr();return}t.textContent=e.text,t.className=Yr+" active",e.class&&t.classList.add(e.class),e.position&&t.classList.add(e.position),t.style.top="0px",t.style.left="0px";let i=t.offsetHeight,l=t.offsetWidth,s=n.getBoundingClientRect(),o=0,r=0,a=5;e.position=="left"?(o=s.top+s.height/2-i/2,r=s.left-l-a):e.position=="right"?(o=s.top+s.height/2-i/2,r=s.right+a):e.position=="top"?(o=s.top-i-a,r=s.left+s.width/2-l/2):e.position=="top-left"?(o=s.top-i-a,r=s.left):e.position=="top-right"?(o=s.top-i-a,r=s.right-l):e.position=="bottom-left"?(o=s.top+s.height+a,r=s.left):e.position=="bottom-right"?(o=s.top+s.height+a,r=s.right-l):(o=s.top+s.height+a,r=s.left+s.width/2-l/2),r+l>document.documentElement.clientWidth&&(r=document.documentElement.clientWidth-l),r=r>=0?r:0,o+i>document.documentElement.clientHeight&&(o=document.documentElement.clientHeight-i),o=o>=0?o:0,t.style.top=o+"px",t.style.left=r+"px"}function Kr(){clearTimeout(Wr),Oi().classList.remove("active"),Oi().activeNode=void 0}function pv(n,e){Oi().activeNode=n,clearTimeout(Wr),Wr=setTimeout(()=>{Oi().classList.add("active"),G1(n,e)},isNaN(e.delay)?0:e.delay)}function Pe(n,e){let t=Hf(e);function i(){pv(n,t)}function l(){Kr()}return n.addEventListener("mouseenter",i),n.addEventListener("mouseleave",l),n.addEventListener("blur",l),(t.hideOnClick===!0||t.hideOnClick===null&&j.isFocusable(n))&&n.addEventListener("click",l),Oi(),{update(s){var o,r;t=Hf(s),(r=(o=Oi())==null?void 0:o.activeNode)!=null&&r.contains(n)&&G1(n,t)},destroy(){var s,o;(o=(s=Oi())==null?void 0:s.activeNode)!=null&&o.contains(n)&&Kr(),n.removeEventListener("mouseenter",i),n.removeEventListener("mouseleave",l),n.removeEventListener("blur",l),n.removeEventListener("click",l)}}}function zf(n,e,t){const i=n.slice();return i[12]=e[t],i}const mv=n=>({}),Vf=n=>({uniqueId:n[4]});function hv(n){let e,t,i=ue(n[3]),l=[];for(let o=0;oA(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;o{s&&(l||(l=Fe(t,Wt,{duration:150,start:.7},!0)),l.run(1))}),s=!0)},o(a){a&&(l||(l=Fe(t,Wt,{duration:150,start:.7},!1)),l.run(0)),s=!1},d(a){a&&v(e),a&&l&&l.end(),o=!1,r()}}}function Bf(n){let e,t,i=Co(n[12])+"",l,s,o,r;return{c(){e=b("div"),t=b("pre"),l=K(i),s=M(),p(e,"class","help-block help-block-error")},m(a,f){w(a,e,f),k(e,t),k(t,l),k(e,s),r=!0},p(a,f){(!r||f&8)&&i!==(i=Co(a[12])+"")&&oe(l,i)},i(a){r||(a&&Ke(()=>{r&&(o||(o=Fe(e,et,{duration:150},!0)),o.run(1))}),r=!0)},o(a){a&&(o||(o=Fe(e,et,{duration:150},!1)),o.run(0)),r=!1},d(a){a&&v(e),a&&o&&o.end()}}}function gv(n){let e,t,i,l,s,o,r;const a=n[9].default,f=wt(a,n,n[8],Vf),u=[_v,hv],c=[];function d(m,h){return m[0]&&m[3].length?0:1}return i=d(n),l=c[i]=u[i](n),{c(){e=b("div"),f&&f.c(),t=M(),l.c(),p(e,"class",n[1]),x(e,"error",n[3].length)},m(m,h){w(m,e,h),f&&f.m(e,null),k(e,t),c[i].m(e,null),n[11](e),s=!0,o||(r=J(e,"click",n[10]),o=!0)},p(m,[h]){f&&f.p&&(!s||h&256)&&$t(f,a,m,m[8],s?St(a,m[8],h,mv):Tt(m[8]),Vf);let _=i;i=d(m),i===_?c[i].p(m,h):(le(),A(c[_],1,1,()=>{c[_]=null}),se(),l=c[i],l?l.p(m,h):(l=c[i]=u[i](m),l.c()),E(l,1),l.m(e,null)),(!s||h&2)&&p(e,"class",m[1]),(!s||h&10)&&x(e,"error",m[3].length)},i(m){s||(E(f,m),E(l),s=!0)},o(m){A(f,m),A(l),s=!1},d(m){m&&v(e),f&&f.d(m),c[i].d(),n[11](null),o=!1,r()}}}const Uf="Invalid value";function Co(n){return typeof n=="object"?(n==null?void 0:n.message)||(n==null?void 0:n.code)||Uf:n||Uf}function bv(n,e,t){let i;Ue(n,mi,_=>t(7,i=_));let{$$slots:l={},$$scope:s}=e;const o="field_"+j.randomString(7);let{name:r=""}=e,{inlineError:a=!1}=e,{class:f=void 0}=e,u,c=[];function d(){li(r)}Ht(()=>(u.addEventListener("input",d),u.addEventListener("change",d),()=>{u.removeEventListener("input",d),u.removeEventListener("change",d)}));function m(_){Ce.call(this,n,_)}function h(_){ee[_?"unshift":"push"](()=>{u=_,t(2,u)})}return n.$$set=_=>{"name"in _&&t(5,r=_.name),"inlineError"in _&&t(0,a=_.inlineError),"class"in _&&t(1,f=_.class),"$$scope"in _&&t(8,s=_.$$scope)},n.$$.update=()=>{n.$$.dirty&160&&t(3,c=j.toArray(j.getNestedVal(i,r)))},[a,f,u,c,o,r,d,i,s,l,m,h]}class ce extends ge{constructor(e){super(),_e(this,e,bv,gv,me,{name:5,inlineError:0,class:1,changed:6})}get changed(){return this.$$.ctx[6]}}function kv(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Email"),l=M(),s=b("input"),p(e,"for",i=n[9]),p(s,"type","email"),p(s,"autocomplete","off"),p(s,"id",o=n[9]),s.required=!0,s.autofocus=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0]),s.focus(),r||(a=J(s,"input",n[5]),r=!0)},p(f,u){u&512&&i!==(i=f[9])&&p(e,"for",i),u&512&&o!==(o=f[9])&&p(s,"id",o),u&1&&s.value!==f[0]&&re(s,f[0])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function yv(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=K("Password"),l=M(),s=b("input"),r=M(),a=b("div"),a.textContent="Minimum 10 characters.",p(e,"for",i=n[9]),p(s,"type","password"),p(s,"autocomplete","new-password"),p(s,"minlength","10"),p(s,"id",o=n[9]),s.required=!0,p(a,"class","help-block")},m(c,d){w(c,e,d),k(e,t),w(c,l,d),w(c,s,d),re(s,n[1]),w(c,r,d),w(c,a,d),f||(u=J(s,"input",n[6]),f=!0)},p(c,d){d&512&&i!==(i=c[9])&&p(e,"for",i),d&512&&o!==(o=c[9])&&p(s,"id",o),d&2&&s.value!==c[1]&&re(s,c[1])},d(c){c&&(v(e),v(l),v(s),v(r),v(a)),f=!1,u()}}}function vv(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Password confirm"),l=M(),s=b("input"),p(e,"for",i=n[9]),p(s,"type","password"),p(s,"minlength","10"),p(s,"id",o=n[9]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[2]),r||(a=J(s,"input",n[7]),r=!0)},p(f,u){u&512&&i!==(i=f[9])&&p(e,"for",i),u&512&&o!==(o=f[9])&&p(s,"id",o),u&4&&s.value!==f[2]&&re(s,f[2])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function wv(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;return l=new ce({props:{class:"form-field required",name:"email",$$slots:{default:[kv,({uniqueId:h})=>({9:h}),({uniqueId:h})=>h?512:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field required",name:"password",$$slots:{default:[yv,({uniqueId:h})=>({9:h}),({uniqueId:h})=>h?512:0]},$$scope:{ctx:n}}}),a=new ce({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[vv,({uniqueId:h})=>({9:h}),({uniqueId:h})=>h?512:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("div"),t.innerHTML="

    Create your first admin account in order to continue

    ",i=M(),B(l.$$.fragment),s=M(),B(o.$$.fragment),r=M(),B(a.$$.fragment),f=M(),u=b("button"),u.innerHTML='Create and login ',p(t,"class","content txt-center m-b-base"),p(u,"type","submit"),p(u,"class","btn btn-lg btn-block btn-next"),x(u,"btn-disabled",n[3]),x(u,"btn-loading",n[3]),p(e,"class","block"),p(e,"autocomplete","off")},m(h,_){w(h,e,_),k(e,t),k(e,i),z(l,e,null),k(e,s),z(o,e,null),k(e,r),z(a,e,null),k(e,f),k(e,u),c=!0,d||(m=J(e,"submit",Be(n[4])),d=!0)},p(h,[_]){const g={};_&1537&&(g.$$scope={dirty:_,ctx:h}),l.$set(g);const y={};_&1538&&(y.$$scope={dirty:_,ctx:h}),o.$set(y);const S={};_&1540&&(S.$$scope={dirty:_,ctx:h}),a.$set(S),(!c||_&8)&&x(u,"btn-disabled",h[3]),(!c||_&8)&&x(u,"btn-loading",h[3])},i(h){c||(E(l.$$.fragment,h),E(o.$$.fragment,h),E(a.$$.fragment,h),c=!0)},o(h){A(l.$$.fragment,h),A(o.$$.fragment,h),A(a.$$.fragment,h),c=!1},d(h){h&&v(e),V(l),V(o),V(a),d=!1,m()}}}function Sv(n,e,t){const i=lt();let l="",s="",o="",r=!1;async function a(){if(!r){t(3,r=!0);try{await ae.admins.create({email:l,password:s,passwordConfirm:o}),await ae.admins.authWithPassword(l,s),i("submit")}catch(d){ae.error(d)}t(3,r=!1)}}function f(){l=this.value,t(0,l)}function u(){s=this.value,t(1,s)}function c(){o=this.value,t(2,o)}return[l,s,o,r,a,f,u,c]}class $v extends ge{constructor(e){super(),_e(this,e,Sv,wv,me,{})}}function Wf(n){let e,t;return e=new Z1({props:{$$slots:{default:[Tv]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&9&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Tv(n){let e,t;return e=new $v({}),e.$on("submit",n[1]),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p:Q,i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Cv(n){let e,t,i=n[0]&&Wf(n);return{c(){i&&i.c(),e=ye()},m(l,s){i&&i.m(l,s),w(l,e,s),t=!0},p(l,[s]){l[0]?i?(i.p(l,s),s&1&&E(i,1)):(i=Wf(l),i.c(),E(i,1),i.m(e.parentNode,e)):i&&(le(),A(i,1,1,()=>{i=null}),se())},i(l){t||(E(i),t=!0)},o(l){A(i),t=!1},d(l){l&&v(e),i&&i.d(l)}}}function Ov(n,e,t){let i=!1;l();function l(){if(t(0,i=!1),new URLSearchParams(window.location.search).has("installer")){ae.logout(!1),t(0,i=!0);return}ae.authStore.isValid?tl("/collections"):ae.logout()}return[i,async()=>{t(0,i=!1),await Qt(),window.location.search=""}]}class Mv extends ge{constructor(e){super(),_e(this,e,Ov,Cv,me,{})}}const It=Cn(""),Oo=Cn(""),Xi=Cn(!1);function Dv(n){let e,t,i,l;return{c(){e=b("input"),p(e,"type","text"),p(e,"id",n[8]),p(e,"placeholder",t=n[0]||n[1])},m(s,o){w(s,e,o),n[13](e),re(e,n[7]),i||(l=J(e,"input",n[14]),i=!0)},p(s,o){o&3&&t!==(t=s[0]||s[1])&&p(e,"placeholder",t),o&128&&e.value!==s[7]&&re(e,s[7])},i:Q,o:Q,d(s){s&&v(e),n[13](null),i=!1,l()}}}function Ev(n){let e,t,i,l;function s(a){n[12](a)}var o=n[4];function r(a,f){let u={id:a[8],singleLine:!0,disableRequestKeys:!0,disableCollectionJoinKeys:!0,extraAutocompleteKeys:a[3],baseCollection:a[2],placeholder:a[0]||a[1]};return a[7]!==void 0&&(u.value=a[7]),{props:u}}return o&&(e=Dt(o,r(n)),ee.push(()=>be(e,"value",s)),e.$on("submit",n[10])),{c(){e&&B(e.$$.fragment),i=ye()},m(a,f){e&&z(e,a,f),w(a,i,f),l=!0},p(a,f){if(f&16&&o!==(o=a[4])){if(e){le();const u=e;A(u.$$.fragment,1,0,()=>{V(u,1)}),se()}o?(e=Dt(o,r(a)),ee.push(()=>be(e,"value",s)),e.$on("submit",a[10]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,i.parentNode,i)):e=null}else if(o){const u={};f&8&&(u.extraAutocompleteKeys=a[3]),f&4&&(u.baseCollection=a[2]),f&3&&(u.placeholder=a[0]||a[1]),!t&&f&128&&(t=!0,u.value=a[7],ke(()=>t=!1)),e.$set(u)}},i(a){l||(e&&E(e.$$.fragment,a),l=!0)},o(a){e&&A(e.$$.fragment,a),l=!1},d(a){a&&v(i),e&&V(e,a)}}}function Yf(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Search',p(e,"type","submit"),p(e,"class","btn btn-expanded-sm btn-sm btn-warning")},m(l,s){w(l,e,s),i=!0},i(l){i||(l&&Ke(()=>{i&&(t||(t=Fe(e,Fn,{duration:150,x:5},!0)),t.run(1))}),i=!0)},o(l){l&&(t||(t=Fe(e,Fn,{duration:150,x:5},!1)),t.run(0)),i=!1},d(l){l&&v(e),l&&t&&t.end()}}}function Kf(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Clear',p(e,"type","button"),p(e,"class","btn btn-transparent btn-sm btn-hint p-l-xs p-r-xs m-l-10")},m(o,r){w(o,e,r),i=!0,l||(s=J(e,"click",n[15]),l=!0)},p:Q,i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Fn,{duration:150,x:5},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Fn,{duration:150,x:5},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function Iv(n){let e,t,i,l,s,o,r,a,f,u,c;const d=[Ev,Dv],m=[];function h(y,S){return y[4]&&!y[5]?0:1}s=h(n),o=m[s]=d[s](n);let _=(n[0].length||n[7].length)&&n[7]!=n[0]&&Yf(),g=(n[0].length||n[7].length)&&Kf(n);return{c(){e=b("form"),t=b("label"),i=b("i"),l=M(),o.c(),r=M(),_&&_.c(),a=M(),g&&g.c(),p(i,"class","ri-search-line"),p(t,"for",n[8]),p(t,"class","m-l-10 txt-xl"),p(e,"class","searchbar")},m(y,S){w(y,e,S),k(e,t),k(t,i),k(e,l),m[s].m(e,null),k(e,r),_&&_.m(e,null),k(e,a),g&&g.m(e,null),f=!0,u||(c=[J(e,"click",Tn(n[11])),J(e,"submit",Be(n[10]))],u=!0)},p(y,[S]){let T=s;s=h(y),s===T?m[s].p(y,S):(le(),A(m[T],1,1,()=>{m[T]=null}),se(),o=m[s],o?o.p(y,S):(o=m[s]=d[s](y),o.c()),E(o,1),o.m(e,r)),(y[0].length||y[7].length)&&y[7]!=y[0]?_?S&129&&E(_,1):(_=Yf(),_.c(),E(_,1),_.m(e,a)):_&&(le(),A(_,1,1,()=>{_=null}),se()),y[0].length||y[7].length?g?(g.p(y,S),S&129&&E(g,1)):(g=Kf(y),g.c(),E(g,1),g.m(e,null)):g&&(le(),A(g,1,1,()=>{g=null}),se())},i(y){f||(E(o),E(_),E(g),f=!0)},o(y){A(o),A(_),A(g),f=!1},d(y){y&&v(e),m[s].d(),_&&_.d(),g&&g.d(),u=!1,$e(c)}}}function Av(n,e,t){const i=lt(),l="search_"+j.randomString(7);let{value:s=""}=e,{placeholder:o='Search term or filter like created > "2022-01-01"...'}=e,{autocompleteCollection:r=j.initCollection()}=e,{extraAutocompleteKeys:a=[]}=e,f,u=!1,c,d="";function m(C=!0){t(7,d=""),C&&(c==null||c.focus()),i("clear")}function h(){t(0,s=d),i("submit",s)}async function _(){f||u||(t(5,u=!0),t(4,f=(await tt(async()=>{const{default:C}=await import("./FilterAutocompleteInput-l9cXyHQU.js");return{default:C}},__vite__mapDeps([0,1]),import.meta.url)).default),t(5,u=!1))}Ht(()=>{_()});function g(C){Ce.call(this,n,C)}function y(C){d=C,t(7,d),t(0,s)}function S(C){ee[C?"unshift":"push"](()=>{c=C,t(6,c)})}function T(){d=this.value,t(7,d),t(0,s)}const $=()=>{m(!1),h()};return n.$$set=C=>{"value"in C&&t(0,s=C.value),"placeholder"in C&&t(1,o=C.placeholder),"autocompleteCollection"in C&&t(2,r=C.autocompleteCollection),"extraAutocompleteKeys"in C&&t(3,a=C.extraAutocompleteKeys)},n.$$.update=()=>{n.$$.dirty&1&&typeof s=="string"&&t(7,d=s)},[s,o,r,a,f,u,c,d,l,m,h,g,y,S,T,$]}class $s extends ge{constructor(e){super(),_e(this,e,Av,Iv,me,{value:0,placeholder:1,autocompleteCollection:2,extraAutocompleteKeys:3})}}function Lv(n){let e,t,i,l,s,o;return{c(){e=b("button"),t=b("i"),p(t,"class","ri-refresh-line svelte-1bvelc2"),p(e,"type","button"),p(e,"aria-label","Refresh"),p(e,"class",i="btn btn-transparent btn-circle "+n[1]+" svelte-1bvelc2"),x(e,"refreshing",n[2])},m(r,a){w(r,e,a),k(e,t),s||(o=[Se(l=Pe.call(null,e,n[0])),J(e,"click",n[3])],s=!0)},p(r,[a]){a&2&&i!==(i="btn btn-transparent btn-circle "+r[1]+" svelte-1bvelc2")&&p(e,"class",i),l&&Ct(l.update)&&a&1&&l.update.call(null,r[0]),a&6&&x(e,"refreshing",r[2])},i:Q,o:Q,d(r){r&&v(e),s=!1,$e(o)}}}function Nv(n,e,t){const i=lt();let{tooltip:l={text:"Refresh",position:"right"}}=e,{class:s=""}=e,o=null;function r(){i("refresh");const a=l;t(0,l=null),clearTimeout(o),t(2,o=setTimeout(()=>{t(2,o=null),t(0,l=a)},150))}return Ht(()=>()=>clearTimeout(o)),n.$$set=a=>{"tooltip"in a&&t(0,l=a.tooltip),"class"in a&&t(1,s=a.class)},[l,s,o,r]}class Zo extends ge{constructor(e){super(),_e(this,e,Nv,Lv,me,{tooltip:0,class:1})}}function Pv(n){let e,t,i,l,s;const o=n[6].default,r=wt(o,n,n[5],null);return{c(){e=b("th"),r&&r.c(),p(e,"tabindex","0"),p(e,"title",n[2]),p(e,"class",t="col-sort "+n[1]),x(e,"col-sort-disabled",n[3]),x(e,"sort-active",n[0]==="-"+n[2]||n[0]==="+"+n[2]),x(e,"sort-desc",n[0]==="-"+n[2]),x(e,"sort-asc",n[0]==="+"+n[2])},m(a,f){w(a,e,f),r&&r.m(e,null),i=!0,l||(s=[J(e,"click",n[7]),J(e,"keydown",n[8])],l=!0)},p(a,[f]){r&&r.p&&(!i||f&32)&&$t(r,o,a,a[5],i?St(o,a[5],f,null):Tt(a[5]),null),(!i||f&4)&&p(e,"title",a[2]),(!i||f&2&&t!==(t="col-sort "+a[1]))&&p(e,"class",t),(!i||f&10)&&x(e,"col-sort-disabled",a[3]),(!i||f&7)&&x(e,"sort-active",a[0]==="-"+a[2]||a[0]==="+"+a[2]),(!i||f&7)&&x(e,"sort-desc",a[0]==="-"+a[2]),(!i||f&7)&&x(e,"sort-asc",a[0]==="+"+a[2])},i(a){i||(E(r,a),i=!0)},o(a){A(r,a),i=!1},d(a){a&&v(e),r&&r.d(a),l=!1,$e(s)}}}function Fv(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,{name:o}=e,{sort:r=""}=e,{disable:a=!1}=e;function f(){a||("-"+o===r?t(0,r="+"+o):t(0,r="-"+o))}const u=()=>f(),c=d=>{(d.code==="Enter"||d.code==="Space")&&(d.preventDefault(),f())};return n.$$set=d=>{"class"in d&&t(1,s=d.class),"name"in d&&t(2,o=d.name),"sort"in d&&t(0,r=d.sort),"disable"in d&&t(3,a=d.disable),"$$scope"in d&&t(5,l=d.$$scope)},[r,s,o,a,f,l,i,u,c]}class Sn extends ge{constructor(e){super(),_e(this,e,Fv,Pv,me,{class:1,name:2,sort:0,disable:3})}}const Rv=n=>({}),Jf=n=>({}),qv=n=>({}),Zf=n=>({});function jv(n){let e,t,i,l,s,o,r,a;const f=n[11].before,u=wt(f,n,n[10],Zf),c=n[11].default,d=wt(c,n,n[10],null),m=n[11].after,h=wt(m,n,n[10],Jf);return{c(){e=b("div"),u&&u.c(),t=M(),i=b("div"),d&&d.c(),s=M(),h&&h.c(),p(i,"class",l="scroller "+n[0]+" "+n[3]+" svelte-3a0gfs"),p(e,"class","scroller-wrapper svelte-3a0gfs")},m(_,g){w(_,e,g),u&&u.m(e,null),k(e,t),k(e,i),d&&d.m(i,null),n[12](i),k(e,s),h&&h.m(e,null),o=!0,r||(a=[J(window,"resize",n[1]),J(i,"scroll",n[1])],r=!0)},p(_,[g]){u&&u.p&&(!o||g&1024)&&$t(u,f,_,_[10],o?St(f,_[10],g,qv):Tt(_[10]),Zf),d&&d.p&&(!o||g&1024)&&$t(d,c,_,_[10],o?St(c,_[10],g,null):Tt(_[10]),null),(!o||g&9&&l!==(l="scroller "+_[0]+" "+_[3]+" svelte-3a0gfs"))&&p(i,"class",l),h&&h.p&&(!o||g&1024)&&$t(h,m,_,_[10],o?St(m,_[10],g,Rv):Tt(_[10]),Jf)},i(_){o||(E(u,_),E(d,_),E(h,_),o=!0)},o(_){A(u,_),A(d,_),A(h,_),o=!1},d(_){_&&v(e),u&&u.d(_),d&&d.d(_),n[12](null),h&&h.d(_),r=!1,$e(a)}}}function Hv(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=lt();let{class:o=""}=e,{vThreshold:r=0}=e,{hThreshold:a=0}=e,{dispatchOnNoScroll:f=!0}=e,u=null,c="",d=null,m,h,_,g,y;function S(){u&&t(2,u.scrollTop=0,u)}function T(){u&&t(2,u.scrollLeft=0,u)}function $(){u&&(t(3,c=""),_=u.clientWidth+2,g=u.clientHeight+2,m=u.scrollWidth-_,h=u.scrollHeight-g,h>0?(t(3,c+=" v-scroll"),r>=g&&t(4,r=0),u.scrollTop-r<=0&&(t(3,c+=" v-scroll-start"),s("vScrollStart")),u.scrollTop+r>=h&&(t(3,c+=" v-scroll-end"),s("vScrollEnd"))):f&&s("vScrollEnd"),m>0?(t(3,c+=" h-scroll"),a>=_&&t(5,a=0),u.scrollLeft-a<=0&&(t(3,c+=" h-scroll-start"),s("hScrollStart")),u.scrollLeft+a>=m&&(t(3,c+=" h-scroll-end"),s("hScrollEnd"))):f&&s("hScrollEnd"))}function C(){d||(d=setTimeout(()=>{$(),d=null},150))}Ht(()=>(C(),y=new MutationObserver(C),y.observe(u,{attributeFilter:["width","height"],childList:!0,subtree:!0}),()=>{y==null||y.disconnect(),clearTimeout(d)}));function O(D){ee[D?"unshift":"push"](()=>{u=D,t(2,u)})}return n.$$set=D=>{"class"in D&&t(0,o=D.class),"vThreshold"in D&&t(4,r=D.vThreshold),"hThreshold"in D&&t(5,a=D.hThreshold),"dispatchOnNoScroll"in D&&t(6,f=D.dispatchOnNoScroll),"$$scope"in D&&t(10,l=D.$$scope)},[o,C,u,c,r,a,f,S,T,$,l,i,O]}class Go extends ge{constructor(e){super(),_e(this,e,Hv,jv,me,{class:0,vThreshold:4,hThreshold:5,dispatchOnNoScroll:6,resetVerticalScroll:7,resetHorizontalScroll:8,refresh:9,throttleRefresh:1})}get resetVerticalScroll(){return this.$$.ctx[7]}get resetHorizontalScroll(){return this.$$.ctx[8]}get refresh(){return this.$$.ctx[9]}get throttleRefresh(){return this.$$.ctx[1]}}function zv(n){let e,t,i=(n[1]||"UNKN")+"",l,s,o,r,a;return{c(){e=b("div"),t=b("span"),l=K(i),s=K(" ("),o=K(n[0]),r=K(")"),p(t,"class","txt"),p(e,"class",a="label log-level-label level-"+n[0]+" svelte-ha6hme")},m(f,u){w(f,e,u),k(e,t),k(t,l),k(t,s),k(t,o),k(t,r)},p(f,[u]){u&2&&i!==(i=(f[1]||"UNKN")+"")&&oe(l,i),u&1&&oe(o,f[0]),u&1&&a!==(a="label log-level-label level-"+f[0]+" svelte-ha6hme")&&p(e,"class",a)},i:Q,o:Q,d(f){f&&v(e)}}}function Vv(n,e,t){let i,{level:l}=e;return n.$$set=s=>{"level"in s&&t(0,l=s.level)},n.$$.update=()=>{var s;n.$$.dirty&1&&t(1,i=(s=U1.find(o=>o.level==l))==null?void 0:s.label)},[l,i]}class X1 extends ge{constructor(e){super(),_e(this,e,Vv,zv,me,{level:0})}}function Bv(n){let e,t=n[0].replace("Z"," UTC")+"",i,l,s;return{c(){e=b("span"),i=K(t),p(e,"class","txt-nowrap")},m(o,r){w(o,e,r),k(e,i),l||(s=Se(Pe.call(null,e,n[1])),l=!0)},p(o,[r]){r&1&&t!==(t=o[0].replace("Z"," UTC")+"")&&oe(i,t)},i:Q,o:Q,d(o){o&&v(e),l=!1,s()}}}function Uv(n,e,t){let{date:i}=e;const l={get text(){return j.formatToLocalDate(i,"yyyy-MM-dd HH:mm:ss.SSS")+" Local"}};return n.$$set=s=>{"date"in s&&t(0,i=s.date)},[i,l]}class Q1 extends ge{constructor(e){super(),_e(this,e,Uv,Bv,me,{date:0})}}function Gf(n,e,t){var o;const i=n.slice();i[31]=e[t];const l=((o=i[31].data)==null?void 0:o.type)=="request";i[32]=l;const s=n2(i[31]);return i[33]=s,i}function Xf(n,e,t){const i=n.slice();return i[36]=e[t],i}function Wv(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),t=b("input"),l=M(),s=b("label"),p(t,"type","checkbox"),p(t,"id","checkbox_0"),t.disabled=i=!n[3].length,t.checked=n[8],p(s,"for","checkbox_0"),p(e,"class","form-field")},m(a,f){w(a,e,f),k(e,t),k(e,l),k(e,s),o||(r=J(t,"change",n[18]),o=!0)},p(a,f){f[0]&8&&i!==(i=!a[3].length)&&(t.disabled=i),f[0]&256&&(t.checked=a[8])},d(a){a&&v(e),o=!1,r()}}}function Yv(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Kv(n){let e;return{c(){e=b("div"),e.innerHTML=' level',p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Jv(n){let e;return{c(){e=b("div"),e.innerHTML=' message',p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Zv(n){let e;return{c(){e=b("div"),e.innerHTML=` created`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Qf(n){let e;function t(s,o){return s[7]?Xv:Gv}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function Gv(n){var r;let e,t,i,l,s,o=((r=n[0])==null?void 0:r.length)&&xf(n);return{c(){e=b("tr"),t=b("td"),i=b("h6"),i.textContent="No logs found.",l=M(),o&&o.c(),s=M(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),k(e,t),k(t,i),k(t,l),o&&o.m(t,null),k(e,s)},p(a,f){var u;(u=a[0])!=null&&u.length?o?o.p(a,f):(o=xf(a),o.c(),o.m(t,null)):o&&(o.d(1),o=null)},d(a){a&&v(e),o&&o.d()}}}function Xv(n){let e;return{c(){e=b("tr"),e.innerHTML=' '},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function xf(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[25]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function eu(n){let e,t=ue(n[33]),i=[];for(let l=0;l',R=M(),p(s,"type","checkbox"),p(s,"id",o="checkbox_"+e[31].id),s.checked=r=e[4][e[31].id],p(f,"for",u="checkbox_"+e[31].id),p(l,"class","form-field"),p(i,"class","bulk-select-col min-width"),p(d,"class","col-type-text col-field-level min-width svelte-91v05h"),p(y,"class","txt-ellipsis"),p(g,"class","flex flex-gap-10"),p(_,"class","col-type-text col-field-message svelte-91v05h"),p(O,"class","col-type-date col-field-created"),p(L,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(U,Y){w(U,t,Y),k(t,i),k(i,l),k(l,s),k(l,a),k(l,f),k(t,c),k(t,d),z(m,d,null),k(t,h),k(t,_),k(_,g),k(g,y),k(y,T),k(_,$),H&&H.m(_,null),k(t,C),k(t,O),z(D,O,null),k(t,I),k(t,L),k(t,R),F=!0,N||(P=[J(s,"change",q),J(l,"click",Tn(e[17])),J(t,"click",W),J(t,"keydown",G)],N=!0)},p(U,Y){e=U,(!F||Y[0]&8&&o!==(o="checkbox_"+e[31].id))&&p(s,"id",o),(!F||Y[0]&24&&r!==(r=e[4][e[31].id]))&&(s.checked=r),(!F||Y[0]&8&&u!==(u="checkbox_"+e[31].id))&&p(f,"for",u);const ie={};Y[0]&8&&(ie.level=e[31].level),m.$set(ie),(!F||Y[0]&8)&&S!==(S=e[31].message+"")&&oe(T,S),e[33].length?H?H.p(e,Y):(H=eu(e),H.c(),H.m(_,null)):H&&(H.d(1),H=null);const te={};Y[0]&8&&(te.date=e[31].created),D.$set(te)},i(U){F||(E(m.$$.fragment,U),E(D.$$.fragment,U),F=!0)},o(U){A(m.$$.fragment,U),A(D.$$.fragment,U),F=!1},d(U){U&&v(t),V(m),H&&H.d(),V(D),N=!1,$e(P)}}}function e2(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S=[],T=new Map,$;function C(G,U){return G[7]?Yv:Wv}let O=C(n),D=O(n);function I(G){n[19](G)}let L={disable:!0,class:"col-field-level min-width",name:"level",$$slots:{default:[Kv]},$$scope:{ctx:n}};n[1]!==void 0&&(L.sort=n[1]),o=new Sn({props:L}),ee.push(()=>be(o,"sort",I));function R(G){n[20](G)}let F={disable:!0,class:"col-type-text col-field-message",name:"data",$$slots:{default:[Jv]},$$scope:{ctx:n}};n[1]!==void 0&&(F.sort=n[1]),f=new Sn({props:F}),ee.push(()=>be(f,"sort",R));function N(G){n[21](G)}let P={disable:!0,class:"col-type-date col-field-created",name:"created",$$slots:{default:[Zv]},$$scope:{ctx:n}};n[1]!==void 0&&(P.sort=n[1]),d=new Sn({props:P}),ee.push(()=>be(d,"sort",N));let q=ue(n[3]);const H=G=>G[31].id;for(let G=0;Gr=!1)),o.$set(Y);const ie={};U[1]&256&&(ie.$$scope={dirty:U,ctx:G}),!u&&U[0]&2&&(u=!0,ie.sort=G[1],ke(()=>u=!1)),f.$set(ie);const te={};U[1]&256&&(te.$$scope={dirty:U,ctx:G}),!m&&U[0]&2&&(m=!0,te.sort=G[1],ke(()=>m=!1)),d.$set(te),U[0]&9369&&(q=ue(G[3]),le(),S=at(S,U,H,1,G,q,T,y,Et,nu,null,Gf),se(),!q.length&&W?W.p(G,U):q.length?W&&(W.d(1),W=null):(W=Qf(G),W.c(),W.m(y,null))),(!$||U[0]&128)&&x(e,"table-loading",G[7])},i(G){if(!$){E(o.$$.fragment,G),E(f.$$.fragment,G),E(d.$$.fragment,G);for(let U=0;ULoad more',p(t,"type","button"),p(t,"class","btn btn-lg btn-secondary btn-expanded"),x(t,"btn-loading",n[7]),x(t,"btn-disabled",n[7]),p(e,"class","block txt-center m-t-sm")},m(s,o){w(s,e,o),k(e,t),i||(l=J(t,"click",n[26]),i=!0)},p(s,o){o[0]&128&&x(t,"btn-loading",s[7]),o[0]&128&&x(t,"btn-disabled",s[7])},d(s){s&&v(e),i=!1,l()}}}function lu(n){let e,t,i,l,s,o,r=n[5]===1?"log":"logs",a,f,u,c,d,m,h,_,g,y,S;return{c(){e=b("div"),t=b("div"),i=K("Selected "),l=b("strong"),s=K(n[5]),o=M(),a=K(r),f=M(),u=b("button"),u.innerHTML='Reset',c=M(),d=b("div"),m=M(),h=b("button"),h.innerHTML='Download as JSON',p(t,"class","txt"),p(u,"type","button"),p(u,"class","btn btn-xs btn-transparent btn-outline p-l-5 p-r-5"),p(d,"class","flex-fill"),p(h,"type","button"),p(h,"class","btn btn-sm"),p(e,"class","bulkbar svelte-91v05h")},m(T,$){w(T,e,$),k(e,t),k(t,i),k(t,l),k(l,s),k(t,o),k(t,a),k(e,f),k(e,u),k(e,c),k(e,d),k(e,m),k(e,h),g=!0,y||(S=[J(u,"click",n[27]),J(h,"click",n[14])],y=!0)},p(T,$){(!g||$[0]&32)&&oe(s,T[5]),(!g||$[0]&32)&&r!==(r=T[5]===1?"log":"logs")&&oe(a,r)},i(T){g||(T&&Ke(()=>{g&&(_||(_=Fe(e,Fn,{duration:150,y:5},!0)),_.run(1))}),g=!0)},o(T){T&&(_||(_=Fe(e,Fn,{duration:150,y:5},!1)),_.run(0)),g=!1},d(T){T&&v(e),T&&_&&_.end(),y=!1,$e(S)}}}function t2(n){let e,t,i,l,s;e=new Go({props:{class:"table-wrapper",$$slots:{default:[e2]},$$scope:{ctx:n}}});let o=n[3].length&&n[9]&&iu(n),r=n[5]&&lu(n);return{c(){B(e.$$.fragment),t=M(),o&&o.c(),i=M(),r&&r.c(),l=ye()},m(a,f){z(e,a,f),w(a,t,f),o&&o.m(a,f),w(a,i,f),r&&r.m(a,f),w(a,l,f),s=!0},p(a,f){const u={};f[0]&411|f[1]&256&&(u.$$scope={dirty:f,ctx:a}),e.$set(u),a[3].length&&a[9]?o?o.p(a,f):(o=iu(a),o.c(),o.m(i.parentNode,i)):o&&(o.d(1),o=null),a[5]?r?(r.p(a,f),f[0]&32&&E(r,1)):(r=lu(a),r.c(),E(r,1),r.m(l.parentNode,l)):r&&(le(),A(r,1,1,()=>{r=null}),se())},i(a){s||(E(e.$$.fragment,a),E(r),s=!0)},o(a){A(e.$$.fragment,a),A(r),s=!1},d(a){a&&(v(t),v(i),v(l)),V(e,a),o&&o.d(a),r&&r.d(a)}}}const su=50,dr=/[-:\. ]/gi;function n2(n){let e=[];if(!n.data)return e;if(n.data.type=="request"){const t=["status","execTime","auth","userIp"];for(let i of t)typeof n.data[i]<"u"&&e.push({key:i});n.data.referer&&!n.data.referer.includes(window.location.host)&&e.push({key:"referer"})}else{const t=Object.keys(n.data);for(const i of t)i!="error"&&i!="details"&&e.length<6&&e.push({key:i})}return n.data.error&&e.push({key:"error",label:"label-danger"}),n.data.details&&e.push({key:"details",label:"label-warning"}),e}function i2(n,e,t){let i,l,s;const o=lt();let{filter:r=""}=e,{presets:a=""}=e,{sort:f="-rowid"}=e,u=[],c=1,d=0,m=!1,h=0,_={};async function g(U=1,Y=!0){t(7,m=!0);const ie=[a,j.normalizeLogsFilter(r)].filter(Boolean).join("&&");return ae.logs.getList(U,su,{sort:f,skipTotal:1,filter:ie}).then(async te=>{var Ne;U<=1&&y();const pe=j.toArray(te.items);if(t(7,m=!1),t(6,c=te.page),t(16,d=((Ne=te.items)==null?void 0:Ne.length)||0),o("load",u.concat(pe)),Y){const He=++h;for(;pe.length&&h==He;){const Xe=pe.splice(0,10);for(let xe of Xe)j.pushOrReplaceByKey(u,xe);t(3,u),await j.yieldToMain()}}else{for(let He of pe)j.pushOrReplaceByKey(u,He);t(3,u)}}).catch(te=>{te!=null&&te.isAbort||(t(7,m=!1),console.warn(te),y(),ae.error(te,!ie||(te==null?void 0:te.status)!=400))})}function y(){t(3,u=[]),t(4,_={}),t(6,c=1),t(16,d=0)}function S(){s?T():$()}function T(){t(4,_={})}function $(){for(const U of u)t(4,_[U.id]=U,_);t(4,_)}function C(U){_[U.id]?delete _[U.id]:t(4,_[U.id]=U,_),t(4,_)}function O(){const U=Object.values(_).sort((te,pe)=>te.createdpe.created?-1:0);if(!U.length)return;if(U.length==1)return j.downloadJson(U[0],"log_"+U[0].created.replaceAll(dr,"")+".json");const Y=U[0].created.replaceAll(dr,""),ie=U[U.length-1].created.replaceAll(dr,"");return j.downloadJson(U,`${U.length}_logs_${ie}_to_${Y}.json`)}function D(U){Ce.call(this,n,U)}const I=()=>S();function L(U){f=U,t(1,f)}function R(U){f=U,t(1,f)}function F(U){f=U,t(1,f)}const N=U=>C(U),P=U=>o("select",U),q=(U,Y)=>{Y.code==="Enter"&&(Y.preventDefault(),o("select",U))},H=()=>t(0,r=""),W=()=>g(c+1),G=()=>T();return n.$$set=U=>{"filter"in U&&t(0,r=U.filter),"presets"in U&&t(15,a=U.presets),"sort"in U&&t(1,f=U.sort)},n.$$.update=()=>{n.$$.dirty[0]&32771&&(typeof f<"u"||typeof r<"u"||typeof a<"u")&&(y(),g(1)),n.$$.dirty[0]&65536&&t(9,i=d>=su),n.$$.dirty[0]&16&&t(5,l=Object.keys(_).length),n.$$.dirty[0]&40&&t(8,s=u.length&&l===u.length)},[r,f,g,u,_,l,c,m,s,i,o,S,T,C,O,a,d,D,I,L,R,F,N,P,q,H,W,G]}class l2 extends ge{constructor(e){super(),_e(this,e,i2,t2,me,{filter:0,presets:15,sort:1,load:2},null,[-1,-1])}get load(){return this.$$.ctx[2]}}/*! - * @kurkle/color v0.3.2 - * https://github.com/kurkle/color#readme - * (c) 2023 Jukka Kurkela - * Released under the MIT License - */function Ts(n){return n+.5|0}const $i=(n,e,t)=>Math.max(Math.min(n,t),e);function Yl(n){return $i(Ts(n*2.55),0,255)}function Mi(n){return $i(Ts(n*255),0,255)}function ui(n){return $i(Ts(n/2.55)/100,0,1)}function ou(n){return $i(Ts(n*100),0,100)}const En={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Jr=[..."0123456789ABCDEF"],s2=n=>Jr[n&15],o2=n=>Jr[(n&240)>>4]+Jr[n&15],qs=n=>(n&240)>>4===(n&15),r2=n=>qs(n.r)&&qs(n.g)&&qs(n.b)&&qs(n.a);function a2(n){var e=n.length,t;return n[0]==="#"&&(e===4||e===5?t={r:255&En[n[1]]*17,g:255&En[n[2]]*17,b:255&En[n[3]]*17,a:e===5?En[n[4]]*17:255}:(e===7||e===9)&&(t={r:En[n[1]]<<4|En[n[2]],g:En[n[3]]<<4|En[n[4]],b:En[n[5]]<<4|En[n[6]],a:e===9?En[n[7]]<<4|En[n[8]]:255})),t}const f2=(n,e)=>n<255?e(n):"";function u2(n){var e=r2(n)?s2:o2;return n?"#"+e(n.r)+e(n.g)+e(n.b)+f2(n.a,e):void 0}const c2=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function x1(n,e,t){const i=e*Math.min(t,1-t),l=(s,o=(s+n/30)%12)=>t-i*Math.max(Math.min(o-3,9-o,1),-1);return[l(0),l(8),l(4)]}function d2(n,e,t){const i=(l,s=(l+n/60)%6)=>t-t*e*Math.max(Math.min(s,4-s,1),0);return[i(5),i(3),i(1)]}function p2(n,e,t){const i=x1(n,1,.5);let l;for(e+t>1&&(l=1/(e+t),e*=l,t*=l),l=0;l<3;l++)i[l]*=1-e-t,i[l]+=e;return i}function m2(n,e,t,i,l){return n===l?(e-t)/i+(e.5?u/(2-s-o):u/(s+o),a=m2(t,i,l,u,s),a=a*60+.5),[a|0,f||0,r]}function Oa(n,e,t,i){return(Array.isArray(e)?n(e[0],e[1],e[2]):n(e,t,i)).map(Mi)}function Ma(n,e,t){return Oa(x1,n,e,t)}function h2(n,e,t){return Oa(p2,n,e,t)}function _2(n,e,t){return Oa(d2,n,e,t)}function eb(n){return(n%360+360)%360}function g2(n){const e=c2.exec(n);let t=255,i;if(!e)return;e[5]!==i&&(t=e[6]?Yl(+e[5]):Mi(+e[5]));const l=eb(+e[2]),s=+e[3]/100,o=+e[4]/100;return e[1]==="hwb"?i=h2(l,s,o):e[1]==="hsv"?i=_2(l,s,o):i=Ma(l,s,o),{r:i[0],g:i[1],b:i[2],a:t}}function b2(n,e){var t=Ca(n);t[0]=eb(t[0]+e),t=Ma(t),n.r=t[0],n.g=t[1],n.b=t[2]}function k2(n){if(!n)return;const e=Ca(n),t=e[0],i=ou(e[1]),l=ou(e[2]);return n.a<255?`hsla(${t}, ${i}%, ${l}%, ${ui(n.a)})`:`hsl(${t}, ${i}%, ${l}%)`}const ru={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},au={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function y2(){const n={},e=Object.keys(au),t=Object.keys(ru);let i,l,s,o,r;for(i=0;i>16&255,s>>8&255,s&255]}return n}let js;function v2(n){js||(js=y2(),js.transparent=[0,0,0,0]);const e=js[n.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:e.length===4?e[3]:255}}const w2=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function S2(n){const e=w2.exec(n);let t=255,i,l,s;if(e){if(e[7]!==i){const o=+e[7];t=e[8]?Yl(o):$i(o*255,0,255)}return i=+e[1],l=+e[3],s=+e[5],i=255&(e[2]?Yl(i):$i(i,0,255)),l=255&(e[4]?Yl(l):$i(l,0,255)),s=255&(e[6]?Yl(s):$i(s,0,255)),{r:i,g:l,b:s,a:t}}}function $2(n){return n&&(n.a<255?`rgba(${n.r}, ${n.g}, ${n.b}, ${ui(n.a)})`:`rgb(${n.r}, ${n.g}, ${n.b})`)}const pr=n=>n<=.0031308?n*12.92:Math.pow(n,1/2.4)*1.055-.055,hl=n=>n<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4);function T2(n,e,t){const i=hl(ui(n.r)),l=hl(ui(n.g)),s=hl(ui(n.b));return{r:Mi(pr(i+t*(hl(ui(e.r))-i))),g:Mi(pr(l+t*(hl(ui(e.g))-l))),b:Mi(pr(s+t*(hl(ui(e.b))-s))),a:n.a+t*(e.a-n.a)}}function Hs(n,e,t){if(n){let i=Ca(n);i[e]=Math.max(0,Math.min(i[e]+i[e]*t,e===0?360:1)),i=Ma(i),n.r=i[0],n.g=i[1],n.b=i[2]}}function tb(n,e){return n&&Object.assign(e||{},n)}function fu(n){var e={r:0,g:0,b:0,a:255};return Array.isArray(n)?n.length>=3&&(e={r:n[0],g:n[1],b:n[2],a:255},n.length>3&&(e.a=Mi(n[3]))):(e=tb(n,{r:0,g:0,b:0,a:1}),e.a=Mi(e.a)),e}function C2(n){return n.charAt(0)==="r"?S2(n):g2(n)}class as{constructor(e){if(e instanceof as)return e;const t=typeof e;let i;t==="object"?i=fu(e):t==="string"&&(i=a2(e)||v2(e)||C2(e)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var e=tb(this._rgb);return e&&(e.a=ui(e.a)),e}set rgb(e){this._rgb=fu(e)}rgbString(){return this._valid?$2(this._rgb):void 0}hexString(){return this._valid?u2(this._rgb):void 0}hslString(){return this._valid?k2(this._rgb):void 0}mix(e,t){if(e){const i=this.rgb,l=e.rgb;let s;const o=t===s?.5:t,r=2*o-1,a=i.a-l.a,f=((r*a===-1?r:(r+a)/(1+r*a))+1)/2;s=1-f,i.r=255&f*i.r+s*l.r+.5,i.g=255&f*i.g+s*l.g+.5,i.b=255&f*i.b+s*l.b+.5,i.a=o*i.a+(1-o)*l.a,this.rgb=i}return this}interpolate(e,t){return e&&(this._rgb=T2(this._rgb,e._rgb,t)),this}clone(){return new as(this.rgb)}alpha(e){return this._rgb.a=Mi(e),this}clearer(e){const t=this._rgb;return t.a*=1-e,this}greyscale(){const e=this._rgb,t=Ts(e.r*.3+e.g*.59+e.b*.11);return e.r=e.g=e.b=t,this}opaquer(e){const t=this._rgb;return t.a*=1+e,this}negate(){const e=this._rgb;return e.r=255-e.r,e.g=255-e.g,e.b=255-e.b,this}lighten(e){return Hs(this._rgb,2,e),this}darken(e){return Hs(this._rgb,2,-e),this}saturate(e){return Hs(this._rgb,1,e),this}desaturate(e){return Hs(this._rgb,1,-e),this}rotate(e){return b2(this._rgb,e),this}}/*! - * Chart.js v4.4.3 - * https://www.chartjs.org - * (c) 2024 Chart.js Contributors - * Released under the MIT License - */function ri(){}const O2=(()=>{let n=0;return()=>n++})();function jt(n){return n===null||typeof n>"u"}function Xt(n){if(Array.isArray&&Array.isArray(n))return!0;const e=Object.prototype.toString.call(n);return e.slice(0,7)==="[object"&&e.slice(-6)==="Array]"}function nt(n){return n!==null&&Object.prototype.toString.call(n)==="[object Object]"}function on(n){return(typeof n=="number"||n instanceof Number)&&isFinite(+n)}function Zn(n,e){return on(n)?n:e}function vt(n,e){return typeof n>"u"?e:n}const M2=(n,e)=>typeof n=="string"&&n.endsWith("%")?parseFloat(n)/100*e:+n;function Rt(n,e,t){if(n&&typeof n.call=="function")return n.apply(t,e)}function _t(n,e,t,i){let l,s,o;if(Xt(n))for(s=n.length,l=0;ln,x:n=>n.x,y:n=>n.y};function I2(n){const e=n.split("."),t=[];let i="";for(const l of e)i+=l,i.endsWith("\\")?i=i.slice(0,-1)+".":(t.push(i),i="");return t}function A2(n){const e=I2(n);return t=>{for(const i of e){if(i==="")break;t=t&&t[i]}return t}}function Eo(n,e){return(uu[e]||(uu[e]=A2(e)))(n)}function Da(n){return n.charAt(0).toUpperCase()+n.slice(1)}const Io=n=>typeof n<"u",Di=n=>typeof n=="function",cu=(n,e)=>{if(n.size!==e.size)return!1;for(const t of n)if(!e.has(t))return!1;return!0};function L2(n){return n.type==="mouseup"||n.type==="click"||n.type==="contextmenu"}const sn=Math.PI,ti=2*sn,N2=ti+sn,Ao=Number.POSITIVE_INFINITY,P2=sn/180,Vn=sn/2,Ri=sn/4,du=sn*2/3,Zr=Math.log10,Cl=Math.sign;function Ql(n,e,t){return Math.abs(n-e)l-s).pop(),e}function us(n){return!isNaN(parseFloat(n))&&isFinite(n)}function R2(n,e){const t=Math.round(n);return t-e<=n&&t+e>=n}function q2(n,e,t){let i,l,s;for(i=0,l=n.length;ia&&f=Math.min(e,t)-i&&n<=Math.max(e,t)+i}function Ea(n,e,t){t=t||(o=>n[o]1;)s=l+i>>1,t(s)?l=s:i=s;return{lo:l,hi:i}}const Yi=(n,e,t,i)=>Ea(n,t,i?l=>{const s=n[l][e];return sn[l][e]Ea(n,t,i=>n[i][e]>=t);function U2(n,e,t){let i=0,l=n.length;for(;ii&&n[l-1]>t;)l--;return i>0||l{const i="_onData"+Da(t),l=n[t];Object.defineProperty(n,t,{configurable:!0,enumerable:!1,value(...s){const o=l.apply(this,s);return n._chartjs.listeners.forEach(r=>{typeof r[i]=="function"&&r[i](...s)}),o}})})}function hu(n,e){const t=n._chartjs;if(!t)return;const i=t.listeners,l=i.indexOf(e);l!==-1&&i.splice(l,1),!(i.length>0)&&(sb.forEach(s=>{delete n[s]}),delete n._chartjs)}function Y2(n){const e=new Set(n);return e.size===n.length?n:Array.from(e)}const ob=function(){return typeof window>"u"?function(n){return n()}:window.requestAnimationFrame}();function rb(n,e){let t=[],i=!1;return function(...l){t=l,i||(i=!0,ob.call(window,()=>{i=!1,n.apply(e,t)}))}}function K2(n,e){let t;return function(...i){return e?(clearTimeout(t),t=setTimeout(n,e,i)):n.apply(this,i),e}}const J2=n=>n==="start"?"left":n==="end"?"right":"center",_u=(n,e,t)=>n==="start"?e:n==="end"?t:(e+t)/2;function Z2(n,e,t){const i=e.length;let l=0,s=i;if(n._sorted){const{iScale:o,_parsed:r}=n,a=o.axis,{min:f,max:u,minDefined:c,maxDefined:d}=o.getUserBounds();c&&(l=Bn(Math.min(Yi(r,a,f).lo,t?i:Yi(e,a,o.getPixelForValue(f)).lo),0,i-1)),d?s=Bn(Math.max(Yi(r,o.axis,u,!0).hi+1,t?0:Yi(e,a,o.getPixelForValue(u),!0).hi+1),l,i)-l:s=i-l}return{start:l,count:s}}function G2(n){const{xScale:e,yScale:t,_scaleRanges:i}=n,l={xmin:e.min,xmax:e.max,ymin:t.min,ymax:t.max};if(!i)return n._scaleRanges=l,!0;const s=i.xmin!==e.min||i.xmax!==e.max||i.ymin!==t.min||i.ymax!==t.max;return Object.assign(i,l),s}const zs=n=>n===0||n===1,gu=(n,e,t)=>-(Math.pow(2,10*(n-=1))*Math.sin((n-e)*ti/t)),bu=(n,e,t)=>Math.pow(2,-10*n)*Math.sin((n-e)*ti/t)+1,xl={linear:n=>n,easeInQuad:n=>n*n,easeOutQuad:n=>-n*(n-2),easeInOutQuad:n=>(n/=.5)<1?.5*n*n:-.5*(--n*(n-2)-1),easeInCubic:n=>n*n*n,easeOutCubic:n=>(n-=1)*n*n+1,easeInOutCubic:n=>(n/=.5)<1?.5*n*n*n:.5*((n-=2)*n*n+2),easeInQuart:n=>n*n*n*n,easeOutQuart:n=>-((n-=1)*n*n*n-1),easeInOutQuart:n=>(n/=.5)<1?.5*n*n*n*n:-.5*((n-=2)*n*n*n-2),easeInQuint:n=>n*n*n*n*n,easeOutQuint:n=>(n-=1)*n*n*n*n+1,easeInOutQuint:n=>(n/=.5)<1?.5*n*n*n*n*n:.5*((n-=2)*n*n*n*n+2),easeInSine:n=>-Math.cos(n*Vn)+1,easeOutSine:n=>Math.sin(n*Vn),easeInOutSine:n=>-.5*(Math.cos(sn*n)-1),easeInExpo:n=>n===0?0:Math.pow(2,10*(n-1)),easeOutExpo:n=>n===1?1:-Math.pow(2,-10*n)+1,easeInOutExpo:n=>zs(n)?n:n<.5?.5*Math.pow(2,10*(n*2-1)):.5*(-Math.pow(2,-10*(n*2-1))+2),easeInCirc:n=>n>=1?n:-(Math.sqrt(1-n*n)-1),easeOutCirc:n=>Math.sqrt(1-(n-=1)*n),easeInOutCirc:n=>(n/=.5)<1?-.5*(Math.sqrt(1-n*n)-1):.5*(Math.sqrt(1-(n-=2)*n)+1),easeInElastic:n=>zs(n)?n:gu(n,.075,.3),easeOutElastic:n=>zs(n)?n:bu(n,.075,.3),easeInOutElastic(n){return zs(n)?n:n<.5?.5*gu(n*2,.1125,.45):.5+.5*bu(n*2-1,.1125,.45)},easeInBack(n){return n*n*((1.70158+1)*n-1.70158)},easeOutBack(n){return(n-=1)*n*((1.70158+1)*n+1.70158)+1},easeInOutBack(n){let e=1.70158;return(n/=.5)<1?.5*(n*n*(((e*=1.525)+1)*n-e)):.5*((n-=2)*n*(((e*=1.525)+1)*n+e)+2)},easeInBounce:n=>1-xl.easeOutBounce(1-n),easeOutBounce(n){return n<1/2.75?7.5625*n*n:n<2/2.75?7.5625*(n-=1.5/2.75)*n+.75:n<2.5/2.75?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375},easeInOutBounce:n=>n<.5?xl.easeInBounce(n*2)*.5:xl.easeOutBounce(n*2-1)*.5+.5};function Ia(n){if(n&&typeof n=="object"){const e=n.toString();return e==="[object CanvasPattern]"||e==="[object CanvasGradient]"}return!1}function ku(n){return Ia(n)?n:new as(n)}function mr(n){return Ia(n)?n:new as(n).saturate(.5).darken(.1).hexString()}const X2=["x","y","borderWidth","radius","tension"],Q2=["color","borderColor","backgroundColor"];function x2(n){n.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),n.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:e=>e!=="onProgress"&&e!=="onComplete"&&e!=="fn"}),n.set("animations",{colors:{type:"color",properties:Q2},numbers:{type:"number",properties:X2}}),n.describe("animations",{_fallback:"animation"}),n.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:e=>e|0}}}})}function ew(n){n.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}const yu=new Map;function tw(n,e){e=e||{};const t=n+JSON.stringify(e);let i=yu.get(t);return i||(i=new Intl.NumberFormat(n,e),yu.set(t,i)),i}function ab(n,e,t){return tw(e,t).format(n)}const fb={values(n){return Xt(n)?n:""+n},numeric(n,e,t){if(n===0)return"0";const i=this.chart.options.locale;let l,s=n;if(t.length>1){const f=Math.max(Math.abs(t[0].value),Math.abs(t[t.length-1].value));(f<1e-4||f>1e15)&&(l="scientific"),s=nw(n,t)}const o=Zr(Math.abs(s)),r=isNaN(o)?1:Math.max(Math.min(-1*Math.floor(o),20),0),a={notation:l,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(a,this.options.ticks.format),ab(n,i,a)},logarithmic(n,e,t){if(n===0)return"0";const i=t[e].significand||n/Math.pow(10,Math.floor(Zr(n)));return[1,2,3,5,10,15].includes(i)||e>.8*t.length?fb.numeric.call(this,n,e,t):""}};function nw(n,e){let t=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;return Math.abs(t)>=1&&n!==Math.floor(n)&&(t=n-Math.floor(n)),t}var ub={formatters:fb};function iw(n){n.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(e,t)=>t.lineWidth,tickColor:(e,t)=>t.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ub.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),n.route("scale.ticks","color","","color"),n.route("scale.grid","color","","borderColor"),n.route("scale.border","color","","borderColor"),n.route("scale.title","color","","color"),n.describe("scale",{_fallback:!1,_scriptable:e=>!e.startsWith("before")&&!e.startsWith("after")&&e!=="callback"&&e!=="parser",_indexable:e=>e!=="borderDash"&&e!=="tickBorderDash"&&e!=="dash"}),n.describe("scales",{_fallback:"scale"}),n.describe("scale.ticks",{_scriptable:e=>e!=="backdropPadding"&&e!=="callback",_indexable:e=>e!=="backdropPadding"})}const Qi=Object.create(null),Xr=Object.create(null);function es(n,e){if(!e)return n;const t=e.split(".");for(let i=0,l=t.length;ii.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(i,l)=>mr(l.backgroundColor),this.hoverBorderColor=(i,l)=>mr(l.borderColor),this.hoverColor=(i,l)=>mr(l.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(e),this.apply(t)}set(e,t){return hr(this,e,t)}get(e){return es(this,e)}describe(e,t){return hr(Xr,e,t)}override(e,t){return hr(Qi,e,t)}route(e,t,i,l){const s=es(this,e),o=es(this,i),r="_"+t;Object.defineProperties(s,{[r]:{value:s[t],writable:!0},[t]:{enumerable:!0,get(){const a=this[r],f=o[l];return nt(a)?Object.assign({},f,a):vt(a,f)},set(a){this[r]=a}}})}apply(e){e.forEach(t=>t(this))}}var Ut=new lw({_scriptable:n=>!n.startsWith("on"),_indexable:n=>n!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[x2,ew,iw]);function sw(n){return!n||jt(n.size)||jt(n.family)?null:(n.style?n.style+" ":"")+(n.weight?n.weight+" ":"")+n.size+"px "+n.family}function vu(n,e,t,i,l){let s=e[l];return s||(s=e[l]=n.measureText(l).width,t.push(l)),s>i&&(i=s),i}function qi(n,e,t){const i=n.currentDevicePixelRatio,l=t!==0?Math.max(t/2,.5):0;return Math.round((e-l)*i)/i+l}function wu(n,e){!e&&!n||(e=e||n.getContext("2d"),e.save(),e.resetTransform(),e.clearRect(0,0,n.width,n.height),e.restore())}function Qr(n,e,t,i){ow(n,e,t,i)}function ow(n,e,t,i,l){let s,o,r,a,f,u,c,d;const m=e.pointStyle,h=e.rotation,_=e.radius;let g=(h||0)*P2;if(m&&typeof m=="object"&&(s=m.toString(),s==="[object HTMLImageElement]"||s==="[object HTMLCanvasElement]")){n.save(),n.translate(t,i),n.rotate(g),n.drawImage(m,-m.width/2,-m.height/2,m.width,m.height),n.restore();return}if(!(isNaN(_)||_<=0)){switch(n.beginPath(),m){default:n.arc(t,i,_,0,ti),n.closePath();break;case"triangle":u=_,n.moveTo(t+Math.sin(g)*u,i-Math.cos(g)*_),g+=du,n.lineTo(t+Math.sin(g)*u,i-Math.cos(g)*_),g+=du,n.lineTo(t+Math.sin(g)*u,i-Math.cos(g)*_),n.closePath();break;case"rectRounded":f=_*.516,a=_-f,o=Math.cos(g+Ri)*a,c=Math.cos(g+Ri)*a,r=Math.sin(g+Ri)*a,d=Math.sin(g+Ri)*a,n.arc(t-c,i-r,f,g-sn,g-Vn),n.arc(t+d,i-o,f,g-Vn,g),n.arc(t+c,i+r,f,g,g+Vn),n.arc(t-d,i+o,f,g+Vn,g+sn),n.closePath();break;case"rect":if(!h){a=Math.SQRT1_2*_,u=a,n.rect(t-u,i-a,2*u,2*a);break}g+=Ri;case"rectRot":c=Math.cos(g)*_,o=Math.cos(g)*_,r=Math.sin(g)*_,d=Math.sin(g)*_,n.moveTo(t-c,i-r),n.lineTo(t+d,i-o),n.lineTo(t+c,i+r),n.lineTo(t-d,i+o),n.closePath();break;case"crossRot":g+=Ri;case"cross":c=Math.cos(g)*_,o=Math.cos(g)*_,r=Math.sin(g)*_,d=Math.sin(g)*_,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o);break;case"star":c=Math.cos(g)*_,o=Math.cos(g)*_,r=Math.sin(g)*_,d=Math.sin(g)*_,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o),g+=Ri,c=Math.cos(g)*_,o=Math.cos(g)*_,r=Math.sin(g)*_,d=Math.sin(g)*_,n.moveTo(t-c,i-r),n.lineTo(t+c,i+r),n.moveTo(t+d,i-o),n.lineTo(t-d,i+o);break;case"line":o=Math.cos(g)*_,r=Math.sin(g)*_,n.moveTo(t-o,i-r),n.lineTo(t+o,i+r);break;case"dash":n.moveTo(t,i),n.lineTo(t+Math.cos(g)*_,i+Math.sin(g)*_);break;case!1:n.closePath();break}n.fill(),e.borderWidth>0&&n.stroke()}}function cs(n,e,t){return t=t||.5,!e||n&&n.x>e.left-t&&n.xe.top-t&&n.y0&&s.strokeColor!=="";let a,f;for(n.save(),n.font=l.string,fw(n,s),a=0;a+n||0;function cb(n,e){const t={},i=nt(e),l=i?Object.keys(e):e,s=nt(n)?i?o=>vt(n[o],n[e[o]]):o=>n[o]:()=>n;for(const o of l)t[o]=hw(s(o));return t}function _w(n){return cb(n,{top:"y",right:"x",bottom:"y",left:"x"})}function fo(n){return cb(n,["topLeft","topRight","bottomLeft","bottomRight"])}function Ei(n){const e=_w(n);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function ei(n,e){n=n||{},e=e||Ut.font;let t=vt(n.size,e.size);typeof t=="string"&&(t=parseInt(t,10));let i=vt(n.style,e.style);i&&!(""+i).match(pw)&&(console.warn('Invalid font style specified: "'+i+'"'),i=void 0);const l={family:vt(n.family,e.family),lineHeight:mw(vt(n.lineHeight,e.lineHeight),t),size:t,style:i,weight:vt(n.weight,e.weight),string:""};return l.string=sw(l),l}function Vs(n,e,t,i){let l,s,o;for(l=0,s=n.length;lt&&r===0?0:r+a;return{min:o(i,-Math.abs(s)),max:o(l,s)}}function ll(n,e){return Object.assign(Object.create(n),e)}function Na(n,e=[""],t,i,l=()=>n[0]){const s=t||n;typeof i>"u"&&(i=hb("_fallback",n));const o={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:n,_rootScopes:s,_fallback:i,_getTarget:l,override:r=>Na([r,...n],e,s,i)};return new Proxy(o,{deleteProperty(r,a){return delete r[a],delete r._keys,delete n[0][a],!0},get(r,a){return pb(r,a,()=>Tw(a,e,n,r))},getOwnPropertyDescriptor(r,a){return Reflect.getOwnPropertyDescriptor(r._scopes[0],a)},getPrototypeOf(){return Reflect.getPrototypeOf(n[0])},has(r,a){return Cu(r).includes(a)},ownKeys(r){return Cu(r)},set(r,a,f){const u=r._storage||(r._storage=l());return r[a]=u[a]=f,delete r._keys,!0}})}function Ol(n,e,t,i){const l={_cacheable:!1,_proxy:n,_context:e,_subProxy:t,_stack:new Set,_descriptors:db(n,i),setContext:s=>Ol(n,s,t,i),override:s=>Ol(n.override(s),e,t,i)};return new Proxy(l,{deleteProperty(s,o){return delete s[o],delete n[o],!0},get(s,o,r){return pb(s,o,()=>kw(s,o,r))},getOwnPropertyDescriptor(s,o){return s._descriptors.allKeys?Reflect.has(n,o)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(n,o)},getPrototypeOf(){return Reflect.getPrototypeOf(n)},has(s,o){return Reflect.has(n,o)},ownKeys(){return Reflect.ownKeys(n)},set(s,o,r){return n[o]=r,delete s[o],!0}})}function db(n,e={scriptable:!0,indexable:!0}){const{_scriptable:t=e.scriptable,_indexable:i=e.indexable,_allKeys:l=e.allKeys}=n;return{allKeys:l,scriptable:t,indexable:i,isScriptable:Di(t)?t:()=>t,isIndexable:Di(i)?i:()=>i}}const bw=(n,e)=>n?n+Da(e):e,Pa=(n,e)=>nt(e)&&n!=="adapters"&&(Object.getPrototypeOf(e)===null||e.constructor===Object);function pb(n,e,t){if(Object.prototype.hasOwnProperty.call(n,e)||e==="constructor")return n[e];const i=t();return n[e]=i,i}function kw(n,e,t){const{_proxy:i,_context:l,_subProxy:s,_descriptors:o}=n;let r=i[e];return Di(r)&&o.isScriptable(e)&&(r=yw(e,r,n,t)),Xt(r)&&r.length&&(r=vw(e,r,n,o.isIndexable)),Pa(e,r)&&(r=Ol(r,l,s&&s[e],o)),r}function yw(n,e,t,i){const{_proxy:l,_context:s,_subProxy:o,_stack:r}=t;if(r.has(n))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+n);r.add(n);let a=e(s,o||i);return r.delete(n),Pa(n,a)&&(a=Fa(l._scopes,l,n,a)),a}function vw(n,e,t,i){const{_proxy:l,_context:s,_subProxy:o,_descriptors:r}=t;if(typeof s.index<"u"&&i(n))return e[s.index%e.length];if(nt(e[0])){const a=e,f=l._scopes.filter(u=>u!==a);e=[];for(const u of a){const c=Fa(f,l,n,u);e.push(Ol(c,s,o&&o[n],r))}}return e}function mb(n,e,t){return Di(n)?n(e,t):n}const ww=(n,e)=>n===!0?e:typeof n=="string"?Eo(e,n):void 0;function Sw(n,e,t,i,l){for(const s of e){const o=ww(t,s);if(o){n.add(o);const r=mb(o._fallback,t,l);if(typeof r<"u"&&r!==t&&r!==i)return r}else if(o===!1&&typeof i<"u"&&t!==i)return null}return!1}function Fa(n,e,t,i){const l=e._rootScopes,s=mb(e._fallback,t,i),o=[...n,...l],r=new Set;r.add(i);let a=Tu(r,o,t,s||t,i);return a===null||typeof s<"u"&&s!==t&&(a=Tu(r,o,s,a,i),a===null)?!1:Na(Array.from(r),[""],l,s,()=>$w(e,t,i))}function Tu(n,e,t,i,l){for(;t;)t=Sw(n,e,t,i,l);return t}function $w(n,e,t){const i=n._getTarget();e in i||(i[e]={});const l=i[e];return Xt(l)&&nt(t)?t:l||{}}function Tw(n,e,t,i){let l;for(const s of e)if(l=hb(bw(s,n),t),typeof l<"u")return Pa(n,l)?Fa(t,i,n,l):l}function hb(n,e){for(const t of e){if(!t)continue;const i=t[n];if(typeof i<"u")return i}}function Cu(n){let e=n._keys;return e||(e=n._keys=Cw(n._scopes)),e}function Cw(n){const e=new Set;for(const t of n)for(const i of Object.keys(t).filter(l=>!l.startsWith("_")))e.add(i);return Array.from(e)}const Ow=Number.EPSILON||1e-14,Ml=(n,e)=>en==="x"?"y":"x";function Mw(n,e,t,i){const l=n.skip?e:n,s=e,o=t.skip?e:t,r=Gr(s,l),a=Gr(o,s);let f=r/(r+a),u=a/(r+a);f=isNaN(f)?0:f,u=isNaN(u)?0:u;const c=i*f,d=i*u;return{previous:{x:s.x-c*(o.x-l.x),y:s.y-c*(o.y-l.y)},next:{x:s.x+d*(o.x-l.x),y:s.y+d*(o.y-l.y)}}}function Dw(n,e,t){const i=n.length;let l,s,o,r,a,f=Ml(n,0);for(let u=0;u!f.skip)),e.cubicInterpolationMode==="monotone")Iw(n,l);else{let f=i?n[n.length-1]:n[0];for(s=0,o=n.length;sn.ownerDocument.defaultView.getComputedStyle(n,null);function Nw(n,e){return Xo(n).getPropertyValue(e)}const Pw=["top","right","bottom","left"];function Zi(n,e,t){const i={};t=t?"-"+t:"";for(let l=0;l<4;l++){const s=Pw[l];i[s]=parseFloat(n[e+"-"+s+t])||0}return i.width=i.left+i.right,i.height=i.top+i.bottom,i}const Fw=(n,e,t)=>(n>0||e>0)&&(!t||!t.shadowRoot);function Rw(n,e){const t=n.touches,i=t&&t.length?t[0]:n,{offsetX:l,offsetY:s}=i;let o=!1,r,a;if(Fw(l,s,n.target))r=l,a=s;else{const f=e.getBoundingClientRect();r=i.clientX-f.left,a=i.clientY-f.top,o=!0}return{x:r,y:a,box:o}}function zi(n,e){if("native"in n)return n;const{canvas:t,currentDevicePixelRatio:i}=e,l=Xo(t),s=l.boxSizing==="border-box",o=Zi(l,"padding"),r=Zi(l,"border","width"),{x:a,y:f,box:u}=Rw(n,t),c=o.left+(u&&r.left),d=o.top+(u&&r.top);let{width:m,height:h}=e;return s&&(m-=o.width+r.width,h-=o.height+r.height),{x:Math.round((a-c)/m*t.width/i),y:Math.round((f-d)/h*t.height/i)}}function qw(n,e,t){let i,l;if(e===void 0||t===void 0){const s=n&&qa(n);if(!s)e=n.clientWidth,t=n.clientHeight;else{const o=s.getBoundingClientRect(),r=Xo(s),a=Zi(r,"border","width"),f=Zi(r,"padding");e=o.width-f.width-a.width,t=o.height-f.height-a.height,i=Lo(r.maxWidth,s,"clientWidth"),l=Lo(r.maxHeight,s,"clientHeight")}}return{width:e,height:t,maxWidth:i||Ao,maxHeight:l||Ao}}const Us=n=>Math.round(n*10)/10;function jw(n,e,t,i){const l=Xo(n),s=Zi(l,"margin"),o=Lo(l.maxWidth,n,"clientWidth")||Ao,r=Lo(l.maxHeight,n,"clientHeight")||Ao,a=qw(n,e,t);let{width:f,height:u}=a;if(l.boxSizing==="content-box"){const d=Zi(l,"border","width"),m=Zi(l,"padding");f-=m.width+d.width,u-=m.height+d.height}return f=Math.max(0,f-s.width),u=Math.max(0,i?f/i:u-s.height),f=Us(Math.min(f,o,a.maxWidth)),u=Us(Math.min(u,r,a.maxHeight)),f&&!u&&(u=Us(f/2)),(e!==void 0||t!==void 0)&&i&&a.height&&u>a.height&&(u=a.height,f=Us(Math.floor(u*i))),{width:f,height:u}}function Ou(n,e,t){const i=e||1,l=Math.floor(n.height*i),s=Math.floor(n.width*i);n.height=Math.floor(n.height),n.width=Math.floor(n.width);const o=n.canvas;return o.style&&(t||!o.style.height&&!o.style.width)&&(o.style.height=`${n.height}px`,o.style.width=`${n.width}px`),n.currentDevicePixelRatio!==i||o.height!==l||o.width!==s?(n.currentDevicePixelRatio=i,o.height=l,o.width=s,n.ctx.setTransform(i,0,0,i,0,0),!0):!1}const Hw=function(){let n=!1;try{const e={get passive(){return n=!0,!1}};Ra()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch{}return n}();function Mu(n,e){const t=Nw(n,e),i=t&&t.match(/^(\d+)(\.\d+)?px$/);return i?+i[1]:void 0}function Vi(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:n.y+t*(e.y-n.y)}}function zw(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:i==="middle"?t<.5?n.y:e.y:i==="after"?t<1?n.y:e.y:t>0?e.y:n.y}}function Vw(n,e,t,i){const l={x:n.cp2x,y:n.cp2y},s={x:e.cp1x,y:e.cp1y},o=Vi(n,l,t),r=Vi(l,s,t),a=Vi(s,e,t),f=Vi(o,r,t),u=Vi(r,a,t);return Vi(f,u,t)}const Bw=function(n,e){return{x(t){return n+n+e-t},setWidth(t){e=t},textAlign(t){return t==="center"?t:t==="right"?"left":"right"},xPlus(t,i){return t-i},leftForLtr(t,i){return t-i}}},Uw=function(){return{x(n){return n},setWidth(n){},textAlign(n){return n},xPlus(n,e){return n+e},leftForLtr(n,e){return n}}};function _r(n,e,t){return n?Bw(e,t):Uw()}function Ww(n,e){let t,i;(e==="ltr"||e==="rtl")&&(t=n.canvas.style,i=[t.getPropertyValue("direction"),t.getPropertyPriority("direction")],t.setProperty("direction",e,"important"),n.prevTextDirection=i)}function Yw(n,e){e!==void 0&&(delete n.prevTextDirection,n.canvas.style.setProperty("direction",e[0],e[1]))}function gb(n){return n==="angle"?{between:ib,compare:z2,normalize:Qn}:{between:lb,compare:(e,t)=>e-t,normalize:e=>e}}function Du({start:n,end:e,count:t,loop:i,style:l}){return{start:n%t,end:e%t,loop:i&&(e-n+1)%t===0,style:l}}function Kw(n,e,t){const{property:i,start:l,end:s}=t,{between:o,normalize:r}=gb(i),a=e.length;let{start:f,end:u,loop:c}=n,d,m;if(c){for(f+=a,u+=a,d=0,m=a;da(l,T,y)&&r(l,T)!==0,C=()=>r(s,y)===0||a(s,T,y),O=()=>_||$(),D=()=>!_||C();for(let I=u,L=u;I<=c;++I)S=e[I%o],!S.skip&&(y=f(S[i]),y!==T&&(_=a(y,l,s),g===null&&O()&&(g=r(y,l)===0?I:L),g!==null&&D()&&(h.push(Du({start:g,end:I,loop:d,count:o,style:m})),g=null),L=I,T=y));return g!==null&&h.push(Du({start:g,end:c,loop:d,count:o,style:m})),h}function kb(n,e){const t=[],i=n.segments;for(let l=0;ll&&n[s%e].skip;)s--;return s%=e,{start:l,end:s}}function Zw(n,e,t,i){const l=n.length,s=[];let o=e,r=n[e],a;for(a=e+1;a<=t;++a){const f=n[a%l];f.skip||f.stop?r.skip||(i=!1,s.push({start:e%l,end:(a-1)%l,loop:i}),e=o=f.stop?a:null):(o=a,r.skip&&(e=a)),r=f}return o!==null&&s.push({start:e%l,end:o%l,loop:i}),s}function Gw(n,e){const t=n.points,i=n.options.spanGaps,l=t.length;if(!l)return[];const s=!!n._loop,{start:o,end:r}=Jw(t,l,s,i);if(i===!0)return Eu(n,[{start:o,end:r,loop:s}],t,e);const a=rr({chart:e,initial:t.initial,numSteps:o,currentStep:Math.min(i-t.start,o)}))}_refresh(){this._request||(this._running=!0,this._request=ob.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let t=0;this._charts.forEach((i,l)=>{if(!i.running||!i.items.length)return;const s=i.items;let o=s.length-1,r=!1,a;for(;o>=0;--o)a=s[o],a._active?(a._total>i.duration&&(i.duration=a._total),a.tick(e),r=!0):(s[o]=s[s.length-1],s.pop());r&&(l.draw(),this._notify(l,i,e,"progress")),s.length||(i.running=!1,this._notify(l,i,e,"complete"),i.initial=!1),t+=s.length}),this._lastDate=e,t===0&&(this._running=!1)}_getAnims(e){const t=this._charts;let i=t.get(e);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},t.set(e,i)),i}listen(e,t,i){this._getAnims(e).listeners[t].push(i)}add(e,t){!t||!t.length||this._getAnims(e).items.push(...t)}has(e){return this._getAnims(e).items.length>0}start(e){const t=this._charts.get(e);t&&(t.running=!0,t.start=Date.now(),t.duration=t.items.reduce((i,l)=>Math.max(i,l._duration),0),this._refresh())}running(e){if(!this._running)return!1;const t=this._charts.get(e);return!(!t||!t.running||!t.items.length)}stop(e){const t=this._charts.get(e);if(!t||!t.items.length)return;const i=t.items;let l=i.length-1;for(;l>=0;--l)i[l].cancel();t.items=[],this._notify(e,t,Date.now(),"complete")}remove(e){return this._charts.delete(e)}}var ai=new xw;const Au="transparent",e3={boolean(n,e,t){return t>.5?e:n},color(n,e,t){const i=ku(n||Au),l=i.valid&&ku(e||Au);return l&&l.valid?l.mix(i,t).hexString():e},number(n,e,t){return n+(e-n)*t}};class t3{constructor(e,t,i,l){const s=t[i];l=Vs([e.to,l,s,e.from]);const o=Vs([e.from,s,l]);this._active=!0,this._fn=e.fn||e3[e.type||typeof o],this._easing=xl[e.easing]||xl.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=t,this._prop=i,this._from=o,this._to=l,this._promises=void 0}active(){return this._active}update(e,t,i){if(this._active){this._notify(!1);const l=this._target[this._prop],s=i-this._start,o=this._duration-s;this._start=i,this._duration=Math.floor(Math.max(o,e.duration)),this._total+=s,this._loop=!!e.loop,this._to=Vs([e.to,t,l,e.from]),this._from=Vs([e.from,l,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){const t=e-this._start,i=this._duration,l=this._prop,s=this._from,o=this._loop,r=this._to;let a;if(this._active=s!==r&&(o||t1?2-a:a,a=this._easing(Math.min(1,Math.max(0,a))),this._target[l]=this._fn(s,r,a)}wait(){const e=this._promises||(this._promises=[]);return new Promise((t,i)=>{e.push({res:t,rej:i})})}_notify(e){const t=e?"res":"rej",i=this._promises||[];for(let l=0;l{const s=e[l];if(!nt(s))return;const o={};for(const r of t)o[r]=s[r];(Xt(s.properties)&&s.properties||[l]).forEach(r=>{(r===l||!i.has(r))&&i.set(r,o)})})}_animateOptions(e,t){const i=t.options,l=i3(e,i);if(!l)return[];const s=this._createAnimations(l,i);return i.$shared&&n3(e.options.$animations,i).then(()=>{e.options=i},()=>{}),s}_createAnimations(e,t){const i=this._properties,l=[],s=e.$animations||(e.$animations={}),o=Object.keys(t),r=Date.now();let a;for(a=o.length-1;a>=0;--a){const f=o[a];if(f.charAt(0)==="$")continue;if(f==="options"){l.push(...this._animateOptions(e,t));continue}const u=t[f];let c=s[f];const d=i.get(f);if(c)if(d&&c.active()){c.update(d,u,r);continue}else c.cancel();if(!d||!d.duration){e[f]=u;continue}s[f]=c=new t3(d,e,f,u),l.push(c)}return l}update(e,t){if(this._properties.size===0){Object.assign(e,t);return}const i=this._createAnimations(e,t);if(i.length)return ai.add(this._chart,i),!0}}function n3(n,e){const t=[],i=Object.keys(e);for(let l=0;l0||!t&&s<0)return l.index}return null}function Ru(n,e){const{chart:t,_cachedMeta:i}=n,l=t._stacks||(t._stacks={}),{iScale:s,vScale:o,index:r}=i,a=s.axis,f=o.axis,u=r3(s,o,i),c=e.length;let d;for(let m=0;mt[i].axis===e).shift()}function u3(n,e){return ll(n,{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:"default",type:"dataset"})}function c3(n,e,t){return ll(n,{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:t,index:e,mode:"default",type:"data"})}function Hl(n,e){const t=n.controller.index,i=n.vScale&&n.vScale.axis;if(i){e=e||n._parsed;for(const l of e){const s=l._stacks;if(!s||s[i]===void 0||s[i][t]===void 0)return;delete s[i][t],s[i]._visualValues!==void 0&&s[i]._visualValues[t]!==void 0&&delete s[i]._visualValues[t]}}}const br=n=>n==="reset"||n==="none",qu=(n,e)=>e?n:Object.assign({},n),d3=(n,e,t)=>n&&!e.hidden&&e._stacked&&{keys:vb(t,!0),values:null};class ts{constructor(e,t){this.chart=e,this._ctx=e.ctx,this.index=t,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=Pu(e.vScale,e),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(e){this.index!==e&&Hl(this._cachedMeta),this.index=e}linkScales(){const e=this.chart,t=this._cachedMeta,i=this.getDataset(),l=(c,d,m,h)=>c==="x"?d:c==="r"?h:m,s=t.xAxisID=vt(i.xAxisID,gr(e,"x")),o=t.yAxisID=vt(i.yAxisID,gr(e,"y")),r=t.rAxisID=vt(i.rAxisID,gr(e,"r")),a=t.indexAxis,f=t.iAxisID=l(a,s,o,r),u=t.vAxisID=l(a,o,s,r);t.xScale=this.getScaleForId(s),t.yScale=this.getScaleForId(o),t.rScale=this.getScaleForId(r),t.iScale=this.getScaleForId(f),t.vScale=this.getScaleForId(u)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){const t=this._cachedMeta;return e===t.iScale?t.vScale:t.iScale}reset(){this._update("reset")}_destroy(){const e=this._cachedMeta;this._data&&hu(this._data,this),e._stacked&&Hl(e)}_dataCheck(){const e=this.getDataset(),t=e.data||(e.data=[]),i=this._data;if(nt(t)){const l=this._cachedMeta;this._data=o3(t,l)}else if(i!==t){if(i){hu(i,this);const l=this._cachedMeta;Hl(l),l._parsed=[]}t&&Object.isExtensible(t)&&W2(t,this),this._syncList=[],this._data=t}}addElements(){const e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){const t=this._cachedMeta,i=this.getDataset();let l=!1;this._dataCheck();const s=t._stacked;t._stacked=Pu(t.vScale,t),t.stack!==i.stack&&(l=!0,Hl(t),t.stack=i.stack),this._resyncElements(e),(l||s!==t._stacked)&&Ru(this,t._parsed)}configure(){const e=this.chart.config,t=e.datasetScopeKeys(this._type),i=e.getOptionScopes(this.getDataset(),t,!0);this.options=e.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,t){const{_cachedMeta:i,_data:l}=this,{iScale:s,_stacked:o}=i,r=s.axis;let a=e===0&&t===l.length?!0:i._sorted,f=e>0&&i._parsed[e-1],u,c,d;if(this._parsing===!1)i._parsed=l,i._sorted=!0,d=l;else{Xt(l[e])?d=this.parseArrayData(i,l,e,t):nt(l[e])?d=this.parseObjectData(i,l,e,t):d=this.parsePrimitiveData(i,l,e,t);const m=()=>c[r]===null||f&&c[r]_||c<_}for(d=0;d=0;--d)if(!h()){this.updateRangeFromParsed(f,e,m,a);break}}return f}getAllParsedValues(e){const t=this._cachedMeta._parsed,i=[];let l,s,o;for(l=0,s=t.length;l=0&&ethis.getContext(i,l,t),_=f.resolveNamedOptions(d,m,h,c);return _.$shared&&(_.$shared=a,s[o]=Object.freeze(qu(_,a))),_}_resolveAnimations(e,t,i){const l=this.chart,s=this._cachedDataOpts,o=`animation-${t}`,r=s[o];if(r)return r;let a;if(l.options.animation!==!1){const u=this.chart.config,c=u.datasetAnimationScopeKeys(this._type,t),d=u.getOptionScopes(this.getDataset(),c);a=u.createResolver(d,this.getContext(e,i,t))}const f=new yb(l,a&&a.animations);return a&&a._cacheable&&(s[o]=Object.freeze(f)),f}getSharedOptions(e){if(e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,t){return!t||br(e)||this.chart._animationsDisabled}_getSharedOptions(e,t){const i=this.resolveDataElementOptions(e,t),l=this._sharedOptions,s=this.getSharedOptions(i),o=this.includeOptions(t,s)||s!==l;return this.updateSharedOptions(s,t,i),{sharedOptions:s,includeOptions:o}}updateElement(e,t,i,l){br(l)?Object.assign(e,i):this._resolveAnimations(t,l).update(e,i)}updateSharedOptions(e,t,i){e&&!br(t)&&this._resolveAnimations(void 0,t).update(e,i)}_setStyle(e,t,i,l){e.active=l;const s=this.getStyle(t,l);this._resolveAnimations(t,i,l).update(e,{options:!l&&this.getSharedOptions(s)||s})}removeHoverStyle(e,t,i){this._setStyle(e,i,"active",!1)}setHoverStyle(e,t,i){this._setStyle(e,i,"active",!0)}_removeDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!1)}_setDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!0)}_resyncElements(e){const t=this._data,i=this._cachedMeta.data;for(const[r,a,f]of this._syncList)this[r](a,f);this._syncList=[];const l=i.length,s=t.length,o=Math.min(s,l);o&&this.parse(0,o),s>l?this._insertElements(l,s-l,e):s{for(f.length+=t,r=f.length-1;r>=o;r--)f[r]=f[r-t]};for(a(s),r=e;r0&&this.getParsed(t-1);for(let C=0;C=S){D.skip=!0;continue}const I=this.getParsed(C),L=jt(I[m]),R=D[d]=o.getPixelForValue(I[d],C),F=D[m]=s||L?r.getBasePixel():r.getPixelForValue(a?this.applyStack(r,I,a):I[m],C);D.skip=isNaN(R)||isNaN(F)||L,D.stop=C>0&&Math.abs(I[d]-$[d])>g,_&&(D.parsed=I,D.raw=f.data[C]),c&&(D.options=u||this.resolveDataElementOptions(C,O.active?"active":l)),y||this.updateElement(O,C,D,l),$=I}}getMaxOverflow(){const e=this._cachedMeta,t=e.dataset,i=t.options&&t.options.borderWidth||0,l=e.data||[];if(!l.length)return i;const s=l[0].size(this.resolveDataElementOptions(0)),o=l[l.length-1].size(this.resolveDataElementOptions(l.length-1));return Math.max(i,s,o)/2}draw(){const e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}}Ze(uo,"id","line"),Ze(uo,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),Ze(uo,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}});function ji(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class ja{constructor(e){Ze(this,"options");this.options=e||{}}static override(e){Object.assign(ja.prototype,e)}init(){}formats(){return ji()}parse(){return ji()}format(){return ji()}add(){return ji()}diff(){return ji()}startOf(){return ji()}endOf(){return ji()}}var wb={_date:ja};function p3(n,e,t,i){const{controller:l,data:s,_sorted:o}=n,r=l._cachedMeta.iScale;if(r&&e===r.axis&&e!=="r"&&o&&s.length){const a=r._reversePixels?B2:Yi;if(i){if(l._sharedOptions){const f=s[0],u=typeof f.getRange=="function"&&f.getRange(e);if(u){const c=a(s,e,t-u),d=a(s,e,t+u);return{lo:c.lo,hi:d.hi}}}}else return a(s,e,t)}return{lo:0,hi:s.length-1}}function Cs(n,e,t,i,l){const s=n.getSortedVisibleDatasetMetas(),o=t[e];for(let r=0,a=s.length;r{a[o](e[t],l)&&(s.push({element:a,datasetIndex:f,index:u}),r=r||a.inRange(e.x,e.y,l))}),i&&!r?[]:s}var g3={evaluateInteractionItems:Cs,modes:{index(n,e,t,i){const l=zi(e,n),s=t.axis||"x",o=t.includeInvisible||!1,r=t.intersect?kr(n,l,s,i,o):yr(n,l,s,!1,i,o),a=[];return r.length?(n.getSortedVisibleDatasetMetas().forEach(f=>{const u=r[0].index,c=f.data[u];c&&!c.skip&&a.push({element:c,datasetIndex:f.index,index:u})}),a):[]},dataset(n,e,t,i){const l=zi(e,n),s=t.axis||"xy",o=t.includeInvisible||!1;let r=t.intersect?kr(n,l,s,i,o):yr(n,l,s,!1,i,o);if(r.length>0){const a=r[0].datasetIndex,f=n.getDatasetMeta(a).data;r=[];for(let u=0;ut.pos===e)}function Hu(n,e){return n.filter(t=>Sb.indexOf(t.pos)===-1&&t.box.axis===e)}function Vl(n,e){return n.sort((t,i)=>{const l=e?i:t,s=e?t:i;return l.weight===s.weight?l.index-s.index:l.weight-s.weight})}function b3(n){const e=[];let t,i,l,s,o,r;for(t=0,i=(n||[]).length;tf.box.fullSize),!0),i=Vl(zl(e,"left"),!0),l=Vl(zl(e,"right")),s=Vl(zl(e,"top"),!0),o=Vl(zl(e,"bottom")),r=Hu(e,"x"),a=Hu(e,"y");return{fullSize:t,leftAndTop:i.concat(s),rightAndBottom:l.concat(a).concat(o).concat(r),chartArea:zl(e,"chartArea"),vertical:i.concat(l).concat(a),horizontal:s.concat(o).concat(r)}}function zu(n,e,t,i){return Math.max(n[t],e[t])+Math.max(n[i],e[i])}function $b(n,e){n.top=Math.max(n.top,e.top),n.left=Math.max(n.left,e.left),n.bottom=Math.max(n.bottom,e.bottom),n.right=Math.max(n.right,e.right)}function w3(n,e,t,i){const{pos:l,box:s}=t,o=n.maxPadding;if(!nt(l)){t.size&&(n[l]-=t.size);const c=i[t.stack]||{size:0,count:1};c.size=Math.max(c.size,t.horizontal?s.height:s.width),t.size=c.size/c.count,n[l]+=t.size}s.getPadding&&$b(o,s.getPadding());const r=Math.max(0,e.outerWidth-zu(o,n,"left","right")),a=Math.max(0,e.outerHeight-zu(o,n,"top","bottom")),f=r!==n.w,u=a!==n.h;return n.w=r,n.h=a,t.horizontal?{same:f,other:u}:{same:u,other:f}}function S3(n){const e=n.maxPadding;function t(i){const l=Math.max(e[i]-n[i],0);return n[i]+=l,l}n.y+=t("top"),n.x+=t("left"),t("right"),t("bottom")}function $3(n,e){const t=e.maxPadding;function i(l){const s={left:0,top:0,right:0,bottom:0};return l.forEach(o=>{s[o]=Math.max(e[o],t[o])}),s}return i(n?["left","right"]:["top","bottom"])}function Kl(n,e,t,i){const l=[];let s,o,r,a,f,u;for(s=0,o=n.length,f=0;s{typeof _.beforeLayout=="function"&&_.beforeLayout()});const u=a.reduce((_,g)=>g.box.options&&g.box.options.display===!1?_:_+1,0)||1,c=Object.freeze({outerWidth:e,outerHeight:t,padding:l,availableWidth:s,availableHeight:o,vBoxMaxWidth:s/2/u,hBoxMaxHeight:o/2}),d=Object.assign({},l);$b(d,Ei(i));const m=Object.assign({maxPadding:d,w:s,h:o,x:l.left,y:l.top},l),h=y3(a.concat(f),c);Kl(r.fullSize,m,c,h),Kl(a,m,c,h),Kl(f,m,c,h)&&Kl(a,m,c,h),S3(m),Vu(r.leftAndTop,m,c,h),m.x+=m.w,m.y+=m.h,Vu(r.rightAndBottom,m,c,h),n.chartArea={left:m.left,top:m.top,right:m.left+m.w,bottom:m.top+m.h,height:m.h,width:m.w},_t(r.chartArea,_=>{const g=_.box;Object.assign(g,n.chartArea),g.update(m.w,m.h,{left:0,top:0,right:0,bottom:0})})}};class Tb{acquireContext(e,t){}releaseContext(e){return!1}addEventListener(e,t,i){}removeEventListener(e,t,i){}getDevicePixelRatio(){return 1}getMaximumSize(e,t,i,l){return t=Math.max(0,t||e.width),i=i||e.height,{width:t,height:Math.max(0,l?Math.floor(t/l):i)}}isAttached(e){return!0}updateConfig(e){}}class T3 extends Tb{acquireContext(e){return e&&e.getContext&&e.getContext("2d")||null}updateConfig(e){e.options.animation=!1}}const co="$chartjs",C3={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},Bu=n=>n===null||n==="";function O3(n,e){const t=n.style,i=n.getAttribute("height"),l=n.getAttribute("width");if(n[co]={initial:{height:i,width:l,style:{display:t.display,height:t.height,width:t.width}}},t.display=t.display||"block",t.boxSizing=t.boxSizing||"border-box",Bu(l)){const s=Mu(n,"width");s!==void 0&&(n.width=s)}if(Bu(i))if(n.style.height==="")n.height=n.width/(e||2);else{const s=Mu(n,"height");s!==void 0&&(n.height=s)}return n}const Cb=Hw?{passive:!0}:!1;function M3(n,e,t){n&&n.addEventListener(e,t,Cb)}function D3(n,e,t){n&&n.canvas&&n.canvas.removeEventListener(e,t,Cb)}function E3(n,e){const t=C3[n.type]||n.type,{x:i,y:l}=zi(n,e);return{type:t,chart:e,native:n,x:i!==void 0?i:null,y:l!==void 0?l:null}}function No(n,e){for(const t of n)if(t===e||t.contains(e))return!0}function I3(n,e,t){const i=n.canvas,l=new MutationObserver(s=>{let o=!1;for(const r of s)o=o||No(r.addedNodes,i),o=o&&!No(r.removedNodes,i);o&&t()});return l.observe(document,{childList:!0,subtree:!0}),l}function A3(n,e,t){const i=n.canvas,l=new MutationObserver(s=>{let o=!1;for(const r of s)o=o||No(r.removedNodes,i),o=o&&!No(r.addedNodes,i);o&&t()});return l.observe(document,{childList:!0,subtree:!0}),l}const ds=new Map;let Uu=0;function Ob(){const n=window.devicePixelRatio;n!==Uu&&(Uu=n,ds.forEach((e,t)=>{t.currentDevicePixelRatio!==n&&e()}))}function L3(n,e){ds.size||window.addEventListener("resize",Ob),ds.set(n,e)}function N3(n){ds.delete(n),ds.size||window.removeEventListener("resize",Ob)}function P3(n,e,t){const i=n.canvas,l=i&&qa(i);if(!l)return;const s=rb((r,a)=>{const f=l.clientWidth;t(r,a),f{const a=r[0],f=a.contentRect.width,u=a.contentRect.height;f===0&&u===0||s(f,u)});return o.observe(l),L3(n,s),o}function vr(n,e,t){t&&t.disconnect(),e==="resize"&&N3(n)}function F3(n,e,t){const i=n.canvas,l=rb(s=>{n.ctx!==null&&t(E3(s,n))},n);return M3(i,e,l),l}class R3 extends Tb{acquireContext(e,t){const i=e&&e.getContext&&e.getContext("2d");return i&&i.canvas===e?(O3(e,t),i):null}releaseContext(e){const t=e.canvas;if(!t[co])return!1;const i=t[co].initial;["height","width"].forEach(s=>{const o=i[s];jt(o)?t.removeAttribute(s):t.setAttribute(s,o)});const l=i.style||{};return Object.keys(l).forEach(s=>{t.style[s]=l[s]}),t.width=t.width,delete t[co],!0}addEventListener(e,t,i){this.removeEventListener(e,t);const l=e.$proxies||(e.$proxies={}),o={attach:I3,detach:A3,resize:P3}[t]||F3;l[t]=o(e,t,i)}removeEventListener(e,t){const i=e.$proxies||(e.$proxies={}),l=i[t];if(!l)return;({attach:vr,detach:vr,resize:vr}[t]||D3)(e,t,l),i[t]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,i,l){return jw(e,t,i,l)}isAttached(e){const t=e&&qa(e);return!!(t&&t.isConnected)}}function q3(n){return!Ra()||typeof OffscreenCanvas<"u"&&n instanceof OffscreenCanvas?T3:R3}class xi{constructor(){Ze(this,"x");Ze(this,"y");Ze(this,"active",!1);Ze(this,"options");Ze(this,"$animations")}tooltipPosition(e){const{x:t,y:i}=this.getProps(["x","y"],e);return{x:t,y:i}}hasValue(){return us(this.x)&&us(this.y)}getProps(e,t){const i=this.$animations;if(!t||!i)return this;const l={};return e.forEach(s=>{l[s]=i[s]&&i[s].active()?i[s]._to:this[s]}),l}}Ze(xi,"defaults",{}),Ze(xi,"defaultRoutes");function j3(n,e){const t=n.options.ticks,i=H3(n),l=Math.min(t.maxTicksLimit||i,i),s=t.major.enabled?V3(e):[],o=s.length,r=s[0],a=s[o-1],f=[];if(o>l)return B3(e,f,s,o/l),f;const u=z3(s,e,l);if(o>0){let c,d;const m=o>1?Math.round((a-r)/(o-1)):null;for(Ks(e,f,u,jt(m)?0:r-m,r),c=0,d=o-1;cl)return a}return Math.max(l,1)}function V3(n){const e=[];let t,i;for(t=0,i=n.length;tn==="left"?"right":n==="right"?"left":n,Wu=(n,e,t)=>e==="top"||e==="left"?n[e]+t:n[e]-t,Yu=(n,e)=>Math.min(e||n,n);function Ku(n,e){const t=[],i=n.length/e,l=n.length;let s=0;for(;so+r)))return a}function K3(n,e){_t(n,t=>{const i=t.gc,l=i.length/2;let s;if(l>e){for(s=0;si?i:t,i=l&&t>i?t:i,{min:Zn(t,Zn(i,t)),max:Zn(i,Zn(t,i))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}getLabelItems(e=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(e))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){Rt(this.options.beforeUpdate,[this])}update(e,t,i){const{beginAtZero:l,grace:s,ticks:o}=this.options,r=o.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=t,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=gw(this,s,l),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const a=r=s||i<=1||!this.isHorizontal()){this.labelRotation=l;return}const u=this._getLabelSizes(),c=u.widest.width,d=u.highest.height,m=Bn(this.chart.width-c,0,this.maxWidth);r=e.offset?this.maxWidth/i:m/(i-1),c+6>r&&(r=m/(i-(e.offset?.5:1)),a=this.maxHeight-Bl(e.grid)-t.padding-Ju(e.title,this.chart.options.font),f=Math.sqrt(c*c+d*d),o=j2(Math.min(Math.asin(Bn((u.highest.height+6)/r,-1,1)),Math.asin(Bn(a/f,-1,1))-Math.asin(Bn(d/f,-1,1)))),o=Math.max(l,Math.min(s,o))),this.labelRotation=o}afterCalculateLabelRotation(){Rt(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){Rt(this.options.beforeFit,[this])}fit(){const e={width:0,height:0},{chart:t,options:{ticks:i,title:l,grid:s}}=this,o=this._isVisible(),r=this.isHorizontal();if(o){const a=Ju(l,t.options.font);if(r?(e.width=this.maxWidth,e.height=Bl(s)+a):(e.height=this.maxHeight,e.width=Bl(s)+a),i.display&&this.ticks.length){const{first:f,last:u,widest:c,highest:d}=this._getLabelSizes(),m=i.padding*2,h=Wi(this.labelRotation),_=Math.cos(h),g=Math.sin(h);if(r){const y=i.mirror?0:g*c.width+_*d.height;e.height=Math.min(this.maxHeight,e.height+y+m)}else{const y=i.mirror?0:_*c.width+g*d.height;e.width=Math.min(this.maxWidth,e.width+y+m)}this._calculatePadding(f,u,g,_)}}this._handleMargins(),r?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,t,i,l){const{ticks:{align:s,padding:o},position:r}=this.options,a=this.labelRotation!==0,f=r!=="top"&&this.axis==="x";if(this.isHorizontal()){const u=this.getPixelForTick(0)-this.left,c=this.right-this.getPixelForTick(this.ticks.length-1);let d=0,m=0;a?f?(d=l*e.width,m=i*t.height):(d=i*e.height,m=l*t.width):s==="start"?m=t.width:s==="end"?d=e.width:s!=="inner"&&(d=e.width/2,m=t.width/2),this.paddingLeft=Math.max((d-u+o)*this.width/(this.width-u),0),this.paddingRight=Math.max((m-c+o)*this.width/(this.width-c),0)}else{let u=t.height/2,c=e.height/2;s==="start"?(u=0,c=e.height):s==="end"&&(u=t.height,c=0),this.paddingTop=u+o,this.paddingBottom=c+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){Rt(this.options.afterFit,[this])}isHorizontal(){const{axis:e,position:t}=this.options;return t==="top"||t==="bottom"||e==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let t,i;for(t=0,i=e.length;t({width:o[L]||0,height:r[L]||0});return{first:I(0),last:I(t-1),widest:I(O),highest:I(D),widths:o,heights:r}}getLabelForValue(e){return e}getPixelForValue(e,t){return NaN}getValueForPixel(e){}getPixelForTick(e){const t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);const t=this._startPixel+e*this._length;return V2(this._alignToPixels?qi(this.chart,t,0):t)}getDecimalForPixel(e){const t=(e-this._startPixel)/this._length;return this._reversePixels?1-t:t}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:e,max:t}=this;return e<0&&t<0?t:e>0&&t>0?e:0}getContext(e){const t=this.ticks||[];if(e>=0&&er*l?r/i:a/l:a*l0}_computeGridLineItems(e){const t=this.axis,i=this.chart,l=this.options,{grid:s,position:o,border:r}=l,a=s.offset,f=this.isHorizontal(),c=this.ticks.length+(a?1:0),d=Bl(s),m=[],h=r.setContext(this.getContext()),_=h.display?h.width:0,g=_/2,y=function(W){return qi(i,W,_)};let S,T,$,C,O,D,I,L,R,F,N,P;if(o==="top")S=y(this.bottom),D=this.bottom-d,L=S-g,F=y(e.top)+g,P=e.bottom;else if(o==="bottom")S=y(this.top),F=e.top,P=y(e.bottom)-g,D=S+g,L=this.top+d;else if(o==="left")S=y(this.right),O=this.right-d,I=S-g,R=y(e.left)+g,N=e.right;else if(o==="right")S=y(this.left),R=e.left,N=y(e.right)-g,O=S+g,I=this.left+d;else if(t==="x"){if(o==="center")S=y((e.top+e.bottom)/2+.5);else if(nt(o)){const W=Object.keys(o)[0],G=o[W];S=y(this.chart.scales[W].getPixelForValue(G))}F=e.top,P=e.bottom,D=S+g,L=D+d}else if(t==="y"){if(o==="center")S=y((e.left+e.right)/2);else if(nt(o)){const W=Object.keys(o)[0],G=o[W];S=y(this.chart.scales[W].getPixelForValue(G))}O=S-g,I=O-d,R=e.left,N=e.right}const q=vt(l.ticks.maxTicksLimit,c),H=Math.max(1,Math.ceil(c/q));for(T=0;T0&&(xe-=He/2);break}te={left:xe,top:Xe,width:He+pe.width,height:Ne+pe.height,color:H.backdropColor}}g.push({label:$,font:L,textOffset:N,options:{rotation:_,color:G,strokeColor:U,strokeWidth:Y,textAlign:ie,textBaseline:P,translation:[C,O],backdrop:te}})}return g}_getXAxisLabelAlignment(){const{position:e,ticks:t}=this.options;if(-Wi(this.labelRotation))return e==="top"?"left":"right";let l="center";return t.align==="start"?l="left":t.align==="end"?l="right":t.align==="inner"&&(l="inner"),l}_getYAxisLabelAlignment(e){const{position:t,ticks:{crossAlign:i,mirror:l,padding:s}}=this.options,o=this._getLabelSizes(),r=e+s,a=o.widest.width;let f,u;return t==="left"?l?(u=this.right+s,i==="near"?f="left":i==="center"?(f="center",u+=a/2):(f="right",u+=a)):(u=this.right-r,i==="near"?f="right":i==="center"?(f="center",u-=a/2):(f="left",u=this.left)):t==="right"?l?(u=this.left+s,i==="near"?f="right":i==="center"?(f="center",u-=a/2):(f="left",u-=a)):(u=this.left+r,i==="near"?f="left":i==="center"?(f="center",u+=a/2):(f="right",u=this.right)):f="right",{textAlign:f,x:u}}_computeLabelArea(){if(this.options.ticks.mirror)return;const e=this.chart,t=this.options.position;if(t==="left"||t==="right")return{top:0,left:this.left,bottom:e.height,right:this.right};if(t==="top"||t==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:e.width}}drawBackground(){const{ctx:e,options:{backgroundColor:t},left:i,top:l,width:s,height:o}=this;t&&(e.save(),e.fillStyle=t,e.fillRect(i,l,s,o),e.restore())}getLineWidthForValue(e){const t=this.options.grid;if(!this._isVisible()||!t.display)return 0;const l=this.ticks.findIndex(s=>s.value===e);return l>=0?t.setContext(this.getContext(l)).lineWidth:0}drawGrid(e){const t=this.options.grid,i=this.ctx,l=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e));let s,o;const r=(a,f,u)=>{!u.width||!u.color||(i.save(),i.lineWidth=u.width,i.strokeStyle=u.color,i.setLineDash(u.borderDash||[]),i.lineDashOffset=u.borderDashOffset,i.beginPath(),i.moveTo(a.x,a.y),i.lineTo(f.x,f.y),i.stroke(),i.restore())};if(t.display)for(s=0,o=l.length;s{this.draw(s)}}]:[{z:i,draw:s=>{this.drawBackground(),this.drawGrid(s),this.drawTitle()}},{z:l,draw:()=>{this.drawBorder()}},{z:t,draw:s=>{this.drawLabels(s)}}]}getMatchingVisibleMetas(e){const t=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",l=[];let s,o;for(s=0,o=t.length;s{const i=t.split("."),l=i.pop(),s=[n].concat(i).join("."),o=e[t].split("."),r=o.pop(),a=o.join(".");Ut.route(s,l,a,r)})}function e4(n){return"id"in n&&"defaults"in n}class t4{constructor(){this.controllers=new Js(ts,"datasets",!0),this.elements=new Js(xi,"elements"),this.plugins=new Js(Object,"plugins"),this.scales=new Js(Os,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each("register",e)}remove(...e){this._each("unregister",e)}addControllers(...e){this._each("register",e,this.controllers)}addElements(...e){this._each("register",e,this.elements)}addPlugins(...e){this._each("register",e,this.plugins)}addScales(...e){this._each("register",e,this.scales)}getController(e){return this._get(e,this.controllers,"controller")}getElement(e){return this._get(e,this.elements,"element")}getPlugin(e){return this._get(e,this.plugins,"plugin")}getScale(e){return this._get(e,this.scales,"scale")}removeControllers(...e){this._each("unregister",e,this.controllers)}removeElements(...e){this._each("unregister",e,this.elements)}removePlugins(...e){this._each("unregister",e,this.plugins)}removeScales(...e){this._each("unregister",e,this.scales)}_each(e,t,i){[...t].forEach(l=>{const s=i||this._getRegistryForType(l);i||s.isForType(l)||s===this.plugins&&l.id?this._exec(e,s,l):_t(l,o=>{const r=i||this._getRegistryForType(o);this._exec(e,r,o)})})}_exec(e,t,i){const l=Da(e);Rt(i["before"+l],[],i),t[e](i),Rt(i["after"+l],[],i)}_getRegistryForType(e){for(let t=0;ts.filter(r=>!o.some(a=>r.plugin.id===a.plugin.id));this._notify(l(t,i),e,"stop"),this._notify(l(i,t),e,"start")}}function i4(n){const e={},t=[],i=Object.keys(Xn.plugins.items);for(let s=0;s1&&Zu(n[0].toLowerCase());if(i)return i}throw new Error(`Cannot determine type of '${n}' axis. Please provide 'axis' or 'position' option.`)}function Gu(n,e,t){if(t[e+"AxisID"]===n)return{axis:e}}function u4(n,e){if(e.data&&e.data.datasets){const t=e.data.datasets.filter(i=>i.xAxisID===n||i.yAxisID===n);if(t.length)return Gu(n,"x",t[0])||Gu(n,"y",t[0])}return{}}function c4(n,e){const t=Qi[n.type]||{scales:{}},i=e.scales||{},l=xr(n.type,e),s=Object.create(null);return Object.keys(i).forEach(o=>{const r=i[o];if(!nt(r))return console.error(`Invalid scale configuration for scale: ${o}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${o}`);const a=ea(o,r,u4(o,n),Ut.scales[r.type]),f=a4(a,l),u=t.scales||{};s[o]=Xl(Object.create(null),[{axis:a},r,u[a],u[f]])}),n.data.datasets.forEach(o=>{const r=o.type||n.type,a=o.indexAxis||xr(r,e),u=(Qi[r]||{}).scales||{};Object.keys(u).forEach(c=>{const d=r4(c,a),m=o[d+"AxisID"]||d;s[m]=s[m]||Object.create(null),Xl(s[m],[{axis:d},i[m],u[c]])})}),Object.keys(s).forEach(o=>{const r=s[o];Xl(r,[Ut.scales[r.type],Ut.scale])}),s}function Mb(n){const e=n.options||(n.options={});e.plugins=vt(e.plugins,{}),e.scales=c4(n,e)}function Db(n){return n=n||{},n.datasets=n.datasets||[],n.labels=n.labels||[],n}function d4(n){return n=n||{},n.data=Db(n.data),Mb(n),n}const Xu=new Map,Eb=new Set;function Zs(n,e){let t=Xu.get(n);return t||(t=e(),Xu.set(n,t),Eb.add(t)),t}const Ul=(n,e,t)=>{const i=Eo(e,t);i!==void 0&&n.add(i)};class p4{constructor(e){this._config=d4(e),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(e){this._config.type=e}get data(){return this._config.data}set data(e){this._config.data=Db(e)}get options(){return this._config.options}set options(e){this._config.options=e}get plugins(){return this._config.plugins}update(){const e=this._config;this.clearCache(),Mb(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Zs(e,()=>[[`datasets.${e}`,""]])}datasetAnimationScopeKeys(e,t){return Zs(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,""]])}datasetElementScopeKeys(e,t){return Zs(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,""]])}pluginScopeKeys(e){const t=e.id,i=this.type;return Zs(`${i}-plugin-${t}`,()=>[[`plugins.${t}`,...e.additionalOptionScopes||[]]])}_cachedScopes(e,t){const i=this._scopeCache;let l=i.get(e);return(!l||t)&&(l=new Map,i.set(e,l)),l}getOptionScopes(e,t,i){const{options:l,type:s}=this,o=this._cachedScopes(e,i),r=o.get(t);if(r)return r;const a=new Set;t.forEach(u=>{e&&(a.add(e),u.forEach(c=>Ul(a,e,c))),u.forEach(c=>Ul(a,l,c)),u.forEach(c=>Ul(a,Qi[s]||{},c)),u.forEach(c=>Ul(a,Ut,c)),u.forEach(c=>Ul(a,Xr,c))});const f=Array.from(a);return f.length===0&&f.push(Object.create(null)),Eb.has(t)&&o.set(t,f),f}chartOptionScopes(){const{options:e,type:t}=this;return[e,Qi[t]||{},Ut.datasets[t]||{},{type:t},Ut,Xr]}resolveNamedOptions(e,t,i,l=[""]){const s={$shared:!0},{resolver:o,subPrefixes:r}=Qu(this._resolverCache,e,l);let a=o;if(h4(o,t)){s.$shared=!1,i=Di(i)?i():i;const f=this.createResolver(e,i,r);a=Ol(o,i,f)}for(const f of t)s[f]=a[f];return s}createResolver(e,t,i=[""],l){const{resolver:s}=Qu(this._resolverCache,e,i);return nt(t)?Ol(s,t,void 0,l):s}}function Qu(n,e,t){let i=n.get(e);i||(i=new Map,n.set(e,i));const l=t.join();let s=i.get(l);return s||(s={resolver:Na(e,t),subPrefixes:t.filter(r=>!r.toLowerCase().includes("hover"))},i.set(l,s)),s}const m4=n=>nt(n)&&Object.getOwnPropertyNames(n).some(e=>Di(n[e]));function h4(n,e){const{isScriptable:t,isIndexable:i}=db(n);for(const l of e){const s=t(l),o=i(l),r=(o||s)&&n[l];if(s&&(Di(r)||m4(r))||o&&Xt(r))return!0}return!1}var _4="4.4.3";const g4=["top","bottom","left","right","chartArea"];function xu(n,e){return n==="top"||n==="bottom"||g4.indexOf(n)===-1&&e==="x"}function ec(n,e){return function(t,i){return t[n]===i[n]?t[e]-i[e]:t[n]-i[n]}}function tc(n){const e=n.chart,t=e.options.animation;e.notifyPlugins("afterRender"),Rt(t&&t.onComplete,[n],e)}function b4(n){const e=n.chart,t=e.options.animation;Rt(t&&t.onProgress,[n],e)}function Ib(n){return Ra()&&typeof n=="string"?n=document.getElementById(n):n&&n.length&&(n=n[0]),n&&n.canvas&&(n=n.canvas),n}const po={},nc=n=>{const e=Ib(n);return Object.values(po).filter(t=>t.canvas===e).pop()};function k4(n,e,t){const i=Object.keys(n);for(const l of i){const s=+l;if(s>=e){const o=n[l];delete n[l],(t>0||s>e)&&(n[s+t]=o)}}}function y4(n,e,t,i){return!t||n.type==="mouseout"?null:i?e:n}function Gs(n,e,t){return n.options.clip?n[t]:e[t]}function v4(n,e){const{xScale:t,yScale:i}=n;return t&&i?{left:Gs(t,e,"left"),right:Gs(t,e,"right"),top:Gs(i,e,"top"),bottom:Gs(i,e,"bottom")}:e}class ci{static register(...e){Xn.add(...e),ic()}static unregister(...e){Xn.remove(...e),ic()}constructor(e,t){const i=this.config=new p4(t),l=Ib(e),s=nc(l);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas with ID '"+s.canvas.id+"' can be reused.");const o=i.createResolver(i.chartOptionScopes(),this.getContext());this.platform=new(i.platform||q3(l)),this.platform.updateConfig(i);const r=this.platform.acquireContext(l,o.aspectRatio),a=r&&r.canvas,f=a&&a.height,u=a&&a.width;if(this.id=O2(),this.ctx=r,this.canvas=a,this.width=u,this.height=f,this._options=o,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new n4,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=K2(c=>this.update(c),o.resizeDelay||0),this._dataChanges=[],po[this.id]=this,!r||!a){console.error("Failed to create chart: can't acquire context from the given item");return}ai.listen(this,"complete",tc),ai.listen(this,"progress",b4),this._initialize(),this.attached&&this.update()}get aspectRatio(){const{options:{aspectRatio:e,maintainAspectRatio:t},width:i,height:l,_aspectRatio:s}=this;return jt(e)?t&&s?s:l?i/l:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return Xn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():Ou(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return wu(this.canvas,this.ctx),this}stop(){return ai.stop(this),this}resize(e,t){ai.running(this)?this._resizeBeforeDraw={width:e,height:t}:this._resize(e,t)}_resize(e,t){const i=this.options,l=this.canvas,s=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(l,e,t,s),r=i.devicePixelRatio||this.platform.getDevicePixelRatio(),a=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,Ou(this,r,!0)&&(this.notifyPlugins("resize",{size:o}),Rt(i.onResize,[this,o],this),this.attached&&this._doResize(a)&&this.render())}ensureScalesHaveIDs(){const t=this.options.scales||{};_t(t,(i,l)=>{i.id=l})}buildOrUpdateScales(){const e=this.options,t=e.scales,i=this.scales,l=Object.keys(i).reduce((o,r)=>(o[r]=!1,o),{});let s=[];t&&(s=s.concat(Object.keys(t).map(o=>{const r=t[o],a=ea(o,r),f=a==="r",u=a==="x";return{options:r,dposition:f?"chartArea":u?"bottom":"left",dtype:f?"radialLinear":u?"category":"linear"}}))),_t(s,o=>{const r=o.options,a=r.id,f=ea(a,r),u=vt(r.type,o.dtype);(r.position===void 0||xu(r.position,f)!==xu(o.dposition))&&(r.position=o.dposition),l[a]=!0;let c=null;if(a in i&&i[a].type===u)c=i[a];else{const d=Xn.getScale(u);c=new d({id:a,type:u,ctx:this.ctx,chart:this}),i[c.id]=c}c.init(r,e)}),_t(l,(o,r)=>{o||delete i[r]}),_t(i,o=>{Ys.configure(this,o,o.options),Ys.addBox(this,o)})}_updateMetasets(){const e=this._metasets,t=this.data.datasets.length,i=e.length;if(e.sort((l,s)=>l.index-s.index),i>t){for(let l=t;lt.length&&delete this._stacks,e.forEach((i,l)=>{t.filter(s=>s===i._dataset).length===0&&this._destroyDatasetMeta(l)})}buildOrUpdateControllers(){const e=[],t=this.data.datasets;let i,l;for(this._removeUnreferencedMetasets(),i=0,l=t.length;i{this.getDatasetMeta(t).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(e){const t=this.config;t.update();const i=this._options=t.createResolver(t.chartOptionScopes(),this.getContext()),l=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:e,cancelable:!0})===!1)return;const s=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let f=0,u=this.data.datasets.length;f{f.reset()}),this._updateDatasets(e),this.notifyPlugins("afterUpdate",{mode:e}),this._layers.sort(ec("z","_idx"));const{_active:r,_lastEvent:a}=this;a?this._eventHandler(a,!0):r.length&&this._updateHoverStyles(r,r,!0),this.render()}_updateScales(){_t(this.scales,e=>{Ys.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const e=this.options,t=new Set(Object.keys(this._listeners)),i=new Set(e.events);(!cu(t,i)||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:e}=this,t=this._getUniformDataChanges()||[];for(const{method:i,start:l,count:s}of t){const o=i==="_removeElements"?-s:s;k4(e,l,o)}}_getUniformDataChanges(){const e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];const t=this.data.datasets.length,i=s=>new Set(e.filter(o=>o[0]===s).map((o,r)=>r+","+o.splice(1).join(","))),l=i(0);for(let s=1;ss.split(",")).map(s=>({method:s[1],start:+s[2],count:+s[3]}))}_updateLayout(e){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;Ys.update(this,this.width,this.height,e);const t=this.chartArea,i=t.width<=0||t.height<=0;this._layers=[],_t(this.boxes,l=>{i&&l.position==="chartArea"||(l.configure&&l.configure(),this._layers.push(...l._layers()))},this),this._layers.forEach((l,s)=>{l._idx=s}),this.notifyPlugins("afterLayout")}_updateDatasets(e){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:e,cancelable:!0})!==!1){for(let t=0,i=this.data.datasets.length;t=0;--t)this._drawDataset(e[t]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(e){const t=this.ctx,i=e._clip,l=!i.disabled,s=v4(e,this.chartArea),o={meta:e,index:e.index,cancelable:!0};this.notifyPlugins("beforeDatasetDraw",o)!==!1&&(l&&Aa(t,{left:i.left===!1?0:s.left-i.left,right:i.right===!1?this.width:s.right+i.right,top:i.top===!1?0:s.top-i.top,bottom:i.bottom===!1?this.height:s.bottom+i.bottom}),e.controller.draw(),l&&La(t),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(e){return cs(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,i,l){const s=g3.modes[t];return typeof s=="function"?s(this,e,i,l):[]}getDatasetMeta(e){const t=this.data.datasets[e],i=this._metasets;let l=i.filter(s=>s&&s._dataset===t).pop();return l||(l={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:t&&t.order||0,index:e,_dataset:t,_parsed:[],_sorted:!1},i.push(l)),l}getContext(){return this.$context||(this.$context=ll(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){const t=this.data.datasets[e];if(!t)return!1;const i=this.getDatasetMeta(e);return typeof i.hidden=="boolean"?!i.hidden:!t.hidden}setDatasetVisibility(e,t){const i=this.getDatasetMeta(e);i.hidden=!t}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,t,i){const l=i?"show":"hide",s=this.getDatasetMeta(e),o=s.controller._resolveAnimations(void 0,l);Io(t)?(s.data[t].hidden=!i,this.update()):(this.setDatasetVisibility(e,i),o.update(s,{visible:i}),this.update(r=>r.datasetIndex===e?l:void 0))}hide(e,t){this._updateVisibility(e,t,!1)}show(e,t){this._updateVisibility(e,t,!0)}_destroyDatasetMeta(e){const t=this._metasets[e];t&&t.controller&&t.controller._destroy(),delete this._metasets[e]}_stop(){let e,t;for(this.stop(),ai.remove(this),e=0,t=this.data.datasets.length;e{t.addEventListener(this,s,o),e[s]=o},l=(s,o,r)=>{s.offsetX=o,s.offsetY=r,this._eventHandler(s)};_t(this.options.events,s=>i(s,l))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const e=this._responsiveListeners,t=this.platform,i=(a,f)=>{t.addEventListener(this,a,f),e[a]=f},l=(a,f)=>{e[a]&&(t.removeEventListener(this,a,f),delete e[a])},s=(a,f)=>{this.canvas&&this.resize(a,f)};let o;const r=()=>{l("attach",r),this.attached=!0,this.resize(),i("resize",s),i("detach",o)};o=()=>{this.attached=!1,l("resize",s),this._stop(),this._resize(0,0),i("attach",r)},t.isAttached(this.canvas)?r():o()}unbindEvents(){_t(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},_t(this._responsiveListeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,t,i){const l=i?"set":"remove";let s,o,r,a;for(t==="dataset"&&(s=this.getDatasetMeta(e[0].datasetIndex),s.controller["_"+l+"DatasetHoverStyle"]()),r=0,a=e.length;r{const r=this.getDatasetMeta(s);if(!r)throw new Error("No dataset found at index "+s);return{datasetIndex:s,element:r.data[o],index:o}});!Mo(i,t)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,t))}notifyPlugins(e,t,i){return this._plugins.notify(this,e,t,i)}isPluginEnabled(e){return this._plugins._cache.filter(t=>t.plugin.id===e).length===1}_updateHoverStyles(e,t,i){const l=this.options.hover,s=(a,f)=>a.filter(u=>!f.some(c=>u.datasetIndex===c.datasetIndex&&u.index===c.index)),o=s(t,e),r=i?e:s(e,t);o.length&&this.updateHoverStyle(o,l.mode,!1),r.length&&l.mode&&this.updateHoverStyle(r,l.mode,!0)}_eventHandler(e,t){const i={event:e,replay:t,cancelable:!0,inChartArea:this.isPointInArea(e)},l=o=>(o.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins("beforeEvent",i,l)===!1)return;const s=this._handleEvent(e,t,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,l),(s||i.changed)&&this.render(),this}_handleEvent(e,t,i){const{_active:l=[],options:s}=this,o=t,r=this._getActiveElements(e,l,i,o),a=L2(e),f=y4(e,this._lastEvent,i,a);i&&(this._lastEvent=null,Rt(s.onHover,[e,r,this],this),a&&Rt(s.onClick,[e,r,this],this));const u=!Mo(r,l);return(u||t)&&(this._active=r,this._updateHoverStyles(r,l,t)),this._lastEvent=f,u}_getActiveElements(e,t,i,l){if(e.type==="mouseout")return[];if(!i)return t;const s=this.options.hover;return this.getElementsAtEventForMode(e,s.mode,s,l)}}Ze(ci,"defaults",Ut),Ze(ci,"instances",po),Ze(ci,"overrides",Qi),Ze(ci,"registry",Xn),Ze(ci,"version",_4),Ze(ci,"getChart",nc);function ic(){return _t(ci.instances,n=>n._plugins.invalidate())}function Ab(n,e,t=e){n.lineCap=vt(t.borderCapStyle,e.borderCapStyle),n.setLineDash(vt(t.borderDash,e.borderDash)),n.lineDashOffset=vt(t.borderDashOffset,e.borderDashOffset),n.lineJoin=vt(t.borderJoinStyle,e.borderJoinStyle),n.lineWidth=vt(t.borderWidth,e.borderWidth),n.strokeStyle=vt(t.borderColor,e.borderColor)}function w4(n,e,t){n.lineTo(t.x,t.y)}function S4(n){return n.stepped?rw:n.tension||n.cubicInterpolationMode==="monotone"?aw:w4}function Lb(n,e,t={}){const i=n.length,{start:l=0,end:s=i-1}=t,{start:o,end:r}=e,a=Math.max(l,o),f=Math.min(s,r),u=lr&&s>r;return{count:i,start:a,loop:e.loop,ilen:f(o+(f?r-$:$))%s,T=()=>{_!==g&&(n.lineTo(u,g),n.lineTo(u,_),n.lineTo(u,y))};for(a&&(m=l[S(0)],n.moveTo(m.x,m.y)),d=0;d<=r;++d){if(m=l[S(d)],m.skip)continue;const $=m.x,C=m.y,O=$|0;O===h?(C<_?_=C:C>g&&(g=C),u=(c*u+$)/++c):(T(),n.lineTo($,C),h=O,c=0,_=g=C),y=C}T()}function ta(n){const e=n.options,t=e.borderDash&&e.borderDash.length;return!n._decimated&&!n._loop&&!e.tension&&e.cubicInterpolationMode!=="monotone"&&!e.stepped&&!t?T4:$4}function C4(n){return n.stepped?zw:n.tension||n.cubicInterpolationMode==="monotone"?Vw:Vi}function O4(n,e,t,i){let l=e._path;l||(l=e._path=new Path2D,e.path(l,t,i)&&l.closePath()),Ab(n,e.options),n.stroke(l)}function M4(n,e,t,i){const{segments:l,options:s}=e,o=ta(e);for(const r of l)Ab(n,s,r.style),n.beginPath(),o(n,e,r,{start:t,end:t+i-1})&&n.closePath(),n.stroke()}const D4=typeof Path2D=="function";function E4(n,e,t,i){D4&&!e.options.segment?O4(n,e,t,i):M4(n,e,t,i)}class Ti extends xi{constructor(e){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,e&&Object.assign(this,e)}updateControlPoints(e,t){const i=this.options;if((i.tension||i.cubicInterpolationMode==="monotone")&&!i.stepped&&!this._pointsUpdated){const l=i.spanGaps?this._loop:this._fullLoop;Lw(this._points,i,e,l,t),this._pointsUpdated=!0}}set points(e){this._points=e,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Gw(this,this.options.segment))}first(){const e=this.segments,t=this.points;return e.length&&t[e[0].start]}last(){const e=this.segments,t=this.points,i=e.length;return i&&t[e[i-1].end]}interpolate(e,t){const i=this.options,l=e[t],s=this.points,o=kb(this,{property:t,start:l,end:l});if(!o.length)return;const r=[],a=C4(i);let f,u;for(f=0,u=o.length;fe!=="borderDash"&&e!=="fill"});function lc(n,e,t,i){const l=n.options,{[t]:s}=n.getProps([t],i);return Math.abs(e-s){r=Ha(o,r,l);const a=l[o],f=l[r];i!==null?(s.push({x:a.x,y:i}),s.push({x:f.x,y:i})):t!==null&&(s.push({x:t,y:a.y}),s.push({x:t,y:f.y}))}),s}function Ha(n,e,t){for(;e>n;e--){const i=t[e];if(!isNaN(i.x)&&!isNaN(i.y))break}return e}function sc(n,e,t,i){return n&&e?i(n[t],e[t]):n?n[t]:e?e[t]:0}function Nb(n,e){let t=[],i=!1;return Xt(n)?(i=!0,t=n):t=A4(n,e),t.length?new Ti({points:t,options:{tension:0},_loop:i,_fullLoop:i}):null}function oc(n){return n&&n.fill!==!1}function L4(n,e,t){let l=n[e].fill;const s=[e];let o;if(!t)return l;for(;l!==!1&&s.indexOf(l)===-1;){if(!on(l))return l;if(o=n[l],!o)return!1;if(o.visible)return l;s.push(l),l=o.fill}return!1}function N4(n,e,t){const i=q4(n);if(nt(i))return isNaN(i.value)?!1:i;let l=parseFloat(i);return on(l)&&Math.floor(l)===l?P4(i[0],e,l,t):["origin","start","end","stack","shape"].indexOf(i)>=0&&i}function P4(n,e,t,i){return(n==="-"||n==="+")&&(t=e+t),t===e||t<0||t>=i?!1:t}function F4(n,e){let t=null;return n==="start"?t=e.bottom:n==="end"?t=e.top:nt(n)?t=e.getPixelForValue(n.value):e.getBasePixel&&(t=e.getBasePixel()),t}function R4(n,e,t){let i;return n==="start"?i=t:n==="end"?i=e.options.reverse?e.min:e.max:nt(n)?i=n.value:i=e.getBaseValue(),i}function q4(n){const e=n.options,t=e.fill;let i=vt(t&&t.target,t);return i===void 0&&(i=!!e.backgroundColor),i===!1||i===null?!1:i===!0?"origin":i}function j4(n){const{scale:e,index:t,line:i}=n,l=[],s=i.segments,o=i.points,r=H4(e,t);r.push(Nb({x:null,y:e.bottom},i));for(let a=0;a=0;--o){const r=l[o].$filler;r&&(r.line.updateControlPoints(s,r.axis),i&&r.fill&&wr(n.ctx,r,s))}},beforeDatasetsDraw(n,e,t){if(t.drawTime!=="beforeDatasetsDraw")return;const i=n.getSortedVisibleDatasetMetas();for(let l=i.length-1;l>=0;--l){const s=i[l].$filler;oc(s)&&wr(n.ctx,s,n.chartArea)}},beforeDatasetDraw(n,e,t){const i=e.meta.$filler;!oc(i)||t.drawTime!=="beforeDatasetDraw"||wr(n.ctx,i,n.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const Jl={average(n){if(!n.length)return!1;let e,t,i=new Set,l=0,s=0;for(e=0,t=n.length;er+a)/i.size,y:l/s}},nearest(n,e){if(!n.length)return!1;let t=e.x,i=e.y,l=Number.POSITIVE_INFINITY,s,o,r;for(s=0,o=n.length;s-1?n.split(` -`):n}function X4(n,e){const{element:t,datasetIndex:i,index:l}=e,s=n.getDatasetMeta(i).controller,{label:o,value:r}=s.getLabelAndValue(l);return{chart:n,label:o,parsed:s.getParsed(l),raw:n.data.datasets[i].data[l],formattedValue:r,dataset:s.getDataset(),dataIndex:l,datasetIndex:i,element:t}}function uc(n,e){const t=n.chart.ctx,{body:i,footer:l,title:s}=n,{boxWidth:o,boxHeight:r}=e,a=ei(e.bodyFont),f=ei(e.titleFont),u=ei(e.footerFont),c=s.length,d=l.length,m=i.length,h=Ei(e.padding);let _=h.height,g=0,y=i.reduce(($,C)=>$+C.before.length+C.lines.length+C.after.length,0);if(y+=n.beforeBody.length+n.afterBody.length,c&&(_+=c*f.lineHeight+(c-1)*e.titleSpacing+e.titleMarginBottom),y){const $=e.displayColors?Math.max(r,a.lineHeight):a.lineHeight;_+=m*$+(y-m)*a.lineHeight+(y-1)*e.bodySpacing}d&&(_+=e.footerMarginTop+d*u.lineHeight+(d-1)*e.footerSpacing);let S=0;const T=function($){g=Math.max(g,t.measureText($).width+S)};return t.save(),t.font=f.string,_t(n.title,T),t.font=a.string,_t(n.beforeBody.concat(n.afterBody),T),S=e.displayColors?o+2+e.boxPadding:0,_t(i,$=>{_t($.before,T),_t($.lines,T),_t($.after,T)}),S=0,t.font=u.string,_t(n.footer,T),t.restore(),g+=h.width,{width:g,height:_}}function Q4(n,e){const{y:t,height:i}=e;return tn.height-i/2?"bottom":"center"}function x4(n,e,t,i){const{x:l,width:s}=i,o=t.caretSize+t.caretPadding;if(n==="left"&&l+s+o>e.width||n==="right"&&l-s-o<0)return!0}function eS(n,e,t,i){const{x:l,width:s}=t,{width:o,chartArea:{left:r,right:a}}=n;let f="center";return i==="center"?f=l<=(r+a)/2?"left":"right":l<=s/2?f="left":l>=o-s/2&&(f="right"),x4(f,n,e,t)&&(f="center"),f}function cc(n,e,t){const i=t.yAlign||e.yAlign||Q4(n,t);return{xAlign:t.xAlign||e.xAlign||eS(n,e,t,i),yAlign:i}}function tS(n,e){let{x:t,width:i}=n;return e==="right"?t-=i:e==="center"&&(t-=i/2),t}function nS(n,e,t){let{y:i,height:l}=n;return e==="top"?i+=t:e==="bottom"?i-=l+t:i-=l/2,i}function dc(n,e,t,i){const{caretSize:l,caretPadding:s,cornerRadius:o}=n,{xAlign:r,yAlign:a}=t,f=l+s,{topLeft:u,topRight:c,bottomLeft:d,bottomRight:m}=fo(o);let h=tS(e,r);const _=nS(e,a,f);return a==="center"?r==="left"?h+=f:r==="right"&&(h-=f):r==="left"?h-=Math.max(u,d)+l:r==="right"&&(h+=Math.max(c,m)+l),{x:Bn(h,0,i.width-e.width),y:Bn(_,0,i.height-e.height)}}function Xs(n,e,t){const i=Ei(t.padding);return e==="center"?n.x+n.width/2:e==="right"?n.x+n.width-i.right:n.x+i.left}function pc(n){return Gn([],fi(n))}function iS(n,e,t){return ll(n,{tooltip:e,tooltipItems:t,type:"tooltip"})}function mc(n,e){const t=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return t?n.override(t):n}const Fb={beforeTitle:ri,title(n){if(n.length>0){const e=n[0],t=e.chart.data.labels,i=t?t.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(i>0&&e.dataIndex"u"?Fb[e].call(t,i):l}class ia extends xi{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const t=this.chart,i=this.options.setContext(this.getContext()),l=i.enabled&&t.options.animation&&i.animations,s=new yb(this.chart,l);return l._cacheable&&(this._cachedAnimations=Object.freeze(s)),s}getContext(){return this.$context||(this.$context=iS(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){const{callbacks:i}=t,l=dn(i,"beforeTitle",this,e),s=dn(i,"title",this,e),o=dn(i,"afterTitle",this,e);let r=[];return r=Gn(r,fi(l)),r=Gn(r,fi(s)),r=Gn(r,fi(o)),r}getBeforeBody(e,t){return pc(dn(t.callbacks,"beforeBody",this,e))}getBody(e,t){const{callbacks:i}=t,l=[];return _t(e,s=>{const o={before:[],lines:[],after:[]},r=mc(i,s);Gn(o.before,fi(dn(r,"beforeLabel",this,s))),Gn(o.lines,dn(r,"label",this,s)),Gn(o.after,fi(dn(r,"afterLabel",this,s))),l.push(o)}),l}getAfterBody(e,t){return pc(dn(t.callbacks,"afterBody",this,e))}getFooter(e,t){const{callbacks:i}=t,l=dn(i,"beforeFooter",this,e),s=dn(i,"footer",this,e),o=dn(i,"afterFooter",this,e);let r=[];return r=Gn(r,fi(l)),r=Gn(r,fi(s)),r=Gn(r,fi(o)),r}_createItems(e){const t=this._active,i=this.chart.data,l=[],s=[],o=[];let r=[],a,f;for(a=0,f=t.length;ae.filter(u,c,d,i))),e.itemSort&&(r=r.sort((u,c)=>e.itemSort(u,c,i))),_t(r,u=>{const c=mc(e.callbacks,u);l.push(dn(c,"labelColor",this,u)),s.push(dn(c,"labelPointStyle",this,u)),o.push(dn(c,"labelTextColor",this,u))}),this.labelColors=l,this.labelPointStyles=s,this.labelTextColors=o,this.dataPoints=r,r}update(e,t){const i=this.options.setContext(this.getContext()),l=this._active;let s,o=[];if(!l.length)this.opacity!==0&&(s={opacity:0});else{const r=Jl[i.position].call(this,l,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const a=this._size=uc(this,i),f=Object.assign({},r,a),u=cc(this.chart,i,f),c=dc(i,f,u,this.chart);this.xAlign=u.xAlign,this.yAlign=u.yAlign,s={opacity:1,x:c.x,y:c.y,width:a.width,height:a.height,caretX:r.x,caretY:r.y}}this._tooltipItems=o,this.$context=void 0,s&&this._resolveAnimations().update(this,s),e&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:t})}drawCaret(e,t,i,l){const s=this.getCaretPosition(e,i,l);t.lineTo(s.x1,s.y1),t.lineTo(s.x2,s.y2),t.lineTo(s.x3,s.y3)}getCaretPosition(e,t,i){const{xAlign:l,yAlign:s}=this,{caretSize:o,cornerRadius:r}=i,{topLeft:a,topRight:f,bottomLeft:u,bottomRight:c}=fo(r),{x:d,y:m}=e,{width:h,height:_}=t;let g,y,S,T,$,C;return s==="center"?($=m+_/2,l==="left"?(g=d,y=g-o,T=$+o,C=$-o):(g=d+h,y=g+o,T=$-o,C=$+o),S=g):(l==="left"?y=d+Math.max(a,u)+o:l==="right"?y=d+h-Math.max(f,c)-o:y=this.caretX,s==="top"?(T=m,$=T-o,g=y-o,S=y+o):(T=m+_,$=T+o,g=y+o,S=y-o),C=T),{x1:g,x2:y,x3:S,y1:T,y2:$,y3:C}}drawTitle(e,t,i){const l=this.title,s=l.length;let o,r,a;if(s){const f=_r(i.rtl,this.x,this.width);for(e.x=Xs(this,i.titleAlign,i),t.textAlign=f.textAlign(i.titleAlign),t.textBaseline="middle",o=ei(i.titleFont),r=i.titleSpacing,t.fillStyle=i.titleColor,t.font=o.string,a=0;aS!==0)?(e.beginPath(),e.fillStyle=s.multiKeyBackground,$u(e,{x:_,y:h,w:f,h:a,radius:y}),e.fill(),e.stroke(),e.fillStyle=o.backgroundColor,e.beginPath(),$u(e,{x:g,y:h+1,w:f-2,h:a-2,radius:y}),e.fill()):(e.fillStyle=s.multiKeyBackground,e.fillRect(_,h,f,a),e.strokeRect(_,h,f,a),e.fillStyle=o.backgroundColor,e.fillRect(g,h+1,f-2,a-2))}e.fillStyle=this.labelTextColors[i]}drawBody(e,t,i){const{body:l}=this,{bodySpacing:s,bodyAlign:o,displayColors:r,boxHeight:a,boxWidth:f,boxPadding:u}=i,c=ei(i.bodyFont);let d=c.lineHeight,m=0;const h=_r(i.rtl,this.x,this.width),_=function(I){t.fillText(I,h.x(e.x+m),e.y+d/2),e.y+=d+s},g=h.textAlign(o);let y,S,T,$,C,O,D;for(t.textAlign=o,t.textBaseline="middle",t.font=c.string,e.x=Xs(this,g,i),t.fillStyle=i.bodyColor,_t(this.beforeBody,_),m=r&&g!=="right"?o==="center"?f/2+u:f+2+u:0,$=0,O=l.length;$0&&t.stroke()}_updateAnimationTarget(e){const t=this.chart,i=this.$animations,l=i&&i.x,s=i&&i.y;if(l||s){const o=Jl[e.position].call(this,this._active,this._eventPosition);if(!o)return;const r=this._size=uc(this,e),a=Object.assign({},o,this._size),f=cc(t,e,a),u=dc(e,a,f,t);(l._to!==u.x||s._to!==u.y)&&(this.xAlign=f.xAlign,this.yAlign=f.yAlign,this.width=r.width,this.height=r.height,this.caretX=o.x,this.caretY=o.y,this._resolveAnimations().update(this,u))}}_willRender(){return!!this.opacity}draw(e){const t=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(t);const l={width:this.width,height:this.height},s={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=Ei(t.padding),r=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;t.enabled&&r&&(e.save(),e.globalAlpha=i,this.drawBackground(s,e,l,t),Ww(e,t.textDirection),s.y+=o.top,this.drawTitle(s,e,t),this.drawBody(s,e,t),this.drawFooter(s,e,t),Yw(e,t.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,t){const i=this._active,l=e.map(({datasetIndex:r,index:a})=>{const f=this.chart.getDatasetMeta(r);if(!f)throw new Error("Cannot find a dataset at index "+r);return{datasetIndex:r,element:f.data[a],index:a}}),s=!Mo(i,l),o=this._positionChanged(l,t);(s||o)&&(this._active=l,this._eventPosition=t,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,t,i=!0){if(t&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const l=this.options,s=this._active||[],o=this._getActiveElements(e,s,t,i),r=this._positionChanged(o,e),a=t||!Mo(o,s)||r;return a&&(this._active=o,(l.enabled||l.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,t))),a}_getActiveElements(e,t,i,l){const s=this.options;if(e.type==="mouseout")return[];if(!l)return t.filter(r=>this.chart.data.datasets[r.datasetIndex]&&this.chart.getDatasetMeta(r.datasetIndex).controller.getParsed(r.index)!==void 0);const o=this.chart.getElementsAtEventForMode(e,s.mode,s,i);return s.reverse&&o.reverse(),o}_positionChanged(e,t){const{caretX:i,caretY:l,options:s}=this,o=Jl[s.position].call(this,e,t);return o!==!1&&(i!==o.x||l!==o.y)}}Ze(ia,"positioners",Jl);var lS={id:"tooltip",_element:ia,positioners:Jl,afterInit(n,e,t){t&&(n.tooltip=new ia({chart:n,options:t}))},beforeUpdate(n,e,t){n.tooltip&&n.tooltip.initialize(t)},reset(n,e,t){n.tooltip&&n.tooltip.initialize(t)},afterDraw(n){const e=n.tooltip;if(e&&e._willRender()){const t={tooltip:e};if(n.notifyPlugins("beforeTooltipDraw",{...t,cancelable:!0})===!1)return;e.draw(n.ctx),n.notifyPlugins("afterTooltipDraw",t)}},afterEvent(n,e){if(n.tooltip){const t=e.replay;n.tooltip.handleEvent(e.event,t,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(n,e)=>e.bodyFont.size,boxWidth:(n,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Fb},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:n=>n!=="filter"&&n!=="itemSort"&&n!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};function sS(n,e){const t=[],{bounds:l,step:s,min:o,max:r,precision:a,count:f,maxTicks:u,maxDigits:c,includeBounds:d}=n,m=s||1,h=u-1,{min:_,max:g}=e,y=!jt(o),S=!jt(r),T=!jt(f),$=(g-_)/(c+1);let C=pu((g-_)/h/m)*m,O,D,I,L;if(C<1e-14&&!y&&!S)return[{value:_},{value:g}];L=Math.ceil(g/C)-Math.floor(_/C),L>h&&(C=pu(L*C/h/m)*m),jt(a)||(O=Math.pow(10,a),C=Math.ceil(C*O)/O),l==="ticks"?(D=Math.floor(_/C)*C,I=Math.ceil(g/C)*C):(D=_,I=g),y&&S&&s&&R2((r-o)/s,C/1e3)?(L=Math.round(Math.min((r-o)/C,u)),C=(r-o)/L,D=o,I=r):T?(D=y?o:D,I=S?r:I,L=f-1,C=(I-D)/L):(L=(I-D)/C,Ql(L,Math.round(L),C/1e3)?L=Math.round(L):L=Math.ceil(L));const R=Math.max(mu(C),mu(D));O=Math.pow(10,jt(a)?R:a),D=Math.round(D*O)/O,I=Math.round(I*O)/O;let F=0;for(y&&(d&&D!==o?(t.push({value:o}),Dr)break;t.push({value:N})}return S&&d&&I!==r?t.length&&Ql(t[t.length-1].value,r,hc(r,$,n))?t[t.length-1].value=r:t.push({value:r}):(!S||I===r)&&t.push({value:I}),t}function hc(n,e,{horizontal:t,minRotation:i}){const l=Wi(i),s=(t?Math.sin(l):Math.cos(l))||.001,o=.75*e*(""+n).length;return Math.min(e/s,o)}class oS extends Os{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,t){return jt(e)||(typeof e=="number"||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){const{beginAtZero:e}=this.options,{minDefined:t,maxDefined:i}=this.getUserBounds();let{min:l,max:s}=this;const o=a=>l=t?l:a,r=a=>s=i?s:a;if(e){const a=Cl(l),f=Cl(s);a<0&&f<0?r(0):a>0&&f>0&&o(0)}if(l===s){let a=s===0?1:Math.abs(s*.05);r(s+a),e||o(l-a)}this.min=l,this.max=s}getTickLimit(){const e=this.options.ticks;let{maxTicksLimit:t,stepSize:i}=e,l;return i?(l=Math.ceil(this.max/i)-Math.floor(this.min/i)+1,l>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${i} would result generating up to ${l} ticks. Limiting to 1000.`),l=1e3)):(l=this.computeTickLimit(),t=t||11),t&&(l=Math.min(t,l)),l}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const e=this.options,t=e.ticks;let i=this.getTickLimit();i=Math.max(2,i);const l={maxTicks:i,bounds:e.bounds,min:e.min,max:e.max,precision:t.precision,step:t.stepSize,count:t.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:t.minRotation||0,includeBounds:t.includeBounds!==!1},s=this._range||this,o=sS(l,s);return e.bounds==="ticks"&&q2(o,this,"value"),e.reverse?(o.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),o}configure(){const e=this.ticks;let t=this.min,i=this.max;if(super.configure(),this.options.offset&&e.length){const l=(i-t)/Math.max(e.length-1,1)/2;t-=l,i+=l}this._startValue=t,this._endValue=i,this._valueRange=i-t}getLabelForValue(e){return ab(e,this.chart.options.locale,this.options.ticks.format)}}class la extends oS{determineDataLimits(){const{min:e,max:t}=this.getMinMax(!0);this.min=on(e)?e:0,this.max=on(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){const e=this.isHorizontal(),t=e?this.width:this.height,i=Wi(this.options.ticks.minRotation),l=(e?Math.sin(i):Math.cos(i))||.001,s=this._resolveTickFontOptions(0);return Math.ceil(t/Math.min(40,s.lineHeight/l))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}}Ze(la,"id","linear"),Ze(la,"defaults",{ticks:{callback:ub.formatters.numeric}});const Qo={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},_n=Object.keys(Qo);function _c(n,e){return n-e}function gc(n,e){if(jt(e))return null;const t=n._adapter,{parser:i,round:l,isoWeekday:s}=n._parseOpts;let o=e;return typeof i=="function"&&(o=i(o)),on(o)||(o=typeof i=="string"?t.parse(o,i):t.parse(o)),o===null?null:(l&&(o=l==="week"&&(us(s)||s===!0)?t.startOf(o,"isoWeek",s):t.startOf(o,l)),+o)}function bc(n,e,t,i){const l=_n.length;for(let s=_n.indexOf(n);s=_n.indexOf(t);s--){const o=_n[s];if(Qo[o].common&&n._adapter.diff(l,i,o)>=e-1)return o}return _n[t?_n.indexOf(t):0]}function aS(n){for(let e=_n.indexOf(n)+1,t=_n.length;e=e?t[i]:t[l];n[s]=!0}}function fS(n,e,t,i){const l=n._adapter,s=+l.startOf(e[0].value,i),o=e[e.length-1].value;let r,a;for(r=s;r<=o;r=+l.add(r,1,i))a=t[r],a>=0&&(e[a].major=!0);return e}function yc(n,e,t){const i=[],l={},s=e.length;let o,r;for(o=0;o+e.value))}initOffsets(e=[]){let t=0,i=0,l,s;this.options.offset&&e.length&&(l=this.getDecimalForValue(e[0]),e.length===1?t=1-l:t=(this.getDecimalForValue(e[1])-l)/2,s=this.getDecimalForValue(e[e.length-1]),e.length===1?i=s:i=(s-this.getDecimalForValue(e[e.length-2]))/2);const o=e.length<3?.5:.25;t=Bn(t,0,o),i=Bn(i,0,o),this._offsets={start:t,end:i,factor:1/(t+1+i)}}_generate(){const e=this._adapter,t=this.min,i=this.max,l=this.options,s=l.time,o=s.unit||bc(s.minUnit,t,i,this._getLabelCapacity(t)),r=vt(l.ticks.stepSize,1),a=o==="week"?s.isoWeekday:!1,f=us(a)||a===!0,u={};let c=t,d,m;if(f&&(c=+e.startOf(c,"isoWeek",a)),c=+e.startOf(c,f?"day":o),e.diff(i,t,o)>1e5*r)throw new Error(t+" and "+i+" are too far apart with stepSize of "+r+" "+o);const h=l.ticks.source==="data"&&this.getDataTimestamps();for(d=c,m=0;d+_)}getLabelForValue(e){const t=this._adapter,i=this.options.time;return i.tooltipFormat?t.format(e,i.tooltipFormat):t.format(e,i.displayFormats.datetime)}format(e,t){const l=this.options.time.displayFormats,s=this._unit,o=t||l[s];return this._adapter.format(e,o)}_tickFormatFunction(e,t,i,l){const s=this.options,o=s.ticks.callback;if(o)return Rt(o,[e,t,i],this);const r=s.time.displayFormats,a=this._unit,f=this._majorUnit,u=a&&r[a],c=f&&r[f],d=i[t],m=f&&c&&d&&d.major;return this._adapter.format(e,l||(m?c:u))}generateTickLabels(e){let t,i,l;for(t=0,i=e.length;t0?r:1}getDataTimestamps(){let e=this._cache.data||[],t,i;if(e.length)return e;const l=this.getMatchingVisibleMetas();if(this._normalized&&l.length)return this._cache.data=l[0].controller.getAllParsedValues(this);for(t=0,i=l.length;t=n[i].pos&&e<=n[l].pos&&({lo:i,hi:l}=Yi(n,"pos",e)),{pos:s,time:r}=n[i],{pos:o,time:a}=n[l]):(e>=n[i].time&&e<=n[l].time&&({lo:i,hi:l}=Yi(n,"time",e)),{time:s,pos:r}=n[i],{time:o,pos:a}=n[l]);const f=o-s;return f?r+(a-r)*(e-s)/f:r}class vc extends ps{constructor(e){super(e),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const e=this._getTimestampsForTable(),t=this._table=this.buildLookupTable(e);this._minPos=Qs(t,this.min),this._tableRange=Qs(t,this.max)-this._minPos,super.initOffsets(e)}buildLookupTable(e){const{min:t,max:i}=this,l=[],s=[];let o,r,a,f,u;for(o=0,r=e.length;o=t&&f<=i&&l.push(f);if(l.length<2)return[{time:t,pos:0},{time:i,pos:1}];for(o=0,r=l.length;ol-s)}_getTimestampsForTable(){let e=this._cache.all||[];if(e.length)return e;const t=this.getDataTimestamps(),i=this.getLabelTimestamps();return t.length&&i.length?e=this.normalize(t.concat(i)):e=t.length?t:i,e=this._cache.all=e,e}getDecimalForValue(e){return(Qs(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){const t=this._offsets,i=this.getDecimalForPixel(e)/t.factor-t.end;return Qs(this._table,i*this._tableRange+this._minPos,!0)}}Ze(vc,"id","timeseries"),Ze(vc,"defaults",ps.defaults);/*! - * chartjs-adapter-luxon v1.3.1 - * https://www.chartjs.org - * (c) 2023 chartjs-adapter-luxon Contributors - * Released under the MIT license - */const uS={datetime:je.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:je.TIME_WITH_SECONDS,minute:je.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};wb._date.override({_id:"luxon",_create:function(n){return je.fromMillis(n,this.options)},init(n){this.options.locale||(this.options.locale=n.locale)},formats:function(){return uS},parse:function(n,e){const t=this.options,i=typeof n;return n===null||i==="undefined"?null:(i==="number"?n=this._create(n):i==="string"?typeof e=="string"?n=je.fromFormat(n,e,t):n=je.fromISO(n,t):n instanceof Date?n=je.fromJSDate(n,t):i==="object"&&!(n instanceof je)&&(n=je.fromObject(n,t)),n.isValid?n.valueOf():null)},format:function(n,e){const t=this._create(n);return typeof e=="string"?t.toFormat(e):t.toLocaleString(e)},add:function(n,e,t){const i={};return i[t]=e,this._create(n).plus(i).valueOf()},diff:function(n,e,t){return this._create(n).diff(this._create(e)).as(t).valueOf()},startOf:function(n,e,t){if(e==="isoWeek"){t=Math.trunc(Math.min(Math.max(0,t),6));const i=this._create(n);return i.minus({days:(i.weekday-t+7)%7}).startOf("day").valueOf()}return e?this._create(n).startOf(e).valueOf():n},endOf:function(n,e){return this._create(n).endOf(e).valueOf()}});function wc(n){let e,t,i;return{c(){e=b("div"),p(e,"class","chart-loader loader svelte-12c378i")},m(l,s){w(l,e,s),i=!0},i(l){i||(l&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150},!0)),t.run(1))}),i=!0)},o(l){l&&(t||(t=Fe(e,Wt,{duration:150},!1)),t.run(0)),i=!1},d(l){l&&v(e),l&&t&&t.end()}}}function cS(n){let e,t,i,l,s,o=n[1]==1?"log":"logs",r,a,f,u,c=n[2]&&wc();return{c(){e=b("div"),t=b("div"),i=K("Found "),l=K(n[1]),s=M(),r=K(o),a=M(),c&&c.c(),f=M(),u=b("canvas"),p(t,"class","total-logs entrance-right svelte-12c378i"),x(t,"hidden",n[2]),p(u,"class","chart-canvas svelte-12c378i"),p(e,"class","chart-wrapper svelte-12c378i"),x(e,"loading",n[2])},m(d,m){w(d,e,m),k(e,t),k(t,i),k(t,l),k(t,s),k(t,r),k(e,a),c&&c.m(e,null),k(e,f),k(e,u),n[8](u)},p(d,[m]){m&2&&oe(l,d[1]),m&2&&o!==(o=d[1]==1?"log":"logs")&&oe(r,o),m&4&&x(t,"hidden",d[2]),d[2]?c?m&4&&E(c,1):(c=wc(),c.c(),E(c,1),c.m(e,f)):c&&(le(),A(c,1,1,()=>{c=null}),se()),m&4&&x(e,"loading",d[2])},i(d){E(c)},o(d){A(c)},d(d){d&&v(e),c&&c.d(),n[8](null)}}}function dS(n,e,t){let{filter:i=""}=e,{presets:l=""}=e,s,o,r=[],a=0,f=!1;async function u(){t(2,f=!0);const m=[l,j.normalizeLogsFilter(i)].filter(Boolean).join("&&");return ae.logs.getStats({filter:m}).then(h=>{c(),h=j.toArray(h);for(let _ of h)r.push({x:new Date(_.date),y:_.total}),t(1,a+=_.total)}).catch(h=>{h!=null&&h.isAbort||(c(),console.warn(h),ae.error(h,!m||(h==null?void 0:h.status)!=400))}).finally(()=>{t(2,f=!1)})}function c(){t(7,r=[]),t(1,a=0)}Ht(()=>(ci.register(Ti,mo,uo,la,ps,G4,lS),t(6,o=new ci(s,{type:"line",data:{datasets:[{label:"Total requests",data:r,borderColor:"#e34562",pointBackgroundColor:"#e34562",backgroundColor:"rgb(239,69,101,0.05)",borderWidth:2,pointRadius:1,pointBorderWidth:0,fill:!0}]},options:{resizeDelay:250,maintainAspectRatio:!1,animation:!1,interaction:{intersect:!1,mode:"index"},scales:{y:{beginAtZero:!0,grid:{color:"#edf0f3"},border:{color:"#e4e9ec"},ticks:{precision:0,maxTicksLimit:4,autoSkip:!0,color:"#666f75"}},x:{type:"time",time:{unit:"hour",tooltipFormat:"DD h a"},grid:{color:m=>{var h;return(h=m.tick)!=null&&h.major?"#edf0f3":""}},color:"#e4e9ec",ticks:{maxTicksLimit:15,autoSkip:!0,maxRotation:0,major:{enabled:!0},color:m=>{var h;return(h=m.tick)!=null&&h.major?"#16161a":"#666f75"}}}},plugins:{legend:{display:!1}}}})),()=>o==null?void 0:o.destroy()));function d(m){ee[m?"unshift":"push"](()=>{s=m,t(0,s)})}return n.$$set=m=>{"filter"in m&&t(3,i=m.filter),"presets"in m&&t(4,l=m.presets)},n.$$.update=()=>{n.$$.dirty&24&&(typeof i<"u"||typeof l<"u")&&u(),n.$$.dirty&192&&typeof r<"u"&&o&&(t(6,o.data.datasets[0].data=r,o),o.update())},[s,a,f,i,l,u,o,r,d]}class pS extends ge{constructor(e){super(),_e(this,e,dS,cS,me,{filter:3,presets:4,load:5})}get load(){return this.$$.ctx[5]}}function mS(n){let e,t,i;return{c(){e=b("div"),t=b("code"),p(t,"class","svelte-s3jkbp"),p(e,"class",i="code-wrapper prism-light "+n[0]+" svelte-s3jkbp")},m(l,s){w(l,e,s),k(e,t),t.innerHTML=n[1]},p(l,[s]){s&2&&(t.innerHTML=l[1]),s&1&&i!==(i="code-wrapper prism-light "+l[0]+" svelte-s3jkbp")&&p(e,"class",i)},i:Q,o:Q,d(l){l&&v(e)}}}function hS(n,e,t){let{content:i=""}=e,{language:l="javascript"}=e,{class:s=""}=e,o="";function r(a){return a=typeof a=="string"?a:"",a=Prism.plugins.NormalizeWhitespace.normalize(a,{"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.highlight(a,Prism.languages[l]||Prism.languages.javascript,l)}return n.$$set=a=>{"content"in a&&t(2,i=a.content),"language"in a&&t(3,l=a.language),"class"in a&&t(0,s=a.class)},n.$$.update=()=>{n.$$.dirty&4&&typeof Prism<"u"&&i&&t(1,o=r(i))},[s,o,i,l]}class Rb extends ge{constructor(e){super(),_e(this,e,hS,mS,me,{content:2,language:3,class:0})}}const _S=n=>({}),Sc=n=>({}),gS=n=>({}),$c=n=>({});function Tc(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T=n[4]&&!n[2]&&Cc(n);const $=n[19].header,C=wt($,n,n[18],$c);let O=n[4]&&n[2]&&Oc(n);const D=n[19].default,I=wt(D,n,n[18],null),L=n[19].footer,R=wt(L,n,n[18],Sc);return{c(){e=b("div"),t=b("div"),l=M(),s=b("div"),o=b("div"),T&&T.c(),r=M(),C&&C.c(),a=M(),O&&O.c(),f=M(),u=b("div"),I&&I.c(),c=M(),d=b("div"),R&&R.c(),p(t,"class","overlay"),p(o,"class","overlay-panel-section panel-header"),p(u,"class","overlay-panel-section panel-content"),p(d,"class","overlay-panel-section panel-footer"),p(s,"class",m="overlay-panel "+n[1]+" "+n[8]),x(s,"popup",n[2]),p(e,"class","overlay-panel-container"),x(e,"padded",n[2]),x(e,"active",n[0])},m(F,N){w(F,e,N),k(e,t),k(e,l),k(e,s),k(s,o),T&&T.m(o,null),k(o,r),C&&C.m(o,null),k(o,a),O&&O.m(o,null),k(s,f),k(s,u),I&&I.m(u,null),n[21](u),k(s,c),k(s,d),R&&R.m(d,null),g=!0,y||(S=[J(t,"click",Be(n[20])),J(u,"scroll",n[22])],y=!0)},p(F,N){n=F,n[4]&&!n[2]?T?(T.p(n,N),N[0]&20&&E(T,1)):(T=Cc(n),T.c(),E(T,1),T.m(o,r)):T&&(le(),A(T,1,1,()=>{T=null}),se()),C&&C.p&&(!g||N[0]&262144)&&$t(C,$,n,n[18],g?St($,n[18],N,gS):Tt(n[18]),$c),n[4]&&n[2]?O?O.p(n,N):(O=Oc(n),O.c(),O.m(o,null)):O&&(O.d(1),O=null),I&&I.p&&(!g||N[0]&262144)&&$t(I,D,n,n[18],g?St(D,n[18],N,null):Tt(n[18]),null),R&&R.p&&(!g||N[0]&262144)&&$t(R,L,n,n[18],g?St(L,n[18],N,_S):Tt(n[18]),Sc),(!g||N[0]&258&&m!==(m="overlay-panel "+n[1]+" "+n[8]))&&p(s,"class",m),(!g||N[0]&262)&&x(s,"popup",n[2]),(!g||N[0]&4)&&x(e,"padded",n[2]),(!g||N[0]&1)&&x(e,"active",n[0])},i(F){g||(F&&Ke(()=>{g&&(i||(i=Fe(t,rs,{duration:wi,opacity:0},!0)),i.run(1))}),E(T),E(C,F),E(I,F),E(R,F),F&&Ke(()=>{g&&(_&&_.end(1),h=Pg(s,Fn,n[2]?{duration:wi,y:-10}:{duration:wi,x:50}),h.start())}),g=!0)},o(F){F&&(i||(i=Fe(t,rs,{duration:wi,opacity:0},!1)),i.run(0)),A(T),A(C,F),A(I,F),A(R,F),h&&h.invalidate(),F&&(_=ca(s,Fn,n[2]?{duration:wi,y:10}:{duration:wi,x:50})),g=!1},d(F){F&&v(e),F&&i&&i.end(),T&&T.d(),C&&C.d(F),O&&O.d(),I&&I.d(F),n[21](null),R&&R.d(F),F&&_&&_.end(),y=!1,$e(S)}}}function Cc(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Close"),p(e,"class","overlay-close")},m(o,r){w(o,e,r),i=!0,l||(s=J(e,"click",Be(n[5])),l=!0)},p(o,r){n=o},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,rs,{duration:wi},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,rs,{duration:wi},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function Oc(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Close"),p(e,"class","btn btn-sm btn-circle btn-transparent btn-close m-l-auto")},m(l,s){w(l,e,s),t||(i=J(e,"click",Be(n[5])),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function bS(n){let e,t,i,l,s=n[0]&&Tc(n);return{c(){e=b("div"),s&&s.c(),p(e,"class","overlay-panel-wrapper"),p(e,"tabindex","-1")},m(o,r){w(o,e,r),s&&s.m(e,null),n[23](e),t=!0,i||(l=[J(window,"resize",n[10]),J(window,"keydown",n[9])],i=!0)},p(o,r){o[0]?s?(s.p(o,r),r[0]&1&&E(s,1)):(s=Tc(o),s.c(),E(s,1),s.m(e,null)):s&&(le(),A(s,1,1,()=>{s=null}),se())},i(o){t||(E(s),t=!0)},o(o){A(s),t=!1},d(o){o&&v(e),s&&s.d(),n[23](null),i=!1,$e(l)}}}let Hi,Sr=[];function qb(){return Hi=Hi||document.querySelector(".overlays"),Hi||(Hi=document.createElement("div"),Hi.classList.add("overlays"),document.body.appendChild(Hi)),Hi}let wi=150;function Mc(){return 1e3+qb().querySelectorAll(".overlay-panel-container.active").length}function kS(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,{active:o=!1}=e,{popup:r=!1}=e,{overlayClose:a=!0}=e,{btnClose:f=!0}=e,{escClose:u=!0}=e,{beforeOpen:c=void 0}=e,{beforeHide:d=void 0}=e;const m=lt(),h="op_"+j.randomString(10);let _,g,y,S,T="",$=o;function C(){typeof c=="function"&&c()===!1||t(0,o=!0)}function O(){typeof d=="function"&&d()===!1||t(0,o=!1)}function D(){return o}async function I(Y){t(17,$=Y),Y?(y=document.activeElement,m("show"),_==null||_.focus()):(clearTimeout(S),m("hide"),y==null||y.focus()),await Qt(),L()}function L(){_&&(o?t(6,_.style.zIndex=Mc(),_):t(6,_.style="",_))}function R(){j.pushUnique(Sr,h),document.body.classList.add("overlay-active")}function F(){j.removeByValue(Sr,h),Sr.length||document.body.classList.remove("overlay-active")}function N(Y){o&&u&&Y.code=="Escape"&&!j.isInput(Y.target)&&_&&_.style.zIndex==Mc()&&(Y.preventDefault(),O())}function P(Y){o&&q(g)}function q(Y,ie){ie&&t(8,T=""),!(!Y||S)&&(S=setTimeout(()=>{if(clearTimeout(S),S=null,!Y)return;if(Y.scrollHeight-Y.offsetHeight>0)t(8,T="scrollable");else{t(8,T="");return}Y.scrollTop==0?t(8,T+=" scroll-top-reached"):Y.scrollTop+Y.offsetHeight==Y.scrollHeight&&t(8,T+=" scroll-bottom-reached")},100))}Ht(()=>(qb().appendChild(_),()=>{var Y;clearTimeout(S),F(),(Y=_==null?void 0:_.classList)==null||Y.add("hidden"),setTimeout(()=>{_==null||_.remove()},0)}));const H=()=>a?O():!0;function W(Y){ee[Y?"unshift":"push"](()=>{g=Y,t(7,g)})}const G=Y=>q(Y.target);function U(Y){ee[Y?"unshift":"push"](()=>{_=Y,t(6,_)})}return n.$$set=Y=>{"class"in Y&&t(1,s=Y.class),"active"in Y&&t(0,o=Y.active),"popup"in Y&&t(2,r=Y.popup),"overlayClose"in Y&&t(3,a=Y.overlayClose),"btnClose"in Y&&t(4,f=Y.btnClose),"escClose"in Y&&t(12,u=Y.escClose),"beforeOpen"in Y&&t(13,c=Y.beforeOpen),"beforeHide"in Y&&t(14,d=Y.beforeHide),"$$scope"in Y&&t(18,l=Y.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&131073&&$!=o&&I(o),n.$$.dirty[0]&128&&q(g,!0),n.$$.dirty[0]&64&&_&&L(),n.$$.dirty[0]&1&&(o?R():F())},[o,s,r,a,f,O,_,g,T,N,P,q,u,c,d,C,D,$,l,i,H,W,G,U]}class Zt extends ge{constructor(e){super(),_e(this,e,kS,bS,me,{class:1,active:0,popup:2,overlayClose:3,btnClose:4,escClose:12,beforeOpen:13,beforeHide:14,show:15,hide:5,isActive:16},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[5]}get isActive(){return this.$$.ctx[16]}}function yS(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"tabindex","-1"),p(e,"role","button"),p(e,"class",t=n[3]?n[2]:n[1]),p(e,"aria-label","Copy to clipboard")},m(o,r){w(o,e,r),l||(s=[Se(i=Pe.call(null,e,n[3]?void 0:n[0])),J(e,"click",Tn(n[4]))],l=!0)},p(o,[r]){r&14&&t!==(t=o[3]?o[2]:o[1])&&p(e,"class",t),i&&Ct(i.update)&&r&9&&i.update.call(null,o[3]?void 0:o[0])},i:Q,o:Q,d(o){o&&v(e),l=!1,$e(s)}}}function vS(n,e,t){let{value:i=""}=e,{tooltip:l="Copy"}=e,{idleClasses:s="ri-file-copy-line txt-sm link-hint"}=e,{successClasses:o="ri-check-line txt-sm txt-success"}=e,{successDuration:r=500}=e,a;function f(){i&&(j.copyToClipboard(i),clearTimeout(a),t(3,a=setTimeout(()=>{clearTimeout(a),t(3,a=null)},r)))}return Ht(()=>()=>{a&&clearTimeout(a)}),n.$$set=u=>{"value"in u&&t(5,i=u.value),"tooltip"in u&&t(0,l=u.tooltip),"idleClasses"in u&&t(1,s=u.idleClasses),"successClasses"in u&&t(2,o=u.successClasses),"successDuration"in u&&t(6,r=u.successDuration)},[l,s,o,a,f,i,r]}class sl extends ge{constructor(e){super(),_e(this,e,vS,yS,me,{value:5,tooltip:0,idleClasses:1,successClasses:2,successDuration:6})}}function Dc(n,e,t){const i=n.slice();i[16]=e[t];const l=i[1].data[i[16]];i[17]=l;const s=i[17]!==null&&typeof i[17]=="object";return i[18]=s,i}function wS(n){let e,t,i,l,s,o,r,a,f,u,c=n[1].id+"",d,m,h,_,g,y,S,T,$,C,O,D,I,L,R,F;a=new sl({props:{value:n[1].id}}),S=new X1({props:{level:n[1].level}}),I=new Q1({props:{date:n[1].created}});let N=!n[4]&&Ec(n),P=ue(n[5](n[1].data)),q=[];for(let W=0;WA(q[W],1,1,()=>{q[W]=null});return{c(){e=b("table"),t=b("tbody"),i=b("tr"),l=b("td"),l.textContent="id",s=M(),o=b("td"),r=b("div"),B(a.$$.fragment),f=M(),u=b("div"),d=K(c),m=M(),h=b("tr"),_=b("td"),_.textContent="level",g=M(),y=b("td"),B(S.$$.fragment),T=M(),$=b("tr"),C=b("td"),C.textContent="created",O=M(),D=b("td"),B(I.$$.fragment),L=M(),N&&N.c(),R=M();for(let W=0;W',p(e,"class","block txt-center")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Ec(n){let e,t,i,l;function s(a,f){return a[1].message?TS:$S}let o=s(n),r=o(n);return{c(){e=b("tr"),t=b("td"),t.textContent="message",i=M(),l=b("td"),r.c(),p(t,"class","min-width txt-hint txt-bold")},m(a,f){w(a,e,f),k(e,t),k(e,i),k(e,l),r.m(l,null)},p(a,f){o===(o=s(a))&&r?r.p(a,f):(r.d(1),r=o(a),r&&(r.c(),r.m(l,null)))},d(a){a&&v(e),r.d()}}}function $S(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function TS(n){let e,t=n[1].message+"",i;return{c(){e=b("span"),i=K(t),p(e,"class","txt")},m(l,s){w(l,e,s),k(e,i)},p(l,s){s&2&&t!==(t=l[1].message+"")&&oe(i,t)},d(l){l&&v(e)}}}function CS(n){let e,t=n[17]+"",i,l=n[4]&&n[16]=="execTime"?"ms":"",s;return{c(){e=b("span"),i=K(t),s=K(l),p(e,"class","txt")},m(o,r){w(o,e,r),k(e,i),k(e,s)},p(o,r){r&2&&t!==(t=o[17]+"")&&oe(i,t),r&18&&l!==(l=o[4]&&o[16]=="execTime"?"ms":"")&&oe(s,l)},i:Q,o:Q,d(o){o&&v(e)}}}function OS(n){let e,t;return e=new Rb({props:{content:n[17],language:"html"}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.content=i[17]),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function MS(n){let e,t=n[17]+"",i;return{c(){e=b("span"),i=K(t),p(e,"class","label label-danger log-error-label svelte-144j2mz")},m(l,s){w(l,e,s),k(e,i)},p(l,s){s&2&&t!==(t=l[17]+"")&&oe(i,t)},i:Q,o:Q,d(l){l&&v(e)}}}function DS(n){let e,t;return e=new Rb({props:{content:JSON.stringify(n[17],null,2)}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.content=JSON.stringify(i[17],null,2)),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function ES(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Ic(n){let e,t,i,l=n[16]+"",s,o,r,a,f,u,c,d;const m=[ES,DS,MS,OS,CS],h=[];function _(g,y){return y&2&&(a=null),a==null&&(a=!!j.isEmpty(g[17])),a?0:g[18]?1:g[16]=="error"?2:g[16]=="details"?3:4}return f=_(n,-1),u=h[f]=m[f](n),{c(){e=b("tr"),t=b("td"),i=K("data."),s=K(l),o=M(),r=b("td"),u.c(),c=M(),p(t,"class","min-width txt-hint txt-bold"),x(t,"v-align-top",n[18])},m(g,y){w(g,e,y),k(e,t),k(t,i),k(t,s),k(e,o),k(e,r),h[f].m(r,null),k(e,c),d=!0},p(g,y){(!d||y&2)&&l!==(l=g[16]+"")&&oe(s,l),(!d||y&34)&&x(t,"v-align-top",g[18]);let S=f;f=_(g,y),f===S?h[f].p(g,y):(le(),A(h[S],1,1,()=>{h[S]=null}),se(),u=h[f],u?u.p(g,y):(u=h[f]=m[f](g),u.c()),E(u,1),u.m(r,null))},i(g){d||(E(u),d=!0)},o(g){A(u),d=!1},d(g){g&&v(e),h[f].d()}}}function IS(n){let e,t,i,l;const s=[SS,wS],o=[];function r(a,f){var u;return a[3]?0:(u=a[1])!=null&&u.id?1:-1}return~(e=r(n))&&(t=o[e]=s[e](n)),{c(){t&&t.c(),i=ye()},m(a,f){~e&&o[e].m(a,f),w(a,i,f),l=!0},p(a,f){let u=e;e=r(a),e===u?~e&&o[e].p(a,f):(t&&(le(),A(o[u],1,1,()=>{o[u]=null}),se()),~e?(t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i)):t=null)},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),~e&&o[e].d(a)}}}function AS(n){let e;return{c(){e=b("h4"),e.textContent="Request log"},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function LS(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),e.innerHTML='Close',t=M(),i=b("button"),l=b("i"),s=M(),o=b("span"),o.textContent="Download as JSON",p(e,"type","button"),p(e,"class","btn btn-transparent"),p(l,"class","ri-download-line"),p(o,"class","txt"),p(i,"type","button"),p(i,"class","btn btn-primary"),i.disabled=n[3]},m(f,u){w(f,e,u),w(f,t,u),w(f,i,u),k(i,l),k(i,s),k(i,o),r||(a=[J(e,"click",n[9]),J(i,"click",n[10])],r=!0)},p(f,u){u&8&&(i.disabled=f[3])},d(f){f&&(v(e),v(t),v(i)),r=!1,$e(a)}}}function NS(n){let e,t,i={class:"overlay-panel-lg log-panel",$$slots:{footer:[LS],header:[AS],default:[IS]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[11](e),e.$on("hide",n[7]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&2097178&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[11](null),V(e,l)}}}const Ac="log_view";function PS(n,e,t){let i;const l=lt();let s,o={},r=!1;function a(T){return u(T).then($=>{t(1,o=$),h()}),s==null?void 0:s.show()}function f(){return ae.cancelRequest(Ac),s==null?void 0:s.hide()}async function u(T){if(T&&typeof T!="string")return t(3,r=!1),T;t(3,r=!0);let $={};try{$=await ae.logs.getOne(T,{requestKey:Ac})}catch(C){C.isAbort||(f(),console.warn("resolveModel:",C),ii(`Unable to load log with id "${T}"`))}return t(3,r=!1),$}const c=["execTime","type","auth","status","method","url","referer","remoteIp","userIp","userAgent","error","details"];function d(T){if(!T)return[];let $=[];for(let O of c)typeof T[O]<"u"&&$.push(O);const C=Object.keys(T);for(let O of C)$.includes(O)||$.push(O);return $}function m(){j.downloadJson(o,"log_"+o.created.replaceAll(/[-:\. ]/gi,"")+".json")}function h(){l("show",o)}function _(){l("hide",o),t(1,o={})}const g=()=>f(),y=()=>m();function S(T){ee[T?"unshift":"push"](()=>{s=T,t(2,s)})}return n.$$.update=()=>{var T;n.$$.dirty&2&&t(4,i=((T=o.data)==null?void 0:T.type)=="request")},[f,o,s,r,i,d,m,_,a,g,y,S]}class FS extends ge{constructor(e){super(),_e(this,e,PS,NS,me,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function RS(n,e,t){const i=n.slice();return i[1]=e[t],i}function qS(n){let e;return{c(){e=b("code"),e.textContent=`${n[1].level}:${n[1].label}`,p(e,"class","txt-xs")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function jS(n){let e,t,i,l=ue(U1),s=[];for(let o=0;o{"class"in l&&t(0,i=l.class)},[i]}class jb extends ge{constructor(e){super(),_e(this,e,HS,jS,me,{class:0})}}function zS(n){let e,t,i,l,s,o,r,a,f;return t=new ce({props:{class:"form-field required",name:"logs.maxDays",$$slots:{default:[BS,({uniqueId:u})=>({22:u}),({uniqueId:u})=>u?4194304:0]},$$scope:{ctx:n}}}),l=new ce({props:{class:"form-field",name:"logs.minLevel",$$slots:{default:[US,({uniqueId:u})=>({22:u}),({uniqueId:u})=>u?4194304:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field form-field-toggle",name:"logs.logIp",$$slots:{default:[WS,({uniqueId:u})=>({22:u}),({uniqueId:u})=>u?4194304:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),B(t.$$.fragment),i=M(),B(l.$$.fragment),s=M(),B(o.$$.fragment),p(e,"id",n[6]),p(e,"class","grid"),p(e,"autocomplete","off")},m(u,c){w(u,e,c),z(t,e,null),k(e,i),z(l,e,null),k(e,s),z(o,e,null),r=!0,a||(f=J(e,"submit",Be(n[7])),a=!0)},p(u,c){const d={};c&12582914&&(d.$$scope={dirty:c,ctx:u}),t.$set(d);const m={};c&12582914&&(m.$$scope={dirty:c,ctx:u}),l.$set(m);const h={};c&12582914&&(h.$$scope={dirty:c,ctx:u}),o.$set(h)},i(u){r||(E(t.$$.fragment,u),E(l.$$.fragment,u),E(o.$$.fragment,u),r=!0)},o(u){A(t.$$.fragment,u),A(l.$$.fragment,u),A(o.$$.fragment,u),r=!1},d(u){u&&v(e),V(t),V(l),V(o),a=!1,f()}}}function VS(n){let e;return{c(){e=b("div"),e.innerHTML='
    ',p(e,"class","block txt-center")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function BS(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=K("Max days retention"),l=M(),s=b("input"),r=M(),a=b("div"),a.innerHTML="Set to 0 to disable logs persistence.",p(e,"for",i=n[22]),p(s,"type","number"),p(s,"id",o=n[22]),s.required=!0,p(a,"class","help-block")},m(c,d){w(c,e,d),k(e,t),w(c,l,d),w(c,s,d),re(s,n[1].logs.maxDays),w(c,r,d),w(c,a,d),f||(u=J(s,"input",n[11]),f=!0)},p(c,d){d&4194304&&i!==(i=c[22])&&p(e,"for",i),d&4194304&&o!==(o=c[22])&&p(s,"id",o),d&2&&it(s.value)!==c[1].logs.maxDays&&re(s,c[1].logs.maxDays)},d(c){c&&(v(e),v(l),v(s),v(r),v(a)),f=!1,u()}}}function US(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;return u=new jb({}),{c(){e=b("label"),t=K("Min log level"),l=M(),s=b("input"),o=M(),r=b("div"),a=b("p"),a.textContent="Logs with level below the minimum will be ignored.",f=M(),B(u.$$.fragment),p(e,"for",i=n[22]),p(s,"type","number"),s.required=!0,p(s,"min","-100"),p(s,"max","100"),p(r,"class","help-block")},m(h,_){w(h,e,_),k(e,t),w(h,l,_),w(h,s,_),re(s,n[1].logs.minLevel),w(h,o,_),w(h,r,_),k(r,a),k(r,f),z(u,r,null),c=!0,d||(m=J(s,"input",n[12]),d=!0)},p(h,_){(!c||_&4194304&&i!==(i=h[22]))&&p(e,"for",i),_&2&&it(s.value)!==h[1].logs.minLevel&&re(s,h[1].logs.minLevel)},i(h){c||(E(u.$$.fragment,h),c=!0)},o(h){A(u.$$.fragment,h),c=!1},d(h){h&&(v(e),v(l),v(s),v(o),v(r)),V(u),d=!1,m()}}}function WS(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable IP logging"),p(e,"type","checkbox"),p(e,"id",t=n[22]),p(l,"for",o=n[22])},m(f,u){w(f,e,u),e.checked=n[1].logs.logIp,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[13]),r=!0)},p(f,u){u&4194304&&t!==(t=f[22])&&p(e,"id",t),u&2&&(e.checked=f[1].logs.logIp),u&4194304&&o!==(o=f[22])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function YS(n){let e,t,i,l;const s=[VS,zS],o=[];function r(a,f){return a[4]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,f){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function KS(n){let e;return{c(){e=b("h4"),e.textContent="Logs settings"},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function JS(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=M(),l=b("button"),s=b("span"),s.textContent="Save changes",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[3],x(l,"btn-loading",n[3])},m(f,u){w(f,e,u),k(e,t),w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"click",n[0]),r=!0)},p(f,u){u&8&&(e.disabled=f[3]),u&40&&o!==(o=!f[5]||f[3])&&(l.disabled=o),u&8&&x(l,"btn-loading",f[3])},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function ZS(n){let e,t,i={popup:!0,class:"admin-panel",beforeHide:n[14],$$slots:{footer:[JS],header:[KS],default:[YS]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[15](e),e.$on("hide",n[16]),e.$on("show",n[17]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&8&&(o.beforeHide=l[14]),s&8388666&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[15](null),V(e,l)}}}function GS(n,e,t){let i,l;const s=lt(),o="logs_settings_"+j.randomString(3);let r,a=!1,f=!1,u={},c={};function d(){return h(),_(),r==null?void 0:r.show()}function m(){return r==null?void 0:r.hide()}function h(){Jt(),t(9,u={}),t(1,c=JSON.parse(JSON.stringify(u||{})))}async function _(){t(4,f=!0);try{const L=await ae.settings.getAll()||{};y(L)}catch(L){ae.error(L)}t(4,f=!1)}async function g(){if(l){t(3,a=!0);try{const L=await ae.settings.update(j.filterRedactedProps(c));y(L),t(3,a=!1),m(),Lt("Successfully saved logs settings."),s("save",L)}catch(L){t(3,a=!1),ae.error(L)}}}function y(L={}){t(1,c={logs:(L==null?void 0:L.logs)||{}}),t(9,u=JSON.parse(JSON.stringify(c)))}function S(){c.logs.maxDays=it(this.value),t(1,c)}function T(){c.logs.minLevel=it(this.value),t(1,c)}function $(){c.logs.logIp=this.checked,t(1,c)}const C=()=>!a;function O(L){ee[L?"unshift":"push"](()=>{r=L,t(2,r)})}function D(L){Ce.call(this,n,L)}function I(L){Ce.call(this,n,L)}return n.$$.update=()=>{n.$$.dirty&512&&t(10,i=JSON.stringify(u)),n.$$.dirty&1026&&t(5,l=i!=JSON.stringify(c))},[m,c,r,a,f,l,o,g,d,u,i,S,T,$,C,O,D,I]}class XS extends ge{constructor(e){super(),_e(this,e,GS,ZS,me,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function QS(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Include requests by admins"),p(e,"type","checkbox"),p(e,"id",t=n[22]),p(l,"for",o=n[22])},m(f,u){w(f,e,u),e.checked=n[2],w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[11]),r=!0)},p(f,u){u&4194304&&t!==(t=f[22])&&p(e,"id",t),u&4&&(e.checked=f[2]),u&4194304&&o!==(o=f[22])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function Lc(n){let e,t;return e=new pS({props:{filter:n[1],presets:n[5]}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.filter=i[1]),l&32&&(s.presets=i[5]),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Nc(n){let e,t,i;function l(o){n[13](o)}let s={presets:n[5]};return n[1]!==void 0&&(s.filter=n[1]),e=new l2({props:s}),ee.push(()=>be(e,"filter",l)),e.$on("select",n[14]),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&32&&(a.presets=o[5]),!t&&r&2&&(t=!0,a.filter=o[1],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function xS(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$=n[4],C,O=n[4],D,I,L,R;f=new Zo({}),f.$on("refresh",n[10]),h=new ce({props:{class:"form-field form-field-toggle m-0",$$slots:{default:[QS,({uniqueId:P})=>({22:P}),({uniqueId:P})=>P?4194304:0]},$$scope:{ctx:n}}}),g=new $s({props:{value:n[1],placeholder:"Search term or filter like `level > 0 && data.auth = 'guest'`",extraAutocompleteKeys:["level","message","data."]}}),g.$on("submit",n[12]),S=new jb({props:{class:"block txt-sm txt-hint m-t-xs m-b-base"}});let F=Lc(n),N=Nc(n);return{c(){e=b("div"),t=b("header"),i=b("nav"),l=b("div"),s=K(n[6]),o=M(),r=b("button"),r.innerHTML='',a=M(),B(f.$$.fragment),u=M(),c=b("div"),d=M(),m=b("div"),B(h.$$.fragment),_=M(),B(g.$$.fragment),y=M(),B(S.$$.fragment),T=M(),F.c(),C=M(),N.c(),D=ye(),p(l,"class","breadcrumb-item"),p(i,"class","breadcrumbs"),p(r,"type","button"),p(r,"aria-label","Logs settings"),p(r,"class","btn btn-transparent btn-circle"),p(c,"class","flex-fill"),p(m,"class","inline-flex"),p(t,"class","page-header"),p(e,"class","page-header-wrapper m-b-0")},m(P,q){w(P,e,q),k(e,t),k(t,i),k(i,l),k(l,s),k(t,o),k(t,r),k(t,a),z(f,t,null),k(t,u),k(t,c),k(t,d),k(t,m),z(h,m,null),k(e,_),z(g,e,null),k(e,y),z(S,e,null),k(e,T),F.m(e,null),w(P,C,q),N.m(P,q),w(P,D,q),I=!0,L||(R=[Se(Pe.call(null,r,{text:"Logs settings",position:"right"})),J(r,"click",n[9])],L=!0)},p(P,q){(!I||q&64)&&oe(s,P[6]);const H={};q&12582916&&(H.$$scope={dirty:q,ctx:P}),h.$set(H);const W={};q&2&&(W.value=P[1]),g.$set(W),q&16&&me($,$=P[4])?(le(),A(F,1,1,Q),se(),F=Lc(P),F.c(),E(F,1),F.m(e,null)):F.p(P,q),q&16&&me(O,O=P[4])?(le(),A(N,1,1,Q),se(),N=Nc(P),N.c(),E(N,1),N.m(D.parentNode,D)):N.p(P,q)},i(P){I||(E(f.$$.fragment,P),E(h.$$.fragment,P),E(g.$$.fragment,P),E(S.$$.fragment,P),E(F),E(N),I=!0)},o(P){A(f.$$.fragment,P),A(h.$$.fragment,P),A(g.$$.fragment,P),A(S.$$.fragment,P),A(F),A(N),I=!1},d(P){P&&(v(e),v(C),v(D)),V(f),V(h),V(g),V(S),F.d(P),N.d(P),L=!1,$e(R)}}}function e$(n){let e,t,i,l,s,o;e=new bn({props:{$$slots:{default:[xS]},$$scope:{ctx:n}}});let r={};i=new FS({props:r}),n[15](i),i.$on("show",n[16]),i.$on("hide",n[17]);let a={};return s=new XS({props:a}),n[18](s),s.$on("save",n[7]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment)},m(f,u){z(e,f,u),w(f,t,u),z(i,f,u),w(f,l,u),z(s,f,u),o=!0},p(f,[u]){const c={};u&8388735&&(c.$$scope={dirty:u,ctx:f}),e.$set(c);const d={};i.$set(d);const m={};s.$set(m)},i(f){o||(E(e.$$.fragment,f),E(i.$$.fragment,f),E(s.$$.fragment,f),o=!0)},o(f){A(e.$$.fragment,f),A(i.$$.fragment,f),A(s.$$.fragment,f),o=!1},d(f){f&&(v(t),v(l)),V(e,f),n[15](null),V(i,f),n[18](null),V(s,f)}}}const xs="logId",Pc="adminRequests",Fc="adminLogRequests";function t$(n,e,t){var L;let i,l,s;Ue(n,jo,R=>t(19,l=R)),Ue(n,It,R=>t(6,s=R)),xt(It,s="Logs",s);const o=new URLSearchParams(l);let r,a,f=1,u=o.get("filter")||"",c=(o.get(Pc)||((L=window.localStorage)==null?void 0:L.getItem(Fc)))<<0,d=c;function m(){t(4,f++,f)}function h(R={}){let F={};F.filter=u||null,F[Pc]=c<<0||null,j.replaceHashQueryParams(Object.assign(F,R))}const _=()=>a==null?void 0:a.show(),g=()=>m();function y(){c=this.checked,t(2,c)}const S=R=>t(1,u=R.detail);function T(R){u=R,t(1,u)}const $=R=>r==null?void 0:r.show(R==null?void 0:R.detail);function C(R){ee[R?"unshift":"push"](()=>{r=R,t(0,r)})}const O=R=>{var N;let F={};F[xs]=((N=R.detail)==null?void 0:N.id)||null,j.replaceHashQueryParams(F)},D=()=>{let R={};R[xs]=null,j.replaceHashQueryParams(R)};function I(R){ee[R?"unshift":"push"](()=>{a=R,t(3,a)})}return n.$$.update=()=>{var R;n.$$.dirty&1&&o.get(xs)&&r&&r.show(o.get(xs)),n.$$.dirty&4&&t(5,i=c?"":'data.auth!="admin"'),n.$$.dirty&260&&d!=c&&(t(8,d=c),(R=window.localStorage)==null||R.setItem(Fc,c<<0),h()),n.$$.dirty&2&&typeof u<"u"&&h()},[r,u,c,a,f,i,s,m,d,_,g,y,S,T,$,C,O,D,I]}class n$ extends ge{constructor(e){super(),_e(this,e,t$,e$,me,{})}}function i$(n){let e,t,i;return{c(){e=b("span"),p(e,"class","dragline svelte-y9un12"),x(e,"dragging",n[1])},m(l,s){w(l,e,s),n[4](e),t||(i=[J(e,"mousedown",n[5]),J(e,"touchstart",n[2])],t=!0)},p(l,[s]){s&2&&x(e,"dragging",l[1])},i:Q,o:Q,d(l){l&&v(e),n[4](null),t=!1,$e(i)}}}function l$(n,e,t){const i=lt();let{tolerance:l=0}=e,s,o=0,r=0,a=0,f=0,u=!1;function c(g){g.stopPropagation(),o=g.clientX,r=g.clientY,a=g.clientX-s.offsetLeft,f=g.clientY-s.offsetTop,document.addEventListener("touchmove",m),document.addEventListener("mousemove",m),document.addEventListener("touchend",d),document.addEventListener("mouseup",d)}function d(g){u&&(g.preventDefault(),t(1,u=!1),s.classList.remove("no-pointer-events"),i("dragstop",{event:g,elem:s})),document.removeEventListener("touchmove",m),document.removeEventListener("mousemove",m),document.removeEventListener("touchend",d),document.removeEventListener("mouseup",d)}function m(g){let y=g.clientX-o,S=g.clientY-r,T=g.clientX-a,$=g.clientY-f;!u&&Math.abs(T-s.offsetLeft){s=g,t(0,s)})}const _=g=>{g.button==0&&c(g)};return n.$$set=g=>{"tolerance"in g&&t(3,l=g.tolerance)},[s,u,c,l,h,_]}class s$ extends ge{constructor(e){super(),_e(this,e,l$,i$,me,{tolerance:3})}}function o$(n){let e,t,i,l,s;const o=n[5].default,r=wt(o,n,n[4],null);return l=new s$({}),l.$on("dragstart",n[7]),l.$on("dragging",n[8]),l.$on("dragstop",n[9]),{c(){e=b("aside"),r&&r.c(),i=M(),B(l.$$.fragment),p(e,"class",t="page-sidebar "+n[0])},m(a,f){w(a,e,f),r&&r.m(e,null),n[6](e),w(a,i,f),z(l,a,f),s=!0},p(a,[f]){r&&r.p&&(!s||f&16)&&$t(r,o,a,a[4],s?St(o,a[4],f,null):Tt(a[4]),null),(!s||f&1&&t!==(t="page-sidebar "+a[0]))&&p(e,"class",t)},i(a){s||(E(r,a),E(l.$$.fragment,a),s=!0)},o(a){A(r,a),A(l.$$.fragment,a),s=!1},d(a){a&&(v(e),v(i)),r&&r.d(a),n[6](null),V(l,a)}}}const Rc="@adminSidebarWidth";function r$(n,e,t){let{$$slots:i={},$$scope:l}=e,{class:s=""}=e,o,r,a=localStorage.getItem(Rc)||null;function f(m){ee[m?"unshift":"push"](()=>{o=m,t(1,o),t(2,a)})}const u=()=>{t(3,r=o.offsetWidth)},c=m=>{t(2,a=r+m.detail.diffX+"px")},d=()=>{j.triggerResize()};return n.$$set=m=>{"class"in m&&t(0,s=m.class),"$$scope"in m&&t(4,l=m.$$scope)},n.$$.update=()=>{n.$$.dirty&6&&a&&o&&(t(1,o.style.width=a,o),localStorage.setItem(Rc,a))},[s,o,a,r,l,i,f,u,c,d]}class Hb extends ge{constructor(e){super(),_e(this,e,r$,o$,me,{class:0})}}const za=Cn({});function fn(n,e,t){za.set({text:n,yesCallback:e,noCallback:t})}function zb(){za.set({})}function qc(n){let e,t,i;const l=n[18].default,s=wt(l,n,n[17],null);return{c(){e=b("div"),s&&s.c(),p(e,"class",n[1]),x(e,"active",n[0])},m(o,r){w(o,e,r),s&&s.m(e,null),n[19](e),i=!0},p(o,r){s&&s.p&&(!i||r[0]&131072)&&$t(s,l,o,o[17],i?St(l,o[17],r,null):Tt(o[17]),null),(!i||r[0]&2)&&p(e,"class",o[1]),(!i||r[0]&3)&&x(e,"active",o[0])},i(o){i||(E(s,o),o&&Ke(()=>{i&&(t||(t=Fe(e,Fn,{duration:150,y:3},!0)),t.run(1))}),i=!0)},o(o){A(s,o),o&&(t||(t=Fe(e,Fn,{duration:150,y:3},!1)),t.run(0)),i=!1},d(o){o&&v(e),s&&s.d(o),n[19](null),o&&t&&t.end()}}}function a$(n){let e,t,i,l,s=n[0]&&qc(n);return{c(){e=b("div"),s&&s.c(),p(e,"class","toggler-container"),p(e,"tabindex","-1"),p(e,"role","menu")},m(o,r){w(o,e,r),s&&s.m(e,null),n[20](e),t=!0,i||(l=[J(window,"click",n[7]),J(window,"mousedown",n[6]),J(window,"keydown",n[5]),J(window,"focusin",n[4])],i=!0)},p(o,r){o[0]?s?(s.p(o,r),r[0]&1&&E(s,1)):(s=qc(o),s.c(),E(s,1),s.m(e,null)):s&&(le(),A(s,1,1,()=>{s=null}),se())},i(o){t||(E(s),t=!0)},o(o){A(s),t=!1},d(o){o&&v(e),s&&s.d(),n[20](null),i=!1,$e(l)}}}function f$(n,e,t){let{$$slots:i={},$$scope:l}=e,{trigger:s=void 0}=e,{active:o=!1}=e,{escClose:r=!0}=e,{autoScroll:a=!0}=e,{closableClass:f="closable"}=e,{class:u=""}=e,c,d,m,h,_,g=!1;const y=lt();function S(Y=0){o&&(clearTimeout(_),_=setTimeout(T,Y))}function T(){o&&(t(0,o=!1),g=!1,clearTimeout(h),clearTimeout(_))}function $(){clearTimeout(_),clearTimeout(h),!o&&(t(0,o=!0),m!=null&&m.contains(c)||c==null||c.focus(),h=setTimeout(()=>{a&&(d!=null&&d.scrollIntoViewIfNeeded?d==null||d.scrollIntoViewIfNeeded():d!=null&&d.scrollIntoView&&(d==null||d.scrollIntoView({behavior:"smooth",block:"nearest"})))},180))}function C(){o?T():$()}function O(Y){return!c||Y.classList.contains(f)||c.contains(Y)&&Y.closest&&Y.closest("."+f)}function D(Y){I(),c==null||c.addEventListener("click",L),c==null||c.addEventListener("keydown",R),t(16,m=Y||(c==null?void 0:c.parentNode)),m==null||m.addEventListener("click",F),m==null||m.addEventListener("keydown",N)}function I(){clearTimeout(h),clearTimeout(_),c==null||c.removeEventListener("click",L),c==null||c.removeEventListener("keydown",R),m==null||m.removeEventListener("click",F),m==null||m.removeEventListener("keydown",N)}function L(Y){Y.stopPropagation(),O(Y.target)&&T()}function R(Y){(Y.code==="Enter"||Y.code==="Space")&&(Y.stopPropagation(),O(Y.target)&&S(150))}function F(Y){Y.preventDefault(),Y.stopPropagation(),C()}function N(Y){(Y.code==="Enter"||Y.code==="Space")&&(Y.preventDefault(),Y.stopPropagation(),C())}function P(Y){o&&!(m!=null&&m.contains(Y.target))&&!(c!=null&&c.contains(Y.target))&&C()}function q(Y){o&&r&&Y.code==="Escape"&&(Y.preventDefault(),T())}function H(Y){o&&(g=!(c!=null&&c.contains(Y.target)))}function W(Y){var ie;o&&g&&!(c!=null&&c.contains(Y.target))&&!(m!=null&&m.contains(Y.target))&&!((ie=Y.target)!=null&&ie.closest(".flatpickr-calendar"))&&T()}Ht(()=>(D(),()=>I()));function G(Y){ee[Y?"unshift":"push"](()=>{d=Y,t(3,d)})}function U(Y){ee[Y?"unshift":"push"](()=>{c=Y,t(2,c)})}return n.$$set=Y=>{"trigger"in Y&&t(8,s=Y.trigger),"active"in Y&&t(0,o=Y.active),"escClose"in Y&&t(9,r=Y.escClose),"autoScroll"in Y&&t(10,a=Y.autoScroll),"closableClass"in Y&&t(11,f=Y.closableClass),"class"in Y&&t(1,u=Y.class),"$$scope"in Y&&t(17,l=Y.$$scope)},n.$$.update=()=>{var Y,ie;n.$$.dirty[0]&260&&c&&D(s),n.$$.dirty[0]&65537&&(o?((Y=m==null?void 0:m.classList)==null||Y.add("active"),m==null||m.setAttribute("aria-expanded",!0),y("show")):((ie=m==null?void 0:m.classList)==null||ie.remove("active"),m==null||m.setAttribute("aria-expanded",!1),y("hide")))},[o,u,c,d,P,q,H,W,s,r,a,f,S,T,$,C,m,l,i,G,U]}class On extends ge{constructor(e){super(),_e(this,e,f$,a$,me,{trigger:8,active:0,escClose:9,autoScroll:10,closableClass:11,class:1,hideWithDelay:12,hide:13,show:14,toggle:15},null,[-1,-1])}get hideWithDelay(){return this.$$.ctx[12]}get hide(){return this.$$.ctx[13]}get show(){return this.$$.ctx[14]}get toggle(){return this.$$.ctx[15]}}function jc(n,e,t){const i=n.slice();return i[27]=e[t],i}function u$(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("input"),l=M(),s=b("label"),o=K("Unique"),p(e,"type","checkbox"),p(e,"id",t=n[30]),e.checked=i=n[3].unique,p(s,"for",r=n[30])},m(u,c){w(u,e,c),w(u,l,c),w(u,s,c),k(s,o),a||(f=J(e,"change",n[19]),a=!0)},p(u,c){c[0]&1073741824&&t!==(t=u[30])&&p(e,"id",t),c[0]&8&&i!==(i=u[3].unique)&&(e.checked=i),c[0]&1073741824&&r!==(r=u[30])&&p(s,"for",r)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function c$(n){let e,t,i,l;function s(a){n[20](a)}var o=n[7];function r(a,f){var c;let u={id:a[30],placeholder:`eg. CREATE INDEX idx_test on ${(c=a[0])==null?void 0:c.name} (created)`,language:"sql-create-index",minHeight:"85"};return a[2]!==void 0&&(u.value=a[2]),{props:u}}return o&&(e=Dt(o,r(n)),ee.push(()=>be(e,"value",s))),{c(){e&&B(e.$$.fragment),i=ye()},m(a,f){e&&z(e,a,f),w(a,i,f),l=!0},p(a,f){var u;if(f[0]&128&&o!==(o=a[7])){if(e){le();const c=e;A(c.$$.fragment,1,0,()=>{V(c,1)}),se()}o?(e=Dt(o,r(a)),ee.push(()=>be(e,"value",s)),B(e.$$.fragment),E(e.$$.fragment,1),z(e,i.parentNode,i)):e=null}else if(o){const c={};f[0]&1073741824&&(c.id=a[30]),f[0]&1&&(c.placeholder=`eg. CREATE INDEX idx_test on ${(u=a[0])==null?void 0:u.name} (created)`),!t&&f[0]&4&&(t=!0,c.value=a[2],ke(()=>t=!1)),e.$set(c)}},i(a){l||(e&&E(e.$$.fragment,a),l=!0)},o(a){e&&A(e.$$.fragment,a),l=!1},d(a){a&&v(i),e&&V(e,a)}}}function d$(n){let e;return{c(){e=b("textarea"),e.disabled=!0,p(e,"rows","7"),p(e,"placeholder","Loading...")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function p$(n){let e,t,i,l;const s=[d$,c$],o=[];function r(a,f){return a[8]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,f){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function Hc(n){let e,t,i,l=ue(n[10]),s=[];for(let o=0;o({30:a}),({uniqueId:a})=>[a?1073741824:0]]},$$scope:{ctx:n}}}),i=new ce({props:{class:"form-field required m-b-sm",name:`indexes.${n[6]||""}`,$$slots:{default:[p$,({uniqueId:a})=>({30:a}),({uniqueId:a})=>[a?1073741824:0]]},$$scope:{ctx:n}}});let r=n[10].length>0&&Hc(n);return{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),r&&r.c(),s=ye()},m(a,f){z(e,a,f),w(a,t,f),z(i,a,f),w(a,l,f),r&&r.m(a,f),w(a,s,f),o=!0},p(a,f){const u={};f[0]&1073741837|f[1]&1&&(u.$$scope={dirty:f,ctx:a}),e.$set(u);const c={};f[0]&64&&(c.name=`indexes.${a[6]||""}`),f[0]&1073742213|f[1]&1&&(c.$$scope={dirty:f,ctx:a}),i.$set(c),a[10].length>0?r?r.p(a,f):(r=Hc(a),r.c(),r.m(s.parentNode,s)):r&&(r.d(1),r=null)},i(a){o||(E(e.$$.fragment,a),E(i.$$.fragment,a),o=!0)},o(a){A(e.$$.fragment,a),A(i.$$.fragment,a),o=!1},d(a){a&&(v(t),v(l),v(s)),V(e,a),V(i,a),r&&r.d(a)}}}function h$(n){let e,t=n[5]?"Update":"Create",i,l;return{c(){e=b("h4"),i=K(t),l=K(" index")},m(s,o){w(s,e,o),k(e,i),k(e,l)},p(s,o){o[0]&32&&t!==(t=s[5]?"Update":"Create")&&oe(i,t)},d(s){s&&v(e)}}}function Vc(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-hint btn-transparent m-r-auto")},m(l,s){w(l,e,s),t||(i=[Se(Pe.call(null,e,{text:"Delete",position:"top"})),J(e,"click",n[16])],t=!0)},p:Q,d(l){l&&v(e),t=!1,$e(i)}}}function _$(n){let e,t,i,l,s,o,r=n[5]!=""&&Vc(n);return{c(){r&&r.c(),e=M(),t=b("button"),t.innerHTML='Cancel',i=M(),l=b("button"),l.innerHTML='Set index',p(t,"type","button"),p(t,"class","btn btn-transparent"),p(l,"type","button"),p(l,"class","btn"),x(l,"btn-disabled",n[9].length<=0)},m(a,f){r&&r.m(a,f),w(a,e,f),w(a,t,f),w(a,i,f),w(a,l,f),s||(o=[J(t,"click",n[17]),J(l,"click",n[18])],s=!0)},p(a,f){a[5]!=""?r?r.p(a,f):(r=Vc(a),r.c(),r.m(e.parentNode,e)):r&&(r.d(1),r=null),f[0]&512&&x(l,"btn-disabled",a[9].length<=0)},d(a){a&&(v(e),v(t),v(i),v(l)),r&&r.d(a),s=!1,$e(o)}}}function g$(n){let e,t;const i=[{popup:!0},n[14]];let l={$$slots:{footer:[_$],header:[h$],default:[m$]},$$scope:{ctx:n}};for(let s=0;sU.name==H);G?j.removeByValue(W.columns,G):j.pushUnique(W.columns,{name:H}),t(2,d=j.buildIndex(W))}Ht(async()=>{t(8,_=!0);try{t(7,h=(await tt(async()=>{const{default:H}=await import("./CodeEditor-CZ0EgQcM.js");return{default:H}},__vite__mapDeps([2,1]),import.meta.url)).default)}catch(H){console.warn(H)}t(8,_=!1)});const O=()=>T(),D=()=>y(),I=()=>$(),L=H=>{t(3,l.unique=H.target.checked,l),t(3,l.tableName=l.tableName||(f==null?void 0:f.name),l),t(2,d=j.buildIndex(l))};function R(H){d=H,t(2,d)}const F=H=>C(H);function N(H){ee[H?"unshift":"push"](()=>{u=H,t(4,u)})}function P(H){Ce.call(this,n,H)}function q(H){Ce.call(this,n,H)}return n.$$set=H=>{e=Ie(Ie({},e),Yt(H)),t(14,r=Ge(e,o)),"collection"in H&&t(0,f=H.collection)},n.$$.update=()=>{var H,W,G;n.$$.dirty[0]&1&&t(10,i=(((W=(H=f==null?void 0:f.schema)==null?void 0:H.filter(U=>!U.toDelete))==null?void 0:W.map(U=>U.name))||[]).concat(["created","updated"])),n.$$.dirty[0]&4&&t(3,l=j.parseIndex(d)),n.$$.dirty[0]&8&&t(9,s=((G=l.columns)==null?void 0:G.map(U=>U.name))||[])},[f,y,d,l,u,c,m,h,_,s,i,T,$,C,r,g,O,D,I,L,R,F,N,P,q]}class k$ extends ge{constructor(e){super(),_e(this,e,b$,g$,me,{collection:0,show:15,hide:1},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[1]}}function Bc(n,e,t){const i=n.slice();i[10]=e[t],i[13]=t;const l=j.parseIndex(i[10]);return i[11]=l,i}function Uc(n){let e;return{c(){e=b("strong"),e.textContent="Unique:"},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Wc(n){var d;let e,t,i,l=((d=n[11].columns)==null?void 0:d.map(Yc).join(", "))+"",s,o,r,a,f,u=n[11].unique&&Uc();function c(){return n[4](n[10],n[13])}return{c(){var m,h;e=b("button"),u&&u.c(),t=M(),i=b("span"),s=K(l),p(i,"class","txt"),p(e,"type","button"),p(e,"class",o="label link-primary "+((h=(m=n[2].indexes)==null?void 0:m[n[13]])!=null&&h.message?"label-danger":"")+" svelte-167lbwu")},m(m,h){var _,g;w(m,e,h),u&&u.m(e,null),k(e,t),k(e,i),k(i,s),a||(f=[Se(r=Pe.call(null,e,((g=(_=n[2].indexes)==null?void 0:_[n[13]])==null?void 0:g.message)||"")),J(e,"click",c)],a=!0)},p(m,h){var _,g,y,S,T;n=m,n[11].unique?u||(u=Uc(),u.c(),u.m(e,t)):u&&(u.d(1),u=null),h&1&&l!==(l=((_=n[11].columns)==null?void 0:_.map(Yc).join(", "))+"")&&oe(s,l),h&4&&o!==(o="label link-primary "+((y=(g=n[2].indexes)==null?void 0:g[n[13]])!=null&&y.message?"label-danger":"")+" svelte-167lbwu")&&p(e,"class",o),r&&Ct(r.update)&&h&4&&r.update.call(null,((T=(S=n[2].indexes)==null?void 0:S[n[13]])==null?void 0:T.message)||"")},d(m){m&&v(e),u&&u.d(),a=!1,$e(f)}}}function y$(n){var $,C,O;let e,t,i=(((C=($=n[0])==null?void 0:$.indexes)==null?void 0:C.length)||0)+"",l,s,o,r,a,f,u,c,d,m,h,_,g=ue(((O=n[0])==null?void 0:O.indexes)||[]),y=[];for(let D=0;Dbe(c,"collection",S)),c.$on("remove",n[8]),c.$on("submit",n[9]),{c(){e=b("div"),t=K("Unique constraints and indexes ("),l=K(i),s=K(")"),o=M(),r=b("div");for(let D=0;D+ New index',u=M(),B(c.$$.fragment),p(e,"class","section-title"),p(f,"type","button"),p(f,"class","btn btn-xs btn-transparent btn-pill btn-outline"),p(r,"class","indexes-list svelte-167lbwu")},m(D,I){w(D,e,I),k(e,t),k(e,l),k(e,s),w(D,o,I),w(D,r,I);for(let L=0;Ld=!1)),c.$set(L)},i(D){m||(E(c.$$.fragment,D),m=!0)},o(D){A(c.$$.fragment,D),m=!1},d(D){D&&(v(e),v(o),v(r),v(u)),ot(y,D),n[6](null),V(c,D),h=!1,_()}}}const Yc=n=>n.name;function v$(n,e,t){let i;Ue(n,mi,m=>t(2,i=m));let{collection:l}=e,s;function o(m,h){for(let _=0;_s==null?void 0:s.show(m,h),a=()=>s==null?void 0:s.show();function f(m){ee[m?"unshift":"push"](()=>{s=m,t(1,s)})}function u(m){l=m,t(0,l)}const c=m=>{for(let h=0;h{o(m.detail.old,m.detail.new)};return n.$$set=m=>{"collection"in m&&t(0,l=m.collection)},[l,s,i,o,r,a,f,u,c,d]}class w$ extends ge{constructor(e){super(),_e(this,e,v$,y$,me,{collection:0})}}function Kc(n,e,t){const i=n.slice();return i[5]=e[t],i}function Jc(n){let e,t,i,l,s,o,r;function a(){return n[3](n[5])}return{c(){e=b("button"),t=b("i"),i=M(),l=b("span"),l.textContent=`${n[5].label}`,s=M(),p(t,"class","icon "+n[5].icon+" svelte-1gz9b6p"),p(t,"aria-hidden","true"),p(l,"class","txt"),p(e,"type","button"),p(e,"role","menuitem"),p(e,"class","dropdown-item svelte-1gz9b6p")},m(f,u){w(f,e,u),k(e,t),k(e,i),k(e,l),k(e,s),o||(r=J(e,"click",a),o=!0)},p(f,u){n=f},d(f){f&&v(e),o=!1,r()}}}function S$(n){let e,t=ue(n[1]),i=[];for(let l=0;lo(a.value);return n.$$set=a=>{"class"in a&&t(0,i=a.class)},[i,s,o,r]}class C$ extends ge{constructor(e){super(),_e(this,e,T$,$$,me,{class:0})}}const O$=n=>({interactive:n&64,hasErrors:n&32}),Zc=n=>({interactive:n[6],hasErrors:n[5]}),M$=n=>({interactive:n&64,hasErrors:n&32}),Gc=n=>({interactive:n[6],hasErrors:n[5]}),D$=n=>({interactive:n&64,hasErrors:n&32}),Xc=n=>({interactive:n[6],hasErrors:n[5]});function Qc(n){let e;return{c(){e=b("div"),e.innerHTML='',p(e,"class","drag-handle-wrapper"),p(e,"draggable",!0),p(e,"aria-label","Sort")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function xc(n){let e,t,i;return{c(){e=b("div"),t=b("span"),i=K(n[4]),p(t,"class","label label-success"),p(e,"class","field-labels")},m(l,s){w(l,e,s),k(e,t),k(t,i)},p(l,s){s&16&&oe(i,l[4])},d(l){l&&v(e)}}}function E$(n){let e,t,i,l,s,o,r,a,f,u,c,d,m=n[0].required&&xc(n);return{c(){m&&m.c(),e=M(),t=b("div"),i=b("i"),s=M(),o=b("input"),p(i,"class",l=j.getFieldTypeIcon(n[0].type)),p(t,"class","form-field-addon prefix no-pointer-events field-type-icon"),x(t,"txt-disabled",!n[6]),p(o,"type","text"),o.required=!0,o.disabled=r=!n[6],o.readOnly=a=n[0].id&&n[0].system,p(o,"spellcheck","false"),o.autofocus=f=!n[0].id,p(o,"placeholder","Field name"),o.value=u=n[0].name},m(h,_){m&&m.m(h,_),w(h,e,_),w(h,t,_),k(t,i),w(h,s,_),w(h,o,_),n[15](o),n[0].id||o.focus(),c||(d=J(o,"input",n[16]),c=!0)},p(h,_){h[0].required?m?m.p(h,_):(m=xc(h),m.c(),m.m(e.parentNode,e)):m&&(m.d(1),m=null),_&1&&l!==(l=j.getFieldTypeIcon(h[0].type))&&p(i,"class",l),_&64&&x(t,"txt-disabled",!h[6]),_&64&&r!==(r=!h[6])&&(o.disabled=r),_&1&&a!==(a=h[0].id&&h[0].system)&&(o.readOnly=a),_&1&&f!==(f=!h[0].id)&&(o.autofocus=f),_&1&&u!==(u=h[0].name)&&o.value!==u&&(o.value=u)},d(h){h&&(v(e),v(t),v(s),v(o)),m&&m.d(h),n[15](null),c=!1,d()}}}function I$(n){let e;return{c(){e=b("span"),p(e,"class","separator")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function A$(n){let e,t,i,l,s;return{c(){e=b("button"),t=b("i"),p(t,"class","ri-settings-3-line"),p(e,"type","button"),p(e,"aria-label","Toggle field options"),p(e,"class",i="btn btn-sm btn-circle options-trigger "+(n[3]?"btn-secondary":"btn-transparent")),p(e,"aria-expanded",n[3]),x(e,"btn-hint",!n[3]&&!n[5]),x(e,"btn-danger",n[5])},m(o,r){w(o,e,r),k(e,t),l||(s=J(e,"click",n[12]),l=!0)},p(o,r){r&8&&i!==(i="btn btn-sm btn-circle options-trigger "+(o[3]?"btn-secondary":"btn-transparent"))&&p(e,"class",i),r&8&&p(e,"aria-expanded",o[3]),r&40&&x(e,"btn-hint",!o[3]&&!o[5]),r&40&&x(e,"btn-danger",o[5])},d(o){o&&v(e),l=!1,s()}}}function L$(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-warning btn-transparent options-trigger"),p(e,"aria-label","Restore")},m(l,s){w(l,e,s),t||(i=[Se(Pe.call(null,e,"Restore")),J(e,"click",n[9])],t=!0)},p:Q,d(l){l&&v(e),t=!1,$e(i)}}}function ed(n){let e,t,i,l,s,o,r,a,f,u,c;const d=n[14].options,m=wt(d,n,n[19],Gc);s=new ce({props:{class:"form-field form-field-toggle",name:"requried",$$slots:{default:[N$,({uniqueId:y})=>({25:y}),({uniqueId:y})=>y?33554432:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field form-field-toggle",name:"presentable",$$slots:{default:[P$,({uniqueId:y})=>({25:y}),({uniqueId:y})=>y?33554432:0]},$$scope:{ctx:n}}});const h=n[14].optionsFooter,_=wt(h,n,n[19],Zc);let g=!n[0].toDelete&&td(n);return{c(){e=b("div"),t=b("div"),m&&m.c(),i=M(),l=b("div"),B(s.$$.fragment),o=M(),B(r.$$.fragment),a=M(),_&&_.c(),f=M(),g&&g.c(),p(t,"class","hidden-empty m-b-sm"),p(l,"class","schema-field-options-footer"),p(e,"class","schema-field-options")},m(y,S){w(y,e,S),k(e,t),m&&m.m(t,null),k(e,i),k(e,l),z(s,l,null),k(l,o),z(r,l,null),k(l,a),_&&_.m(l,null),k(l,f),g&&g.m(l,null),c=!0},p(y,S){m&&m.p&&(!c||S&524384)&&$t(m,d,y,y[19],c?St(d,y[19],S,M$):Tt(y[19]),Gc);const T={};S&34078737&&(T.$$scope={dirty:S,ctx:y}),s.$set(T);const $={};S&34078721&&($.$$scope={dirty:S,ctx:y}),r.$set($),_&&_.p&&(!c||S&524384)&&$t(_,h,y,y[19],c?St(h,y[19],S,O$):Tt(y[19]),Zc),y[0].toDelete?g&&(le(),A(g,1,1,()=>{g=null}),se()):g?(g.p(y,S),S&1&&E(g,1)):(g=td(y),g.c(),E(g,1),g.m(l,null))},i(y){c||(E(m,y),E(s.$$.fragment,y),E(r.$$.fragment,y),E(_,y),E(g),y&&Ke(()=>{c&&(u||(u=Fe(e,et,{duration:150},!0)),u.run(1))}),c=!0)},o(y){A(m,y),A(s.$$.fragment,y),A(r.$$.fragment,y),A(_,y),A(g),y&&(u||(u=Fe(e,et,{duration:150},!1)),u.run(0)),c=!1},d(y){y&&v(e),m&&m.d(y),V(s),V(r),_&&_.d(y),g&&g.d(),y&&u&&u.end()}}}function N$(n){let e,t,i,l,s,o,r,a,f,u,c,d;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),o=K(n[4]),r=M(),a=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[25]),p(s,"class","txt"),p(a,"class","ri-information-line link-hint"),p(l,"for",u=n[25])},m(m,h){w(m,e,h),e.checked=n[0].required,w(m,i,h),w(m,l,h),k(l,s),k(s,o),k(l,r),k(l,a),c||(d=[J(e,"change",n[17]),Se(f=Pe.call(null,a,{text:`Requires the field value NOT to be ${j.zeroDefaultStr(n[0])}.`}))],c=!0)},p(m,h){h&33554432&&t!==(t=m[25])&&p(e,"id",t),h&1&&(e.checked=m[0].required),h&16&&oe(o,m[4]),f&&Ct(f.update)&&h&1&&f.update.call(null,{text:`Requires the field value NOT to be ${j.zeroDefaultStr(m[0])}.`}),h&33554432&&u!==(u=m[25])&&p(l,"for",u)},d(m){m&&(v(e),v(i),v(l)),c=!1,$e(d)}}}function P$(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Presentable",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[25]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[25])},m(c,d){w(c,e,d),e.checked=n[0].presentable,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[18]),Se(Pe.call(null,r,{text:"Whether the field should be preferred in the Admin UI relation listings (default to auto)."}))],f=!0)},p(c,d){d&33554432&&t!==(t=c[25])&&p(e,"id",t),d&1&&(e.checked=c[0].presentable),d&33554432&&a!==(a=c[25])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function td(n){let e,t,i,l,s,o,r;return o=new On({props:{class:"dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width",$$slots:{default:[F$]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),l=b("i"),s=M(),B(o.$$.fragment),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"aria-label","More"),p(i,"class","btn btn-circle btn-sm btn-transparent"),p(t,"class","inline-flex flex-gap-sm flex-nowrap"),p(e,"class","m-l-auto txt-right")},m(a,f){w(a,e,f),k(e,t),k(t,i),k(i,l),k(i,s),z(o,i,null),r=!0},p(a,f){const u={};f&524288&&(u.$$scope={dirty:f,ctx:a}),o.$set(u)},i(a){r||(E(o.$$.fragment,a),r=!0)},o(a){A(o.$$.fragment,a),r=!1},d(a){a&&v(e),V(o)}}}function F$(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Duplicate',t=M(),i=b("button"),i.innerHTML='Remove',p(e,"type","button"),p(e,"class","dropdown-item"),p(e,"role","menuitem"),p(i,"type","button"),p(i,"class","dropdown-item"),p(i,"role","menuitem")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),l||(s=[J(e,"click",Be(n[10])),J(i,"click",Be(n[8]))],l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,$e(s)}}}function R$(n){let e,t,i,l,s,o,r,a,f,u=n[6]&&Qc();l=new ce({props:{class:"form-field required m-0 "+(n[6]?"":"disabled"),name:"schema."+n[1]+".name",inlineError:!0,$$slots:{default:[E$]},$$scope:{ctx:n}}});const c=n[14].default,d=wt(c,n,n[19],Xc),m=d||I$();function h(S,T){if(S[0].toDelete)return L$;if(S[6])return A$}let _=h(n),g=_&&_(n),y=n[6]&&n[3]&&ed(n);return{c(){e=b("div"),t=b("div"),u&&u.c(),i=M(),B(l.$$.fragment),s=M(),m&&m.c(),o=M(),g&&g.c(),r=M(),y&&y.c(),p(t,"class","schema-field-header"),p(e,"class","schema-field"),x(e,"required",n[0].required),x(e,"expanded",n[6]&&n[3]),x(e,"deleted",n[0].toDelete)},m(S,T){w(S,e,T),k(e,t),u&&u.m(t,null),k(t,i),z(l,t,null),k(t,s),m&&m.m(t,null),k(t,o),g&&g.m(t,null),k(e,r),y&&y.m(e,null),f=!0},p(S,[T]){S[6]?u||(u=Qc(),u.c(),u.m(t,i)):u&&(u.d(1),u=null);const $={};T&64&&($.class="form-field required m-0 "+(S[6]?"":"disabled")),T&2&&($.name="schema."+S[1]+".name"),T&524373&&($.$$scope={dirty:T,ctx:S}),l.$set($),d&&d.p&&(!f||T&524384)&&$t(d,c,S,S[19],f?St(c,S[19],T,D$):Tt(S[19]),Xc),_===(_=h(S))&&g?g.p(S,T):(g&&g.d(1),g=_&&_(S),g&&(g.c(),g.m(t,null))),S[6]&&S[3]?y?(y.p(S,T),T&72&&E(y,1)):(y=ed(S),y.c(),E(y,1),y.m(e,null)):y&&(le(),A(y,1,1,()=>{y=null}),se()),(!f||T&1)&&x(e,"required",S[0].required),(!f||T&72)&&x(e,"expanded",S[6]&&S[3]),(!f||T&1)&&x(e,"deleted",S[0].toDelete)},i(S){f||(E(l.$$.fragment,S),E(m,S),E(y),S&&Ke(()=>{f&&(a||(a=Fe(e,et,{duration:150},!0)),a.run(1))}),f=!0)},o(S){A(l.$$.fragment,S),A(m,S),A(y),S&&(a||(a=Fe(e,et,{duration:150},!1)),a.run(0)),f=!1},d(S){S&&v(e),u&&u.d(),V(l),m&&m.d(S),g&&g.d(),y&&y.d(),S&&a&&a.end()}}}let $r=[];function q$(n,e,t){let i,l,s,o;Ue(n,mi,N=>t(13,o=N));let{$$slots:r={},$$scope:a}=e;const f="f_"+j.randomString(8),u=lt(),c={bool:"Nonfalsey",number:"Nonzero"};let{key:d=""}=e,{field:m=j.initSchemaField()}=e,h,_=!1;function g(){m.id?t(0,m.toDelete=!0,m):(C(),u("remove"))}function y(){t(0,m.toDelete=!1,m),Jt({})}function S(){m.toDelete||(C(),u("duplicate"))}function T(N){return j.slugify(N)}function $(){t(3,_=!0),D()}function C(){t(3,_=!1)}function O(){_?C():$()}function D(){for(let N of $r)N.id!=f&&N.collapse()}Ht(()=>($r.push({id:f,collapse:C}),m.onMountSelect&&(t(0,m.onMountSelect=!1,m),h==null||h.select()),()=>{j.removeByKey($r,"id",f)}));function I(N){ee[N?"unshift":"push"](()=>{h=N,t(2,h)})}const L=N=>{const P=m.name;t(0,m.name=T(N.target.value),m),N.target.value=m.name,u("rename",{oldName:P,newName:m.name})};function R(){m.required=this.checked,t(0,m)}function F(){m.presentable=this.checked,t(0,m)}return n.$$set=N=>{"key"in N&&t(1,d=N.key),"field"in N&&t(0,m=N.field),"$$scope"in N&&t(19,a=N.$$scope)},n.$$.update=()=>{n.$$.dirty&1&&m.toDelete&&m.originalName&&m.name!==m.originalName&&t(0,m.name=m.originalName,m),n.$$.dirty&1&&!m.originalName&&m.name&&t(0,m.originalName=m.name,m),n.$$.dirty&1&&typeof m.toDelete>"u"&&t(0,m.toDelete=!1,m),n.$$.dirty&1&&m.required&&t(0,m.nullable=!1,m),n.$$.dirty&1&&t(6,i=!m.toDelete&&!(m.id&&m.system)),n.$$.dirty&8194&&t(5,l=!j.isEmpty(j.getNestedVal(o,`schema.${d}`))),n.$$.dirty&1&&t(4,s=c[m==null?void 0:m.type]||"Nonempty")},[m,d,h,_,s,l,i,u,g,y,S,T,O,o,r,I,L,R,F,a]}class si extends ge{constructor(e){super(),_e(this,e,q$,R$,me,{key:1,field:0})}}function j$(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Min length"),l=M(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10]),p(s,"step","1"),p(s,"min","0")},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.min),r||(a=J(s,"input",n[3]),r=!0)},p(f,u){u&1024&&i!==(i=f[10])&&p(e,"for",i),u&1024&&o!==(o=f[10])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.min&&re(s,f[0].options.min)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function H$(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("label"),t=K("Max length"),l=M(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10]),p(s,"step","1"),p(s,"min",r=n[0].options.min||0)},m(u,c){w(u,e,c),k(e,t),w(u,l,c),w(u,s,c),re(s,n[0].options.max),a||(f=J(s,"input",n[4]),a=!0)},p(u,c){c&1024&&i!==(i=u[10])&&p(e,"for",i),c&1024&&o!==(o=u[10])&&p(s,"id",o),c&1&&r!==(r=u[0].options.min||0)&&p(s,"min",r),c&1&&it(s.value)!==u[0].options.max&&re(s,u[0].options.max)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function z$(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Regex pattern"),l=M(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","text"),p(s,"id",o=n[10]),p(s,"placeholder","Valid Go regular expression, eg. ^\\w+$")},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.pattern),r||(a=J(s,"input",n[5]),r=!0)},p(f,u){u&1024&&i!==(i=f[10])&&p(e,"for",i),u&1024&&o!==(o=f[10])&&p(s,"id",o),u&1&&s.value!==f[0].options.pattern&&re(s,f[0].options.pattern)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function V$(n){let e,t,i,l,s,o,r,a,f,u;return i=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[j$,({uniqueId:c})=>({10:c}),({uniqueId:c})=>c?1024:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[H$,({uniqueId:c})=>({10:c}),({uniqueId:c})=>c?1024:0]},$$scope:{ctx:n}}}),f=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.pattern",$$slots:{default:[z$,({uniqueId:c})=>({10:c}),({uniqueId:c})=>c?1024:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),a=b("div"),B(f.$$.fragment),p(t,"class","col-sm-3"),p(s,"class","col-sm-3"),p(a,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(c,d){w(c,e,d),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,r),k(e,a),z(f,a,null),u=!0},p(c,d){const m={};d&2&&(m.name="schema."+c[1]+".options.min"),d&3073&&(m.$$scope={dirty:d,ctx:c}),i.$set(m);const h={};d&2&&(h.name="schema."+c[1]+".options.max"),d&3073&&(h.$$scope={dirty:d,ctx:c}),o.$set(h);const _={};d&2&&(_.name="schema."+c[1]+".options.pattern"),d&3073&&(_.$$scope={dirty:d,ctx:c}),f.$set(_)},i(c){u||(E(i.$$.fragment,c),E(o.$$.fragment,c),E(f.$$.fragment,c),u=!0)},o(c){A(i.$$.fragment,c),A(o.$$.fragment,c),A(f.$$.fragment,c),u=!1},d(c){c&&v(e),V(i),V(o),V(f)}}}function B$(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[6](r)}let o={$$slots:{options:[V$]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};a&2051&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function U$(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(){s.options.min=it(this.value),t(0,s)}function a(){s.options.max=it(this.value),t(0,s)}function f(){s.options.pattern=this.value,t(0,s)}function u(h){s=h,t(0,s)}function c(h){Ce.call(this,n,h)}function d(h){Ce.call(this,n,h)}function m(h){Ce.call(this,n,h)}return n.$$set=h=>{e=Ie(Ie({},e),Yt(h)),t(2,l=Ge(e,i)),"field"in h&&t(0,s=h.field),"key"in h&&t(1,o=h.key)},[s,o,l,r,a,f,u,c,d,m]}class W$ extends ge{constructor(e){super(),_e(this,e,U$,B$,me,{field:0,key:1})}}function Y$(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Min"),l=M(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10])},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.min),r||(a=J(s,"input",n[4]),r=!0)},p(f,u){u&1024&&i!==(i=f[10])&&p(e,"for",i),u&1024&&o!==(o=f[10])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.min&&re(s,f[0].options.min)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function K$(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("label"),t=K("Max"),l=M(),s=b("input"),p(e,"for",i=n[10]),p(s,"type","number"),p(s,"id",o=n[10]),p(s,"min",r=n[0].options.min)},m(u,c){w(u,e,c),k(e,t),w(u,l,c),w(u,s,c),re(s,n[0].options.max),a||(f=J(s,"input",n[5]),a=!0)},p(u,c){c&1024&&i!==(i=u[10])&&p(e,"for",i),c&1024&&o!==(o=u[10])&&p(s,"id",o),c&1&&r!==(r=u[0].options.min)&&p(s,"min",r),c&1&&it(s.value)!==u[0].options.max&&re(s,u[0].options.max)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function J$(n){let e,t,i,l,s,o,r;return i=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[Y$,({uniqueId:a})=>({10:a}),({uniqueId:a})=>a?1024:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[K$,({uniqueId:a})=>({10:a}),({uniqueId:a})=>a?1024:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,f){w(a,e,f),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),r=!0},p(a,f){const u={};f&2&&(u.name="schema."+a[1]+".options.min"),f&3073&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};f&2&&(c.name="schema."+a[1]+".options.max"),f&3073&&(c.$$scope={dirty:f,ctx:a}),o.$set(c)},i(a){r||(E(i.$$.fragment,a),E(o.$$.fragment,a),r=!0)},o(a){A(i.$$.fragment,a),A(o.$$.fragment,a),r=!1},d(a){a&&v(e),V(i),V(o)}}}function Z$(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="No decimals",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[10]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[10])},m(c,d){w(c,e,d),e.checked=n[0].options.noDecimal,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[3]),Se(Pe.call(null,r,{text:"Existing decimal numbers will not be affected."}))],f=!0)},p(c,d){d&1024&&t!==(t=c[10])&&p(e,"id",t),d&1&&(e.checked=c[0].options.noDecimal),d&1024&&a!==(a=c[10])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function G$(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",name:"schema."+n[1]+".options.noDecimal",$$slots:{default:[Z$,({uniqueId:i})=>({10:i}),({uniqueId:i})=>i?1024:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="schema."+i[1]+".options.noDecimal"),l&3073&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function X$(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[6](r)}let o={$$slots:{optionsFooter:[G$],options:[J$]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};a&2051&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function Q$(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(){s.options.noDecimal=this.checked,t(0,s)}function a(){s.options.min=it(this.value),t(0,s)}function f(){s.options.max=it(this.value),t(0,s)}function u(h){s=h,t(0,s)}function c(h){Ce.call(this,n,h)}function d(h){Ce.call(this,n,h)}function m(h){Ce.call(this,n,h)}return n.$$set=h=>{e=Ie(Ie({},e),Yt(h)),t(2,l=Ge(e,i)),"field"in h&&t(0,s=h.field),"key"in h&&t(1,o=h.key)},[s,o,l,r,a,f,u,c,d,m]}class x$ extends ge{constructor(e){super(),_e(this,e,Q$,X$,me,{field:0,key:1})}}function eT(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[3](r)}let o={};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[4]),e.$on("remove",n[5]),e.$on("duplicate",n[6]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function tT(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(c){s=c,t(0,s)}function a(c){Ce.call(this,n,c)}function f(c){Ce.call(this,n,c)}function u(c){Ce.call(this,n,c)}return n.$$set=c=>{e=Ie(Ie({},e),Yt(c)),t(2,l=Ge(e,i)),"field"in c&&t(0,s=c.field),"key"in c&&t(1,o=c.key)},[s,o,l,r,a,f,u]}class nT extends ge{constructor(e){super(),_e(this,e,tT,eT,me,{field:0,key:1})}}function iT(n){let e,t,i,l,s=[{type:t=n[5].type||"text"},{value:n[4]},{disabled:n[3]},{readOnly:n[2]},n[5]],o={};for(let r=0;r{t(0,o=j.splitNonEmpty(c.target.value,r))};return n.$$set=c=>{e=Ie(Ie({},e),Yt(c)),t(5,s=Ge(e,l)),"value"in c&&t(0,o=c.value),"separator"in c&&t(1,r=c.separator),"readonly"in c&&t(2,a=c.readonly),"disabled"in c&&t(3,f=c.disabled)},n.$$.update=()=>{n.$$.dirty&3&&t(4,i=j.joinNonEmpty(o,r+" "))},[o,r,a,f,i,s,u]}class Nl extends ge{constructor(e){super(),_e(this,e,lT,iT,me,{value:0,separator:1,readonly:2,disabled:3})}}function sT(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;function h(g){n[3](g)}let _={id:n[9],disabled:!j.isEmpty(n[0].options.onlyDomains)};return n[0].options.exceptDomains!==void 0&&(_.value=n[0].options.exceptDomains),r=new Nl({props:_}),ee.push(()=>be(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Except domains",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),u.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[9]),p(u,"class","help-block")},m(g,y){w(g,e,y),k(e,t),k(e,i),k(e,l),w(g,o,y),z(r,g,y),w(g,f,y),w(g,u,y),c=!0,d||(m=Se(Pe.call(null,l,{text:`List of domains that are NOT allowed. - This field is disabled if "Only domains" is set.`,position:"top"})),d=!0)},p(g,y){(!c||y&512&&s!==(s=g[9]))&&p(e,"for",s);const S={};y&512&&(S.id=g[9]),y&1&&(S.disabled=!j.isEmpty(g[0].options.onlyDomains)),!a&&y&1&&(a=!0,S.value=g[0].options.exceptDomains,ke(()=>a=!1)),r.$set(S)},i(g){c||(E(r.$$.fragment,g),c=!0)},o(g){A(r.$$.fragment,g),c=!1},d(g){g&&(v(e),v(o),v(f),v(u)),V(r,g),d=!1,m()}}}function oT(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;function h(g){n[4](g)}let _={id:n[9]+".options.onlyDomains",disabled:!j.isEmpty(n[0].options.exceptDomains)};return n[0].options.onlyDomains!==void 0&&(_.value=n[0].options.onlyDomains),r=new Nl({props:_}),ee.push(()=>be(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Only domains",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),u.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[9]+".options.onlyDomains"),p(u,"class","help-block")},m(g,y){w(g,e,y),k(e,t),k(e,i),k(e,l),w(g,o,y),z(r,g,y),w(g,f,y),w(g,u,y),c=!0,d||(m=Se(Pe.call(null,l,{text:`List of domains that are ONLY allowed. - This field is disabled if "Except domains" is set.`,position:"top"})),d=!0)},p(g,y){(!c||y&512&&s!==(s=g[9]+".options.onlyDomains"))&&p(e,"for",s);const S={};y&512&&(S.id=g[9]+".options.onlyDomains"),y&1&&(S.disabled=!j.isEmpty(g[0].options.exceptDomains)),!a&&y&1&&(a=!0,S.value=g[0].options.onlyDomains,ke(()=>a=!1)),r.$set(S)},i(g){c||(E(r.$$.fragment,g),c=!0)},o(g){A(r.$$.fragment,g),c=!1},d(g){g&&(v(e),v(o),v(f),v(u)),V(r,g),d=!1,m()}}}function rT(n){let e,t,i,l,s,o,r;return i=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.exceptDomains",$$slots:{default:[sT,({uniqueId:a})=>({9:a}),({uniqueId:a})=>a?512:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.onlyDomains",$$slots:{default:[oT,({uniqueId:a})=>({9:a}),({uniqueId:a})=>a?512:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,f){w(a,e,f),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),r=!0},p(a,f){const u={};f&2&&(u.name="schema."+a[1]+".options.exceptDomains"),f&1537&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};f&2&&(c.name="schema."+a[1]+".options.onlyDomains"),f&1537&&(c.$$scope={dirty:f,ctx:a}),o.$set(c)},i(a){r||(E(i.$$.fragment,a),E(o.$$.fragment,a),r=!0)},o(a){A(i.$$.fragment,a),A(o.$$.fragment,a),r=!1},d(a){a&&v(e),V(i),V(o)}}}function aT(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[5](r)}let o={$$slots:{options:[rT]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[6]),e.$on("remove",n[7]),e.$on("duplicate",n[8]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};a&1027&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function fT(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(m){n.$$.not_equal(s.options.exceptDomains,m)&&(s.options.exceptDomains=m,t(0,s))}function a(m){n.$$.not_equal(s.options.onlyDomains,m)&&(s.options.onlyDomains=m,t(0,s))}function f(m){s=m,t(0,s)}function u(m){Ce.call(this,n,m)}function c(m){Ce.call(this,n,m)}function d(m){Ce.call(this,n,m)}return n.$$set=m=>{e=Ie(Ie({},e),Yt(m)),t(2,l=Ge(e,i)),"field"in m&&t(0,s=m.field),"key"in m&&t(1,o=m.key)},[s,o,l,r,a,f,u,c,d]}class Vb extends ge{constructor(e){super(),_e(this,e,fT,aT,me,{field:0,key:1})}}function uT(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[3](r)}let o={};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[4]),e.$on("remove",n[5]),e.$on("duplicate",n[6]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function cT(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(c){s=c,t(0,s)}function a(c){Ce.call(this,n,c)}function f(c){Ce.call(this,n,c)}function u(c){Ce.call(this,n,c)}return n.$$set=c=>{e=Ie(Ie({},e),Yt(c)),t(2,l=Ge(e,i)),"field"in c&&t(0,s=c.field),"key"in c&&t(1,o=c.key)},[s,o,l,r,a,f,u]}class dT extends ge{constructor(e){super(),_e(this,e,cT,uT,me,{field:0,key:1})}}function pT(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Strip urls domain",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[9]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[9])},m(c,d){w(c,e,d),e.checked=n[0].options.convertUrls,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[3]),Se(Pe.call(null,r,{text:"This could help making the editor content more portable between environments since there will be no local base url to replace."}))],f=!0)},p(c,d){d&512&&t!==(t=c[9])&&p(e,"id",t),d&1&&(e.checked=c[0].options.convertUrls),d&512&&a!==(a=c[9])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function mT(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",name:"schema."+n[1]+".options.convertUrls",$$slots:{default:[pT,({uniqueId:i})=>({9:i}),({uniqueId:i})=>i?512:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="schema."+i[1]+".options.convertUrls"),l&1537&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function hT(n){let e,t,i;const l=[{key:n[1]},n[2]];function s(r){n[4](r)}let o={$$slots:{optionsFooter:[mT]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[5]),e.$on("remove",n[6]),e.$on("duplicate",n[7]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&6?pt(l,[a&2&&{key:r[1]},a&4&&Ot(r[2])]):{};a&1027&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function _T(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;function r(){t(0,s.options={convertUrls:!1},s)}function a(){s.options.convertUrls=this.checked,t(0,s)}function f(m){s=m,t(0,s)}function u(m){Ce.call(this,n,m)}function c(m){Ce.call(this,n,m)}function d(m){Ce.call(this,n,m)}return n.$$set=m=>{e=Ie(Ie({},e),Yt(m)),t(2,l=Ge(e,i)),"field"in m&&t(0,s=m.field),"key"in m&&t(1,o=m.key)},n.$$.update=()=>{n.$$.dirty&1&&j.isEmpty(s.options)&&r()},[s,o,l,a,f,u,c,d]}class gT extends ge{constructor(e){super(),_e(this,e,_T,hT,me,{field:0,key:1})}}var Tr=["onChange","onClose","onDayCreate","onDestroy","onKeyDown","onMonthChange","onOpen","onParseConfig","onReady","onValueUpdate","onYearChange","onPreCalendarPosition"],wl={_disable:[],allowInput:!1,allowInvalidPreload:!1,altFormat:"F j, Y",altInput:!1,altInputClass:"form-control input",animate:typeof window=="object"&&window.navigator.userAgent.indexOf("MSIE")===-1,ariaDateFormat:"F j, Y",autoFillDefaultTime:!0,clickOpens:!0,closeOnSelect:!0,conjunction:", ",dateFormat:"Y-m-d",defaultHour:12,defaultMinute:0,defaultSeconds:0,disable:[],disableMobile:!1,enableSeconds:!1,enableTime:!1,errorHandler:function(n){return typeof console<"u"&&console.warn(n)},getWeek:function(n){var e=new Date(n.getTime());e.setHours(0,0,0,0),e.setDate(e.getDate()+3-(e.getDay()+6)%7);var t=new Date(e.getFullYear(),0,4);return 1+Math.round(((e.getTime()-t.getTime())/864e5-3+(t.getDay()+6)%7)/7)},hourIncrement:1,ignoredFocusElements:[],inline:!1,locale:"default",minuteIncrement:5,mode:"single",monthSelectorType:"dropdown",nextArrow:"",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},ms={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(n){var e=n%100;if(e>3&&e<21)return"th";switch(e%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",monthAriaLabel:"Month",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},pn=function(n,e){return e===void 0&&(e=2),("000"+n).slice(e*-1)},In=function(n){return n===!0?1:0};function nd(n,e){var t;return function(){var i=this,l=arguments;clearTimeout(t),t=setTimeout(function(){return n.apply(i,l)},e)}}var Cr=function(n){return n instanceof Array?n:[n]};function an(n,e,t){if(t===!0)return n.classList.add(e);n.classList.remove(e)}function dt(n,e,t){var i=window.document.createElement(n);return e=e||"",t=t||"",i.className=e,t!==void 0&&(i.textContent=t),i}function eo(n){for(;n.firstChild;)n.removeChild(n.firstChild)}function Bb(n,e){if(e(n))return n;if(n.parentNode)return Bb(n.parentNode,e)}function to(n,e){var t=dt("div","numInputWrapper"),i=dt("input","numInput "+n),l=dt("span","arrowUp"),s=dt("span","arrowDown");if(navigator.userAgent.indexOf("MSIE 9.0")===-1?i.type="number":(i.type="text",i.pattern="\\d*"),e!==void 0)for(var o in e)i.setAttribute(o,e[o]);return t.appendChild(i),t.appendChild(l),t.appendChild(s),t}function yn(n){try{if(typeof n.composedPath=="function"){var e=n.composedPath();return e[0]}return n.target}catch{return n.target}}var Or=function(){},Po=function(n,e,t){return t.months[e?"shorthand":"longhand"][n]},bT={D:Or,F:function(n,e,t){n.setMonth(t.months.longhand.indexOf(e))},G:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},H:function(n,e){n.setHours(parseFloat(e))},J:function(n,e){n.setDate(parseFloat(e))},K:function(n,e,t){n.setHours(n.getHours()%12+12*In(new RegExp(t.amPM[1],"i").test(e)))},M:function(n,e,t){n.setMonth(t.months.shorthand.indexOf(e))},S:function(n,e){n.setSeconds(parseFloat(e))},U:function(n,e){return new Date(parseFloat(e)*1e3)},W:function(n,e,t){var i=parseInt(e),l=new Date(n.getFullYear(),0,2+(i-1)*7,0,0,0,0);return l.setDate(l.getDate()-l.getDay()+t.firstDayOfWeek),l},Y:function(n,e){n.setFullYear(parseFloat(e))},Z:function(n,e){return new Date(e)},d:function(n,e){n.setDate(parseFloat(e))},h:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},i:function(n,e){n.setMinutes(parseFloat(e))},j:function(n,e){n.setDate(parseFloat(e))},l:Or,m:function(n,e){n.setMonth(parseFloat(e)-1)},n:function(n,e){n.setMonth(parseFloat(e)-1)},s:function(n,e){n.setSeconds(parseFloat(e))},u:function(n,e){return new Date(parseFloat(e))},w:Or,y:function(n,e){n.setFullYear(2e3+parseFloat(e))}},Bi={D:"",F:"",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},ns={Z:function(n){return n.toISOString()},D:function(n,e,t){return e.weekdays.shorthand[ns.w(n,e,t)]},F:function(n,e,t){return Po(ns.n(n,e,t)-1,!1,e)},G:function(n,e,t){return pn(ns.h(n,e,t))},H:function(n){return pn(n.getHours())},J:function(n,e){return e.ordinal!==void 0?n.getDate()+e.ordinal(n.getDate()):n.getDate()},K:function(n,e){return e.amPM[In(n.getHours()>11)]},M:function(n,e){return Po(n.getMonth(),!0,e)},S:function(n){return pn(n.getSeconds())},U:function(n){return n.getTime()/1e3},W:function(n,e,t){return t.getWeek(n)},Y:function(n){return pn(n.getFullYear(),4)},d:function(n){return pn(n.getDate())},h:function(n){return n.getHours()%12?n.getHours()%12:12},i:function(n){return pn(n.getMinutes())},j:function(n){return n.getDate()},l:function(n,e){return e.weekdays.longhand[n.getDay()]},m:function(n){return pn(n.getMonth()+1)},n:function(n){return n.getMonth()+1},s:function(n){return n.getSeconds()},u:function(n){return n.getTime()},w:function(n){return n.getDay()},y:function(n){return String(n.getFullYear()).substring(2)}},Ub=function(n){var e=n.config,t=e===void 0?wl:e,i=n.l10n,l=i===void 0?ms:i,s=n.isMobile,o=s===void 0?!1:s;return function(r,a,f){var u=f||l;return t.formatDate!==void 0&&!o?t.formatDate(r,a,u):a.split("").map(function(c,d,m){return ns[c]&&m[d-1]!=="\\"?ns[c](r,u,t):c!=="\\"?c:""}).join("")}},sa=function(n){var e=n.config,t=e===void 0?wl:e,i=n.l10n,l=i===void 0?ms:i;return function(s,o,r,a){if(!(s!==0&&!s)){var f=a||l,u,c=s;if(s instanceof Date)u=new Date(s.getTime());else if(typeof s!="string"&&s.toFixed!==void 0)u=new Date(s);else if(typeof s=="string"){var d=o||(t||wl).dateFormat,m=String(s).trim();if(m==="today")u=new Date,r=!0;else if(t&&t.parseDate)u=t.parseDate(s,d);else if(/Z$/.test(m)||/GMT$/.test(m))u=new Date(s);else{for(var h=void 0,_=[],g=0,y=0,S="";gMath.min(e,t)&&n=0?new Date:new Date(t.config.minDate.getTime()),ne=Dr(t.config);X.setHours(ne.hours,ne.minutes,ne.seconds,X.getMilliseconds()),t.selectedDates=[X],t.latestSelectedDateObj=X}Z!==void 0&&Z.type!=="blur"&&oi(Z);var fe=t._input.value;c(),Kt(),t._input.value!==fe&&t._debouncedChange()}function f(Z,X){return Z%12+12*In(X===t.l10n.amPM[1])}function u(Z){switch(Z%24){case 0:case 12:return 12;default:return Z%12}}function c(){if(!(t.hourElement===void 0||t.minuteElement===void 0)){var Z=(parseInt(t.hourElement.value.slice(-2),10)||0)%24,X=(parseInt(t.minuteElement.value,10)||0)%60,ne=t.secondElement!==void 0?(parseInt(t.secondElement.value,10)||0)%60:0;t.amPM!==void 0&&(Z=f(Z,t.amPM.textContent));var fe=t.config.minTime!==void 0||t.config.minDate&&t.minDateHasTime&&t.latestSelectedDateObj&&vn(t.latestSelectedDateObj,t.config.minDate,!0)===0,Te=t.config.maxTime!==void 0||t.config.maxDate&&t.maxDateHasTime&&t.latestSelectedDateObj&&vn(t.latestSelectedDateObj,t.config.maxDate,!0)===0;if(t.config.maxTime!==void 0&&t.config.minTime!==void 0&&t.config.minTime>t.config.maxTime){var Le=Mr(t.config.minTime.getHours(),t.config.minTime.getMinutes(),t.config.minTime.getSeconds()),Je=Mr(t.config.maxTime.getHours(),t.config.maxTime.getMinutes(),t.config.maxTime.getSeconds()),qe=Mr(Z,X,ne);if(qe>Je&&qe=12)]),t.secondElement!==void 0&&(t.secondElement.value=pn(ne)))}function h(Z){var X=yn(Z),ne=parseInt(X.value)+(Z.delta||0);(ne/1e3>1||Z.key==="Enter"&&!/[^\d]/.test(ne.toString()))&&Mt(ne)}function _(Z,X,ne,fe){if(X instanceof Array)return X.forEach(function(Te){return _(Z,Te,ne,fe)});if(Z instanceof Array)return Z.forEach(function(Te){return _(Te,X,ne,fe)});Z.addEventListener(X,ne,fe),t._handlers.push({remove:function(){return Z.removeEventListener(X,ne,fe)}})}function g(){bt("onChange")}function y(){if(t.config.wrap&&["open","close","toggle","clear"].forEach(function(ne){Array.prototype.forEach.call(t.element.querySelectorAll("[data-"+ne+"]"),function(fe){return _(fe,"click",t[ne])})}),t.isMobile){al();return}var Z=nd(ze,50);if(t._debouncedChange=nd(g,wT),t.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&_(t.daysContainer,"mouseover",function(ne){t.config.mode==="range"&&Ae(yn(ne))}),_(t._input,"keydown",De),t.calendarContainer!==void 0&&_(t.calendarContainer,"keydown",De),!t.config.inline&&!t.config.static&&_(window,"resize",Z),window.ontouchstart!==void 0?_(window.document,"touchstart",xe):_(window.document,"mousedown",xe),_(window.document,"focus",xe,{capture:!0}),t.config.clickOpens===!0&&(_(t._input,"focus",t.open),_(t._input,"click",t.open)),t.daysContainer!==void 0&&(_(t.monthNav,"click",ut),_(t.monthNav,["keyup","increment"],h),_(t.daysContainer,"click",ol)),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0){var X=function(ne){return yn(ne).select()};_(t.timeContainer,["increment"],a),_(t.timeContainer,"blur",a,{capture:!0}),_(t.timeContainer,"click",T),_([t.hourElement,t.minuteElement],["focus","click"],X),t.secondElement!==void 0&&_(t.secondElement,"focus",function(){return t.secondElement&&t.secondElement.select()}),t.amPM!==void 0&&_(t.amPM,"click",function(ne){a(ne)})}t.config.allowInput&&_(t._input,"blur",Gt)}function S(Z,X){var ne=Z!==void 0?t.parseDate(Z):t.latestSelectedDateObj||(t.config.minDate&&t.config.minDate>t.now?t.config.minDate:t.config.maxDate&&t.config.maxDate1),t.calendarContainer.appendChild(Z);var Te=t.config.appendTo!==void 0&&t.config.appendTo.nodeType!==void 0;if((t.config.inline||t.config.static)&&(t.calendarContainer.classList.add(t.config.inline?"inline":"static"),t.config.inline&&(!Te&&t.element.parentNode?t.element.parentNode.insertBefore(t.calendarContainer,t._input.nextSibling):t.config.appendTo!==void 0&&t.config.appendTo.appendChild(t.calendarContainer)),t.config.static)){var Le=dt("div","flatpickr-wrapper");t.element.parentNode&&t.element.parentNode.insertBefore(Le,t.element),Le.appendChild(t.element),t.altInput&&Le.appendChild(t.altInput),Le.appendChild(t.calendarContainer)}!t.config.static&&!t.config.inline&&(t.config.appendTo!==void 0?t.config.appendTo:window.document.body).appendChild(t.calendarContainer)}function O(Z,X,ne,fe){var Te=ft(X,!0),Le=dt("span",Z,X.getDate().toString());return Le.dateObj=X,Le.$i=fe,Le.setAttribute("aria-label",t.formatDate(X,t.config.ariaDateFormat)),Z.indexOf("hidden")===-1&&vn(X,t.now)===0&&(t.todayDateElem=Le,Le.classList.add("today"),Le.setAttribute("aria-current","date")),Te?(Le.tabIndex=-1,Mn(X)&&(Le.classList.add("selected"),t.selectedDateElem=Le,t.config.mode==="range"&&(an(Le,"startRange",t.selectedDates[0]&&vn(X,t.selectedDates[0],!0)===0),an(Le,"endRange",t.selectedDates[1]&&vn(X,t.selectedDates[1],!0)===0),Z==="nextMonthDay"&&Le.classList.add("inRange")))):Le.classList.add("flatpickr-disabled"),t.config.mode==="range"&&he(X)&&!Mn(X)&&Le.classList.add("inRange"),t.weekNumbers&&t.config.showMonths===1&&Z!=="prevMonthDay"&&fe%7===6&&t.weekNumbers.insertAdjacentHTML("beforeend",""+t.config.getWeek(X)+""),bt("onDayCreate",Le),Le}function D(Z){Z.focus(),t.config.mode==="range"&&Ae(Z)}function I(Z){for(var X=Z>0?0:t.config.showMonths-1,ne=Z>0?t.config.showMonths:-1,fe=X;fe!=ne;fe+=Z)for(var Te=t.daysContainer.children[fe],Le=Z>0?0:Te.children.length-1,Je=Z>0?Te.children.length:-1,qe=Le;qe!=Je;qe+=Z){var Qe=Te.children[qe];if(Qe.className.indexOf("hidden")===-1&&ft(Qe.dateObj))return Qe}}function L(Z,X){for(var ne=Z.className.indexOf("Month")===-1?Z.dateObj.getMonth():t.currentMonth,fe=X>0?t.config.showMonths:-1,Te=X>0?1:-1,Le=ne-t.currentMonth;Le!=fe;Le+=Te)for(var Je=t.daysContainer.children[Le],qe=ne-t.currentMonth===Le?Z.$i+X:X<0?Je.children.length-1:0,Qe=Je.children.length,Re=qe;Re>=0&&Re0?Qe:-1);Re+=Te){var Ve=Je.children[Re];if(Ve.className.indexOf("hidden")===-1&&ft(Ve.dateObj)&&Math.abs(Z.$i-Re)>=Math.abs(X))return D(Ve)}t.changeMonth(Te),R(I(Te),0)}function R(Z,X){var ne=s(),fe=mt(ne||document.body),Te=Z!==void 0?Z:fe?ne:t.selectedDateElem!==void 0&&mt(t.selectedDateElem)?t.selectedDateElem:t.todayDateElem!==void 0&&mt(t.todayDateElem)?t.todayDateElem:I(X>0?1:-1);Te===void 0?t._input.focus():fe?L(Te,X):D(Te)}function F(Z,X){for(var ne=(new Date(Z,X,1).getDay()-t.l10n.firstDayOfWeek+7)%7,fe=t.utils.getDaysInMonth((X-1+12)%12,Z),Te=t.utils.getDaysInMonth(X,Z),Le=window.document.createDocumentFragment(),Je=t.config.showMonths>1,qe=Je?"prevMonthDay hidden":"prevMonthDay",Qe=Je?"nextMonthDay hidden":"nextMonthDay",Re=fe+1-ne,Ve=0;Re<=fe;Re++,Ve++)Le.appendChild(O("flatpickr-day "+qe,new Date(Z,X-1,Re),Re,Ve));for(Re=1;Re<=Te;Re++,Ve++)Le.appendChild(O("flatpickr-day",new Date(Z,X,Re),Re,Ve));for(var yt=Te+1;yt<=42-ne&&(t.config.showMonths===1||Ve%7!==0);yt++,Ve++)Le.appendChild(O("flatpickr-day "+Qe,new Date(Z,X+1,yt%Te),yt,Ve));var Jn=dt("div","dayContainer");return Jn.appendChild(Le),Jn}function N(){if(t.daysContainer!==void 0){eo(t.daysContainer),t.weekNumbers&&eo(t.weekNumbers);for(var Z=document.createDocumentFragment(),X=0;X1||t.config.monthSelectorType!=="dropdown")){var Z=function(fe){return t.config.minDate!==void 0&&t.currentYear===t.config.minDate.getFullYear()&&fet.config.maxDate.getMonth())};t.monthsDropdownContainer.tabIndex=-1,t.monthsDropdownContainer.innerHTML="";for(var X=0;X<12;X++)if(Z(X)){var ne=dt("option","flatpickr-monthDropdown-month");ne.value=new Date(t.currentYear,X).getMonth().toString(),ne.textContent=Po(X,t.config.shorthandCurrentMonth,t.l10n),ne.tabIndex=-1,t.currentMonth===X&&(ne.selected=!0),t.monthsDropdownContainer.appendChild(ne)}}}function q(){var Z=dt("div","flatpickr-month"),X=window.document.createDocumentFragment(),ne;t.config.showMonths>1||t.config.monthSelectorType==="static"?ne=dt("span","cur-month"):(t.monthsDropdownContainer=dt("select","flatpickr-monthDropdown-months"),t.monthsDropdownContainer.setAttribute("aria-label",t.l10n.monthAriaLabel),_(t.monthsDropdownContainer,"change",function(Je){var qe=yn(Je),Qe=parseInt(qe.value,10);t.changeMonth(Qe-t.currentMonth),bt("onMonthChange")}),P(),ne=t.monthsDropdownContainer);var fe=to("cur-year",{tabindex:"-1"}),Te=fe.getElementsByTagName("input")[0];Te.setAttribute("aria-label",t.l10n.yearAriaLabel),t.config.minDate&&Te.setAttribute("min",t.config.minDate.getFullYear().toString()),t.config.maxDate&&(Te.setAttribute("max",t.config.maxDate.getFullYear().toString()),Te.disabled=!!t.config.minDate&&t.config.minDate.getFullYear()===t.config.maxDate.getFullYear());var Le=dt("div","flatpickr-current-month");return Le.appendChild(ne),Le.appendChild(fe),X.appendChild(Le),Z.appendChild(X),{container:Z,yearElement:Te,monthElement:ne}}function H(){eo(t.monthNav),t.monthNav.appendChild(t.prevMonthNav),t.config.showMonths&&(t.yearElements=[],t.monthElements=[]);for(var Z=t.config.showMonths;Z--;){var X=q();t.yearElements.push(X.yearElement),t.monthElements.push(X.monthElement),t.monthNav.appendChild(X.container)}t.monthNav.appendChild(t.nextMonthNav)}function W(){return t.monthNav=dt("div","flatpickr-months"),t.yearElements=[],t.monthElements=[],t.prevMonthNav=dt("span","flatpickr-prev-month"),t.prevMonthNav.innerHTML=t.config.prevArrow,t.nextMonthNav=dt("span","flatpickr-next-month"),t.nextMonthNav.innerHTML=t.config.nextArrow,H(),Object.defineProperty(t,"_hidePrevMonthArrow",{get:function(){return t.__hidePrevMonthArrow},set:function(Z){t.__hidePrevMonthArrow!==Z&&(an(t.prevMonthNav,"flatpickr-disabled",Z),t.__hidePrevMonthArrow=Z)}}),Object.defineProperty(t,"_hideNextMonthArrow",{get:function(){return t.__hideNextMonthArrow},set:function(Z){t.__hideNextMonthArrow!==Z&&(an(t.nextMonthNav,"flatpickr-disabled",Z),t.__hideNextMonthArrow=Z)}}),t.currentYearElement=t.yearElements[0],Oe(),t.monthNav}function G(){t.calendarContainer.classList.add("hasTime"),t.config.noCalendar&&t.calendarContainer.classList.add("noCalendar");var Z=Dr(t.config);t.timeContainer=dt("div","flatpickr-time"),t.timeContainer.tabIndex=-1;var X=dt("span","flatpickr-time-separator",":"),ne=to("flatpickr-hour",{"aria-label":t.l10n.hourAriaLabel});t.hourElement=ne.getElementsByTagName("input")[0];var fe=to("flatpickr-minute",{"aria-label":t.l10n.minuteAriaLabel});if(t.minuteElement=fe.getElementsByTagName("input")[0],t.hourElement.tabIndex=t.minuteElement.tabIndex=-1,t.hourElement.value=pn(t.latestSelectedDateObj?t.latestSelectedDateObj.getHours():t.config.time_24hr?Z.hours:u(Z.hours)),t.minuteElement.value=pn(t.latestSelectedDateObj?t.latestSelectedDateObj.getMinutes():Z.minutes),t.hourElement.setAttribute("step",t.config.hourIncrement.toString()),t.minuteElement.setAttribute("step",t.config.minuteIncrement.toString()),t.hourElement.setAttribute("min",t.config.time_24hr?"0":"1"),t.hourElement.setAttribute("max",t.config.time_24hr?"23":"12"),t.hourElement.setAttribute("maxlength","2"),t.minuteElement.setAttribute("min","0"),t.minuteElement.setAttribute("max","59"),t.minuteElement.setAttribute("maxlength","2"),t.timeContainer.appendChild(ne),t.timeContainer.appendChild(X),t.timeContainer.appendChild(fe),t.config.time_24hr&&t.timeContainer.classList.add("time24hr"),t.config.enableSeconds){t.timeContainer.classList.add("hasSeconds");var Te=to("flatpickr-second");t.secondElement=Te.getElementsByTagName("input")[0],t.secondElement.value=pn(t.latestSelectedDateObj?t.latestSelectedDateObj.getSeconds():Z.seconds),t.secondElement.setAttribute("step",t.minuteElement.getAttribute("step")),t.secondElement.setAttribute("min","0"),t.secondElement.setAttribute("max","59"),t.secondElement.setAttribute("maxlength","2"),t.timeContainer.appendChild(dt("span","flatpickr-time-separator",":")),t.timeContainer.appendChild(Te)}return t.config.time_24hr||(t.amPM=dt("span","flatpickr-am-pm",t.l10n.amPM[In((t.latestSelectedDateObj?t.hourElement.value:t.config.defaultHour)>11)]),t.amPM.title=t.l10n.toggleTitle,t.amPM.tabIndex=-1,t.timeContainer.appendChild(t.amPM)),t.timeContainer}function U(){t.weekdayContainer?eo(t.weekdayContainer):t.weekdayContainer=dt("div","flatpickr-weekdays");for(var Z=t.config.showMonths;Z--;){var X=dt("div","flatpickr-weekdaycontainer");t.weekdayContainer.appendChild(X)}return Y(),t.weekdayContainer}function Y(){if(t.weekdayContainer){var Z=t.l10n.firstDayOfWeek,X=id(t.l10n.weekdays.shorthand);Z>0&&Z - `+X.join("")+` - - `}}function ie(){t.calendarContainer.classList.add("hasWeeks");var Z=dt("div","flatpickr-weekwrapper");Z.appendChild(dt("span","flatpickr-weekday",t.l10n.weekAbbreviation));var X=dt("div","flatpickr-weeks");return Z.appendChild(X),{weekWrapper:Z,weekNumbers:X}}function te(Z,X){X===void 0&&(X=!0);var ne=X?Z:Z-t.currentMonth;ne<0&&t._hidePrevMonthArrow===!0||ne>0&&t._hideNextMonthArrow===!0||(t.currentMonth+=ne,(t.currentMonth<0||t.currentMonth>11)&&(t.currentYear+=t.currentMonth>11?1:-1,t.currentMonth=(t.currentMonth+12)%12,bt("onYearChange"),P()),N(),bt("onMonthChange"),Oe())}function pe(Z,X){if(Z===void 0&&(Z=!0),X===void 0&&(X=!0),t.input.value="",t.altInput!==void 0&&(t.altInput.value=""),t.mobileInput!==void 0&&(t.mobileInput.value=""),t.selectedDates=[],t.latestSelectedDateObj=void 0,X===!0&&(t.currentYear=t._initialDate.getFullYear(),t.currentMonth=t._initialDate.getMonth()),t.config.enableTime===!0){var ne=Dr(t.config),fe=ne.hours,Te=ne.minutes,Le=ne.seconds;m(fe,Te,Le)}t.redraw(),Z&&bt("onChange")}function Ne(){t.isOpen=!1,t.isMobile||(t.calendarContainer!==void 0&&t.calendarContainer.classList.remove("open"),t._input!==void 0&&t._input.classList.remove("active")),bt("onClose")}function He(){t.config!==void 0&&bt("onDestroy");for(var Z=t._handlers.length;Z--;)t._handlers[Z].remove();if(t._handlers=[],t.mobileInput)t.mobileInput.parentNode&&t.mobileInput.parentNode.removeChild(t.mobileInput),t.mobileInput=void 0;else if(t.calendarContainer&&t.calendarContainer.parentNode)if(t.config.static&&t.calendarContainer.parentNode){var X=t.calendarContainer.parentNode;if(X.lastChild&&X.removeChild(X.lastChild),X.parentNode){for(;X.firstChild;)X.parentNode.insertBefore(X.firstChild,X);X.parentNode.removeChild(X)}}else t.calendarContainer.parentNode.removeChild(t.calendarContainer);t.altInput&&(t.input.type="text",t.altInput.parentNode&&t.altInput.parentNode.removeChild(t.altInput),delete t.altInput),t.input&&(t.input.type=t.input._type,t.input.classList.remove("flatpickr-input"),t.input.removeAttribute("readonly")),["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach(function(ne){try{delete t[ne]}catch{}})}function Xe(Z){return t.calendarContainer.contains(Z)}function xe(Z){if(t.isOpen&&!t.config.inline){var X=yn(Z),ne=Xe(X),fe=X===t.input||X===t.altInput||t.element.contains(X)||Z.path&&Z.path.indexOf&&(~Z.path.indexOf(t.input)||~Z.path.indexOf(t.altInput)),Te=!fe&&!ne&&!Xe(Z.relatedTarget),Le=!t.config.ignoredFocusElements.some(function(Je){return Je.contains(X)});Te&&Le&&(t.config.allowInput&&t.setDate(t._input.value,!1,t.config.altInput?t.config.altFormat:t.config.dateFormat),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0&&t.input.value!==""&&t.input.value!==void 0&&a(),t.close(),t.config&&t.config.mode==="range"&&t.selectedDates.length===1&&t.clear(!1))}}function Mt(Z){if(!(!Z||t.config.minDate&&Zt.config.maxDate.getFullYear())){var X=Z,ne=t.currentYear!==X;t.currentYear=X||t.currentYear,t.config.maxDate&&t.currentYear===t.config.maxDate.getFullYear()?t.currentMonth=Math.min(t.config.maxDate.getMonth(),t.currentMonth):t.config.minDate&&t.currentYear===t.config.minDate.getFullYear()&&(t.currentMonth=Math.max(t.config.minDate.getMonth(),t.currentMonth)),ne&&(t.redraw(),bt("onYearChange"),P())}}function ft(Z,X){var ne;X===void 0&&(X=!0);var fe=t.parseDate(Z,void 0,X);if(t.config.minDate&&fe&&vn(fe,t.config.minDate,X!==void 0?X:!t.minDateHasTime)<0||t.config.maxDate&&fe&&vn(fe,t.config.maxDate,X!==void 0?X:!t.maxDateHasTime)>0)return!1;if(!t.config.enable&&t.config.disable.length===0)return!0;if(fe===void 0)return!1;for(var Te=!!t.config.enable,Le=(ne=t.config.enable)!==null&&ne!==void 0?ne:t.config.disable,Je=0,qe=void 0;Je=qe.from.getTime()&&fe.getTime()<=qe.to.getTime())return Te}return!Te}function mt(Z){return t.daysContainer!==void 0?Z.className.indexOf("hidden")===-1&&Z.className.indexOf("flatpickr-disabled")===-1&&t.daysContainer.contains(Z):!1}function Gt(Z){var X=Z.target===t._input,ne=t._input.value.trimEnd()!==ht();X&&ne&&!(Z.relatedTarget&&Xe(Z.relatedTarget))&&t.setDate(t._input.value,!0,Z.target===t.altInput?t.config.altFormat:t.config.dateFormat)}function De(Z){var X=yn(Z),ne=t.config.wrap?n.contains(X):X===t._input,fe=t.config.allowInput,Te=t.isOpen&&(!fe||!ne),Le=t.config.inline&&ne&&!fe;if(Z.keyCode===13&&ne){if(fe)return t.setDate(t._input.value,!0,X===t.altInput?t.config.altFormat:t.config.dateFormat),t.close(),X.blur();t.open()}else if(Xe(X)||Te||Le){var Je=!!t.timeContainer&&t.timeContainer.contains(X);switch(Z.keyCode){case 13:Je?(Z.preventDefault(),a(),Ai()):ol(Z);break;case 27:Z.preventDefault(),Ai();break;case 8:case 46:ne&&!t.config.allowInput&&(Z.preventDefault(),t.clear());break;case 37:case 39:if(!Je&&!ne){Z.preventDefault();var qe=s();if(t.daysContainer!==void 0&&(fe===!1||qe&&mt(qe))){var Qe=Z.keyCode===39?1:-1;Z.ctrlKey?(Z.stopPropagation(),te(Qe),R(I(1),0)):R(void 0,Qe)}}else t.hourElement&&t.hourElement.focus();break;case 38:case 40:Z.preventDefault();var Re=Z.keyCode===40?1:-1;t.daysContainer&&X.$i!==void 0||X===t.input||X===t.altInput?Z.ctrlKey?(Z.stopPropagation(),Mt(t.currentYear-Re),R(I(1),0)):Je||R(void 0,Re*7):X===t.currentYearElement?Mt(t.currentYear-Re):t.config.enableTime&&(!Je&&t.hourElement&&t.hourElement.focus(),a(Z),t._debouncedChange());break;case 9:if(Je){var Ve=[t.hourElement,t.minuteElement,t.secondElement,t.amPM].concat(t.pluginElements).filter(function(kn){return kn}),yt=Ve.indexOf(X);if(yt!==-1){var Jn=Ve[yt+(Z.shiftKey?-1:1)];Z.preventDefault(),(Jn||t._input).focus()}}else!t.config.noCalendar&&t.daysContainer&&t.daysContainer.contains(X)&&Z.shiftKey&&(Z.preventDefault(),t._input.focus());break}}if(t.amPM!==void 0&&X===t.amPM)switch(Z.key){case t.l10n.amPM[0].charAt(0):case t.l10n.amPM[0].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[0],c(),Kt();break;case t.l10n.amPM[1].charAt(0):case t.l10n.amPM[1].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[1],c(),Kt();break}(ne||Xe(X))&&bt("onKeyDown",Z)}function Ae(Z,X){if(X===void 0&&(X="flatpickr-day"),!(t.selectedDates.length!==1||Z&&(!Z.classList.contains(X)||Z.classList.contains("flatpickr-disabled")))){for(var ne=Z?Z.dateObj.getTime():t.days.firstElementChild.dateObj.getTime(),fe=t.parseDate(t.selectedDates[0],void 0,!0).getTime(),Te=Math.min(ne,t.selectedDates[0].getTime()),Le=Math.max(ne,t.selectedDates[0].getTime()),Je=!1,qe=0,Qe=0,Re=Te;ReTe&&Reqe)?qe=Re:Re>fe&&(!Qe||Re ."+X));Ve.forEach(function(yt){var Jn=yt.dateObj,kn=Jn.getTime(),Fl=qe>0&&kn0&&kn>Qe;if(Fl){yt.classList.add("notAllowed"),["inRange","startRange","endRange"].forEach(function(ul){yt.classList.remove(ul)});return}else if(Je&&!Fl)return;["startRange","inRange","endRange","notAllowed"].forEach(function(ul){yt.classList.remove(ul)}),Z!==void 0&&(Z.classList.add(ne<=t.selectedDates[0].getTime()?"startRange":"endRange"),fene&&kn===fe&&yt.classList.add("endRange"),kn>=qe&&(Qe===0||kn<=Qe)&&kT(kn,fe,ne)&&yt.classList.add("inRange"))})}}function ze(){t.isOpen&&!t.config.static&&!t.config.inline&&zt()}function gt(Z,X){if(X===void 0&&(X=t._positionElement),t.isMobile===!0){if(Z){Z.preventDefault();var ne=yn(Z);ne&&ne.blur()}t.mobileInput!==void 0&&(t.mobileInput.focus(),t.mobileInput.click()),bt("onOpen");return}else if(t._input.disabled||t.config.inline)return;var fe=t.isOpen;t.isOpen=!0,fe||(t.calendarContainer.classList.add("open"),t._input.classList.add("active"),bt("onOpen"),zt(X)),t.config.enableTime===!0&&t.config.noCalendar===!0&&t.config.allowInput===!1&&(Z===void 0||!t.timeContainer.contains(Z.relatedTarget))&&setTimeout(function(){return t.hourElement.select()},50)}function de(Z){return function(X){var ne=t.config["_"+Z+"Date"]=t.parseDate(X,t.config.dateFormat),fe=t.config["_"+(Z==="min"?"max":"min")+"Date"];ne!==void 0&&(t[Z==="min"?"minDateHasTime":"maxDateHasTime"]=ne.getHours()>0||ne.getMinutes()>0||ne.getSeconds()>0),t.selectedDates&&(t.selectedDates=t.selectedDates.filter(function(Te){return ft(Te)}),!t.selectedDates.length&&Z==="min"&&d(ne),Kt()),t.daysContainer&&(qn(),ne!==void 0?t.currentYearElement[Z]=ne.getFullYear().toString():t.currentYearElement.removeAttribute(Z),t.currentYearElement.disabled=!!fe&&ne!==void 0&&fe.getFullYear()===ne.getFullYear())}}function ve(){var Z=["wrap","weekNumbers","allowInput","allowInvalidPreload","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],X=tn(tn({},JSON.parse(JSON.stringify(n.dataset||{}))),e),ne={};t.config.parseDate=X.parseDate,t.config.formatDate=X.formatDate,Object.defineProperty(t.config,"enable",{get:function(){return t.config._enable},set:function(Ve){t.config._enable=Kn(Ve)}}),Object.defineProperty(t.config,"disable",{get:function(){return t.config._disable},set:function(Ve){t.config._disable=Kn(Ve)}});var fe=X.mode==="time";if(!X.dateFormat&&(X.enableTime||fe)){var Te=Bt.defaultConfig.dateFormat||wl.dateFormat;ne.dateFormat=X.noCalendar||fe?"H:i"+(X.enableSeconds?":S":""):Te+" H:i"+(X.enableSeconds?":S":"")}if(X.altInput&&(X.enableTime||fe)&&!X.altFormat){var Le=Bt.defaultConfig.altFormat||wl.altFormat;ne.altFormat=X.noCalendar||fe?"h:i"+(X.enableSeconds?":S K":" K"):Le+(" h:i"+(X.enableSeconds?":S":"")+" K")}Object.defineProperty(t.config,"minDate",{get:function(){return t.config._minDate},set:de("min")}),Object.defineProperty(t.config,"maxDate",{get:function(){return t.config._maxDate},set:de("max")});var Je=function(Ve){return function(yt){t.config[Ve==="min"?"_minTime":"_maxTime"]=t.parseDate(yt,"H:i:S")}};Object.defineProperty(t.config,"minTime",{get:function(){return t.config._minTime},set:Je("min")}),Object.defineProperty(t.config,"maxTime",{get:function(){return t.config._maxTime},set:Je("max")}),X.mode==="time"&&(t.config.noCalendar=!0,t.config.enableTime=!0),Object.assign(t.config,ne,X);for(var qe=0;qe-1?t.config[Re]=Cr(Qe[Re]).map(o).concat(t.config[Re]):typeof X[Re]>"u"&&(t.config[Re]=Qe[Re])}X.altInputClass||(t.config.altInputClass=we().className+" "+t.config.altInputClass),bt("onParseConfig")}function we(){return t.config.wrap?n.querySelector("[data-input]"):n}function Ye(){typeof t.config.locale!="object"&&typeof Bt.l10ns[t.config.locale]>"u"&&t.config.errorHandler(new Error("flatpickr: invalid locale "+t.config.locale)),t.l10n=tn(tn({},Bt.l10ns.default),typeof t.config.locale=="object"?t.config.locale:t.config.locale!=="default"?Bt.l10ns[t.config.locale]:void 0),Bi.D="("+t.l10n.weekdays.shorthand.join("|")+")",Bi.l="("+t.l10n.weekdays.longhand.join("|")+")",Bi.M="("+t.l10n.months.shorthand.join("|")+")",Bi.F="("+t.l10n.months.longhand.join("|")+")",Bi.K="("+t.l10n.amPM[0]+"|"+t.l10n.amPM[1]+"|"+t.l10n.amPM[0].toLowerCase()+"|"+t.l10n.amPM[1].toLowerCase()+")";var Z=tn(tn({},e),JSON.parse(JSON.stringify(n.dataset||{})));Z.time_24hr===void 0&&Bt.defaultConfig.time_24hr===void 0&&(t.config.time_24hr=t.l10n.time_24hr),t.formatDate=Ub(t),t.parseDate=sa({config:t.config,l10n:t.l10n})}function zt(Z){if(typeof t.config.position=="function")return void t.config.position(t,Z);if(t.calendarContainer!==void 0){bt("onPreCalendarPosition");var X=Z||t._positionElement,ne=Array.prototype.reduce.call(t.calendarContainer.children,function(r0,a0){return r0+a0.offsetHeight},0),fe=t.calendarContainer.offsetWidth,Te=t.config.position.split(" "),Le=Te[0],Je=Te.length>1?Te[1]:null,qe=X.getBoundingClientRect(),Qe=window.innerHeight-qe.bottom,Re=Le==="above"||Le!=="below"&&Qene,Ve=window.pageYOffset+qe.top+(Re?-ne-2:X.offsetHeight+2);if(an(t.calendarContainer,"arrowTop",!Re),an(t.calendarContainer,"arrowBottom",Re),!t.config.inline){var yt=window.pageXOffset+qe.left,Jn=!1,kn=!1;Je==="center"?(yt-=(fe-qe.width)/2,Jn=!0):Je==="right"&&(yt-=fe-qe.width,kn=!0),an(t.calendarContainer,"arrowLeft",!Jn&&!kn),an(t.calendarContainer,"arrowCenter",Jn),an(t.calendarContainer,"arrowRight",kn);var Fl=window.document.body.offsetWidth-(window.pageXOffset+qe.right),ul=yt+fe>window.document.body.offsetWidth,e0=Fl+fe>window.document.body.offsetWidth;if(an(t.calendarContainer,"rightMost",ul),!t.config.static)if(t.calendarContainer.style.top=Ve+"px",!ul)t.calendarContainer.style.left=yt+"px",t.calendarContainer.style.right="auto";else if(!e0)t.calendarContainer.style.left="auto",t.calendarContainer.style.right=Fl+"px";else{var er=cn();if(er===void 0)return;var t0=window.document.body.offsetWidth,n0=Math.max(0,t0/2-fe/2),i0=".flatpickr-calendar.centerMost:before",l0=".flatpickr-calendar.centerMost:after",s0=er.cssRules.length,o0="{left:"+qe.left+"px;right:auto;}";an(t.calendarContainer,"rightMost",!1),an(t.calendarContainer,"centerMost",!0),er.insertRule(i0+","+l0+o0,s0),t.calendarContainer.style.left=n0+"px",t.calendarContainer.style.right="auto"}}}}function cn(){for(var Z=null,X=0;Xt.currentMonth+t.config.showMonths-1)&&t.config.mode!=="range";if(t.selectedDateElem=fe,t.config.mode==="single")t.selectedDates=[Te];else if(t.config.mode==="multiple"){var Je=Mn(Te);Je?t.selectedDates.splice(parseInt(Je),1):t.selectedDates.push(Te)}else t.config.mode==="range"&&(t.selectedDates.length===2&&t.clear(!1,!1),t.latestSelectedDateObj=Te,t.selectedDates.push(Te),vn(Te,t.selectedDates[0],!0)!==0&&t.selectedDates.sort(function(Ve,yt){return Ve.getTime()-yt.getTime()}));if(c(),Le){var qe=t.currentYear!==Te.getFullYear();t.currentYear=Te.getFullYear(),t.currentMonth=Te.getMonth(),qe&&(bt("onYearChange"),P()),bt("onMonthChange")}if(Oe(),N(),Kt(),!Le&&t.config.mode!=="range"&&t.config.showMonths===1?D(fe):t.selectedDateElem!==void 0&&t.hourElement===void 0&&t.selectedDateElem&&t.selectedDateElem.focus(),t.hourElement!==void 0&&t.hourElement!==void 0&&t.hourElement.focus(),t.config.closeOnSelect){var Qe=t.config.mode==="single"&&!t.config.enableTime,Re=t.config.mode==="range"&&t.selectedDates.length===2&&!t.config.enableTime;(Qe||Re)&&Ai()}g()}}var gi={locale:[Ye,Y],showMonths:[H,r,U],minDate:[S],maxDate:[S],positionElement:[bi],clickOpens:[function(){t.config.clickOpens===!0?(_(t._input,"focus",t.open),_(t._input,"click",t.open)):(t._input.removeEventListener("focus",t.open),t._input.removeEventListener("click",t.open))}]};function Ee(Z,X){if(Z!==null&&typeof Z=="object"){Object.assign(t.config,Z);for(var ne in Z)gi[ne]!==void 0&&gi[ne].forEach(function(fe){return fe()})}else t.config[Z]=X,gi[Z]!==void 0?gi[Z].forEach(function(fe){return fe()}):Tr.indexOf(Z)>-1&&(t.config[Z]=Cr(X));t.redraw(),Kt(!0)}function Nt(Z,X){var ne=[];if(Z instanceof Array)ne=Z.map(function(fe){return t.parseDate(fe,X)});else if(Z instanceof Date||typeof Z=="number")ne=[t.parseDate(Z,X)];else if(typeof Z=="string")switch(t.config.mode){case"single":case"time":ne=[t.parseDate(Z,X)];break;case"multiple":ne=Z.split(t.config.conjunction).map(function(fe){return t.parseDate(fe,X)});break;case"range":ne=Z.split(t.l10n.rangeSeparator).map(function(fe){return t.parseDate(fe,X)});break}else t.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(Z)));t.selectedDates=t.config.allowInvalidPreload?ne:ne.filter(function(fe){return fe instanceof Date&&ft(fe,!1)}),t.config.mode==="range"&&t.selectedDates.sort(function(fe,Te){return fe.getTime()-Te.getTime()})}function Li(Z,X,ne){if(X===void 0&&(X=!1),ne===void 0&&(ne=t.config.dateFormat),Z!==0&&!Z||Z instanceof Array&&Z.length===0)return t.clear(X);Nt(Z,ne),t.latestSelectedDateObj=t.selectedDates[t.selectedDates.length-1],t.redraw(),S(void 0,X),d(),t.selectedDates.length===0&&t.clear(!1),Kt(X),X&&bt("onChange")}function Kn(Z){return Z.slice().map(function(X){return typeof X=="string"||typeof X=="number"||X instanceof Date?t.parseDate(X,void 0,!0):X&&typeof X=="object"&&X.from&&X.to?{from:t.parseDate(X.from,void 0),to:t.parseDate(X.to,void 0)}:X}).filter(function(X){return X})}function rl(){t.selectedDates=[],t.now=t.parseDate(t.config.now)||new Date;var Z=t.config.defaultDate||((t.input.nodeName==="INPUT"||t.input.nodeName==="TEXTAREA")&&t.input.placeholder&&t.input.value===t.input.placeholder?null:t.input.value);Z&&Nt(Z,t.config.dateFormat),t._initialDate=t.selectedDates.length>0?t.selectedDates[0]:t.config.minDate&&t.config.minDate.getTime()>t.now.getTime()?t.config.minDate:t.config.maxDate&&t.config.maxDate.getTime()0&&(t.latestSelectedDateObj=t.selectedDates[0]),t.config.minTime!==void 0&&(t.config.minTime=t.parseDate(t.config.minTime,"H:i")),t.config.maxTime!==void 0&&(t.config.maxTime=t.parseDate(t.config.maxTime,"H:i")),t.minDateHasTime=!!t.config.minDate&&(t.config.minDate.getHours()>0||t.config.minDate.getMinutes()>0||t.config.minDate.getSeconds()>0),t.maxDateHasTime=!!t.config.maxDate&&(t.config.maxDate.getHours()>0||t.config.maxDate.getMinutes()>0||t.config.maxDate.getSeconds()>0)}function Pl(){if(t.input=we(),!t.input){t.config.errorHandler(new Error("Invalid input element specified"));return}t.input._type=t.input.type,t.input.type="text",t.input.classList.add("flatpickr-input"),t._input=t.input,t.config.altInput&&(t.altInput=dt(t.input.nodeName,t.config.altInputClass),t._input=t.altInput,t.altInput.placeholder=t.input.placeholder,t.altInput.disabled=t.input.disabled,t.altInput.required=t.input.required,t.altInput.tabIndex=t.input.tabIndex,t.altInput.type="text",t.input.setAttribute("type","hidden"),!t.config.static&&t.input.parentNode&&t.input.parentNode.insertBefore(t.altInput,t.input.nextSibling)),t.config.allowInput||t._input.setAttribute("readonly","readonly"),bi()}function bi(){t._positionElement=t.config.positionElement||t._input}function al(){var Z=t.config.enableTime?t.config.noCalendar?"time":"datetime-local":"date";t.mobileInput=dt("input",t.input.className+" flatpickr-mobile"),t.mobileInput.tabIndex=1,t.mobileInput.type=Z,t.mobileInput.disabled=t.input.disabled,t.mobileInput.required=t.input.required,t.mobileInput.placeholder=t.input.placeholder,t.mobileFormatStr=Z==="datetime-local"?"Y-m-d\\TH:i:S":Z==="date"?"Y-m-d":"H:i:S",t.selectedDates.length>0&&(t.mobileInput.defaultValue=t.mobileInput.value=t.formatDate(t.selectedDates[0],t.mobileFormatStr)),t.config.minDate&&(t.mobileInput.min=t.formatDate(t.config.minDate,"Y-m-d")),t.config.maxDate&&(t.mobileInput.max=t.formatDate(t.config.maxDate,"Y-m-d")),t.input.getAttribute("step")&&(t.mobileInput.step=String(t.input.getAttribute("step"))),t.input.type="hidden",t.altInput!==void 0&&(t.altInput.type="hidden");try{t.input.parentNode&&t.input.parentNode.insertBefore(t.mobileInput,t.input.nextSibling)}catch{}_(t.mobileInput,"change",function(X){t.setDate(yn(X).value,!1,t.mobileFormatStr),bt("onChange"),bt("onClose")})}function fl(Z){if(t.isOpen===!0)return t.close();t.open(Z)}function bt(Z,X){if(t.config!==void 0){var ne=t.config[Z];if(ne!==void 0&&ne.length>0)for(var fe=0;ne[fe]&&fe=0&&vn(Z,t.selectedDates[1])<=0}function Oe(){t.config.noCalendar||t.isMobile||!t.monthNav||(t.yearElements.forEach(function(Z,X){var ne=new Date(t.currentYear,t.currentMonth,1);ne.setMonth(t.currentMonth+X),t.config.showMonths>1||t.config.monthSelectorType==="static"?t.monthElements[X].textContent=Po(ne.getMonth(),t.config.shorthandCurrentMonth,t.l10n)+" ":t.monthsDropdownContainer.value=ne.getMonth().toString(),Z.value=ne.getFullYear().toString()}),t._hidePrevMonthArrow=t.config.minDate!==void 0&&(t.currentYear===t.config.minDate.getFullYear()?t.currentMonth<=t.config.minDate.getMonth():t.currentYeart.config.maxDate.getMonth():t.currentYear>t.config.maxDate.getFullYear()))}function ht(Z){var X=Z||(t.config.altInput?t.config.altFormat:t.config.dateFormat);return t.selectedDates.map(function(ne){return t.formatDate(ne,X)}).filter(function(ne,fe,Te){return t.config.mode!=="range"||t.config.enableTime||Te.indexOf(ne)===fe}).join(t.config.mode!=="range"?t.config.conjunction:t.l10n.rangeSeparator)}function Kt(Z){Z===void 0&&(Z=!0),t.mobileInput!==void 0&&t.mobileFormatStr&&(t.mobileInput.value=t.latestSelectedDateObj!==void 0?t.formatDate(t.latestSelectedDateObj,t.mobileFormatStr):""),t.input.value=ht(t.config.dateFormat),t.altInput!==void 0&&(t.altInput.value=ht(t.config.altFormat)),Z!==!1&&bt("onValueUpdate")}function ut(Z){var X=yn(Z),ne=t.prevMonthNav.contains(X),fe=t.nextMonthNav.contains(X);ne||fe?te(ne?-1:1):t.yearElements.indexOf(X)>=0?X.select():X.classList.contains("arrowUp")?t.changeYear(t.currentYear+1):X.classList.contains("arrowDown")&&t.changeYear(t.currentYear-1)}function oi(Z){Z.preventDefault();var X=Z.type==="keydown",ne=yn(Z),fe=ne;t.amPM!==void 0&&ne===t.amPM&&(t.amPM.textContent=t.l10n.amPM[In(t.amPM.textContent===t.l10n.amPM[0])]);var Te=parseFloat(fe.getAttribute("min")),Le=parseFloat(fe.getAttribute("max")),Je=parseFloat(fe.getAttribute("step")),qe=parseInt(fe.value,10),Qe=Z.delta||(X?Z.which===38?1:-1:0),Re=qe+Je*Qe;if(typeof fe.value<"u"&&fe.value.length===2){var Ve=fe===t.hourElement,yt=fe===t.minuteElement;ReLe&&(Re=fe===t.hourElement?Re-Le-In(!t.amPM):Te,yt&&$(void 0,1,t.hourElement)),t.amPM&&Ve&&(Je===1?Re+qe===23:Math.abs(Re-qe)>Je)&&(t.amPM.textContent=t.l10n.amPM[In(t.amPM.textContent===t.l10n.amPM[0])]),fe.value=pn(Re)}}return l(),t}function Sl(n,e){for(var t=Array.prototype.slice.call(n).filter(function(o){return o instanceof HTMLElement}),i=[],l=0;lt===e[i]))}function OT(n,e,t){const i=["value","formattedValue","element","dateFormat","options","input","flatpickr"];let l=Ge(e,i),{$$slots:s={},$$scope:o}=e;const r=new Set(["onChange","onOpen","onClose","onMonthChange","onYearChange","onReady","onValueUpdate","onDayCreate"]);let{value:a=void 0,formattedValue:f="",element:u=void 0,dateFormat:c=void 0}=e,{options:d={}}=e,m=!1,{input:h=void 0,flatpickr:_=void 0}=e;Ht(()=>{const $=u??h,C=y(d);return C.onReady.push((O,D,I)=>{a===void 0&&S(O,D,I),Qt().then(()=>{t(8,m=!0)})}),t(3,_=Bt($,Object.assign(C,u?{wrap:!0}:{}))),()=>{_.destroy()}});const g=lt();function y($={}){$=Object.assign({},$);for(const C of r){const O=(D,I,L)=>{g(CT(C),[D,I,L])};C in $?(Array.isArray($[C])||($[C]=[$[C]]),$[C].push(O)):$[C]=[O]}return $.onChange&&!$.onChange.includes(S)&&$.onChange.push(S),$}function S($,C,O){const D=ld(O,$);!sd(a,D)&&(a||D)&&t(2,a=D),t(4,f=C)}function T($){ee[$?"unshift":"push"](()=>{h=$,t(0,h)})}return n.$$set=$=>{e=Ie(Ie({},e),Yt($)),t(1,l=Ge(e,i)),"value"in $&&t(2,a=$.value),"formattedValue"in $&&t(4,f=$.formattedValue),"element"in $&&t(5,u=$.element),"dateFormat"in $&&t(6,c=$.dateFormat),"options"in $&&t(7,d=$.options),"input"in $&&t(0,h=$.input),"flatpickr"in $&&t(3,_=$.flatpickr),"$$scope"in $&&t(9,o=$.$$scope)},n.$$.update=()=>{if(n.$$.dirty&332&&_&&m&&(sd(a,ld(_,_.selectedDates))||_.setDate(a,!0,c)),n.$$.dirty&392&&_&&m)for(const[$,C]of Object.entries(y(d)))_.set($,C)},[h,l,a,_,f,u,c,d,m,o,s,T]}class Va extends ge{constructor(e){super(),_e(this,e,OT,TT,me,{value:2,formattedValue:4,element:5,dateFormat:6,options:7,input:0,flatpickr:3})}}function MT(n){let e,t,i,l,s,o,r,a;function f(d){n[6](d)}function u(d){n[7](d)}let c={id:n[16],options:j.defaultFlatpickrOptions()};return n[2]!==void 0&&(c.value=n[2]),n[0].options.min!==void 0&&(c.formattedValue=n[0].options.min),s=new Va({props:c}),ee.push(()=>be(s,"value",f)),ee.push(()=>be(s,"formattedValue",u)),s.$on("close",n[8]),{c(){e=b("label"),t=K("Min date (UTC)"),l=M(),B(s.$$.fragment),p(e,"for",i=n[16])},m(d,m){w(d,e,m),k(e,t),w(d,l,m),z(s,d,m),a=!0},p(d,m){(!a||m&65536&&i!==(i=d[16]))&&p(e,"for",i);const h={};m&65536&&(h.id=d[16]),!o&&m&4&&(o=!0,h.value=d[2],ke(()=>o=!1)),!r&&m&1&&(r=!0,h.formattedValue=d[0].options.min,ke(()=>r=!1)),s.$set(h)},i(d){a||(E(s.$$.fragment,d),a=!0)},o(d){A(s.$$.fragment,d),a=!1},d(d){d&&(v(e),v(l)),V(s,d)}}}function DT(n){let e,t,i,l,s,o,r,a;function f(d){n[9](d)}function u(d){n[10](d)}let c={id:n[16],options:j.defaultFlatpickrOptions()};return n[3]!==void 0&&(c.value=n[3]),n[0].options.max!==void 0&&(c.formattedValue=n[0].options.max),s=new Va({props:c}),ee.push(()=>be(s,"value",f)),ee.push(()=>be(s,"formattedValue",u)),s.$on("close",n[11]),{c(){e=b("label"),t=K("Max date (UTC)"),l=M(),B(s.$$.fragment),p(e,"for",i=n[16])},m(d,m){w(d,e,m),k(e,t),w(d,l,m),z(s,d,m),a=!0},p(d,m){(!a||m&65536&&i!==(i=d[16]))&&p(e,"for",i);const h={};m&65536&&(h.id=d[16]),!o&&m&8&&(o=!0,h.value=d[3],ke(()=>o=!1)),!r&&m&1&&(r=!0,h.formattedValue=d[0].options.max,ke(()=>r=!1)),s.$set(h)},i(d){a||(E(s.$$.fragment,d),a=!0)},o(d){A(s.$$.fragment,d),a=!1},d(d){d&&(v(e),v(l)),V(s,d)}}}function ET(n){let e,t,i,l,s,o,r;return i=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[MT,({uniqueId:a})=>({16:a}),({uniqueId:a})=>a?65536:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[DT,({uniqueId:a})=>({16:a}),({uniqueId:a})=>a?65536:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),p(t,"class","col-sm-6"),p(s,"class","col-sm-6"),p(e,"class","grid grid-sm")},m(a,f){w(a,e,f),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),r=!0},p(a,f){const u={};f&2&&(u.name="schema."+a[1]+".options.min"),f&196613&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};f&2&&(c.name="schema."+a[1]+".options.max"),f&196617&&(c.$$scope={dirty:f,ctx:a}),o.$set(c)},i(a){r||(E(i.$$.fragment,a),E(o.$$.fragment,a),r=!0)},o(a){A(i.$$.fragment,a),A(o.$$.fragment,a),r=!1},d(a){a&&v(e),V(i),V(o)}}}function IT(n){let e,t,i;const l=[{key:n[1]},n[5]];function s(r){n[12](r)}let o={$$slots:{options:[ET]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[13]),e.$on("remove",n[14]),e.$on("duplicate",n[15]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&34?pt(l,[a&2&&{key:r[1]},a&32&&Ot(r[5])]):{};a&131087&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function AT(n,e,t){var $,C;const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e,r=($=s==null?void 0:s.options)==null?void 0:$.min,a=(C=s==null?void 0:s.options)==null?void 0:C.max;function f(O,D){O.detail&&O.detail.length==3&&t(0,s.options[D]=O.detail[1],s)}function u(O){r=O,t(2,r),t(0,s)}function c(O){n.$$.not_equal(s.options.min,O)&&(s.options.min=O,t(0,s))}const d=O=>f(O,"min");function m(O){a=O,t(3,a),t(0,s)}function h(O){n.$$.not_equal(s.options.max,O)&&(s.options.max=O,t(0,s))}const _=O=>f(O,"max");function g(O){s=O,t(0,s)}function y(O){Ce.call(this,n,O)}function S(O){Ce.call(this,n,O)}function T(O){Ce.call(this,n,O)}return n.$$set=O=>{e=Ie(Ie({},e),Yt(O)),t(5,l=Ge(e,i)),"field"in O&&t(0,s=O.field),"key"in O&&t(1,o=O.key)},n.$$.update=()=>{var O,D,I,L;n.$$.dirty&5&&r!=((O=s==null?void 0:s.options)==null?void 0:O.min)&&t(2,r=(D=s==null?void 0:s.options)==null?void 0:D.min),n.$$.dirty&9&&a!=((I=s==null?void 0:s.options)==null?void 0:I.max)&&t(3,a=(L=s==null?void 0:s.options)==null?void 0:L.max)},[s,o,r,a,f,l,u,c,d,m,h,_,g,y,S,T]}class LT extends ge{constructor(e){super(),_e(this,e,AT,IT,me,{field:0,key:1})}}const NT=n=>({}),od=n=>({});function rd(n,e,t){const i=n.slice();return i[48]=e[t],i}const PT=n=>({}),ad=n=>({});function fd(n,e,t){const i=n.slice();return i[48]=e[t],i[52]=t,i}function ud(n){let e,t,i;return{c(){e=b("div"),t=K(n[2]),i=M(),p(e,"class","block txt-placeholder"),x(e,"link-hint",!n[5]&&!n[6])},m(l,s){w(l,e,s),k(e,t),k(e,i)},p(l,s){s[0]&4&&oe(t,l[2]),s[0]&96&&x(e,"link-hint",!l[5]&&!l[6])},d(l){l&&v(e)}}}function FT(n){let e,t=n[48]+"",i;return{c(){e=b("span"),i=K(t),p(e,"class","txt")},m(l,s){w(l,e,s),k(e,i)},p(l,s){s[0]&1&&t!==(t=l[48]+"")&&oe(i,t)},i:Q,o:Q,d(l){l&&v(e)}}}function RT(n){let e,t,i;const l=[{item:n[48]},n[11]];var s=n[10];function o(r,a){let f={};for(let u=0;u{V(f,1)}),se()}s?(e=Dt(s,o(r,a)),B(e.$$.fragment),E(e.$$.fragment,1),z(e,t.parentNode,t)):e=null}else if(s){const f=a[0]&2049?pt(l,[a[0]&1&&{item:r[48]},a[0]&2048&&Ot(r[11])]):{};e.$set(f)}},i(r){i||(e&&E(e.$$.fragment,r),i=!0)},o(r){e&&A(e.$$.fragment,r),i=!1},d(r){r&&v(t),e&&V(e,r)}}}function cd(n){let e,t,i;function l(){return n[36](n[48])}return{c(){e=b("span"),e.innerHTML='',p(e,"class","clear")},m(s,o){w(s,e,o),t||(i=[Se(Pe.call(null,e,"Clear")),J(e,"click",Tn(Be(l)))],t=!0)},p(s,o){n=s},d(s){s&&v(e),t=!1,$e(i)}}}function dd(n){let e,t,i,l,s,o;const r=[RT,FT],a=[];function f(c,d){return c[10]?0:1}t=f(n),i=a[t]=r[t](n);let u=(n[4]||n[8])&&cd(n);return{c(){e=b("div"),i.c(),l=M(),u&&u.c(),s=M(),p(e,"class","option")},m(c,d){w(c,e,d),a[t].m(e,null),k(e,l),u&&u.m(e,null),k(e,s),o=!0},p(c,d){let m=t;t=f(c),t===m?a[t].p(c,d):(le(),A(a[m],1,1,()=>{a[m]=null}),se(),i=a[t],i?i.p(c,d):(i=a[t]=r[t](c),i.c()),E(i,1),i.m(e,l)),c[4]||c[8]?u?u.p(c,d):(u=cd(c),u.c(),u.m(e,s)):u&&(u.d(1),u=null)},i(c){o||(E(i),o=!0)},o(c){A(i),o=!1},d(c){c&&v(e),a[t].d(),u&&u.d()}}}function pd(n){let e,t,i={class:"dropdown dropdown-block options-dropdown dropdown-left "+(n[7]?"dropdown-upside":""),trigger:n[20],$$slots:{default:[HT]},$$scope:{ctx:n}};return e=new On({props:i}),n[41](e),e.$on("show",n[26]),e.$on("hide",n[42]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,s){const o={};s[0]&128&&(o.class="dropdown dropdown-block options-dropdown dropdown-left "+(l[7]?"dropdown-upside":"")),s[0]&1048576&&(o.trigger=l[20]),s[0]&6451722|s[1]&8192&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[41](null),V(e,l)}}}function md(n){let e,t,i,l,s,o,r,a,f=n[17].length&&hd(n);return{c(){e=b("div"),t=b("label"),i=b("div"),i.innerHTML='',l=M(),s=b("input"),o=M(),f&&f.c(),p(i,"class","addon p-r-0"),s.autofocus=!0,p(s,"type","text"),p(s,"placeholder",n[3]),p(t,"class","input-group"),p(e,"class","form-field form-field-sm options-search")},m(u,c){w(u,e,c),k(e,t),k(t,i),k(t,l),k(t,s),re(s,n[17]),k(t,o),f&&f.m(t,null),s.focus(),r||(a=J(s,"input",n[38]),r=!0)},p(u,c){c[0]&8&&p(s,"placeholder",u[3]),c[0]&131072&&s.value!==u[17]&&re(s,u[17]),u[17].length?f?f.p(u,c):(f=hd(u),f.c(),f.m(t,null)):f&&(f.d(1),f=null)},d(u){u&&v(e),f&&f.d(),r=!1,a()}}}function hd(n){let e,t,i,l;return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","btn btn-sm btn-circle btn-transparent clear"),p(e,"class","addon suffix p-r-5")},m(s,o){w(s,e,o),k(e,t),i||(l=J(t,"click",Tn(Be(n[23]))),i=!0)},p:Q,d(s){s&&v(e),i=!1,l()}}}function _d(n){let e,t=n[1]&&gd(n);return{c(){t&&t.c(),e=ye()},m(i,l){t&&t.m(i,l),w(i,e,l)},p(i,l){i[1]?t?t.p(i,l):(t=gd(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){i&&v(e),t&&t.d(i)}}}function gd(n){let e,t;return{c(){e=b("div"),t=K(n[1]),p(e,"class","txt-missing")},m(i,l){w(i,e,l),k(e,t)},p(i,l){l[0]&2&&oe(t,i[1])},d(i){i&&v(e)}}}function qT(n){let e=n[48]+"",t;return{c(){t=K(e)},m(i,l){w(i,t,l)},p(i,l){l[0]&4194304&&e!==(e=i[48]+"")&&oe(t,e)},i:Q,o:Q,d(i){i&&v(t)}}}function jT(n){let e,t,i;const l=[{item:n[48]},n[13]];var s=n[12];function o(r,a){let f={};for(let u=0;u{V(f,1)}),se()}s?(e=Dt(s,o(r,a)),B(e.$$.fragment),E(e.$$.fragment,1),z(e,t.parentNode,t)):e=null}else if(s){const f=a[0]&4202496?pt(l,[a[0]&4194304&&{item:r[48]},a[0]&8192&&Ot(r[13])]):{};e.$set(f)}},i(r){i||(e&&E(e.$$.fragment,r),i=!0)},o(r){e&&A(e.$$.fragment,r),i=!1},d(r){r&&v(t),e&&V(e,r)}}}function bd(n){let e,t,i,l,s,o,r;const a=[jT,qT],f=[];function u(m,h){return m[12]?0:1}t=u(n),i=f[t]=a[t](n);function c(...m){return n[39](n[48],...m)}function d(...m){return n[40](n[48],...m)}return{c(){e=b("div"),i.c(),l=M(),p(e,"tabindex","0"),p(e,"class","dropdown-item option"),p(e,"role","menuitem"),x(e,"closable",n[9]),x(e,"selected",n[21](n[48]))},m(m,h){w(m,e,h),f[t].m(e,null),k(e,l),s=!0,o||(r=[J(e,"click",c),J(e,"keydown",d)],o=!0)},p(m,h){n=m;let _=t;t=u(n),t===_?f[t].p(n,h):(le(),A(f[_],1,1,()=>{f[_]=null}),se(),i=f[t],i?i.p(n,h):(i=f[t]=a[t](n),i.c()),E(i,1),i.m(e,l)),(!s||h[0]&512)&&x(e,"closable",n[9]),(!s||h[0]&6291456)&&x(e,"selected",n[21](n[48]))},i(m){s||(E(i),s=!0)},o(m){A(i),s=!1},d(m){m&&v(e),f[t].d(),o=!1,$e(r)}}}function HT(n){let e,t,i,l,s,o=n[14]&&md(n);const r=n[35].beforeOptions,a=wt(r,n,n[44],ad);let f=ue(n[22]),u=[];for(let _=0;_A(u[_],1,1,()=>{u[_]=null});let d=null;f.length||(d=_d(n));const m=n[35].afterOptions,h=wt(m,n,n[44],od);return{c(){o&&o.c(),e=M(),a&&a.c(),t=M(),i=b("div");for(let _=0;_A(a[d],1,1,()=>{a[d]=null});let u=null;r.length||(u=ud(n));let c=!n[5]&&!n[6]&&pd(n);return{c(){e=b("div"),t=b("div");for(let d=0;d{c=null}),se()),(!o||m[0]&32768&&s!==(s="select "+d[15]))&&p(e,"class",s),(!o||m[0]&32896)&&x(e,"upside",d[7]),(!o||m[0]&32784)&&x(e,"multiple",d[4]),(!o||m[0]&32800)&&x(e,"disabled",d[5]),(!o||m[0]&32832)&&x(e,"readonly",d[6])},i(d){if(!o){for(let m=0;mwe(Ye,ve))||[]}function Ne(de,ve){de.preventDefault(),y&&d?W(ve):H(ve)}function He(de,ve){(de.code==="Enter"||de.code==="Space")&&(Ne(de,ve),S&&Y())}function Xe(){te(),setTimeout(()=>{const de=N==null?void 0:N.querySelector(".dropdown-item.option.selected");de&&(de.focus(),de.scrollIntoView({block:"nearest"}))},0)}function xe(de){de.stopPropagation(),!h&&!m&&(R==null||R.toggle())}Ht(()=>{const de=document.querySelectorAll(`label[for="${r}"]`);for(const ve of de)ve.addEventListener("click",xe);return()=>{for(const ve of de)ve.removeEventListener("click",xe)}});const Mt=de=>q(de);function ft(de){ee[de?"unshift":"push"](()=>{P=de,t(20,P)})}function mt(){F=this.value,t(17,F)}const Gt=(de,ve)=>Ne(ve,de),De=(de,ve)=>He(ve,de);function Ae(de){ee[de?"unshift":"push"](()=>{R=de,t(18,R)})}function ze(de){Ce.call(this,n,de)}function gt(de){ee[de?"unshift":"push"](()=>{N=de,t(19,N)})}return n.$$set=de=>{"id"in de&&t(27,r=de.id),"noOptionsText"in de&&t(1,a=de.noOptionsText),"selectPlaceholder"in de&&t(2,f=de.selectPlaceholder),"searchPlaceholder"in de&&t(3,u=de.searchPlaceholder),"items"in de&&t(28,c=de.items),"multiple"in de&&t(4,d=de.multiple),"disabled"in de&&t(5,m=de.disabled),"readonly"in de&&t(6,h=de.readonly),"upside"in de&&t(7,_=de.upside),"selected"in de&&t(0,g=de.selected),"toggle"in de&&t(8,y=de.toggle),"closable"in de&&t(9,S=de.closable),"labelComponent"in de&&t(10,T=de.labelComponent),"labelComponentProps"in de&&t(11,$=de.labelComponentProps),"optionComponent"in de&&t(12,C=de.optionComponent),"optionComponentProps"in de&&t(13,O=de.optionComponentProps),"searchable"in de&&t(14,D=de.searchable),"searchFunc"in de&&t(29,I=de.searchFunc),"class"in de&&t(15,L=de.class),"$$scope"in de&&t(44,o=de.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&268435456&&c&&(ie(),te()),n.$$.dirty[0]&268566528&&t(22,i=pe(c,F)),n.$$.dirty[0]&1&&t(21,l=function(de){const ve=j.toArray(g);return j.inArray(ve,de)})},[g,a,f,u,d,m,h,_,y,S,T,$,C,O,D,L,q,F,R,N,P,l,i,te,Ne,He,Xe,r,c,I,H,W,G,U,Y,s,Mt,ft,mt,Gt,De,Ae,ze,gt,o]}class Wb extends ge{constructor(e){super(),_e(this,e,BT,zT,me,{id:27,noOptionsText:1,selectPlaceholder:2,searchPlaceholder:3,items:28,multiple:4,disabled:5,readonly:6,upside:7,selected:0,toggle:8,closable:9,labelComponent:10,labelComponentProps:11,optionComponent:12,optionComponentProps:13,searchable:14,searchFunc:29,class:15,deselectItem:16,selectItem:30,toggleItem:31,reset:32,showDropdown:33,hideDropdown:34},null,[-1,-1])}get deselectItem(){return this.$$.ctx[16]}get selectItem(){return this.$$.ctx[30]}get toggleItem(){return this.$$.ctx[31]}get reset(){return this.$$.ctx[32]}get showDropdown(){return this.$$.ctx[33]}get hideDropdown(){return this.$$.ctx[34]}}function kd(n){let e,t;return{c(){e=b("i"),p(e,"class",t="icon "+n[0].icon)},m(i,l){w(i,e,l)},p(i,l){l&1&&t!==(t="icon "+i[0].icon)&&p(e,"class",t)},d(i){i&&v(e)}}}function UT(n){let e,t,i=(n[0].label||n[0].name||n[0].title||n[0].id||n[0].value)+"",l,s=n[0].icon&&kd(n);return{c(){s&&s.c(),e=M(),t=b("span"),l=K(i),p(t,"class","txt")},m(o,r){s&&s.m(o,r),w(o,e,r),w(o,t,r),k(t,l)},p(o,[r]){o[0].icon?s?s.p(o,r):(s=kd(o),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null),r&1&&i!==(i=(o[0].label||o[0].name||o[0].title||o[0].id||o[0].value)+"")&&oe(l,i)},i:Q,o:Q,d(o){o&&(v(e),v(t)),s&&s.d(o)}}}function WT(n,e,t){let{item:i={}}=e;return n.$$set=l=>{"item"in l&&t(0,i=l.item)},[i]}class yd extends ge{constructor(e){super(),_e(this,e,WT,UT,me,{item:0})}}const YT=n=>({}),vd=n=>({});function KT(n){let e;const t=n[8].afterOptions,i=wt(t,n,n[12],vd);return{c(){i&&i.c()},m(l,s){i&&i.m(l,s),e=!0},p(l,s){i&&i.p&&(!e||s&4096)&&$t(i,t,l,l[12],e?St(t,l[12],s,YT):Tt(l[12]),vd)},i(l){e||(E(i,l),e=!0)},o(l){A(i,l),e=!1},d(l){i&&i.d(l)}}}function JT(n){let e,t,i;const l=[{items:n[1]},{multiple:n[2]},{labelComponent:n[3]},{optionComponent:n[4]},n[5]];function s(r){n[9](r)}let o={$$slots:{afterOptions:[KT]},$$scope:{ctx:n}};for(let r=0;rbe(e,"selected",s)),e.$on("show",n[10]),e.$on("hide",n[11]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&62?pt(l,[a&2&&{items:r[1]},a&4&&{multiple:r[2]},a&8&&{labelComponent:r[3]},a&16&&{optionComponent:r[4]},a&32&&Ot(r[5])]):{};a&4096&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.selected=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function ZT(n,e,t){const i=["items","multiple","selected","labelComponent","optionComponent","selectionKey","keyOfSelected"];let l=Ge(e,i),{$$slots:s={},$$scope:o}=e,{items:r=[]}=e,{multiple:a=!1}=e,{selected:f=a?[]:void 0}=e,{labelComponent:u=yd}=e,{optionComponent:c=yd}=e,{selectionKey:d="value"}=e,{keyOfSelected:m=a?[]:void 0}=e;function h(T){T=j.toArray(T,!0);let $=[];for(let C of T){const O=j.findByKey(r,d,C);O&&$.push(O)}T.length&&!$.length||t(0,f=a?$:$[0])}async function _(T){let $=j.toArray(T,!0).map(C=>C[d]);r.length&&t(6,m=a?$:$[0])}function g(T){f=T,t(0,f)}function y(T){Ce.call(this,n,T)}function S(T){Ce.call(this,n,T)}return n.$$set=T=>{e=Ie(Ie({},e),Yt(T)),t(5,l=Ge(e,i)),"items"in T&&t(1,r=T.items),"multiple"in T&&t(2,a=T.multiple),"selected"in T&&t(0,f=T.selected),"labelComponent"in T&&t(3,u=T.labelComponent),"optionComponent"in T&&t(4,c=T.optionComponent),"selectionKey"in T&&t(7,d=T.selectionKey),"keyOfSelected"in T&&t(6,m=T.keyOfSelected),"$$scope"in T&&t(12,o=T.$$scope)},n.$$.update=()=>{n.$$.dirty&66&&r&&h(m),n.$$.dirty&1&&_(f)},[f,r,a,u,c,l,m,d,s,g,y,S,o]}class hi extends ge{constructor(e){super(),_e(this,e,ZT,JT,me,{items:1,multiple:2,selected:0,labelComponent:3,optionComponent:4,selectionKey:7,keyOfSelected:6})}}function GT(n){let e,t,i,l,s,o;function r(f){n[7](f)}let a={id:n[14],placeholder:"Choices: eg. optionA, optionB",required:!0,readonly:!n[15]};return n[0].options.values!==void 0&&(a.value=n[0].options.values),t=new Nl({props:a}),ee.push(()=>be(t,"value",r)),{c(){e=b("div"),B(t.$$.fragment)},m(f,u){w(f,e,u),z(t,e,null),l=!0,s||(o=Se(Pe.call(null,e,{text:"Choices (comma separated)",position:"top-left",delay:700})),s=!0)},p(f,u){const c={};u&16384&&(c.id=f[14]),u&32768&&(c.readonly=!f[15]),!i&&u&1&&(i=!0,c.value=f[0].options.values,ke(()=>i=!1)),t.$set(c)},i(f){l||(E(t.$$.fragment,f),l=!0)},o(f){A(t.$$.fragment,f),l=!1},d(f){f&&v(e),V(t),s=!1,o()}}}function XT(n){let e,t,i;function l(o){n[8](o)}let s={id:n[14],items:n[3],readonly:!n[15]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new hi({props:s}),ee.push(()=>be(e,"keyOfSelected",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&16384&&(a.id=o[14]),r&32768&&(a.readonly=!o[15]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function QT(n){let e,t,i,l,s,o,r,a,f,u;return i=new ce({props:{class:"form-field required "+(n[15]?"":"readonly"),inlineError:!0,name:"schema."+n[1]+".options.values",$$slots:{default:[GT,({uniqueId:c})=>({14:c}),({uniqueId:c})=>c?16384:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field form-field-single-multiple-select "+(n[15]?"":"readonly"),inlineError:!0,$$slots:{default:[XT,({uniqueId:c})=>({14:c}),({uniqueId:c})=>c?16384:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=M(),B(i.$$.fragment),l=M(),s=b("div"),o=M(),B(r.$$.fragment),a=M(),f=b("div"),p(e,"class","separator"),p(s,"class","separator"),p(f,"class","separator")},m(c,d){w(c,e,d),w(c,t,d),z(i,c,d),w(c,l,d),w(c,s,d),w(c,o,d),z(r,c,d),w(c,a,d),w(c,f,d),u=!0},p(c,d){const m={};d&32768&&(m.class="form-field required "+(c[15]?"":"readonly")),d&2&&(m.name="schema."+c[1]+".options.values"),d&114689&&(m.$$scope={dirty:d,ctx:c}),i.$set(m);const h={};d&32768&&(h.class="form-field form-field-single-multiple-select "+(c[15]?"":"readonly")),d&114692&&(h.$$scope={dirty:d,ctx:c}),r.$set(h)},i(c){u||(E(i.$$.fragment,c),E(r.$$.fragment,c),u=!0)},o(c){A(i.$$.fragment,c),A(r.$$.fragment,c),u=!1},d(c){c&&(v(e),v(t),v(l),v(s),v(o),v(a),v(f)),V(i,c),V(r,c)}}}function wd(n){let e,t;return e=new ce({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[xT,({uniqueId:i})=>({14:i}),({uniqueId:i})=>i?16384:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="schema."+i[1]+".options.maxSelect"),l&81921&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function xT(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Max select"),l=M(),s=b("input"),p(e,"for",i=n[14]),p(s,"id",o=n[14]),p(s,"type","number"),p(s,"step","1"),p(s,"min","2"),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.maxSelect),r||(a=J(s,"input",n[6]),r=!0)},p(f,u){u&16384&&i!==(i=f[14])&&p(e,"for",i),u&16384&&o!==(o=f[14])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.maxSelect&&re(s,f[0].options.maxSelect)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function eC(n){let e,t,i=!n[2]&&wd(n);return{c(){i&&i.c(),e=ye()},m(l,s){i&&i.m(l,s),w(l,e,s),t=!0},p(l,s){l[2]?i&&(le(),A(i,1,1,()=>{i=null}),se()):i?(i.p(l,s),s&4&&E(i,1)):(i=wd(l),i.c(),E(i,1),i.m(e.parentNode,e))},i(l){t||(E(i),t=!0)},o(l){A(i),t=!1},d(l){l&&v(e),i&&i.d(l)}}}function tC(n){let e,t,i;const l=[{key:n[1]},n[4]];function s(r){n[9](r)}let o={$$slots:{options:[eC],default:[QT,({interactive:r})=>({15:r}),({interactive:r})=>r?32768:0]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[10]),e.$on("remove",n[11]),e.$on("duplicate",n[12]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&18?pt(l,[a&2&&{key:r[1]},a&16&&Ot(r[4])]):{};a&98311&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function nC(n,e,t){var S;const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;const r=[{label:"Single",value:!0},{label:"Multiple",value:!1}];let a=((S=s.options)==null?void 0:S.maxSelect)<=1,f=a;function u(){t(0,s.options={maxSelect:1,values:[]},s),t(2,a=!0),t(5,f=a)}function c(){s.options.maxSelect=it(this.value),t(0,s),t(5,f),t(2,a)}function d(T){n.$$.not_equal(s.options.values,T)&&(s.options.values=T,t(0,s),t(5,f),t(2,a))}function m(T){a=T,t(2,a)}function h(T){s=T,t(0,s),t(5,f),t(2,a)}function _(T){Ce.call(this,n,T)}function g(T){Ce.call(this,n,T)}function y(T){Ce.call(this,n,T)}return n.$$set=T=>{e=Ie(Ie({},e),Yt(T)),t(4,l=Ge(e,i)),"field"in T&&t(0,s=T.field),"key"in T&&t(1,o=T.key)},n.$$.update=()=>{var T,$;n.$$.dirty&37&&f!=a&&(t(5,f=a),a?t(0,s.options.maxSelect=1,s):t(0,s.options.maxSelect=(($=(T=s.options)==null?void 0:T.values)==null?void 0:$.length)||2,s)),n.$$.dirty&1&&j.isEmpty(s.options)&&u()},[s,o,a,r,l,f,c,d,m,h,_,g,y]}class iC extends ge{constructor(e){super(),_e(this,e,nC,tC,me,{field:0,key:1})}}function lC(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("label"),t=K("Max size "),i=b("small"),i.textContent="(bytes)",s=M(),o=b("input"),p(e,"for",l=n[11]),p(o,"type","number"),p(o,"id",r=n[11]),p(o,"step","1"),p(o,"min","0")},m(u,c){w(u,e,c),k(e,t),k(e,i),w(u,s,c),w(u,o,c),re(o,n[0].options.maxSize),a||(f=J(o,"input",n[4]),a=!0)},p(u,c){c&2048&&l!==(l=u[11])&&p(e,"for",l),c&2048&&r!==(r=u[11])&&p(o,"id",r),c&1&&it(o.value)!==u[0].options.maxSize&&re(o,u[0].options.maxSize)},d(u){u&&(v(e),v(s),v(o)),a=!1,f()}}}function sC(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line txt-sm")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function oC(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line txt-sm")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Sd(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D='"{"a":1,"b":2}"',I,L,R,F,N,P,q,H,W,G,U,Y,ie;return{c(){e=b("div"),t=b("div"),i=b("div"),l=K("In order to support seamlessly both "),s=b("code"),s.textContent="application/json",o=K(` and - `),r=b("code"),r.textContent="multipart/form-data",a=K(` - requests, the following normalization rules are applied if the `),f=b("code"),f.textContent="json",u=K(` field - is a - `),c=b("strong"),c.textContent="plain string",d=K(`: - `),m=b("ul"),h=b("li"),h.innerHTML=""true" is converted to the json true",_=M(),g=b("li"),g.innerHTML=""false" is converted to the json false",y=M(),S=b("li"),S.innerHTML=""null" is converted to the json null",T=M(),$=b("li"),$.innerHTML=""[1,2,3]" is converted to the json [1,2,3]",C=M(),O=b("li"),I=K(D),L=K(" is converted to the json "),R=b("code"),R.textContent='{"a":1,"b":2}',F=M(),N=b("li"),N.textContent="numeric strings are converted to json number",P=M(),q=b("li"),q.textContent="double quoted strings are left as they are (aka. without normalizations)",H=M(),W=b("li"),W.textContent="any other string (empty string too) is double quoted",G=K(` - Alternatively, if you want to avoid the string value normalizations, you can wrap your - data inside an object, eg.`),U=b("code"),U.textContent='{"data": anything}',p(i,"class","content"),p(t,"class","alert alert-warning m-b-0 m-t-10"),p(e,"class","block")},m(te,pe){w(te,e,pe),k(e,t),k(t,i),k(i,l),k(i,s),k(i,o),k(i,r),k(i,a),k(i,f),k(i,u),k(i,c),k(i,d),k(i,m),k(m,h),k(m,_),k(m,g),k(m,y),k(m,S),k(m,T),k(m,$),k(m,C),k(m,O),k(O,I),k(O,L),k(O,R),k(m,F),k(m,N),k(m,P),k(m,q),k(m,H),k(m,W),k(i,G),k(i,U),ie=!0},i(te){ie||(te&&Ke(()=>{ie&&(Y||(Y=Fe(e,et,{duration:150},!0)),Y.run(1))}),ie=!0)},o(te){te&&(Y||(Y=Fe(e,et,{duration:150},!1)),Y.run(0)),ie=!1},d(te){te&&v(e),te&&Y&&Y.end()}}}function rC(n){let e,t,i,l,s,o,r,a,f,u,c;e=new ce({props:{class:"form-field required m-b-sm",name:"schema."+n[1]+".options.maxSize",$$slots:{default:[lC,({uniqueId:g})=>({11:g}),({uniqueId:g})=>g?2048:0]},$$scope:{ctx:n}}});function d(g,y){return g[2]?oC:sC}let m=d(n),h=m(n),_=n[2]&&Sd();return{c(){B(e.$$.fragment),t=M(),i=b("button"),l=b("strong"),l.textContent="String value normalizations",s=M(),h.c(),r=M(),_&&_.c(),a=ye(),p(l,"class","txt"),p(i,"type","button"),p(i,"class",o="btn btn-sm "+(n[2]?"btn-secondary":"btn-hint btn-transparent"))},m(g,y){z(e,g,y),w(g,t,y),w(g,i,y),k(i,l),k(i,s),h.m(i,null),w(g,r,y),_&&_.m(g,y),w(g,a,y),f=!0,u||(c=J(i,"click",n[5]),u=!0)},p(g,y){const S={};y&2&&(S.name="schema."+g[1]+".options.maxSize"),y&6145&&(S.$$scope={dirty:y,ctx:g}),e.$set(S),m!==(m=d(g))&&(h.d(1),h=m(g),h&&(h.c(),h.m(i,null))),(!f||y&4&&o!==(o="btn btn-sm "+(g[2]?"btn-secondary":"btn-hint btn-transparent")))&&p(i,"class",o),g[2]?_?y&4&&E(_,1):(_=Sd(),_.c(),E(_,1),_.m(a.parentNode,a)):_&&(le(),A(_,1,1,()=>{_=null}),se())},i(g){f||(E(e.$$.fragment,g),E(_),f=!0)},o(g){A(e.$$.fragment,g),A(_),f=!1},d(g){g&&(v(t),v(i),v(r),v(a)),V(e,g),h.d(),_&&_.d(g),u=!1,c()}}}function aC(n){let e,t,i;const l=[{key:n[1]},n[3]];function s(r){n[6](r)}let o={$$slots:{options:[rC]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[7]),e.$on("remove",n[8]),e.$on("duplicate",n[9]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&10?pt(l,[a&2&&{key:r[1]},a&8&&Ot(r[3])]):{};a&4103&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function fC(n,e,t){const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e,r=!1;function a(){t(0,s.options={maxSize:2e6},s)}function f(){s.options.maxSize=it(this.value),t(0,s)}const u=()=>{t(2,r=!r)};function c(_){s=_,t(0,s)}function d(_){Ce.call(this,n,_)}function m(_){Ce.call(this,n,_)}function h(_){Ce.call(this,n,_)}return n.$$set=_=>{e=Ie(Ie({},e),Yt(_)),t(3,l=Ge(e,i)),"field"in _&&t(0,s=_.field),"key"in _&&t(1,o=_.key)},n.$$.update=()=>{n.$$.dirty&1&&j.isEmpty(s.options)&&a()},[s,o,r,l,f,u,c,d,m,h]}class uC extends ge{constructor(e){super(),_e(this,e,fC,aC,me,{field:0,key:1})}}function cC(n){let e,t=(n[0].ext||"N/A")+"",i,l,s,o=n[0].mimeType+"",r;return{c(){e=b("span"),i=K(t),l=M(),s=b("small"),r=K(o),p(e,"class","txt"),p(s,"class","txt-hint")},m(a,f){w(a,e,f),k(e,i),w(a,l,f),w(a,s,f),k(s,r)},p(a,[f]){f&1&&t!==(t=(a[0].ext||"N/A")+"")&&oe(i,t),f&1&&o!==(o=a[0].mimeType+"")&&oe(r,o)},i:Q,o:Q,d(a){a&&(v(e),v(l),v(s))}}}function dC(n,e,t){let{item:i={}}=e;return n.$$set=l=>{"item"in l&&t(0,i=l.item)},[i]}class $d extends ge{constructor(e){super(),_e(this,e,dC,cC,me,{item:0})}}const pC=[{ext:".xpm",mimeType:"image/x-xpixmap"},{ext:".7z",mimeType:"application/x-7z-compressed"},{ext:".zip",mimeType:"application/zip"},{ext:".xlsx",mimeType:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},{ext:".docx",mimeType:"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},{ext:".pptx",mimeType:"application/vnd.openxmlformats-officedocument.presentationml.presentation"},{ext:".epub",mimeType:"application/epub+zip"},{ext:".jar",mimeType:"application/jar"},{ext:".odt",mimeType:"application/vnd.oasis.opendocument.text"},{ext:".ott",mimeType:"application/vnd.oasis.opendocument.text-template"},{ext:".ods",mimeType:"application/vnd.oasis.opendocument.spreadsheet"},{ext:".ots",mimeType:"application/vnd.oasis.opendocument.spreadsheet-template"},{ext:".odp",mimeType:"application/vnd.oasis.opendocument.presentation"},{ext:".otp",mimeType:"application/vnd.oasis.opendocument.presentation-template"},{ext:".odg",mimeType:"application/vnd.oasis.opendocument.graphics"},{ext:".otg",mimeType:"application/vnd.oasis.opendocument.graphics-template"},{ext:".odf",mimeType:"application/vnd.oasis.opendocument.formula"},{ext:".odc",mimeType:"application/vnd.oasis.opendocument.chart"},{ext:".sxc",mimeType:"application/vnd.sun.xml.calc"},{ext:".pdf",mimeType:"application/pdf"},{ext:".fdf",mimeType:"application/vnd.fdf"},{ext:"",mimeType:"application/x-ole-storage"},{ext:".msi",mimeType:"application/x-ms-installer"},{ext:".aaf",mimeType:"application/octet-stream"},{ext:".msg",mimeType:"application/vnd.ms-outlook"},{ext:".xls",mimeType:"application/vnd.ms-excel"},{ext:".pub",mimeType:"application/vnd.ms-publisher"},{ext:".ppt",mimeType:"application/vnd.ms-powerpoint"},{ext:".doc",mimeType:"application/msword"},{ext:".ps",mimeType:"application/postscript"},{ext:".psd",mimeType:"image/vnd.adobe.photoshop"},{ext:".p7s",mimeType:"application/pkcs7-signature"},{ext:".ogg",mimeType:"application/ogg"},{ext:".oga",mimeType:"audio/ogg"},{ext:".ogv",mimeType:"video/ogg"},{ext:".png",mimeType:"image/png"},{ext:".png",mimeType:"image/vnd.mozilla.apng"},{ext:".jpg",mimeType:"image/jpeg"},{ext:".jxl",mimeType:"image/jxl"},{ext:".jp2",mimeType:"image/jp2"},{ext:".jpf",mimeType:"image/jpx"},{ext:".jpm",mimeType:"image/jpm"},{ext:".jxs",mimeType:"image/jxs"},{ext:".gif",mimeType:"image/gif"},{ext:".webp",mimeType:"image/webp"},{ext:".exe",mimeType:"application/vnd.microsoft.portable-executable"},{ext:"",mimeType:"application/x-elf"},{ext:"",mimeType:"application/x-object"},{ext:"",mimeType:"application/x-executable"},{ext:".so",mimeType:"application/x-sharedlib"},{ext:"",mimeType:"application/x-coredump"},{ext:".a",mimeType:"application/x-archive"},{ext:".deb",mimeType:"application/vnd.debian.binary-package"},{ext:".tar",mimeType:"application/x-tar"},{ext:".xar",mimeType:"application/x-xar"},{ext:".bz2",mimeType:"application/x-bzip2"},{ext:".fits",mimeType:"application/fits"},{ext:".tiff",mimeType:"image/tiff"},{ext:".bmp",mimeType:"image/bmp"},{ext:".ico",mimeType:"image/x-icon"},{ext:".mp3",mimeType:"audio/mpeg"},{ext:".flac",mimeType:"audio/flac"},{ext:".midi",mimeType:"audio/midi"},{ext:".ape",mimeType:"audio/ape"},{ext:".mpc",mimeType:"audio/musepack"},{ext:".amr",mimeType:"audio/amr"},{ext:".wav",mimeType:"audio/wav"},{ext:".aiff",mimeType:"audio/aiff"},{ext:".au",mimeType:"audio/basic"},{ext:".mpeg",mimeType:"video/mpeg"},{ext:".mov",mimeType:"video/quicktime"},{ext:".mqv",mimeType:"video/quicktime"},{ext:".mp4",mimeType:"video/mp4"},{ext:".webm",mimeType:"video/webm"},{ext:".3gp",mimeType:"video/3gpp"},{ext:".3g2",mimeType:"video/3gpp2"},{ext:".avi",mimeType:"video/x-msvideo"},{ext:".flv",mimeType:"video/x-flv"},{ext:".mkv",mimeType:"video/x-matroska"},{ext:".asf",mimeType:"video/x-ms-asf"},{ext:".aac",mimeType:"audio/aac"},{ext:".voc",mimeType:"audio/x-unknown"},{ext:".mp4",mimeType:"audio/mp4"},{ext:".m4a",mimeType:"audio/x-m4a"},{ext:".m3u",mimeType:"application/vnd.apple.mpegurl"},{ext:".m4v",mimeType:"video/x-m4v"},{ext:".rmvb",mimeType:"application/vnd.rn-realmedia-vbr"},{ext:".gz",mimeType:"application/gzip"},{ext:".class",mimeType:"application/x-java-applet"},{ext:".swf",mimeType:"application/x-shockwave-flash"},{ext:".crx",mimeType:"application/x-chrome-extension"},{ext:".ttf",mimeType:"font/ttf"},{ext:".woff",mimeType:"font/woff"},{ext:".woff2",mimeType:"font/woff2"},{ext:".otf",mimeType:"font/otf"},{ext:".ttc",mimeType:"font/collection"},{ext:".eot",mimeType:"application/vnd.ms-fontobject"},{ext:".wasm",mimeType:"application/wasm"},{ext:".shx",mimeType:"application/vnd.shx"},{ext:".shp",mimeType:"application/vnd.shp"},{ext:".dbf",mimeType:"application/x-dbf"},{ext:".dcm",mimeType:"application/dicom"},{ext:".rar",mimeType:"application/x-rar-compressed"},{ext:".djvu",mimeType:"image/vnd.djvu"},{ext:".mobi",mimeType:"application/x-mobipocket-ebook"},{ext:".lit",mimeType:"application/x-ms-reader"},{ext:".bpg",mimeType:"image/bpg"},{ext:".sqlite",mimeType:"application/vnd.sqlite3"},{ext:".dwg",mimeType:"image/vnd.dwg"},{ext:".nes",mimeType:"application/vnd.nintendo.snes.rom"},{ext:".lnk",mimeType:"application/x-ms-shortcut"},{ext:".macho",mimeType:"application/x-mach-binary"},{ext:".qcp",mimeType:"audio/qcelp"},{ext:".icns",mimeType:"image/x-icns"},{ext:".heic",mimeType:"image/heic"},{ext:".heic",mimeType:"image/heic-sequence"},{ext:".heif",mimeType:"image/heif"},{ext:".heif",mimeType:"image/heif-sequence"},{ext:".hdr",mimeType:"image/vnd.radiance"},{ext:".mrc",mimeType:"application/marc"},{ext:".mdb",mimeType:"application/x-msaccess"},{ext:".accdb",mimeType:"application/x-msaccess"},{ext:".zst",mimeType:"application/zstd"},{ext:".cab",mimeType:"application/vnd.ms-cab-compressed"},{ext:".rpm",mimeType:"application/x-rpm"},{ext:".xz",mimeType:"application/x-xz"},{ext:".lz",mimeType:"application/lzip"},{ext:".torrent",mimeType:"application/x-bittorrent"},{ext:".cpio",mimeType:"application/x-cpio"},{ext:"",mimeType:"application/tzif"},{ext:".xcf",mimeType:"image/x-xcf"},{ext:".pat",mimeType:"image/x-gimp-pat"},{ext:".gbr",mimeType:"image/x-gimp-gbr"},{ext:".glb",mimeType:"model/gltf-binary"},{ext:".avif",mimeType:"image/avif"},{ext:".cab",mimeType:"application/x-installshield"},{ext:".jxr",mimeType:"image/jxr"},{ext:".txt",mimeType:"text/plain"},{ext:".html",mimeType:"text/html"},{ext:".svg",mimeType:"image/svg+xml"},{ext:".xml",mimeType:"text/xml"},{ext:".rss",mimeType:"application/rss+xml"},{ext:".atom",mimeType:"applicatiotom+xml"},{ext:".x3d",mimeType:"model/x3d+xml"},{ext:".kml",mimeType:"application/vnd.google-earth.kml+xml"},{ext:".xlf",mimeType:"application/x-xliff+xml"},{ext:".dae",mimeType:"model/vnd.collada+xml"},{ext:".gml",mimeType:"application/gml+xml"},{ext:".gpx",mimeType:"application/gpx+xml"},{ext:".tcx",mimeType:"application/vnd.garmin.tcx+xml"},{ext:".amf",mimeType:"application/x-amf"},{ext:".3mf",mimeType:"application/vnd.ms-package.3dmanufacturing-3dmodel+xml"},{ext:".xfdf",mimeType:"application/vnd.adobe.xfdf"},{ext:".owl",mimeType:"application/owl+xml"},{ext:".php",mimeType:"text/x-php"},{ext:".js",mimeType:"application/javascript"},{ext:".lua",mimeType:"text/x-lua"},{ext:".pl",mimeType:"text/x-perl"},{ext:".py",mimeType:"text/x-python"},{ext:".json",mimeType:"application/json"},{ext:".geojson",mimeType:"application/geo+json"},{ext:".har",mimeType:"application/json"},{ext:".ndjson",mimeType:"application/x-ndjson"},{ext:".rtf",mimeType:"text/rtf"},{ext:".srt",mimeType:"application/x-subrip"},{ext:".tcl",mimeType:"text/x-tcl"},{ext:".csv",mimeType:"text/csv"},{ext:".tsv",mimeType:"text/tab-separated-values"},{ext:".vcf",mimeType:"text/vcard"},{ext:".ics",mimeType:"text/calendar"},{ext:".warc",mimeType:"application/warc"},{ext:".vtt",mimeType:"text/vtt"},{ext:"",mimeType:"application/octet-stream"}];function mC(n){let e,t,i;function l(o){n[16](o)}let s={id:n[23],items:n[4],readonly:!n[24]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new hi({props:s}),ee.push(()=>be(e,"keyOfSelected",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&8388608&&(a.id=o[23]),r&16777216&&(a.readonly=!o[24]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function hC(n){let e,t,i,l,s,o;return i=new ce({props:{class:"form-field form-field-single-multiple-select "+(n[24]?"":"readonly"),inlineError:!0,$$slots:{default:[mC,({uniqueId:r})=>({23:r}),({uniqueId:r})=>r?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=M(),B(i.$$.fragment),l=M(),s=b("div"),p(e,"class","separator"),p(s,"class","separator")},m(r,a){w(r,e,a),w(r,t,a),z(i,r,a),w(r,l,a),w(r,s,a),o=!0},p(r,a){const f={};a&16777216&&(f.class="form-field form-field-single-multiple-select "+(r[24]?"":"readonly")),a&58720260&&(f.$$scope={dirty:a,ctx:r}),i.$set(f)},i(r){o||(E(i.$$.fragment,r),o=!0)},o(r){A(i.$$.fragment,r),o=!1},d(r){r&&(v(e),v(t),v(l),v(s)),V(i,r)}}}function _C(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("button"),e.innerHTML='Images (jpg, png, svg, gif, webp)',t=M(),i=b("button"),i.innerHTML='Documents (pdf, doc/docx, xls/xlsx)',l=M(),s=b("button"),s.innerHTML='Videos (mp4, avi, mov, 3gp)',o=M(),r=b("button"),r.innerHTML='Archives (zip, 7zip, rar)',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem"),p(i,"type","button"),p(i,"class","dropdown-item closable"),p(i,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(s,"role","menuitem"),p(r,"type","button"),p(r,"class","dropdown-item closable"),p(r,"role","menuitem")},m(u,c){w(u,e,c),w(u,t,c),w(u,i,c),w(u,l,c),w(u,s,c),w(u,o,c),w(u,r,c),a||(f=[J(e,"click",n[9]),J(i,"click",n[10]),J(s,"click",n[11]),J(r,"click",n[12])],a=!0)},p:Q,d(u){u&&(v(e),v(t),v(i),v(l),v(s),v(o),v(r)),a=!1,$e(f)}}}function gC(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T;function $(O){n[8](O)}let C={id:n[23],multiple:!0,searchable:!0,closable:!1,selectionKey:"mimeType",selectPlaceholder:"No restriction",items:n[3],labelComponent:$d,optionComponent:$d};return n[0].options.mimeTypes!==void 0&&(C.keyOfSelected=n[0].options.mimeTypes),r=new hi({props:C}),ee.push(()=>be(r,"keyOfSelected",$)),g=new On({props:{class:"dropdown dropdown-sm dropdown-nowrap dropdown-left",$$slots:{default:[_C]},$$scope:{ctx:n}}}),{c(){e=b("label"),t=b("span"),t.textContent="Allowed mime types",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),c=b("div"),d=b("span"),d.textContent="Choose presets",m=M(),h=b("i"),_=M(),B(g.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(d,"class","txt link-primary"),p(h,"class","ri-arrow-drop-down-fill"),p(h,"aria-hidden","true"),p(c,"tabindex","0"),p(c,"role","button"),p(c,"class","inline-flex flex-gap-0"),p(u,"class","help-block")},m(O,D){w(O,e,D),k(e,t),k(e,i),k(e,l),w(O,o,D),z(r,O,D),w(O,f,D),w(O,u,D),k(u,c),k(c,d),k(c,m),k(c,h),k(c,_),z(g,c,null),y=!0,S||(T=Se(Pe.call(null,l,{text:`Allow files ONLY with the listed mime types. - Leave empty for no restriction.`,position:"top"})),S=!0)},p(O,D){(!y||D&8388608&&s!==(s=O[23]))&&p(e,"for",s);const I={};D&8388608&&(I.id=O[23]),D&8&&(I.items=O[3]),!a&&D&1&&(a=!0,I.keyOfSelected=O[0].options.mimeTypes,ke(()=>a=!1)),r.$set(I);const L={};D&33554433&&(L.$$scope={dirty:D,ctx:O}),g.$set(L)},i(O){y||(E(r.$$.fragment,O),E(g.$$.fragment,O),y=!0)},o(O){A(r.$$.fragment,O),A(g.$$.fragment,O),y=!1},d(O){O&&(v(e),v(o),v(f),v(u)),V(r,O),V(g),S=!1,T()}}}function bC(n){let e;return{c(){e=b("ul"),e.innerHTML=`
  • WxH - (eg. 100x50) - crop to WxH viewbox (from center)
  • WxHt - (eg. 100x50t) - crop to WxH viewbox (from top)
  • WxHb - (eg. 100x50b) - crop to WxH viewbox (from bottom)
  • WxHf - (eg. 100x50f) - fit inside a WxH viewbox (without cropping)
  • 0xH - (eg. 0x50) - resize to H height preserving the aspect ratio
  • Wx0 - (eg. 100x0) - resize to W width preserving the aspect ratio
  • `,p(e,"class","m-0")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function kC(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C;function O(I){n[13](I)}let D={id:n[23],placeholder:"eg. 50x50, 480x720"};return n[0].options.thumbs!==void 0&&(D.value=n[0].options.thumbs),r=new Nl({props:D}),ee.push(()=>be(r,"value",O)),S=new On({props:{class:"dropdown dropdown-sm dropdown-center dropdown-nowrap p-r-10",$$slots:{default:[bC]},$$scope:{ctx:n}}}),{c(){e=b("label"),t=b("span"),t.textContent="Thumb sizes",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),c=b("span"),c.textContent="Use comma as separator.",d=M(),m=b("button"),h=b("span"),h.textContent="Supported formats",_=M(),g=b("i"),y=M(),B(S.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(c,"class","txt"),p(h,"class","txt link-primary"),p(g,"class","ri-arrow-drop-down-fill"),p(g,"aria-hidden","true"),p(m,"type","button"),p(m,"class","inline-flex flex-gap-0"),p(u,"class","help-block")},m(I,L){w(I,e,L),k(e,t),k(e,i),k(e,l),w(I,o,L),z(r,I,L),w(I,f,L),w(I,u,L),k(u,c),k(u,d),k(u,m),k(m,h),k(m,_),k(m,g),k(m,y),z(S,m,null),T=!0,$||(C=Se(Pe.call(null,l,{text:"List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.",position:"top"})),$=!0)},p(I,L){(!T||L&8388608&&s!==(s=I[23]))&&p(e,"for",s);const R={};L&8388608&&(R.id=I[23]),!a&&L&1&&(a=!0,R.value=I[0].options.thumbs,ke(()=>a=!1)),r.$set(R);const F={};L&33554432&&(F.$$scope={dirty:L,ctx:I}),S.$set(F)},i(I){T||(E(r.$$.fragment,I),E(S.$$.fragment,I),T=!0)},o(I){A(r.$$.fragment,I),A(S.$$.fragment,I),T=!1},d(I){I&&(v(e),v(o),v(f),v(u)),V(r,I),V(S),$=!1,C()}}}function yC(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=K("Max file size"),l=M(),s=b("input"),r=M(),a=b("div"),a.textContent="Must be in bytes.",p(e,"for",i=n[23]),p(s,"type","number"),p(s,"id",o=n[23]),p(s,"step","1"),p(s,"min","0"),p(a,"class","help-block")},m(c,d){w(c,e,d),k(e,t),w(c,l,d),w(c,s,d),re(s,n[0].options.maxSize),w(c,r,d),w(c,a,d),f||(u=J(s,"input",n[14]),f=!0)},p(c,d){d&8388608&&i!==(i=c[23])&&p(e,"for",i),d&8388608&&o!==(o=c[23])&&p(s,"id",o),d&1&&it(s.value)!==c[0].options.maxSize&&re(s,c[0].options.maxSize)},d(c){c&&(v(e),v(l),v(s),v(r),v(a)),f=!1,u()}}}function Td(n){let e,t,i;return t=new ce({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[vC,({uniqueId:l})=>({23:l}),({uniqueId:l})=>l?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","col-sm-3")},m(l,s){w(l,e,s),z(t,e,null),i=!0},p(l,s){const o={};s&2&&(o.name="schema."+l[1]+".options.maxSelect"),s&41943041&&(o.$$scope={dirty:s,ctx:l}),t.$set(o)},i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function vC(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Max select"),l=M(),s=b("input"),p(e,"for",i=n[23]),p(s,"id",o=n[23]),p(s,"type","number"),p(s,"step","1"),p(s,"min","2"),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.maxSelect),r||(a=J(s,"input",n[15]),r=!0)},p(f,u){u&8388608&&i!==(i=f[23])&&p(e,"for",i),u&8388608&&o!==(o=f[23])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.maxSelect&&re(s,f[0].options.maxSelect)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function wC(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;i=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.mimeTypes",$$slots:{default:[gC,({uniqueId:_})=>({23:_}),({uniqueId:_})=>_?8388608:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.thumbs",$$slots:{default:[kC,({uniqueId:_})=>({23:_}),({uniqueId:_})=>_?8388608:0]},$$scope:{ctx:n}}}),u=new ce({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSize",$$slots:{default:[yC,({uniqueId:_})=>({23:_}),({uniqueId:_})=>_?8388608:0]},$$scope:{ctx:n}}});let h=!n[2]&&Td(n);return{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),a=M(),f=b("div"),B(u.$$.fragment),d=M(),h&&h.c(),p(t,"class","col-sm-12"),p(s,"class",r=n[2]?"col-sm-8":"col-sm-6"),p(f,"class",c=n[2]?"col-sm-4":"col-sm-3"),p(e,"class","grid grid-sm")},m(_,g){w(_,e,g),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,a),k(e,f),z(u,f,null),k(e,d),h&&h.m(e,null),m=!0},p(_,g){const y={};g&2&&(y.name="schema."+_[1]+".options.mimeTypes"),g&41943049&&(y.$$scope={dirty:g,ctx:_}),i.$set(y);const S={};g&2&&(S.name="schema."+_[1]+".options.thumbs"),g&41943041&&(S.$$scope={dirty:g,ctx:_}),o.$set(S),(!m||g&4&&r!==(r=_[2]?"col-sm-8":"col-sm-6"))&&p(s,"class",r);const T={};g&2&&(T.name="schema."+_[1]+".options.maxSize"),g&41943041&&(T.$$scope={dirty:g,ctx:_}),u.$set(T),(!m||g&4&&c!==(c=_[2]?"col-sm-4":"col-sm-3"))&&p(f,"class",c),_[2]?h&&(le(),A(h,1,1,()=>{h=null}),se()):h?(h.p(_,g),g&4&&E(h,1)):(h=Td(_),h.c(),E(h,1),h.m(e,null))},i(_){m||(E(i.$$.fragment,_),E(o.$$.fragment,_),E(u.$$.fragment,_),E(h),m=!0)},o(_){A(i.$$.fragment,_),A(o.$$.fragment,_),A(u.$$.fragment,_),A(h),m=!1},d(_){_&&v(e),V(i),V(o),V(u),h&&h.d()}}}function SC(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Protected",r=M(),a=b("a"),a.textContent="(Learn more)",p(e,"type","checkbox"),p(e,"id",t=n[23]),p(s,"class","txt"),p(l,"for",o=n[23]),p(a,"href","https://pocketbase.io/docs/files-handling/#protected-files"),p(a,"class","toggle-info txt-sm txt-hint m-l-5"),p(a,"target","_blank"),p(a,"rel","noopener")},m(c,d){w(c,e,d),e.checked=n[0].options.protected,w(c,i,d),w(c,l,d),k(l,s),w(c,r,d),w(c,a,d),f||(u=J(e,"change",n[7]),f=!0)},p(c,d){d&8388608&&t!==(t=c[23])&&p(e,"id",t),d&1&&(e.checked=c[0].options.protected),d&8388608&&o!==(o=c[23])&&p(l,"for",o)},d(c){c&&(v(e),v(i),v(l),v(r),v(a)),f=!1,u()}}}function $C(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",name:"schema."+n[1]+".options.protected",$$slots:{default:[SC,({uniqueId:i})=>({23:i}),({uniqueId:i})=>i?8388608:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&2&&(s.name="schema."+i[1]+".options.protected"),l&41943041&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function TC(n){let e,t,i;const l=[{key:n[1]},n[5]];function s(r){n[17](r)}let o={$$slots:{optionsFooter:[$C],options:[wC],default:[hC,({interactive:r})=>({24:r}),({interactive:r})=>r?16777216:0]},$$scope:{ctx:n}};for(let r=0;rbe(e,"field",s)),e.$on("rename",n[18]),e.$on("remove",n[19]),e.$on("duplicate",n[20]),{c(){B(e.$$.fragment)},m(r,a){z(e,r,a),i=!0},p(r,[a]){const f=a&34?pt(l,[a&2&&{key:r[1]},a&32&&Ot(r[5])]):{};a&50331663&&(f.$$scope={dirty:a,ctx:r}),!t&&a&1&&(t=!0,f.field=r[0],ke(()=>t=!1)),e.$set(f)},i(r){i||(E(e.$$.fragment,r),i=!0)},o(r){A(e.$$.fragment,r),i=!1},d(r){V(e,r)}}}function CC(n,e,t){var F;const i=["field","key"];let l=Ge(e,i),{field:s}=e,{key:o=""}=e;const r=[{label:"Single",value:!0},{label:"Multiple",value:!1}];let a=pC.slice(),f=((F=s.options)==null?void 0:F.maxSelect)<=1,u=f;function c(){t(0,s.options={maxSelect:1,maxSize:5242880,thumbs:[],mimeTypes:[]},s),t(2,f=!0),t(6,u=f)}function d(){if(j.isEmpty(s.options.mimeTypes))return;const N=[];for(const P of s.options.mimeTypes)a.find(q=>q.mimeType===P)||N.push({mimeType:P});N.length&&t(3,a=a.concat(N))}function m(){s.options.protected=this.checked,t(0,s),t(6,u),t(2,f)}function h(N){n.$$.not_equal(s.options.mimeTypes,N)&&(s.options.mimeTypes=N,t(0,s),t(6,u),t(2,f))}const _=()=>{t(0,s.options.mimeTypes=["image/jpeg","image/png","image/svg+xml","image/gif","image/webp"],s)},g=()=>{t(0,s.options.mimeTypes=["application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],s)},y=()=>{t(0,s.options.mimeTypes=["video/mp4","video/x-ms-wmv","video/quicktime","video/3gpp"],s)},S=()=>{t(0,s.options.mimeTypes=["application/zip","application/x-7z-compressed","application/x-rar-compressed"],s)};function T(N){n.$$.not_equal(s.options.thumbs,N)&&(s.options.thumbs=N,t(0,s),t(6,u),t(2,f))}function $(){s.options.maxSize=it(this.value),t(0,s),t(6,u),t(2,f)}function C(){s.options.maxSelect=it(this.value),t(0,s),t(6,u),t(2,f)}function O(N){f=N,t(2,f)}function D(N){s=N,t(0,s),t(6,u),t(2,f)}function I(N){Ce.call(this,n,N)}function L(N){Ce.call(this,n,N)}function R(N){Ce.call(this,n,N)}return n.$$set=N=>{e=Ie(Ie({},e),Yt(N)),t(5,l=Ge(e,i)),"field"in N&&t(0,s=N.field),"key"in N&&t(1,o=N.key)},n.$$.update=()=>{var N,P;n.$$.dirty&69&&u!=f&&(t(6,u=f),f?t(0,s.options.maxSelect=1,s):t(0,s.options.maxSelect=((P=(N=s.options)==null?void 0:N.values)==null?void 0:P.length)||99,s)),n.$$.dirty&1&&(j.isEmpty(s.options)?c():d())},[s,o,f,a,r,l,u,m,h,_,g,y,S,T,$,C,O,D,I,L,R]}class OC extends ge{constructor(e){super(),_e(this,e,CC,TC,me,{field:0,key:1})}}function MC(n){let e,t,i,l,s;return{c(){e=b("hr"),t=M(),i=b("button"),i.innerHTML=' New collection',p(i,"type","button"),p(i,"class","btn btn-transparent btn-block btn-sm")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),l||(s=J(i,"click",n[14]),l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,s()}}}function DC(n){let e,t,i;function l(o){n[15](o)}let s={id:n[24],searchable:n[5].length>5,selectPlaceholder:"Select collection *",noOptionsText:"No collections found",selectionKey:"id",items:n[5],readonly:!n[25]||n[0].id,$$slots:{afterOptions:[MC]},$$scope:{ctx:n}};return n[0].options.collectionId!==void 0&&(s.keyOfSelected=n[0].options.collectionId),e=new hi({props:s}),ee.push(()=>be(e,"keyOfSelected",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&16777216&&(a.id=o[24]),r&32&&(a.searchable=o[5].length>5),r&32&&(a.items=o[5]),r&33554433&&(a.readonly=!o[25]||o[0].id),r&67108872&&(a.$$scope={dirty:r,ctx:o}),!t&&r&1&&(t=!0,a.keyOfSelected=o[0].options.collectionId,ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function EC(n){let e,t,i;function l(o){n[16](o)}let s={id:n[24],items:n[6],readonly:!n[25]};return n[2]!==void 0&&(s.keyOfSelected=n[2]),e=new hi({props:s}),ee.push(()=>be(e,"keyOfSelected",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&16777216&&(a.id=o[24]),r&33554432&&(a.readonly=!o[25]),!t&&r&4&&(t=!0,a.keyOfSelected=o[2],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function IC(n){let e,t,i,l,s,o,r,a,f,u;return i=new ce({props:{class:"form-field required "+(n[25]?"":"readonly"),inlineError:!0,name:"schema."+n[1]+".options.collectionId",$$slots:{default:[DC,({uniqueId:c})=>({24:c}),({uniqueId:c})=>c?16777216:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field form-field-single-multiple-select "+(n[25]?"":"readonly"),inlineError:!0,$$slots:{default:[EC,({uniqueId:c})=>({24:c}),({uniqueId:c})=>c?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=M(),B(i.$$.fragment),l=M(),s=b("div"),o=M(),B(r.$$.fragment),a=M(),f=b("div"),p(e,"class","separator"),p(s,"class","separator"),p(f,"class","separator")},m(c,d){w(c,e,d),w(c,t,d),z(i,c,d),w(c,l,d),w(c,s,d),w(c,o,d),z(r,c,d),w(c,a,d),w(c,f,d),u=!0},p(c,d){const m={};d&33554432&&(m.class="form-field required "+(c[25]?"":"readonly")),d&2&&(m.name="schema."+c[1]+".options.collectionId"),d&117440553&&(m.$$scope={dirty:d,ctx:c}),i.$set(m);const h={};d&33554432&&(h.class="form-field form-field-single-multiple-select "+(c[25]?"":"readonly")),d&117440516&&(h.$$scope={dirty:d,ctx:c}),r.$set(h)},i(c){u||(E(i.$$.fragment,c),E(r.$$.fragment,c),u=!0)},o(c){A(i.$$.fragment,c),A(r.$$.fragment,c),u=!1},d(c){c&&(v(e),v(t),v(l),v(s),v(o),v(a),v(f)),V(i,c),V(r,c)}}}function Cd(n){let e,t,i,l,s,o;return t=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.minSelect",$$slots:{default:[AC,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),s=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[LC,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),B(t.$$.fragment),i=M(),l=b("div"),B(s.$$.fragment),p(e,"class","col-sm-6"),p(l,"class","col-sm-6")},m(r,a){w(r,e,a),z(t,e,null),w(r,i,a),w(r,l,a),z(s,l,null),o=!0},p(r,a){const f={};a&2&&(f.name="schema."+r[1]+".options.minSelect"),a&83886081&&(f.$$scope={dirty:a,ctx:r}),t.$set(f);const u={};a&2&&(u.name="schema."+r[1]+".options.maxSelect"),a&83886081&&(u.$$scope={dirty:a,ctx:r}),s.$set(u)},i(r){o||(E(t.$$.fragment,r),E(s.$$.fragment,r),o=!0)},o(r){A(t.$$.fragment,r),A(s.$$.fragment,r),o=!1},d(r){r&&(v(e),v(i),v(l)),V(t),V(s)}}}function AC(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Min select"),l=M(),s=b("input"),p(e,"for",i=n[24]),p(s,"type","number"),p(s,"id",o=n[24]),p(s,"step","1"),p(s,"min","1"),p(s,"placeholder","No min limit")},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.minSelect),r||(a=J(s,"input",n[11]),r=!0)},p(f,u){u&16777216&&i!==(i=f[24])&&p(e,"for",i),u&16777216&&o!==(o=f[24])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.minSelect&&re(s,f[0].options.minSelect)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function LC(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("label"),t=K("Max select"),l=M(),s=b("input"),p(e,"for",i=n[24]),p(s,"type","number"),p(s,"id",o=n[24]),p(s,"step","1"),p(s,"placeholder","No max limit"),p(s,"min",r=n[0].options.minSelect||2)},m(u,c){w(u,e,c),k(e,t),w(u,l,c),w(u,s,c),re(s,n[0].options.maxSelect),a||(f=J(s,"input",n[12]),a=!0)},p(u,c){c&16777216&&i!==(i=u[24])&&p(e,"for",i),c&16777216&&o!==(o=u[24])&&p(s,"id",o),c&1&&r!==(r=u[0].options.minSelect||2)&&p(s,"min",r),c&1&&it(s.value)!==u[0].options.maxSelect&&re(s,u[0].options.maxSelect)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function NC(n){let e,t,i,l,s,o,r,a,f,u,c,d;function m(_){n[13](_)}let h={id:n[24],items:n[7]};return n[0].options.cascadeDelete!==void 0&&(h.keyOfSelected=n[0].options.cascadeDelete),a=new hi({props:h}),ee.push(()=>be(a,"keyOfSelected",m)),{c(){e=b("label"),t=b("span"),t.textContent="Cascade delete",i=M(),l=b("i"),r=M(),B(a.$$.fragment),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",o=n[24])},m(_,g){var y,S;w(_,e,g),k(e,t),k(e,i),k(e,l),w(_,r,g),z(a,_,g),u=!0,c||(d=Se(s=Pe.call(null,l,{text:[`Whether on ${((y=n[4])==null?void 0:y.name)||"relation"} record deletion to delete also the current corresponding collection record(s).`,n[2]?null:`For "Multiple" relation fields the cascade delete is triggered only when all ${((S=n[4])==null?void 0:S.name)||"relation"} ids are removed from the corresponding record.`].filter(Boolean).join(` - -`),position:"top"})),c=!0)},p(_,g){var S,T;s&&Ct(s.update)&&g&20&&s.update.call(null,{text:[`Whether on ${((S=_[4])==null?void 0:S.name)||"relation"} record deletion to delete also the current corresponding collection record(s).`,_[2]?null:`For "Multiple" relation fields the cascade delete is triggered only when all ${((T=_[4])==null?void 0:T.name)||"relation"} ids are removed from the corresponding record.`].filter(Boolean).join(` - -`),position:"top"}),(!u||g&16777216&&o!==(o=_[24]))&&p(e,"for",o);const y={};g&16777216&&(y.id=_[24]),!f&&g&1&&(f=!0,y.keyOfSelected=_[0].options.cascadeDelete,ke(()=>f=!1)),a.$set(y)},i(_){u||(E(a.$$.fragment,_),u=!0)},o(_){A(a.$$.fragment,_),u=!1},d(_){_&&(v(e),v(r)),V(a,_),c=!1,d()}}}function PC(n){let e,t,i,l,s,o=!n[2]&&Cd(n);return l=new ce({props:{class:"form-field",name:"schema."+n[1]+".options.cascadeDelete",$$slots:{default:[NC,({uniqueId:r})=>({24:r}),({uniqueId:r})=>r?16777216:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),o&&o.c(),t=M(),i=b("div"),B(l.$$.fragment),p(i,"class","col-sm-12"),p(e,"class","grid grid-sm")},m(r,a){w(r,e,a),o&&o.m(e,null),k(e,t),k(e,i),z(l,i,null),s=!0},p(r,a){r[2]?o&&(le(),A(o,1,1,()=>{o=null}),se()):o?(o.p(r,a),a&4&&E(o,1)):(o=Cd(r),o.c(),E(o,1),o.m(e,t));const f={};a&2&&(f.name="schema."+r[1]+".options.cascadeDelete"),a&83886101&&(f.$$scope={dirty:a,ctx:r}),l.$set(f)},i(r){s||(E(o),E(l.$$.fragment,r),s=!0)},o(r){A(o),A(l.$$.fragment,r),s=!1},d(r){r&&v(e),o&&o.d(),V(l)}}}function FC(n){let e,t,i,l,s;const o=[{key:n[1]},n[8]];function r(u){n[17](u)}let a={$$slots:{options:[PC],default:[IC,({interactive:u})=>({25:u}),({interactive:u})=>u?33554432:0]},$$scope:{ctx:n}};for(let u=0;ube(e,"field",r)),e.$on("rename",n[18]),e.$on("remove",n[19]),e.$on("duplicate",n[20]);let f={};return l=new Ba({props:f}),n[21](l),l.$on("save",n[22]),{c(){B(e.$$.fragment),i=M(),B(l.$$.fragment)},m(u,c){z(e,u,c),w(u,i,c),z(l,u,c),s=!0},p(u,[c]){const d=c&258?pt(o,[c&2&&{key:u[1]},c&256&&Ot(u[8])]):{};c&100663359&&(d.$$scope={dirty:c,ctx:u}),!t&&c&1&&(t=!0,d.field=u[0],ke(()=>t=!1)),e.$set(d);const m={};l.$set(m)},i(u){s||(E(e.$$.fragment,u),E(l.$$.fragment,u),s=!0)},o(u){A(e.$$.fragment,u),A(l.$$.fragment,u),s=!1},d(u){u&&v(i),V(e,u),n[21](null),V(l,u)}}}function RC(n,e,t){var N;let i,l;const s=["field","key"];let o=Ge(e,s),r;Ue(n,Rn,P=>t(10,r=P));let{field:a}=e,{key:f=""}=e;const u=[{label:"Single",value:!0},{label:"Multiple",value:!1}],c=[{label:"False",value:!1},{label:"True",value:!0}];let d=null,m=((N=a.options)==null?void 0:N.maxSelect)==1,h=m;function _(){t(0,a.options={maxSelect:1,collectionId:null,cascadeDelete:!1},a),t(2,m=!0),t(9,h=m)}function g(){a.options.minSelect=it(this.value),t(0,a),t(9,h),t(2,m)}function y(){a.options.maxSelect=it(this.value),t(0,a),t(9,h),t(2,m)}function S(P){n.$$.not_equal(a.options.cascadeDelete,P)&&(a.options.cascadeDelete=P,t(0,a),t(9,h),t(2,m))}const T=()=>d==null?void 0:d.show();function $(P){n.$$.not_equal(a.options.collectionId,P)&&(a.options.collectionId=P,t(0,a),t(9,h),t(2,m))}function C(P){m=P,t(2,m)}function O(P){a=P,t(0,a),t(9,h),t(2,m)}function D(P){Ce.call(this,n,P)}function I(P){Ce.call(this,n,P)}function L(P){Ce.call(this,n,P)}function R(P){ee[P?"unshift":"push"](()=>{d=P,t(3,d)})}const F=P=>{var q,H;(H=(q=P==null?void 0:P.detail)==null?void 0:q.collection)!=null&&H.id&&P.detail.collection.type!="view"&&t(0,a.options.collectionId=P.detail.collection.id,a)};return n.$$set=P=>{e=Ie(Ie({},e),Yt(P)),t(8,o=Ge(e,s)),"field"in P&&t(0,a=P.field),"key"in P&&t(1,f=P.key)},n.$$.update=()=>{n.$$.dirty&1024&&t(5,i=r.filter(P=>P.type!="view")),n.$$.dirty&516&&h!=m&&(t(9,h=m),m?(t(0,a.options.minSelect=null,a),t(0,a.options.maxSelect=1,a)):t(0,a.options.maxSelect=null,a)),n.$$.dirty&1&&j.isEmpty(a.options)&&_(),n.$$.dirty&1025&&t(4,l=r.find(P=>P.id==a.options.collectionId)||null)},[a,f,m,d,l,i,u,c,o,h,r,g,y,S,T,$,C,O,D,I,L,R,F]}class qC extends ge{constructor(e){super(),_e(this,e,RC,FC,me,{field:0,key:1})}}const jC=n=>({dragging:n&4,dragover:n&8}),Od=n=>({dragging:n[2],dragover:n[3]});function HC(n){let e,t,i,l,s;const o=n[10].default,r=wt(o,n,n[9],Od);return{c(){e=b("div"),r&&r.c(),p(e,"draggable",t=!n[1]),p(e,"class","draggable svelte-28orm4"),x(e,"dragging",n[2]),x(e,"dragover",n[3])},m(a,f){w(a,e,f),r&&r.m(e,null),i=!0,l||(s=[J(e,"dragover",Be(n[11])),J(e,"dragleave",Be(n[12])),J(e,"dragend",n[13]),J(e,"dragstart",n[14]),J(e,"drop",n[15])],l=!0)},p(a,[f]){r&&r.p&&(!i||f&524)&&$t(r,o,a,a[9],i?St(o,a[9],f,jC):Tt(a[9]),Od),(!i||f&2&&t!==(t=!a[1]))&&p(e,"draggable",t),(!i||f&4)&&x(e,"dragging",a[2]),(!i||f&8)&&x(e,"dragover",a[3])},i(a){i||(E(r,a),i=!0)},o(a){A(r,a),i=!1},d(a){a&&v(e),r&&r.d(a),l=!1,$e(s)}}}function zC(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=lt();let{index:o}=e,{list:r=[]}=e,{group:a="default"}=e,{disabled:f=!1}=e,{dragHandleClass:u=""}=e,c=!1,d=!1;function m($,C){if(!(!$||f)){if(u&&!$.target.classList.contains(u)){t(3,d=!1),t(2,c=!1),$.preventDefault();return}t(2,c=!0),$.dataTransfer.effectAllowed="move",$.dataTransfer.dropEffect="move",$.dataTransfer.setData("text/plain",JSON.stringify({index:C,group:a})),s("drag",$)}}function h($,C){if(t(3,d=!1),t(2,c=!1),!$||f)return;$.dataTransfer.dropEffect="move";let O={};try{O=JSON.parse($.dataTransfer.getData("text/plain"))}catch{}if(O.group!=a)return;const D=O.index<<0;D{t(3,d=!0)},g=()=>{t(3,d=!1)},y=()=>{t(3,d=!1),t(2,c=!1)},S=$=>m($,o),T=$=>h($,o);return n.$$set=$=>{"index"in $&&t(0,o=$.index),"list"in $&&t(6,r=$.list),"group"in $&&t(7,a=$.group),"disabled"in $&&t(1,f=$.disabled),"dragHandleClass"in $&&t(8,u=$.dragHandleClass),"$$scope"in $&&t(9,l=$.$$scope)},[o,f,c,d,m,h,r,a,u,l,i,_,g,y,S,T]}class Ms extends ge{constructor(e){super(),_e(this,e,zC,HC,me,{index:0,list:6,group:7,disabled:1,dragHandleClass:8})}}function Md(n,e,t){const i=n.slice();return i[19]=e[t],i[20]=e,i[21]=t,i}function Dd(n){let e,t,i,l,s,o,r,a;return{c(){e=K(`, - `),t=b("code"),t.textContent="username",i=K(` , - `),l=b("code"),l.textContent="email",s=K(` , - `),o=b("code"),o.textContent="emailVisibility",r=K(` , - `),a=b("code"),a.textContent="verified",p(t,"class","txt-sm"),p(l,"class","txt-sm"),p(o,"class","txt-sm"),p(a,"class","txt-sm")},m(f,u){w(f,e,u),w(f,t,u),w(f,i,u),w(f,l,u),w(f,s,u),w(f,o,u),w(f,r,u),w(f,a,u)},d(f){f&&(v(e),v(t),v(i),v(l),v(s),v(o),v(r),v(a))}}}function VC(n){let e,t,i,l;function s(u){n[7](u,n[19],n[20],n[21])}function o(){return n[8](n[21])}function r(){return n[9](n[21])}var a=n[1][n[19].type];function f(u,c){let d={key:u[5](u[19])};return u[19]!==void 0&&(d.field=u[19]),{props:d}}return a&&(e=Dt(a,f(n)),ee.push(()=>be(e,"field",s)),e.$on("remove",o),e.$on("duplicate",r),e.$on("rename",n[10])),{c(){e&&B(e.$$.fragment),i=M()},m(u,c){e&&z(e,u,c),w(u,i,c),l=!0},p(u,c){if(n=u,c&1&&a!==(a=n[1][n[19].type])){if(e){le();const d=e;A(d.$$.fragment,1,0,()=>{V(d,1)}),se()}a?(e=Dt(a,f(n)),ee.push(()=>be(e,"field",s)),e.$on("remove",o),e.$on("duplicate",r),e.$on("rename",n[10]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,i.parentNode,i)):e=null}else if(a){const d={};c&1&&(d.key=n[5](n[19])),!t&&c&1&&(t=!0,d.field=n[19],ke(()=>t=!1)),e.$set(d)}},i(u){l||(e&&E(e.$$.fragment,u),l=!0)},o(u){e&&A(e.$$.fragment,u),l=!1},d(u){u&&v(i),e&&V(e,u)}}}function Ed(n,e){let t,i,l,s;function o(a){e[11](a)}let r={index:e[21],disabled:e[19].toDelete||e[19].id&&e[19].system,dragHandleClass:"drag-handle-wrapper",$$slots:{default:[VC]},$$scope:{ctx:e}};return e[0].schema!==void 0&&(r.list=e[0].schema),i=new Ms({props:r}),ee.push(()=>be(i,"list",o)),i.$on("drag",e[12]),i.$on("sort",e[13]),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f&1&&(u.index=e[21]),f&1&&(u.disabled=e[19].toDelete||e[19].id&&e[19].system),f&4194305&&(u.$$scope={dirty:f,ctx:e}),!l&&f&1&&(l=!0,u.list=e[0].schema,ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function BC(n){let e,t,i,l,s,o,r,a,f,u,c,d,m=[],h=new Map,_,g,y,S,T,$,C,O,D=n[0].type==="auth"&&Dd(),I=ue(n[0].schema);const L=N=>N[19];for(let N=0;Nbe($,"collection",R)),{c(){e=b("div"),t=b("p"),i=K(`System fields: - `),l=b("code"),l.textContent="id",s=K(` , - `),o=b("code"),o.textContent="created",r=K(` , - `),a=b("code"),a.textContent="updated",f=M(),D&&D.c(),u=K(` - .`),c=M(),d=b("div");for(let N=0;NC=!1)),$.$set(q)},i(N){if(!O){for(let P=0;PI.name===O))}function c(O){return i.findIndex(D=>D===O)}function d(O,D){var I,L;!((I=l==null?void 0:l.schema)!=null&&I.length)||O===D||!D||(L=l==null?void 0:l.schema)!=null&&L.find(R=>R.name==O&&!R.toDelete)||t(0,l.indexes=l.indexes.map(R=>j.replaceIndexColumn(R,O,D)),l)}function m(O,D,I,L){I[L]=O,t(0,l)}const h=O=>o(O),_=O=>r(O),g=O=>d(O.detail.oldName,O.detail.newName);function y(O){n.$$.not_equal(l.schema,O)&&(l.schema=O,t(0,l))}const S=O=>{if(!O.detail)return;const D=O.detail.target;D.style.opacity=0,setTimeout(()=>{var I;(I=D==null?void 0:D.style)==null||I.removeProperty("opacity")},0),O.detail.dataTransfer.setDragImage(D,0,0)},T=()=>{Jt({})},$=O=>a(O.detail);function C(O){l=O,t(0,l)}return n.$$set=O=>{"collection"in O&&t(0,l=O.collection)},n.$$.update=()=>{n.$$.dirty&1&&typeof l.schema>"u"&&t(0,l.schema=[],l),n.$$.dirty&1&&(i=l.schema.filter(O=>!O.toDelete)||[])},[l,s,o,r,a,c,d,m,h,_,g,y,S,T,$,C]}class WC extends ge{constructor(e){super(),_e(this,e,UC,BC,me,{collection:0})}}const YC=n=>({isAdminOnly:n&512}),Id=n=>({isAdminOnly:n[9]}),KC=n=>({isAdminOnly:n&512}),Ad=n=>({isAdminOnly:n[9]}),JC=n=>({isAdminOnly:n&512}),Ld=n=>({isAdminOnly:n[9]});function ZC(n){let e,t;return e=new ce({props:{class:"form-field rule-field "+(n[4]?"requied":"")+" "+(n[9]?"disabled":""),name:n[3],$$slots:{default:[XC,({uniqueId:i})=>({18:i}),({uniqueId:i})=>i?262144:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&528&&(s.class="form-field rule-field "+(i[4]?"requied":"")+" "+(i[9]?"disabled":"")),l&8&&(s.name=i[3]),l&295655&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function GC(n){let e;return{c(){e=b("div"),e.innerHTML='',p(e,"class","txt-center")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Nd(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Set Admins only',p(e,"type","button"),p(e,"class","btn btn-sm btn-transparent btn-hint lock-toggle svelte-1akuazq")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[11]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function Pd(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Unlock and set custom rule
    ',p(e,"type","button"),p(e,"class","unlock-overlay svelte-1akuazq"),p(e,"aria-label","Unlock and set custom rule")},m(o,r){w(o,e,r),i=!0,l||(s=J(e,"click",n[10]),l=!0)},p:Q,i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.98},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.98},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function XC(n){let e,t,i,l,s,o,r=n[9]?"- Admins only":"",a,f,u,c,d,m,h,_,g,y,S;const T=n[12].beforeLabel,$=wt(T,n,n[15],Ld),C=n[12].afterLabel,O=wt(C,n,n[15],Ad);let D=!n[9]&&Nd(n);function I(q){n[14](q)}var L=n[7];function R(q,H){let W={id:q[18],baseCollection:q[1],disabled:q[9],placeholder:q[9]?"":q[5]};return q[0]!==void 0&&(W.value=q[0]),{props:W}}L&&(m=Dt(L,R(n)),n[13](m),ee.push(()=>be(m,"value",I)));let F=n[9]&&Pd(n);const N=n[12].default,P=wt(N,n,n[15],Id);return{c(){e=b("div"),t=b("label"),$&&$.c(),i=M(),l=b("span"),s=K(n[2]),o=M(),a=K(r),f=M(),O&&O.c(),u=M(),D&&D.c(),d=M(),m&&B(m.$$.fragment),_=M(),F&&F.c(),g=M(),y=b("div"),P&&P.c(),p(l,"class","txt"),x(l,"txt-hint",n[9]),p(t,"for",c=n[18]),p(e,"class","input-wrapper svelte-1akuazq"),p(y,"class","help-block")},m(q,H){w(q,e,H),k(e,t),$&&$.m(t,null),k(t,i),k(t,l),k(l,s),k(l,o),k(l,a),k(t,f),O&&O.m(t,null),k(t,u),D&&D.m(t,null),k(e,d),m&&z(m,e,null),k(e,_),F&&F.m(e,null),w(q,g,H),w(q,y,H),P&&P.m(y,null),S=!0},p(q,H){if($&&$.p&&(!S||H&33280)&&$t($,T,q,q[15],S?St(T,q[15],H,JC):Tt(q[15]),Ld),(!S||H&4)&&oe(s,q[2]),(!S||H&512)&&r!==(r=q[9]?"- Admins only":"")&&oe(a,r),(!S||H&512)&&x(l,"txt-hint",q[9]),O&&O.p&&(!S||H&33280)&&$t(O,C,q,q[15],S?St(C,q[15],H,KC):Tt(q[15]),Ad),q[9]?D&&(D.d(1),D=null):D?D.p(q,H):(D=Nd(q),D.c(),D.m(t,null)),(!S||H&262144&&c!==(c=q[18]))&&p(t,"for",c),H&128&&L!==(L=q[7])){if(m){le();const W=m;A(W.$$.fragment,1,0,()=>{V(W,1)}),se()}L?(m=Dt(L,R(q)),q[13](m),ee.push(()=>be(m,"value",I)),B(m.$$.fragment),E(m.$$.fragment,1),z(m,e,_)):m=null}else if(L){const W={};H&262144&&(W.id=q[18]),H&2&&(W.baseCollection=q[1]),H&512&&(W.disabled=q[9]),H&544&&(W.placeholder=q[9]?"":q[5]),!h&&H&1&&(h=!0,W.value=q[0],ke(()=>h=!1)),m.$set(W)}q[9]?F?(F.p(q,H),H&512&&E(F,1)):(F=Pd(q),F.c(),E(F,1),F.m(e,null)):F&&(le(),A(F,1,1,()=>{F=null}),se()),P&&P.p&&(!S||H&33280)&&$t(P,N,q,q[15],S?St(N,q[15],H,YC):Tt(q[15]),Id)},i(q){S||(E($,q),E(O,q),m&&E(m.$$.fragment,q),E(F),E(P,q),S=!0)},o(q){A($,q),A(O,q),m&&A(m.$$.fragment,q),A(F),A(P,q),S=!1},d(q){q&&(v(e),v(g),v(y)),$&&$.d(q),O&&O.d(q),D&&D.d(),n[13](null),m&&V(m),F&&F.d(),P&&P.d(q)}}}function QC(n){let e,t,i,l;const s=[GC,ZC],o=[];function r(a,f){return a[8]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,[f]){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}let Fd;function xC(n,e,t){let i,{$$slots:l={},$$scope:s}=e,{collection:o=null}=e,{rule:r=null}=e,{label:a="Rule"}=e,{formKey:f="rule"}=e,{required:u=!1}=e,{placeholder:c="Leave empty to grant everyone access..."}=e,d=null,m=null,h=Fd,_=!1;g();async function g(){h||_||(t(8,_=!0),t(7,h=(await tt(async()=>{const{default:C}=await import("./FilterAutocompleteInput-l9cXyHQU.js");return{default:C}},__vite__mapDeps([0,1]),import.meta.url)).default),Fd=h,t(8,_=!1))}async function y(){t(0,r=m||""),await Qt(),d==null||d.focus()}async function S(){m=r,t(0,r=null)}function T(C){ee[C?"unshift":"push"](()=>{d=C,t(6,d)})}function $(C){r=C,t(0,r)}return n.$$set=C=>{"collection"in C&&t(1,o=C.collection),"rule"in C&&t(0,r=C.rule),"label"in C&&t(2,a=C.label),"formKey"in C&&t(3,f=C.formKey),"required"in C&&t(4,u=C.required),"placeholder"in C&&t(5,c=C.placeholder),"$$scope"in C&&t(15,s=C.$$scope)},n.$$.update=()=>{n.$$.dirty&1&&t(9,i=r===null)},[r,o,a,f,u,c,d,h,_,i,y,S,l,T,$,s]}class $l extends ge{constructor(e){super(),_e(this,e,xC,QC,me,{collection:1,rule:0,label:2,formKey:3,required:4,placeholder:5})}}function Rd(n,e,t){const i=n.slice();return i[11]=e[t],i}function qd(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I,L=ue(n[2]),R=[];for(let F=0;F@request filter:",c=M(),d=b("div"),d.innerHTML="@request.headers.* @request.query.* @request.data.* @request.auth.*",m=M(),h=b("hr"),_=M(),g=b("p"),g.innerHTML="You could also add constraints and query other collections using the @collection filter:",y=M(),S=b("div"),S.innerHTML="@collection.ANY_COLLECTION_NAME.*",T=M(),$=b("hr"),C=M(),O=b("p"),O.innerHTML=`Example rule: -
    @request.auth.id != "" && created > "2022-01-01 00:00:00"`,p(l,"class","m-b-0"),p(o,"class","inline-flex flex-gap-5"),p(a,"class","m-t-10 m-b-5"),p(u,"class","m-b-0"),p(d,"class","inline-flex flex-gap-5"),p(h,"class","m-t-10 m-b-5"),p(g,"class","m-b-0"),p(S,"class","inline-flex flex-gap-5"),p($,"class","m-t-10 m-b-5"),p(i,"class","content"),p(t,"class","alert alert-warning m-0")},m(F,N){w(F,e,N),k(e,t),k(t,i),k(i,l),k(i,s),k(i,o);for(let P=0;P{I&&(D||(D=Fe(e,et,{duration:150},!0)),D.run(1))}),I=!0)},o(F){F&&(D||(D=Fe(e,et,{duration:150},!1)),D.run(0)),I=!1},d(F){F&&v(e),ot(R,F),F&&D&&D.end()}}}function jd(n){let e,t=n[11]+"",i;return{c(){e=b("code"),i=K(t)},m(l,s){w(l,e,s),k(e,i)},p(l,s){s&4&&t!==(t=l[11]+"")&&oe(i,t)},d(l){l&&v(e)}}}function Hd(n){let e,t,i,l,s,o,r,a,f;function u(g){n[6](g)}let c={label:"Create rule",formKey:"createRule",collection:n[0],$$slots:{afterLabel:[e5,({isAdminOnly:g})=>({10:g}),({isAdminOnly:g})=>g?1024:0]},$$scope:{ctx:n}};n[0].createRule!==void 0&&(c.rule=n[0].createRule),e=new $l({props:c}),ee.push(()=>be(e,"rule",u));function d(g){n[7](g)}let m={label:"Update rule",formKey:"updateRule",collection:n[0]};n[0].updateRule!==void 0&&(m.rule=n[0].updateRule),l=new $l({props:m}),ee.push(()=>be(l,"rule",d));function h(g){n[8](g)}let _={label:"Delete rule",formKey:"deleteRule",collection:n[0]};return n[0].deleteRule!==void 0&&(_.rule=n[0].deleteRule),r=new $l({props:_}),ee.push(()=>be(r,"rule",h)),{c(){B(e.$$.fragment),i=M(),B(l.$$.fragment),o=M(),B(r.$$.fragment)},m(g,y){z(e,g,y),w(g,i,y),z(l,g,y),w(g,o,y),z(r,g,y),f=!0},p(g,y){const S={};y&1&&(S.collection=g[0]),y&17408&&(S.$$scope={dirty:y,ctx:g}),!t&&y&1&&(t=!0,S.rule=g[0].createRule,ke(()=>t=!1)),e.$set(S);const T={};y&1&&(T.collection=g[0]),!s&&y&1&&(s=!0,T.rule=g[0].updateRule,ke(()=>s=!1)),l.$set(T);const $={};y&1&&($.collection=g[0]),!a&&y&1&&(a=!0,$.rule=g[0].deleteRule,ke(()=>a=!1)),r.$set($)},i(g){f||(E(e.$$.fragment,g),E(l.$$.fragment,g),E(r.$$.fragment,g),f=!0)},o(g){A(e.$$.fragment,g),A(l.$$.fragment,g),A(r.$$.fragment,g),f=!1},d(g){g&&(v(i),v(o)),V(e,g),V(l,g),V(r,g)}}}function zd(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-information-line link-hint")},m(l,s){w(l,e,s),t||(i=Se(Pe.call(null,e,{text:'The Create rule is executed after a "dry save" of the submitted data, giving you access to the main record fields as in every other rule.',position:"top"})),t=!0)},d(l){l&&v(e),t=!1,i()}}}function e5(n){let e,t=!n[10]&&zd();return{c(){t&&t.c(),e=ye()},m(i,l){t&&t.m(i,l),w(i,e,l)},p(i,l){i[10]?t&&(t.d(1),t=null):t||(t=zd(),t.c(),t.m(e.parentNode,e))},d(i){i&&v(e),t&&t.d(i)}}}function Vd(n){let e,t,i;function l(o){n[9](o)}let s={label:"Manage rule",formKey:"options.manageRule",placeholder:"",required:n[0].options.manageRule!==null,collection:n[0],$$slots:{default:[t5]},$$scope:{ctx:n}};return n[0].options.manageRule!==void 0&&(s.rule=n[0].options.manageRule),e=new $l({props:s}),ee.push(()=>be(e,"rule",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&1&&(a.required=o[0].options.manageRule!==null),r&1&&(a.collection=o[0]),r&16384&&(a.$$scope={dirty:r,ctx:o}),!t&&r&1&&(t=!0,a.rule=o[0].options.manageRule,ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function t5(n){let e,t,i;return{c(){e=b("p"),e.textContent=`This API rule gives admin-like permissions to allow fully managing the auth record(s), eg. - changing the password without requiring to enter the old one, directly updating the verified - state or email, etc.`,t=M(),i=b("p"),i.innerHTML="This rule is executed in addition to the create and update API rules."},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},p:Q,d(l){l&&(v(e),v(t),v(i))}}}function n5(n){var N,P;let e,t,i,l,s,o=n[1]?"Hide available fields":"Show available fields",r,a,f,u,c,d,m,h,_,g,y,S,T,$,C=n[1]&&qd(n);function O(q){n[4](q)}let D={label:"List/Search rule",formKey:"listRule",collection:n[0]};n[0].listRule!==void 0&&(D.rule=n[0].listRule),u=new $l({props:D}),ee.push(()=>be(u,"rule",O));function I(q){n[5](q)}let L={label:"View rule",formKey:"viewRule",collection:n[0]};n[0].viewRule!==void 0&&(L.rule=n[0].viewRule),m=new $l({props:L}),ee.push(()=>be(m,"rule",I));let R=((N=n[0])==null?void 0:N.type)!=="view"&&Hd(n),F=((P=n[0])==null?void 0:P.type)==="auth"&&Vd(n);return{c(){e=b("div"),t=b("div"),i=b("p"),i.innerHTML=`All rules follow the -
    PocketBase filter syntax and operators - .`,l=M(),s=b("button"),r=K(o),a=M(),C&&C.c(),f=M(),B(u.$$.fragment),d=M(),B(m.$$.fragment),_=M(),R&&R.c(),g=M(),F&&F.c(),y=ye(),p(s,"type","button"),p(s,"class","expand-handle txt-sm txt-bold txt-nowrap link-hint"),p(t,"class","flex txt-sm txt-hint m-b-5"),p(e,"class","block m-b-sm handle")},m(q,H){w(q,e,H),k(e,t),k(t,i),k(t,l),k(t,s),k(s,r),k(e,a),C&&C.m(e,null),w(q,f,H),z(u,q,H),w(q,d,H),z(m,q,H),w(q,_,H),R&&R.m(q,H),w(q,g,H),F&&F.m(q,H),w(q,y,H),S=!0,T||($=J(s,"click",n[3]),T=!0)},p(q,[H]){var U,Y;(!S||H&2)&&o!==(o=q[1]?"Hide available fields":"Show available fields")&&oe(r,o),q[1]?C?(C.p(q,H),H&2&&E(C,1)):(C=qd(q),C.c(),E(C,1),C.m(e,null)):C&&(le(),A(C,1,1,()=>{C=null}),se());const W={};H&1&&(W.collection=q[0]),!c&&H&1&&(c=!0,W.rule=q[0].listRule,ke(()=>c=!1)),u.$set(W);const G={};H&1&&(G.collection=q[0]),!h&&H&1&&(h=!0,G.rule=q[0].viewRule,ke(()=>h=!1)),m.$set(G),((U=q[0])==null?void 0:U.type)!=="view"?R?(R.p(q,H),H&1&&E(R,1)):(R=Hd(q),R.c(),E(R,1),R.m(g.parentNode,g)):R&&(le(),A(R,1,1,()=>{R=null}),se()),((Y=q[0])==null?void 0:Y.type)==="auth"?F?(F.p(q,H),H&1&&E(F,1)):(F=Vd(q),F.c(),E(F,1),F.m(y.parentNode,y)):F&&(le(),A(F,1,1,()=>{F=null}),se())},i(q){S||(E(C),E(u.$$.fragment,q),E(m.$$.fragment,q),E(R),E(F),S=!0)},o(q){A(C),A(u.$$.fragment,q),A(m.$$.fragment,q),A(R),A(F),S=!1},d(q){q&&(v(e),v(f),v(d),v(_),v(g),v(y)),C&&C.d(),V(u,q),V(m,q),R&&R.d(q),F&&F.d(q),T=!1,$()}}}function i5(n,e,t){let i,{collection:l}=e,s=!1;const o=()=>t(1,s=!s);function r(m){n.$$.not_equal(l.listRule,m)&&(l.listRule=m,t(0,l))}function a(m){n.$$.not_equal(l.viewRule,m)&&(l.viewRule=m,t(0,l))}function f(m){n.$$.not_equal(l.createRule,m)&&(l.createRule=m,t(0,l))}function u(m){n.$$.not_equal(l.updateRule,m)&&(l.updateRule=m,t(0,l))}function c(m){n.$$.not_equal(l.deleteRule,m)&&(l.deleteRule=m,t(0,l))}function d(m){n.$$.not_equal(l.options.manageRule,m)&&(l.options.manageRule=m,t(0,l))}return n.$$set=m=>{"collection"in m&&t(0,l=m.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(2,i=j.getAllCollectionIdentifiers(l))},[l,s,i,o,r,a,f,u,c,d]}class l5 extends ge{constructor(e){super(),_e(this,e,i5,n5,me,{collection:0})}}function Bd(n,e,t){const i=n.slice();return i[9]=e[t],i}function s5(n){let e,t,i,l;function s(a){n[5](a)}var o=n[1];function r(a,f){let u={id:a[8],placeholder:"eg. SELECT id, name from posts",language:"sql-select",minHeight:"150"};return a[0].options.query!==void 0&&(u.value=a[0].options.query),{props:u}}return o&&(e=Dt(o,r(n)),ee.push(()=>be(e,"value",s)),e.$on("change",n[6])),{c(){e&&B(e.$$.fragment),i=ye()},m(a,f){e&&z(e,a,f),w(a,i,f),l=!0},p(a,f){if(f&2&&o!==(o=a[1])){if(e){le();const u=e;A(u.$$.fragment,1,0,()=>{V(u,1)}),se()}o?(e=Dt(o,r(a)),ee.push(()=>be(e,"value",s)),e.$on("change",a[6]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,i.parentNode,i)):e=null}else if(o){const u={};f&256&&(u.id=a[8]),!t&&f&1&&(t=!0,u.value=a[0].options.query,ke(()=>t=!1)),e.$set(u)}},i(a){l||(e&&E(e.$$.fragment,a),l=!0)},o(a){e&&A(e.$$.fragment,a),l=!1},d(a){a&&v(i),e&&V(e,a)}}}function o5(n){let e;return{c(){e=b("textarea"),e.disabled=!0,p(e,"rows","7"),p(e,"placeholder","Loading...")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Ud(n){let e,t,i=ue(n[3]),l=[];for(let s=0;s
  • Wildcard columns (*) are not supported.
  • The query must have a unique id column. -
    - If your query doesn't have a suitable one, you can use the universal - (ROW_NUMBER() OVER()) as id.
  • Expressions must be aliased with a valid formatted field name (eg. - MAX(balance) as maxBalance).
  • `,f=M(),_&&_.c(),u=ye(),p(t,"class","txt"),p(e,"for",i=n[8]),p(a,"class","help-block")},m(g,y){w(g,e,y),k(e,t),w(g,l,y),m[s].m(g,y),w(g,r,y),w(g,a,y),w(g,f,y),_&&_.m(g,y),w(g,u,y),c=!0},p(g,y){(!c||y&256&&i!==(i=g[8]))&&p(e,"for",i);let S=s;s=h(g),s===S?m[s].p(g,y):(le(),A(m[S],1,1,()=>{m[S]=null}),se(),o=m[s],o?o.p(g,y):(o=m[s]=d[s](g),o.c()),E(o,1),o.m(r.parentNode,r)),g[3].length?_?_.p(g,y):(_=Ud(g),_.c(),_.m(u.parentNode,u)):_&&(_.d(1),_=null)},i(g){c||(E(o),c=!0)},o(g){A(o),c=!1},d(g){g&&(v(e),v(l),v(r),v(a),v(f),v(u)),m[s].d(g),_&&_.d(g)}}}function a5(n){let e,t;return e=new ce({props:{class:"form-field required "+(n[3].length?"error":""),name:"options.query",$$slots:{default:[r5,({uniqueId:i})=>({8:i}),({uniqueId:i})=>i?256:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&8&&(s.class="form-field required "+(i[3].length?"error":"")),l&4367&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function f5(n,e,t){let i;Ue(n,mi,c=>t(4,i=c));let{collection:l}=e,s,o=!1,r=[];function a(c){var h;t(3,r=[]);const d=j.getNestedVal(c,"schema",null);if(j.isEmpty(d))return;if(d!=null&&d.message){r.push(d==null?void 0:d.message);return}const m=j.extractColumnsFromQuery((h=l==null?void 0:l.options)==null?void 0:h.query);j.removeByValue(m,"id"),j.removeByValue(m,"created"),j.removeByValue(m,"updated");for(let _ in d)for(let g in d[_]){const y=d[_][g].message,S=m[_]||_;r.push(j.sentenize(S+": "+y))}}Ht(async()=>{t(2,o=!0);try{t(1,s=(await tt(async()=>{const{default:c}=await import("./CodeEditor-CZ0EgQcM.js");return{default:c}},__vite__mapDeps([2,1]),import.meta.url)).default)}catch(c){console.warn(c)}t(2,o=!1)});function f(c){n.$$.not_equal(l.options.query,c)&&(l.options.query=c,t(0,l))}const u=()=>{r.length&&li("schema")};return n.$$set=c=>{"collection"in c&&t(0,l=c.collection)},n.$$.update=()=>{n.$$.dirty&16&&a(i)},[l,s,o,r,i,f,u]}class u5 extends ge{constructor(e){super(),_e(this,e,f5,a5,me,{collection:0})}}const c5=n=>({active:n&1}),Yd=n=>({active:n[0]});function Kd(n){let e,t,i;const l=n[15].default,s=wt(l,n,n[14],null);return{c(){e=b("div"),s&&s.c(),p(e,"class","accordion-content")},m(o,r){w(o,e,r),s&&s.m(e,null),i=!0},p(o,r){s&&s.p&&(!i||r&16384)&&$t(s,l,o,o[14],i?St(l,o[14],r,null):Tt(o[14]),null)},i(o){i||(E(s,o),o&&Ke(()=>{i&&(t||(t=Fe(e,et,{duration:150},!0)),t.run(1))}),i=!0)},o(o){A(s,o),o&&(t||(t=Fe(e,et,{duration:150},!1)),t.run(0)),i=!1},d(o){o&&v(e),s&&s.d(o),o&&t&&t.end()}}}function d5(n){let e,t,i,l,s,o,r;const a=n[15].header,f=wt(a,n,n[14],Yd);let u=n[0]&&Kd(n);return{c(){e=b("div"),t=b("button"),f&&f.c(),i=M(),u&&u.c(),p(t,"type","button"),p(t,"class","accordion-header"),p(t,"draggable",n[2]),p(t,"aria-expanded",n[0]),x(t,"interactive",n[3]),p(e,"class",l="accordion "+(n[7]?"drag-over":"")+" "+n[1]),x(e,"active",n[0])},m(c,d){w(c,e,d),k(e,t),f&&f.m(t,null),k(e,i),u&&u.m(e,null),n[22](e),s=!0,o||(r=[J(t,"click",Be(n[17])),J(t,"drop",Be(n[18])),J(t,"dragstart",n[19]),J(t,"dragenter",n[20]),J(t,"dragleave",n[21]),J(t,"dragover",Be(n[16]))],o=!0)},p(c,[d]){f&&f.p&&(!s||d&16385)&&$t(f,a,c,c[14],s?St(a,c[14],d,c5):Tt(c[14]),Yd),(!s||d&4)&&p(t,"draggable",c[2]),(!s||d&1)&&p(t,"aria-expanded",c[0]),(!s||d&8)&&x(t,"interactive",c[3]),c[0]?u?(u.p(c,d),d&1&&E(u,1)):(u=Kd(c),u.c(),E(u,1),u.m(e,null)):u&&(le(),A(u,1,1,()=>{u=null}),se()),(!s||d&130&&l!==(l="accordion "+(c[7]?"drag-over":"")+" "+c[1]))&&p(e,"class",l),(!s||d&131)&&x(e,"active",c[0])},i(c){s||(E(f,c),E(u),s=!0)},o(c){A(f,c),A(u),s=!1},d(c){c&&v(e),f&&f.d(c),u&&u.d(),n[22](null),o=!1,$e(r)}}}function p5(n,e,t){let{$$slots:i={},$$scope:l}=e;const s=lt();let o,r,{class:a=""}=e,{draggable:f=!1}=e,{active:u=!1}=e,{interactive:c=!0}=e,{single:d=!1}=e,m=!1;function h(){return!!u}function _(){S(),t(0,u=!0),s("expand")}function g(){t(0,u=!1),clearTimeout(r),s("collapse")}function y(){s("toggle"),u?g():_()}function S(){if(d&&o.closest(".accordions")){const R=o.closest(".accordions").querySelectorAll(".accordion.active .accordion-header.interactive");for(const F of R)F.click()}}Ht(()=>()=>clearTimeout(r));function T(R){Ce.call(this,n,R)}const $=()=>c&&y(),C=R=>{f&&(t(7,m=!1),S(),s("drop",R))},O=R=>f&&s("dragstart",R),D=R=>{f&&(t(7,m=!0),s("dragenter",R))},I=R=>{f&&(t(7,m=!1),s("dragleave",R))};function L(R){ee[R?"unshift":"push"](()=>{o=R,t(6,o)})}return n.$$set=R=>{"class"in R&&t(1,a=R.class),"draggable"in R&&t(2,f=R.draggable),"active"in R&&t(0,u=R.active),"interactive"in R&&t(3,c=R.interactive),"single"in R&&t(9,d=R.single),"$$scope"in R&&t(14,l=R.$$scope)},n.$$.update=()=>{n.$$.dirty&8257&&u&&(clearTimeout(r),t(13,r=setTimeout(()=>{o!=null&&o.scrollIntoViewIfNeeded?o.scrollIntoViewIfNeeded():o!=null&&o.scrollIntoView&&o.scrollIntoView({behavior:"smooth",block:"nearest"})},200)))},[u,a,f,c,y,S,o,m,s,d,h,_,g,r,l,i,T,$,C,O,D,I,L]}class ho extends ge{constructor(e){super(),_e(this,e,p5,d5,me,{class:1,draggable:2,active:0,interactive:3,single:9,isExpanded:10,expand:11,collapse:12,toggle:4,collapseSiblings:5})}get isExpanded(){return this.$$.ctx[10]}get expand(){return this.$$.ctx[11]}get collapse(){return this.$$.ctx[12]}get toggle(){return this.$$.ctx[4]}get collapseSiblings(){return this.$$.ctx[5]}}function m5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(l,"for",o=n[13])},m(f,u){w(f,e,u),e.checked=n[0].options.allowUsernameAuth,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[5]),r=!0)},p(f,u){u&8192&&t!==(t=f[13])&&p(e,"id",t),u&1&&(e.checked=f[0].options.allowUsernameAuth),u&8192&&o!==(o=f[13])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function h5(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle m-b-0",name:"options.allowUsernameAuth",$$slots:{default:[m5,({uniqueId:i})=>({13:i}),({uniqueId:i})=>i?8192:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&24577&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function _5(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function g5(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Jd(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function b5(n){let e,t,i,l,s,o;function r(c,d){return c[0].options.allowUsernameAuth?g5:_5}let a=r(n),f=a(n),u=n[3]&&Jd();return{c(){e=b("div"),e.innerHTML=' Username/Password',t=M(),i=b("div"),l=M(),f.c(),s=M(),u&&u.c(),o=ye(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){w(c,e,d),w(c,t,d),w(c,i,d),w(c,l,d),f.m(c,d),w(c,s,d),u&&u.m(c,d),w(c,o,d)},p(c,d){a!==(a=r(c))&&(f.d(1),f=a(c),f&&(f.c(),f.m(s.parentNode,s))),c[3]?u?d&8&&E(u,1):(u=Jd(),u.c(),E(u,1),u.m(o.parentNode,o)):u&&(le(),A(u,1,1,()=>{u=null}),se())},d(c){c&&(v(e),v(t),v(i),v(l),v(s),v(o)),f.d(c),u&&u.d(c)}}}function k5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(l,"for",o=n[13])},m(f,u){w(f,e,u),e.checked=n[0].options.allowEmailAuth,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[6]),r=!0)},p(f,u){u&8192&&t!==(t=f[13])&&p(e,"id",t),u&1&&(e.checked=f[0].options.allowEmailAuth),u&8192&&o!==(o=f[13])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function Zd(n){let e,t,i,l,s,o,r,a;return i=new ce({props:{class:"form-field "+(j.isEmpty(n[0].options.onlyEmailDomains)?"":"disabled"),name:"options.exceptEmailDomains",$$slots:{default:[y5,({uniqueId:f})=>({13:f}),({uniqueId:f})=>f?8192:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field "+(j.isEmpty(n[0].options.exceptEmailDomains)?"":"disabled"),name:"options.onlyEmailDomains",$$slots:{default:[v5,({uniqueId:f})=>({13:f}),({uniqueId:f})=>f?8192:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(e,"class","grid grid-sm p-t-sm")},m(f,u){w(f,e,u),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),a=!0},p(f,u){const c={};u&1&&(c.class="form-field "+(j.isEmpty(f[0].options.onlyEmailDomains)?"":"disabled")),u&24577&&(c.$$scope={dirty:u,ctx:f}),i.$set(c);const d={};u&1&&(d.class="form-field "+(j.isEmpty(f[0].options.exceptEmailDomains)?"":"disabled")),u&24577&&(d.$$scope={dirty:u,ctx:f}),o.$set(d)},i(f){a||(E(i.$$.fragment,f),E(o.$$.fragment,f),f&&Ke(()=>{a&&(r||(r=Fe(e,et,{duration:150},!0)),r.run(1))}),a=!0)},o(f){A(i.$$.fragment,f),A(o.$$.fragment,f),f&&(r||(r=Fe(e,et,{duration:150},!1)),r.run(0)),a=!1},d(f){f&&v(e),V(i),V(o),f&&r&&r.end()}}}function y5(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;function h(g){n[7](g)}let _={id:n[13],disabled:!j.isEmpty(n[0].options.onlyEmailDomains)};return n[0].options.exceptEmailDomains!==void 0&&(_.value=n[0].options.exceptEmailDomains),r=new Nl({props:_}),ee.push(()=>be(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Except domains",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),u.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[13]),p(u,"class","help-block")},m(g,y){w(g,e,y),k(e,t),k(e,i),k(e,l),w(g,o,y),z(r,g,y),w(g,f,y),w(g,u,y),c=!0,d||(m=Se(Pe.call(null,l,{text:`Email domains that are NOT allowed to sign up. - This field is disabled if "Only domains" is set.`,position:"top"})),d=!0)},p(g,y){(!c||y&8192&&s!==(s=g[13]))&&p(e,"for",s);const S={};y&8192&&(S.id=g[13]),y&1&&(S.disabled=!j.isEmpty(g[0].options.onlyEmailDomains)),!a&&y&1&&(a=!0,S.value=g[0].options.exceptEmailDomains,ke(()=>a=!1)),r.$set(S)},i(g){c||(E(r.$$.fragment,g),c=!0)},o(g){A(r.$$.fragment,g),c=!1},d(g){g&&(v(e),v(o),v(f),v(u)),V(r,g),d=!1,m()}}}function v5(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;function h(g){n[8](g)}let _={id:n[13],disabled:!j.isEmpty(n[0].options.exceptEmailDomains)};return n[0].options.onlyEmailDomains!==void 0&&(_.value=n[0].options.onlyEmailDomains),r=new Nl({props:_}),ee.push(()=>be(r,"value",h)),{c(){e=b("label"),t=b("span"),t.textContent="Only domains",i=M(),l=b("i"),o=M(),B(r.$$.fragment),f=M(),u=b("div"),u.textContent="Use comma as separator.",p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[13]),p(u,"class","help-block")},m(g,y){w(g,e,y),k(e,t),k(e,i),k(e,l),w(g,o,y),z(r,g,y),w(g,f,y),w(g,u,y),c=!0,d||(m=Se(Pe.call(null,l,{text:`Email domains that are ONLY allowed to sign up. - This field is disabled if "Except domains" is set.`,position:"top"})),d=!0)},p(g,y){(!c||y&8192&&s!==(s=g[13]))&&p(e,"for",s);const S={};y&8192&&(S.id=g[13]),y&1&&(S.disabled=!j.isEmpty(g[0].options.exceptEmailDomains)),!a&&y&1&&(a=!0,S.value=g[0].options.onlyEmailDomains,ke(()=>a=!1)),r.$set(S)},i(g){c||(E(r.$$.fragment,g),c=!0)},o(g){A(r.$$.fragment,g),c=!1},d(g){g&&(v(e),v(o),v(f),v(u)),V(r,g),d=!1,m()}}}function w5(n){let e,t,i,l;e=new ce({props:{class:"form-field form-field-toggle m-0",name:"options.allowEmailAuth",$$slots:{default:[k5,({uniqueId:o})=>({13:o}),({uniqueId:o})=>o?8192:0]},$$scope:{ctx:n}}});let s=n[0].options.allowEmailAuth&&Zd(n);return{c(){B(e.$$.fragment),t=M(),s&&s.c(),i=ye()},m(o,r){z(e,o,r),w(o,t,r),s&&s.m(o,r),w(o,i,r),l=!0},p(o,r){const a={};r&24577&&(a.$$scope={dirty:r,ctx:o}),e.$set(a),o[0].options.allowEmailAuth?s?(s.p(o,r),r&1&&E(s,1)):(s=Zd(o),s.c(),E(s,1),s.m(i.parentNode,i)):s&&(le(),A(s,1,1,()=>{s=null}),se())},i(o){l||(E(e.$$.fragment,o),E(s),l=!0)},o(o){A(e.$$.fragment,o),A(s),l=!1},d(o){o&&(v(t),v(i)),V(e,o),s&&s.d(o)}}}function S5(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function $5(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Gd(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function T5(n){let e,t,i,l,s,o;function r(c,d){return c[0].options.allowEmailAuth?$5:S5}let a=r(n),f=a(n),u=n[2]&&Gd();return{c(){e=b("div"),e.innerHTML=' Email/Password',t=M(),i=b("div"),l=M(),f.c(),s=M(),u&&u.c(),o=ye(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){w(c,e,d),w(c,t,d),w(c,i,d),w(c,l,d),f.m(c,d),w(c,s,d),u&&u.m(c,d),w(c,o,d)},p(c,d){a!==(a=r(c))&&(f.d(1),f=a(c),f&&(f.c(),f.m(s.parentNode,s))),c[2]?u?d&4&&E(u,1):(u=Gd(),u.c(),E(u,1),u.m(o.parentNode,o)):u&&(le(),A(u,1,1,()=>{u=null}),se())},d(c){c&&(v(e),v(t),v(i),v(l),v(s),v(o)),f.d(c),u&&u.d(c)}}}function C5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(l,"for",o=n[13])},m(f,u){w(f,e,u),e.checked=n[0].options.allowOAuth2Auth,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[9]),r=!0)},p(f,u){u&8192&&t!==(t=f[13])&&p(e,"id",t),u&1&&(e.checked=f[0].options.allowOAuth2Auth),u&8192&&o!==(o=f[13])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function Xd(n){let e,t,i;return{c(){e=b("div"),e.innerHTML='',p(e,"class","block")},m(l,s){w(l,e,s),i=!0},i(l){i||(l&&Ke(()=>{i&&(t||(t=Fe(e,et,{duration:150},!0)),t.run(1))}),i=!0)},o(l){l&&(t||(t=Fe(e,et,{duration:150},!1)),t.run(0)),i=!1},d(l){l&&v(e),l&&t&&t.end()}}}function O5(n){let e,t,i,l;e=new ce({props:{class:"form-field form-field-toggle m-b-0",name:"options.allowOAuth2Auth",$$slots:{default:[C5,({uniqueId:o})=>({13:o}),({uniqueId:o})=>o?8192:0]},$$scope:{ctx:n}}});let s=n[0].options.allowOAuth2Auth&&Xd();return{c(){B(e.$$.fragment),t=M(),s&&s.c(),i=ye()},m(o,r){z(e,o,r),w(o,t,r),s&&s.m(o,r),w(o,i,r),l=!0},p(o,r){const a={};r&24577&&(a.$$scope={dirty:r,ctx:o}),e.$set(a),o[0].options.allowOAuth2Auth?s?r&1&&E(s,1):(s=Xd(),s.c(),E(s,1),s.m(i.parentNode,i)):s&&(le(),A(s,1,1,()=>{s=null}),se())},i(o){l||(E(e.$$.fragment,o),E(s),l=!0)},o(o){A(e.$$.fragment,o),A(s),l=!1},d(o){o&&(v(t),v(i)),V(e,o),s&&s.d(o)}}}function M5(n){let e;return{c(){e=b("span"),e.textContent="Disabled",p(e,"class","label")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function D5(n){let e;return{c(){e=b("span"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Qd(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function E5(n){let e,t,i,l,s,o;function r(c,d){return c[0].options.allowOAuth2Auth?D5:M5}let a=r(n),f=a(n),u=n[1]&&Qd();return{c(){e=b("div"),e.innerHTML=' OAuth2',t=M(),i=b("div"),l=M(),f.c(),s=M(),u&&u.c(),o=ye(),p(e,"class","inline-flex"),p(i,"class","flex-fill")},m(c,d){w(c,e,d),w(c,t,d),w(c,i,d),w(c,l,d),f.m(c,d),w(c,s,d),u&&u.m(c,d),w(c,o,d)},p(c,d){a!==(a=r(c))&&(f.d(1),f=a(c),f&&(f.c(),f.m(s.parentNode,s))),c[1]?u?d&2&&E(u,1):(u=Qd(),u.c(),E(u,1),u.m(o.parentNode,o)):u&&(le(),A(u,1,1,()=>{u=null}),se())},d(c){c&&(v(e),v(t),v(i),v(l),v(s),v(o)),f.d(c),u&&u.d(c)}}}function I5(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Minimum password length"),l=M(),s=b("input"),p(e,"for",i=n[13]),p(s,"type","number"),p(s,"id",o=n[13]),s.required=!0,p(s,"min","6"),p(s,"max","72")},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].options.minPasswordLength),r||(a=J(s,"input",n[10]),r=!0)},p(f,u){u&8192&&i!==(i=f[13])&&p(e,"for",i),u&8192&&o!==(o=f[13])&&p(s,"id",o),u&1&&it(s.value)!==f[0].options.minPasswordLength&&re(s,f[0].options.minPasswordLength)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function A5(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Always require email",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(s,"class","txt"),p(r,"class","ri-information-line txt-sm link-hint"),p(l,"for",a=n[13])},m(c,d){w(c,e,d),e.checked=n[0].options.requireEmail,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[11]),Se(Pe.call(null,r,{text:`The constraint is applied only for new records. -Also note that some OAuth2 providers (like Twitter), don't return an email and the authentication may fail if the email field is required.`,position:"right"}))],f=!0)},p(c,d){d&8192&&t!==(t=c[13])&&p(e,"id",t),d&1&&(e.checked=c[0].options.requireEmail),d&8192&&a!==(a=c[13])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function L5(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Forbid authentication for unverified users",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(s,"class","txt"),p(r,"class","ri-information-line txt-sm link-hint"),p(l,"for",a=n[13])},m(c,d){w(c,e,d),e.checked=n[0].options.onlyVerified,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[12]),Se(Pe.call(null,r,{text:["If enabled, it returns 403 for new unverified user authentication requests.","If you need more granular control, don't enable this option and instead use the `@request.auth.verified = true` rule in the specific collection(s) you are targeting."].join(` -`),position:"right"}))],f=!0)},p(c,d){d&8192&&t!==(t=c[13])&&p(e,"id",t),d&1&&(e.checked=c[0].options.onlyVerified),d&8192&&a!==(a=c[13])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function N5(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T;return l=new ho({props:{single:!0,$$slots:{header:[b5],default:[h5]},$$scope:{ctx:n}}}),o=new ho({props:{single:!0,$$slots:{header:[T5],default:[w5]},$$scope:{ctx:n}}}),a=new ho({props:{single:!0,$$slots:{header:[E5],default:[O5]},$$scope:{ctx:n}}}),h=new ce({props:{class:"form-field required",name:"options.minPasswordLength",$$slots:{default:[I5,({uniqueId:$})=>({13:$}),({uniqueId:$})=>$?8192:0]},$$scope:{ctx:n}}}),g=new ce({props:{class:"form-field form-field-toggle m-b-sm",name:"options.requireEmail",$$slots:{default:[A5,({uniqueId:$})=>({13:$}),({uniqueId:$})=>$?8192:0]},$$scope:{ctx:n}}}),S=new ce({props:{class:"form-field form-field-toggle m-b-sm",name:"options.onlyVerified",$$slots:{default:[L5,({uniqueId:$})=>({13:$}),({uniqueId:$})=>$?8192:0]},$$scope:{ctx:n}}}),{c(){e=b("h4"),e.textContent="Auth methods",t=M(),i=b("div"),B(l.$$.fragment),s=M(),B(o.$$.fragment),r=M(),B(a.$$.fragment),f=M(),u=b("hr"),c=M(),d=b("h4"),d.textContent="General",m=M(),B(h.$$.fragment),_=M(),B(g.$$.fragment),y=M(),B(S.$$.fragment),p(e,"class","section-title"),p(i,"class","accordions"),p(d,"class","section-title")},m($,C){w($,e,C),w($,t,C),w($,i,C),z(l,i,null),k(i,s),z(o,i,null),k(i,r),z(a,i,null),w($,f,C),w($,u,C),w($,c,C),w($,d,C),w($,m,C),z(h,$,C),w($,_,C),z(g,$,C),w($,y,C),z(S,$,C),T=!0},p($,[C]){const O={};C&16393&&(O.$$scope={dirty:C,ctx:$}),l.$set(O);const D={};C&16389&&(D.$$scope={dirty:C,ctx:$}),o.$set(D);const I={};C&16387&&(I.$$scope={dirty:C,ctx:$}),a.$set(I);const L={};C&24577&&(L.$$scope={dirty:C,ctx:$}),h.$set(L);const R={};C&24577&&(R.$$scope={dirty:C,ctx:$}),g.$set(R);const F={};C&24577&&(F.$$scope={dirty:C,ctx:$}),S.$set(F)},i($){T||(E(l.$$.fragment,$),E(o.$$.fragment,$),E(a.$$.fragment,$),E(h.$$.fragment,$),E(g.$$.fragment,$),E(S.$$.fragment,$),T=!0)},o($){A(l.$$.fragment,$),A(o.$$.fragment,$),A(a.$$.fragment,$),A(h.$$.fragment,$),A(g.$$.fragment,$),A(S.$$.fragment,$),T=!1},d($){$&&(v(e),v(t),v(i),v(f),v(u),v(c),v(d),v(m),v(_),v(y)),V(l),V(o),V(a),V(h,$),V(g,$),V(S,$)}}}function P5(n,e,t){let i,l,s,o;Ue(n,mi,g=>t(4,o=g));let{collection:r}=e;function a(){r.options.allowUsernameAuth=this.checked,t(0,r)}function f(){r.options.allowEmailAuth=this.checked,t(0,r)}function u(g){n.$$.not_equal(r.options.exceptEmailDomains,g)&&(r.options.exceptEmailDomains=g,t(0,r))}function c(g){n.$$.not_equal(r.options.onlyEmailDomains,g)&&(r.options.onlyEmailDomains=g,t(0,r))}function d(){r.options.allowOAuth2Auth=this.checked,t(0,r)}function m(){r.options.minPasswordLength=it(this.value),t(0,r)}function h(){r.options.requireEmail=this.checked,t(0,r)}function _(){r.options.onlyVerified=this.checked,t(0,r)}return n.$$set=g=>{"collection"in g&&t(0,r=g.collection)},n.$$.update=()=>{var g,y,S,T;n.$$.dirty&1&&r.type==="auth"&&j.isEmpty(r.options)&&t(0,r.options={allowEmailAuth:!0,allowUsernameAuth:!0,allowOAuth2Auth:!0,minPasswordLength:8},r),n.$$.dirty&16&&t(2,l=!j.isEmpty((g=o==null?void 0:o.options)==null?void 0:g.allowEmailAuth)||!j.isEmpty((y=o==null?void 0:o.options)==null?void 0:y.onlyEmailDomains)||!j.isEmpty((S=o==null?void 0:o.options)==null?void 0:S.exceptEmailDomains)),n.$$.dirty&16&&t(1,s=!j.isEmpty((T=o==null?void 0:o.options)==null?void 0:T.allowOAuth2Auth))},t(3,i=!1),[r,s,l,i,o,a,f,u,c,d,m,h,_]}class F5 extends ge{constructor(e){super(),_e(this,e,P5,N5,me,{collection:0})}}function xd(n,e,t){const i=n.slice();return i[18]=e[t],i}function ep(n,e,t){const i=n.slice();return i[18]=e[t],i}function tp(n,e,t){const i=n.slice();return i[18]=e[t],i}function np(n){let e;return{c(){e=b("p"),e.textContent="All data associated with the removed fields will be permanently deleted!"},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function ip(n){let e,t,i,l,s=n[3]&&lp(n),o=!n[4]&&sp(n);return{c(){e=b("h6"),e.textContent="Changes:",t=M(),i=b("ul"),s&&s.c(),l=M(),o&&o.c(),p(i,"class","changes-list svelte-xqpcsf")},m(r,a){w(r,e,a),w(r,t,a),w(r,i,a),s&&s.m(i,null),k(i,l),o&&o.m(i,null)},p(r,a){r[3]?s?s.p(r,a):(s=lp(r),s.c(),s.m(i,l)):s&&(s.d(1),s=null),r[4]?o&&(o.d(1),o=null):o?o.p(r,a):(o=sp(r),o.c(),o.m(i,null))},d(r){r&&(v(e),v(t),v(i)),s&&s.d(),o&&o.d()}}}function lp(n){var m,h;let e,t,i,l,s=((m=n[1])==null?void 0:m.name)+"",o,r,a,f,u,c=((h=n[2])==null?void 0:h.name)+"",d;return{c(){e=b("li"),t=b("div"),i=K(`Renamed collection - `),l=b("strong"),o=K(s),r=M(),a=b("i"),f=M(),u=b("strong"),d=K(c),p(l,"class","txt-strikethrough txt-hint"),p(a,"class","ri-arrow-right-line txt-sm"),p(u,"class","txt"),p(t,"class","inline-flex"),p(e,"class","svelte-xqpcsf")},m(_,g){w(_,e,g),k(e,t),k(t,i),k(t,l),k(l,o),k(t,r),k(t,a),k(t,f),k(t,u),k(u,d)},p(_,g){var y,S;g&2&&s!==(s=((y=_[1])==null?void 0:y.name)+"")&&oe(o,s),g&4&&c!==(c=((S=_[2])==null?void 0:S.name)+"")&&oe(d,c)},d(_){_&&v(e)}}}function sp(n){let e,t,i,l=ue(n[6]),s=[];for(let u=0;u
    ',i=M(),l=b("div"),s=b("p"),s.textContent=`If any of the collection changes is part of another collection rule, filter or view query, - you'll have to update it manually!`,o=M(),f&&f.c(),r=M(),u&&u.c(),a=ye(),p(t,"class","icon"),p(l,"class","content txt-bold"),p(e,"class","alert alert-warning")},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),k(l,s),k(l,o),f&&f.m(l,null),w(c,r,d),u&&u.m(c,d),w(c,a,d)},p(c,d){c[7].length?f||(f=np(),f.c(),f.m(l,null)):f&&(f.d(1),f=null),c[9]?u?u.p(c,d):(u=ip(c),u.c(),u.m(a.parentNode,a)):u&&(u.d(1),u=null)},d(c){c&&(v(e),v(r),v(a)),f&&f.d(),u&&u.d(c)}}}function q5(n){let e;return{c(){e=b("h4"),e.textContent="Confirm collection changes"},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function j5(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML='Cancel',t=M(),i=b("button"),i.innerHTML='Confirm',e.autofocus=!0,p(e,"type","button"),p(e,"class","btn btn-transparent"),p(i,"type","button"),p(i,"class","btn btn-expanded")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),e.focus(),l||(s=[J(e,"click",n[12]),J(i,"click",n[13])],l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,$e(s)}}}function H5(n){let e,t,i={class:"confirm-changes-panel",popup:!0,$$slots:{footer:[j5],header:[q5],default:[R5]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[14](e),e.$on("hide",n[15]),e.$on("show",n[16]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&33555422&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[14](null),V(e,l)}}}function z5(n,e,t){let i,l,s,o,r,a;const f=lt();let u,c,d;async function m(C,O){t(1,c=C),t(2,d=O),await Qt(),i||s.length||o.length||r.length?u==null||u.show():_()}function h(){u==null||u.hide()}function _(){h(),f("confirm")}const g=()=>h(),y=()=>_();function S(C){ee[C?"unshift":"push"](()=>{u=C,t(5,u)})}function T(C){Ce.call(this,n,C)}function $(C){Ce.call(this,n,C)}return n.$$.update=()=>{var C,O,D;n.$$.dirty&6&&t(3,i=(c==null?void 0:c.name)!=(d==null?void 0:d.name)),n.$$.dirty&4&&t(4,l=(d==null?void 0:d.type)==="view"),n.$$.dirty&4&&t(8,s=((C=d==null?void 0:d.schema)==null?void 0:C.filter(I=>I.id&&!I.toDelete&&I.originalName!=I.name))||[]),n.$$.dirty&4&&t(7,o=((O=d==null?void 0:d.schema)==null?void 0:O.filter(I=>I.id&&I.toDelete))||[]),n.$$.dirty&6&&t(6,r=((D=d==null?void 0:d.schema)==null?void 0:D.filter(I=>{var R,F,N;const L=(R=c==null?void 0:c.schema)==null?void 0:R.find(P=>P.id==I.id);return L?((F=L.options)==null?void 0:F.maxSelect)!=1&&((N=I.options)==null?void 0:N.maxSelect)==1:!1}))||[]),n.$$.dirty&24&&t(9,a=!l||i)},[h,c,d,i,l,u,r,o,s,a,_,m,g,y,S,T,$]}class V5 extends ge{constructor(e){super(),_e(this,e,z5,H5,me,{show:11,hide:0})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[0]}}function fp(n,e,t){const i=n.slice();return i[50]=e[t][0],i[51]=e[t][1],i}function B5(n){let e,t,i;function l(o){n[36](o)}let s={};return n[2]!==void 0&&(s.collection=n[2]),e=new WC({props:s}),ee.push(()=>be(e,"collection",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&4&&(t=!0,a.collection=o[2],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function U5(n){let e,t,i;function l(o){n[35](o)}let s={};return n[2]!==void 0&&(s.collection=n[2]),e=new u5({props:s}),ee.push(()=>be(e,"collection",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&4&&(t=!0,a.collection=o[2],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function up(n){let e,t,i,l;function s(r){n[37](r)}let o={};return n[2]!==void 0&&(o.collection=n[2]),t=new l5({props:o}),ee.push(()=>be(t,"collection",s)),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","tab-item active")},m(r,a){w(r,e,a),z(t,e,null),l=!0},p(r,a){const f={};!i&&a[0]&4&&(i=!0,f.collection=r[2],ke(()=>i=!1)),t.$set(f)},i(r){l||(E(t.$$.fragment,r),l=!0)},o(r){A(t.$$.fragment,r),l=!1},d(r){r&&v(e),V(t)}}}function cp(n){let e,t,i,l;function s(r){n[38](r)}let o={};return n[2]!==void 0&&(o.collection=n[2]),t=new F5({props:o}),ee.push(()=>be(t,"collection",s)),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","tab-item"),x(e,"active",n[3]===Dl)},m(r,a){w(r,e,a),z(t,e,null),l=!0},p(r,a){const f={};!i&&a[0]&4&&(i=!0,f.collection=r[2],ke(()=>i=!1)),t.$set(f),(!l||a[0]&8)&&x(e,"active",r[3]===Dl)},i(r){l||(E(t.$$.fragment,r),l=!0)},o(r){A(t.$$.fragment,r),l=!1},d(r){r&&v(e),V(t)}}}function W5(n){let e,t,i,l,s,o,r;const a=[U5,B5],f=[];function u(m,h){return m[14]?0:1}i=u(n),l=f[i]=a[i](n);let c=n[3]===hs&&up(n),d=n[15]&&cp(n);return{c(){e=b("div"),t=b("div"),l.c(),s=M(),c&&c.c(),o=M(),d&&d.c(),p(t,"class","tab-item"),x(t,"active",n[3]===Ci),p(e,"class","tabs-content svelte-12y0yzb")},m(m,h){w(m,e,h),k(e,t),f[i].m(t,null),k(e,s),c&&c.m(e,null),k(e,o),d&&d.m(e,null),r=!0},p(m,h){let _=i;i=u(m),i===_?f[i].p(m,h):(le(),A(f[_],1,1,()=>{f[_]=null}),se(),l=f[i],l?l.p(m,h):(l=f[i]=a[i](m),l.c()),E(l,1),l.m(t,null)),(!r||h[0]&8)&&x(t,"active",m[3]===Ci),m[3]===hs?c?(c.p(m,h),h[0]&8&&E(c,1)):(c=up(m),c.c(),E(c,1),c.m(e,o)):c&&(le(),A(c,1,1,()=>{c=null}),se()),m[15]?d?(d.p(m,h),h[0]&32768&&E(d,1)):(d=cp(m),d.c(),E(d,1),d.m(e,null)):d&&(le(),A(d,1,1,()=>{d=null}),se())},i(m){r||(E(l),E(c),E(d),r=!0)},o(m){A(l),A(c),A(d),r=!1},d(m){m&&v(e),f[i].d(),c&&c.d(),d&&d.d()}}}function dp(n){let e,t,i,l,s,o,r;return o=new On({props:{class:"dropdown dropdown-right m-t-5",$$slots:{default:[Y5]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=M(),i=b("div"),l=b("i"),s=M(),B(o.$$.fragment),p(e,"class","flex-fill"),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"aria-label","More collection options"),p(i,"class","btn btn-sm btn-circle btn-transparent flex-gap-0")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),k(i,l),k(i,s),z(o,i,null),r=!0},p(a,f){const u={};f[1]&8388608&&(u.$$scope={dirty:f,ctx:a}),o.$set(u)},i(a){r||(E(o.$$.fragment,a),r=!0)},o(a){A(o.$$.fragment,a),r=!1},d(a){a&&(v(e),v(t),v(i)),V(o)}}}function Y5(n){let e,t,i,l,s;return{c(){e=b("button"),e.innerHTML=' Duplicate',t=M(),i=b("button"),i.innerHTML=' Delete',p(e,"type","button"),p(e,"class","dropdown-item"),p(e,"role","menuitem"),p(i,"type","button"),p(i,"class","dropdown-item txt-danger"),p(i,"role","menuitem")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),l||(s=[J(e,"click",n[27]),J(i,"click",Tn(Be(n[28])))],l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,$e(s)}}}function pp(n){let e,t,i,l;return i=new On({props:{class:"dropdown dropdown-right dropdown-nowrap m-t-5",$$slots:{default:[K5]},$$scope:{ctx:n}}}),{c(){e=b("i"),t=M(),B(i.$$.fragment),p(e,"class","ri-arrow-down-s-fill"),p(e,"aria-hidden","true")},m(s,o){w(s,e,o),w(s,t,o),z(i,s,o),l=!0},p(s,o){const r={};o[0]&68|o[1]&8388608&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(i.$$.fragment,s),l=!0)},o(s){A(i.$$.fragment,s),l=!1},d(s){s&&(v(e),v(t)),V(i,s)}}}function mp(n){let e,t,i,l,s,o=n[51]+"",r,a,f,u,c;function d(){return n[30](n[50])}return{c(){e=b("button"),t=b("i"),l=M(),s=b("span"),r=K(o),a=K(" collection"),f=M(),p(t,"class",i=Un(j.getCollectionTypeIcon(n[50]))+" svelte-12y0yzb"),p(t,"aria-hidden","true"),p(s,"class","txt"),p(e,"type","button"),p(e,"role","menuitem"),p(e,"class","dropdown-item closable"),x(e,"selected",n[50]==n[2].type)},m(m,h){w(m,e,h),k(e,t),k(e,l),k(e,s),k(s,r),k(s,a),k(e,f),u||(c=J(e,"click",d),u=!0)},p(m,h){n=m,h[0]&64&&i!==(i=Un(j.getCollectionTypeIcon(n[50]))+" svelte-12y0yzb")&&p(t,"class",i),h[0]&64&&o!==(o=n[51]+"")&&oe(r,o),h[0]&68&&x(e,"selected",n[50]==n[2].type)},d(m){m&&v(e),u=!1,c()}}}function K5(n){let e,t=ue(Object.entries(n[6])),i=[];for(let l=0;l{N=null}),se()):N?(N.p(q,H),H[0]&4&&E(N,1)):(N=pp(q),N.c(),E(N,1),N.m(d,null)),(!L||H[0]&4&&$!==($=q[2].id?-1:0))&&p(d,"tabindex",$),(!L||H[0]&4&&C!==(C=q[2].id?"":"button"))&&p(d,"role",C),(!L||H[0]&4&&O!==(O="btn btn-sm p-r-10 p-l-10 "+(q[2].id?"btn-transparent":"btn-outline")))&&p(d,"class",O),(!L||H[0]&4)&&x(d,"btn-disabled",!!q[2].id),q[2].system?P||(P=hp(),P.c(),P.m(I.parentNode,I)):P&&(P.d(1),P=null)},i(q){L||(E(N),L=!0)},o(q){A(N),L=!1},d(q){q&&(v(e),v(l),v(s),v(u),v(c),v(D),v(I)),N&&N.d(),P&&P.d(q),R=!1,F()}}}function _p(n){let e,t,i,l,s,o;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(r,a){w(r,e,a),l=!0,s||(o=Se(t=Pe.call(null,e,n[11])),s=!0)},p(r,a){t&&Ct(t.update)&&a[0]&2048&&t.update.call(null,r[11])},i(r){l||(r&&Ke(()=>{l&&(i||(i=Fe(e,Wt,{duration:150,start:.7},!0)),i.run(1))}),l=!0)},o(r){r&&(i||(i=Fe(e,Wt,{duration:150,start:.7},!1)),i.run(0)),l=!1},d(r){r&&v(e),r&&i&&i.end(),s=!1,o()}}}function gp(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,"Has errors")),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function bp(n){var a,f,u;let e,t,i,l=!j.isEmpty((a=n[5])==null?void 0:a.options)&&!((u=(f=n[5])==null?void 0:f.options)!=null&&u.manageRule),s,o,r=l&&kp();return{c(){e=b("button"),t=b("span"),t.textContent="Options",i=M(),r&&r.c(),p(t,"class","txt"),p(e,"type","button"),p(e,"class","tab-item"),x(e,"active",n[3]===Dl)},m(c,d){w(c,e,d),k(e,t),k(e,i),r&&r.m(e,null),s||(o=J(e,"click",n[34]),s=!0)},p(c,d){var m,h,_;d[0]&32&&(l=!j.isEmpty((m=c[5])==null?void 0:m.options)&&!((_=(h=c[5])==null?void 0:h.options)!=null&&_.manageRule)),l?r?d[0]&32&&E(r,1):(r=kp(),r.c(),E(r,1),r.m(e,null)):r&&(le(),A(r,1,1,()=>{r=null}),se()),d[0]&8&&x(e,"active",c[3]===Dl)},d(c){c&&v(e),r&&r.d(),s=!1,o()}}}function kp(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,"Has errors")),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function Z5(n){var H,W,G,U,Y,ie,te;let e,t=n[2].id?"Edit collection":"New collection",i,l,s,o,r,a,f,u,c,d,m,h=n[14]?"Query":"Fields",_,g,y=!j.isEmpty(n[11]),S,T,$,C,O=!j.isEmpty((H=n[5])==null?void 0:H.listRule)||!j.isEmpty((W=n[5])==null?void 0:W.viewRule)||!j.isEmpty((G=n[5])==null?void 0:G.createRule)||!j.isEmpty((U=n[5])==null?void 0:U.updateRule)||!j.isEmpty((Y=n[5])==null?void 0:Y.deleteRule)||!j.isEmpty((te=(ie=n[5])==null?void 0:ie.options)==null?void 0:te.manageRule),D,I,L,R,F=!!n[2].id&&!n[2].system&&dp(n);r=new ce({props:{class:"form-field collection-field-name required m-b-0 "+(n[13]?"disabled":""),name:"name",$$slots:{default:[J5,({uniqueId:pe})=>({49:pe}),({uniqueId:pe})=>[0,pe?262144:0]]},$$scope:{ctx:n}}});let N=y&&_p(n),P=O&&gp(),q=n[15]&&bp(n);return{c(){e=b("h4"),i=K(t),l=M(),F&&F.c(),s=M(),o=b("form"),B(r.$$.fragment),a=M(),f=b("input"),u=M(),c=b("div"),d=b("button"),m=b("span"),_=K(h),g=M(),N&&N.c(),S=M(),T=b("button"),$=b("span"),$.textContent="API Rules",C=M(),P&&P.c(),D=M(),q&&q.c(),p(e,"class","upsert-panel-title svelte-12y0yzb"),p(f,"type","submit"),p(f,"class","hidden"),p(f,"tabindex","-1"),p(o,"class","block"),p(m,"class","txt"),p(d,"type","button"),p(d,"class","tab-item"),x(d,"active",n[3]===Ci),p($,"class","txt"),p(T,"type","button"),p(T,"class","tab-item"),x(T,"active",n[3]===hs),p(c,"class","tabs-header stretched")},m(pe,Ne){w(pe,e,Ne),k(e,i),w(pe,l,Ne),F&&F.m(pe,Ne),w(pe,s,Ne),w(pe,o,Ne),z(r,o,null),k(o,a),k(o,f),w(pe,u,Ne),w(pe,c,Ne),k(c,d),k(d,m),k(m,_),k(d,g),N&&N.m(d,null),k(c,S),k(c,T),k(T,$),k(T,C),P&&P.m(T,null),k(c,D),q&&q.m(c,null),I=!0,L||(R=[J(o,"submit",Be(n[31])),J(d,"click",n[32]),J(T,"click",n[33])],L=!0)},p(pe,Ne){var Xe,xe,Mt,ft,mt,Gt,De;(!I||Ne[0]&4)&&t!==(t=pe[2].id?"Edit collection":"New collection")&&oe(i,t),pe[2].id&&!pe[2].system?F?(F.p(pe,Ne),Ne[0]&4&&E(F,1)):(F=dp(pe),F.c(),E(F,1),F.m(s.parentNode,s)):F&&(le(),A(F,1,1,()=>{F=null}),se());const He={};Ne[0]&8192&&(He.class="form-field collection-field-name required m-b-0 "+(pe[13]?"disabled":"")),Ne[0]&41028|Ne[1]&8650752&&(He.$$scope={dirty:Ne,ctx:pe}),r.$set(He),(!I||Ne[0]&16384)&&h!==(h=pe[14]?"Query":"Fields")&&oe(_,h),Ne[0]&2048&&(y=!j.isEmpty(pe[11])),y?N?(N.p(pe,Ne),Ne[0]&2048&&E(N,1)):(N=_p(pe),N.c(),E(N,1),N.m(d,null)):N&&(le(),A(N,1,1,()=>{N=null}),se()),(!I||Ne[0]&8)&&x(d,"active",pe[3]===Ci),Ne[0]&32&&(O=!j.isEmpty((Xe=pe[5])==null?void 0:Xe.listRule)||!j.isEmpty((xe=pe[5])==null?void 0:xe.viewRule)||!j.isEmpty((Mt=pe[5])==null?void 0:Mt.createRule)||!j.isEmpty((ft=pe[5])==null?void 0:ft.updateRule)||!j.isEmpty((mt=pe[5])==null?void 0:mt.deleteRule)||!j.isEmpty((De=(Gt=pe[5])==null?void 0:Gt.options)==null?void 0:De.manageRule)),O?P?Ne[0]&32&&E(P,1):(P=gp(),P.c(),E(P,1),P.m(T,null)):P&&(le(),A(P,1,1,()=>{P=null}),se()),(!I||Ne[0]&8)&&x(T,"active",pe[3]===hs),pe[15]?q?q.p(pe,Ne):(q=bp(pe),q.c(),q.m(c,null)):q&&(q.d(1),q=null)},i(pe){I||(E(F),E(r.$$.fragment,pe),E(N),E(P),I=!0)},o(pe){A(F),A(r.$$.fragment,pe),A(N),A(P),I=!1},d(pe){pe&&(v(e),v(l),v(s),v(o),v(u),v(c)),F&&F.d(pe),V(r),N&&N.d(),P&&P.d(),q&&q.d(),L=!1,$e(R)}}}function G5(n){let e,t,i,l,s,o=n[2].id?"Save changes":"Create",r,a,f,u;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=M(),l=b("button"),s=b("span"),r=K(o),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[9],p(s,"class","txt"),p(l,"type","button"),p(l,"class","btn btn-expanded"),l.disabled=a=!n[12]||n[9],x(l,"btn-loading",n[9])},m(c,d){w(c,e,d),k(e,t),w(c,i,d),w(c,l,d),k(l,s),k(s,r),f||(u=[J(e,"click",n[25]),J(l,"click",n[26])],f=!0)},p(c,d){d[0]&512&&(e.disabled=c[9]),d[0]&4&&o!==(o=c[2].id?"Save changes":"Create")&&oe(r,o),d[0]&4608&&a!==(a=!c[12]||c[9])&&(l.disabled=a),d[0]&512&&x(l,"btn-loading",c[9])},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function X5(n){let e,t,i,l,s={class:"overlay-panel-lg colored-header collection-panel",escClose:!1,overlayClose:!n[9],beforeHide:n[39],$$slots:{footer:[G5],header:[Z5],default:[W5]},$$scope:{ctx:n}};e=new Zt({props:s}),n[40](e),e.$on("hide",n[41]),e.$on("show",n[42]);let o={};return i=new V5({props:o}),n[43](i),i.$on("confirm",n[44]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(r,a){z(e,r,a),w(r,t,a),z(i,r,a),l=!0},p(r,a){const f={};a[0]&512&&(f.overlayClose=!r[9]),a[0]&1040&&(f.beforeHide=r[39]),a[0]&64108|a[1]&8388608&&(f.$$scope={dirty:a,ctx:r}),e.$set(f);const u={};i.$set(u)},i(r){l||(E(e.$$.fragment,r),E(i.$$.fragment,r),l=!0)},o(r){A(e.$$.fragment,r),A(i.$$.fragment,r),l=!1},d(r){r&&v(t),n[40](null),V(e,r),n[43](null),V(i,r)}}}const Ci="schema",hs="api_rules",Dl="options",Q5="base",yp="auth",vp="view";function Er(n){return JSON.stringify(n)}function x5(n,e,t){let i,l,s,o,r,a;Ue(n,mi,ve=>t(5,a=ve));const f={};f[Q5]="Base",f[vp]="View",f[yp]="Auth";const u=lt();let c,d,m=null,h=j.initCollection(),_=!1,g=!1,y=Ci,S=Er(h),T="";function $(ve){t(3,y=ve)}function C(ve){return I(ve),t(10,g=!0),$(Ci),c==null?void 0:c.show()}function O(){return c==null?void 0:c.hide()}function D(){t(10,g=!1),O()}async function I(ve){Jt({}),typeof ve<"u"?(t(23,m=ve),t(2,h=structuredClone(ve))):(t(23,m=null),t(2,h=j.initCollection())),t(2,h.schema=h.schema||[],h),t(2,h.originalName=h.name||"",h),await Qt(),t(24,S=Er(h))}function L(){h.id?d==null||d.show(m,h):R()}function R(){if(_)return;t(9,_=!0);const ve=F();let we;h.id?we=ae.collections.update(h.id,ve):we=ae.collections.create(ve),we.then(Ye=>{wa(),lv(Ye),t(10,g=!1),O(),Lt(h.id?"Successfully updated collection.":"Successfully created collection."),u("save",{isNew:!h.id,collection:Ye})}).catch(Ye=>{ae.error(Ye)}).finally(()=>{t(9,_=!1)})}function F(){const ve=Object.assign({},h);ve.schema=ve.schema.slice(0);for(let we=ve.schema.length-1;we>=0;we--)ve.schema[we].toDelete&&ve.schema.splice(we,1);return ve}function N(){m!=null&&m.id&&fn(`Do you really want to delete collection "${m.name}" and all its records?`,()=>ae.collections.delete(m.id).then(()=>{O(),Lt(`Successfully deleted collection "${m.name}".`),u("delete",m),sv(m)}).catch(ve=>{ae.error(ve)}))}function P(ve){t(2,h.type=ve,h),li("schema")}function q(){o?fn("You have unsaved changes. Do you really want to discard them?",()=>{H()}):H()}async function H(){const ve=m?structuredClone(m):null;if(ve){if(ve.id="",ve.created="",ve.updated="",ve.name+="_duplicate",!j.isEmpty(ve.schema))for(const we of ve.schema)we.id="";if(!j.isEmpty(ve.indexes))for(let we=0;weO(),G=()=>L(),U=()=>q(),Y=()=>N(),ie=ve=>{t(2,h.name=j.slugify(ve.target.value),h),ve.target.value=h.name},te=ve=>P(ve),pe=()=>{r&&L()},Ne=()=>$(Ci),He=()=>$(hs),Xe=()=>$(Dl);function xe(ve){h=ve,t(2,h),t(23,m)}function Mt(ve){h=ve,t(2,h),t(23,m)}function ft(ve){h=ve,t(2,h),t(23,m)}function mt(ve){h=ve,t(2,h),t(23,m)}const Gt=()=>o&&g?(fn("You have unsaved changes. Do you really want to close the panel?",()=>{t(10,g=!1),O()}),!1):!0;function De(ve){ee[ve?"unshift":"push"](()=>{c=ve,t(7,c)})}function Ae(ve){Ce.call(this,n,ve)}function ze(ve){Ce.call(this,n,ve)}function gt(ve){ee[ve?"unshift":"push"](()=>{d=ve,t(8,d)})}const de=()=>R();return n.$$.update=()=>{var ve,we;n.$$.dirty[0]&4&&h.type==="view"&&(t(2,h.createRule=null,h),t(2,h.updateRule=null,h),t(2,h.deleteRule=null,h),t(2,h.indexes=[],h)),n.$$.dirty[0]&8388612&&h.name&&(m==null?void 0:m.name)!=h.name&&h.indexes.length>0&&t(2,h.indexes=(ve=h.indexes)==null?void 0:ve.map(Ye=>j.replaceIndexTableName(Ye,h.name)),h),n.$$.dirty[0]&4&&t(15,i=h.type===yp),n.$$.dirty[0]&4&&t(14,l=h.type===vp),n.$$.dirty[0]&32&&(a.schema||(we=a.options)!=null&&we.query?t(11,T=j.getNestedVal(a,"schema.message")||"Has errors"):t(11,T="")),n.$$.dirty[0]&4&&t(13,s=!!h.id&&h.system),n.$$.dirty[0]&16777220&&t(4,o=S!=Er(h)),n.$$.dirty[0]&20&&t(12,r=!h.id||o),n.$$.dirty[0]&12&&y===Dl&&h.type!=="auth"&&$(Ci)},[$,O,h,y,o,a,f,c,d,_,g,T,r,s,l,i,L,R,N,P,q,C,D,m,S,W,G,U,Y,ie,te,pe,Ne,He,Xe,xe,Mt,ft,mt,Gt,De,Ae,ze,gt,de]}class Ba extends ge{constructor(e){super(),_e(this,e,x5,X5,me,{changeTab:0,show:21,hide:1,forceHide:22},null,[-1,-1])}get changeTab(){return this.$$.ctx[0]}get show(){return this.$$.ctx[21]}get hide(){return this.$$.ctx[1]}get forceHide(){return this.$$.ctx[22]}}function e6(n){let e;return{c(){e=b("i"),p(e,"class","ri-pushpin-line m-l-auto svelte-1u3ag8h")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function t6(n){let e;return{c(){e=b("i"),p(e,"class","ri-unpin-line svelte-1u3ag8h")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function n6(n){let e,t,i,l,s,o=n[0].name+"",r,a,f,u,c,d,m,h;function _(S,T){return S[1]?t6:e6}let g=_(n),y=g(n);return{c(){var S;e=b("a"),t=b("i"),l=M(),s=b("span"),r=K(o),a=M(),f=b("span"),y.c(),p(t,"class",i=Un(j.getCollectionTypeIcon(n[0].type))+" svelte-1u3ag8h"),p(t,"aria-hidden","true"),p(s,"class","txt m-r-auto"),p(f,"class","btn btn-xs btn-circle btn-hint btn-transparent pin-collection svelte-1u3ag8h"),p(f,"aria-label","Pin collection"),p(f,"aria-hidden","true"),p(e,"href",c="/collections?collectionId="+n[0].id),p(e,"class","sidebar-list-item svelte-1u3ag8h"),p(e,"title",d=n[0].name),x(e,"active",((S=n[2])==null?void 0:S.id)===n[0].id)},m(S,T){w(S,e,T),k(e,t),k(e,l),k(e,s),k(s,r),k(e,a),k(e,f),y.m(f,null),m||(h=[Se(u=Pe.call(null,f,{position:"right",text:(n[1]?"Unpin":"Pin")+" collection"})),J(f,"click",Tn(Be(n[5]))),Se(nn.call(null,e))],m=!0)},p(S,[T]){var $;T&1&&i!==(i=Un(j.getCollectionTypeIcon(S[0].type))+" svelte-1u3ag8h")&&p(t,"class",i),T&1&&o!==(o=S[0].name+"")&&oe(r,o),g!==(g=_(S))&&(y.d(1),y=g(S),y&&(y.c(),y.m(f,null))),u&&Ct(u.update)&&T&2&&u.update.call(null,{position:"right",text:(S[1]?"Unpin":"Pin")+" collection"}),T&1&&c!==(c="/collections?collectionId="+S[0].id)&&p(e,"href",c),T&1&&d!==(d=S[0].name)&&p(e,"title",d),T&5&&x(e,"active",(($=S[2])==null?void 0:$.id)===S[0].id)},i:Q,o:Q,d(S){S&&v(e),y.d(),m=!1,$e(h)}}}function i6(n,e,t){let i,l;Ue(n,Yn,f=>t(2,l=f));let{collection:s}=e,{pinnedIds:o}=e;function r(f){o.includes(f.id)?j.removeByValue(o,f.id):o.push(f.id),t(4,o)}const a=()=>r(s);return n.$$set=f=>{"collection"in f&&t(0,s=f.collection),"pinnedIds"in f&&t(4,o=f.pinnedIds)},n.$$.update=()=>{n.$$.dirty&17&&t(1,i=o.includes(s.id))},[s,i,l,r,o,a]}class Yb extends ge{constructor(e){super(),_e(this,e,i6,n6,me,{collection:0,pinnedIds:4})}}function wp(n,e,t){const i=n.slice();return i[22]=e[t],i}function Sp(n,e,t){const i=n.slice();return i[22]=e[t],i}function $p(n){let e,t,i=[],l=new Map,s,o,r=ue(n[6]);const a=f=>f[22].id;for(let f=0;fbe(i,"pinnedIds",o)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f&64&&(u.collection=e[22]),!l&&f&2&&(l=!0,u.pinnedIds=e[1],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function Cp(n){let e,t=[],i=new Map,l,s,o=n[6].length&&Op(),r=ue(n[5]);const a=f=>f[22].id;for(let f=0;fbe(i,"pinnedIds",o)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f&32&&(u.collection=e[22]),!l&&f&2&&(l=!0,u.pinnedIds=e[1],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function Dp(n){let e;return{c(){e=b("p"),e.textContent="No collections found.",p(e,"class","txt-hint m-t-10 m-b-10 txt-center")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Ep(n){let e,t,i,l;return{c(){e=b("footer"),t=b("button"),t.innerHTML=' New collection',p(t,"type","button"),p(t,"class","btn btn-block btn-outline"),p(e,"class","sidebar-footer")},m(s,o){w(s,e,o),k(e,t),i||(l=J(t,"click",n[16]),i=!0)},p:Q,d(s){s&&v(e),i=!1,l()}}}function l6(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S=n[6].length&&$p(n),T=n[5].length&&Cp(n),$=n[3].length&&!n[2].length&&Dp(),C=!n[9]&&Ep(n);return{c(){e=b("header"),t=b("div"),i=b("div"),l=b("button"),l.innerHTML='',s=M(),o=b("input"),r=M(),a=b("hr"),f=M(),u=b("div"),S&&S.c(),c=M(),T&&T.c(),d=M(),$&&$.c(),m=M(),C&&C.c(),h=ye(),p(l,"type","button"),p(l,"class","btn btn-xs btn-transparent btn-circle btn-clear"),x(l,"hidden",!n[7]),p(i,"class","form-field-addon"),p(o,"type","text"),p(o,"placeholder","Search collections..."),p(o,"name","collections-search"),p(t,"class","form-field search"),x(t,"active",n[7]),p(e,"class","sidebar-header"),p(a,"class","m-t-5 m-b-xs"),p(u,"class","sidebar-content"),x(u,"fade",n[8]),x(u,"sidebar-content-compact",n[2].length>20)},m(O,D){w(O,e,D),k(e,t),k(t,i),k(i,l),k(t,s),k(t,o),re(o,n[0]),w(O,r,D),w(O,a,D),w(O,f,D),w(O,u,D),S&&S.m(u,null),k(u,c),T&&T.m(u,null),k(u,d),$&&$.m(u,null),w(O,m,D),C&&C.m(O,D),w(O,h,D),_=!0,g||(y=[J(l,"click",n[12]),J(o,"input",n[13])],g=!0)},p(O,D){(!_||D&128)&&x(l,"hidden",!O[7]),D&1&&o.value!==O[0]&&re(o,O[0]),(!_||D&128)&&x(t,"active",O[7]),O[6].length?S?(S.p(O,D),D&64&&E(S,1)):(S=$p(O),S.c(),E(S,1),S.m(u,c)):S&&(le(),A(S,1,1,()=>{S=null}),se()),O[5].length?T?(T.p(O,D),D&32&&E(T,1)):(T=Cp(O),T.c(),E(T,1),T.m(u,d)):T&&(le(),A(T,1,1,()=>{T=null}),se()),O[3].length&&!O[2].length?$||($=Dp(),$.c(),$.m(u,null)):$&&($.d(1),$=null),(!_||D&256)&&x(u,"fade",O[8]),(!_||D&4)&&x(u,"sidebar-content-compact",O[2].length>20),O[9]?C&&(C.d(1),C=null):C?C.p(O,D):(C=Ep(O),C.c(),C.m(h.parentNode,h))},i(O){_||(E(S),E(T),_=!0)},o(O){A(S),A(T),_=!1},d(O){O&&(v(e),v(r),v(a),v(f),v(u),v(m),v(h)),S&&S.d(),T&&T.d(),$&&$.d(),C&&C.d(O),g=!1,$e(y)}}}function s6(n){let e,t,i,l;e=new Hb({props:{class:"collection-sidebar",$$slots:{default:[l6]},$$scope:{ctx:n}}});let s={};return i=new Ba({props:s}),n[17](i),i.$on("save",n[18]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(o,r){z(e,o,r),w(o,t,r),z(i,o,r),l=!0},p(o,[r]){const a={};r&134218751&&(a.$$scope={dirty:r,ctx:o}),e.$set(a);const f={};i.$set(f)},i(o){l||(E(e.$$.fragment,o),E(i.$$.fragment,o),l=!0)},o(o){A(e.$$.fragment,o),A(i.$$.fragment,o),l=!1},d(o){o&&v(t),V(e,o),n[17](null),V(i,o)}}}const Ip="@pinnedCollections";function o6(){setTimeout(()=>{const n=document.querySelector(".collection-sidebar .sidebar-list-item.active");n&&(n==null||n.scrollIntoView({block:"nearest"}))},0)}function r6(n,e,t){let i,l,s,o,r,a,f,u,c;Ue(n,Rn,L=>t(11,a=L)),Ue(n,Yn,L=>t(19,f=L)),Ue(n,To,L=>t(8,u=L)),Ue(n,Xi,L=>t(9,c=L));let d,m="",h=[];g();function _(L){xt(Yn,f=L,f)}function g(){t(1,h=[]);try{const L=localStorage.getItem(Ip);L&&t(1,h=JSON.parse(L)||[])}catch{}}function y(){t(1,h=h.filter(L=>!!a.find(R=>R.id==L)))}const S=()=>t(0,m="");function T(){m=this.value,t(0,m)}function $(L){h=L,t(1,h)}function C(L){h=L,t(1,h)}const O=()=>d==null?void 0:d.show();function D(L){ee[L?"unshift":"push"](()=>{d=L,t(4,d)})}const I=L=>{var R;(R=L.detail)!=null&&R.isNew&&L.detail.collection&&_(L.detail.collection)};return n.$$.update=()=>{n.$$.dirty&2048&&a&&(y(),o6()),n.$$.dirty&1&&t(3,i=m.replace(/\s+/g,"").toLowerCase()),n.$$.dirty&1&&t(7,l=m!==""),n.$$.dirty&2&&h&&localStorage.setItem(Ip,JSON.stringify(h)),n.$$.dirty&2057&&t(2,s=a.filter(L=>L.id==m||L.name.replace(/\s+/g,"").toLowerCase().includes(i))),n.$$.dirty&6&&t(6,o=s.filter(L=>h.includes(L.id))),n.$$.dirty&6&&t(5,r=s.filter(L=>!h.includes(L.id)))},[m,h,s,i,d,r,o,l,u,c,_,a,S,T,$,C,O,D,I]}class a6 extends ge{constructor(e){super(),_e(this,e,r6,s6,me,{})}}function Ap(n,e,t){const i=n.slice();return i[14]=e[t][0],i[15]=e[t][1],i}function Lp(n){n[18]=n[19].default}function Np(n,e,t){const i=n.slice();return i[14]=e[t][0],i[15]=e[t][1],i[21]=t,i}function Pp(n){let e;return{c(){e=b("hr"),p(e,"class","m-t-sm m-b-sm")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Fp(n,e){let t,i=e[21]===Object.keys(e[6]).length,l,s,o=e[15].label+"",r,a,f,u,c=i&&Pp();function d(){return e[9](e[14])}return{key:n,first:null,c(){t=ye(),c&&c.c(),l=M(),s=b("button"),r=K(o),a=M(),p(s,"type","button"),p(s,"class","sidebar-item"),x(s,"active",e[5]===e[14]),this.first=t},m(m,h){w(m,t,h),c&&c.m(m,h),w(m,l,h),w(m,s,h),k(s,r),k(s,a),f||(u=J(s,"click",d),f=!0)},p(m,h){e=m,h&8&&(i=e[21]===Object.keys(e[6]).length),i?c||(c=Pp(),c.c(),c.m(l.parentNode,l)):c&&(c.d(1),c=null),h&8&&o!==(o=e[15].label+"")&&oe(r,o),h&40&&x(s,"active",e[5]===e[14])},d(m){m&&(v(t),v(l),v(s)),c&&c.d(m),f=!1,u()}}}function Rp(n){let e,t,i,l={ctx:n,current:null,token:null,hasCatch:!1,pending:c6,then:u6,catch:f6,value:19,blocks:[,,,]};return Xa(t=n[15].component,l),{c(){e=ye(),l.block.c()},m(s,o){w(s,e,o),l.block.m(s,l.anchor=o),l.mount=()=>e.parentNode,l.anchor=e,i=!0},p(s,o){n=s,l.ctx=n,o&8&&t!==(t=n[15].component)&&Xa(t,l)||O0(l,n,o)},i(s){i||(E(l.block),i=!0)},o(s){for(let o=0;o<3;o+=1){const r=l.blocks[o];A(r)}i=!1},d(s){s&&v(e),l.block.d(s),l.token=null,l=null}}}function f6(n){return{c:Q,m:Q,p:Q,i:Q,o:Q,d:Q}}function u6(n){Lp(n);let e,t,i;return e=new n[18]({props:{collection:n[2]}}),{c(){B(e.$$.fragment),t=M()},m(l,s){z(e,l,s),w(l,t,s),i=!0},p(l,s){Lp(l);const o={};s&4&&(o.collection=l[2]),e.$set(o)},i(l){i||(E(e.$$.fragment,l),i=!0)},o(l){A(e.$$.fragment,l),i=!1},d(l){l&&v(t),V(e,l)}}}function c6(n){return{c:Q,m:Q,p:Q,i:Q,o:Q,d:Q}}function qp(n,e){let t,i,l,s=e[5]===e[14]&&Rp(e);return{key:n,first:null,c(){t=ye(),s&&s.c(),i=ye(),this.first=t},m(o,r){w(o,t,r),s&&s.m(o,r),w(o,i,r),l=!0},p(o,r){e=o,e[5]===e[14]?s?(s.p(e,r),r&40&&E(s,1)):(s=Rp(e),s.c(),E(s,1),s.m(i.parentNode,i)):s&&(le(),A(s,1,1,()=>{s=null}),se())},i(o){l||(E(s),l=!0)},o(o){A(s),l=!1},d(o){o&&(v(t),v(i)),s&&s.d(o)}}}function d6(n){let e,t,i,l=[],s=new Map,o,r,a=[],f=new Map,u,c=ue(Object.entries(n[3]));const d=_=>_[14];for(let _=0;__[14];for(let _=0;_Close',p(e,"type","button"),p(e,"class","btn btn-transparent")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[8]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function m6(n){let e,t,i={class:"docs-panel",$$slots:{footer:[p6],default:[d6]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[10](e),e.$on("hide",n[11]),e.$on("show",n[12]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&4194348&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[10](null),V(e,l)}}}function h6(n,e,t){const i={list:{label:"List/Search",component:tt(()=>import("./ListApiDocs-DX-LwRkY.js"),__vite__mapDeps([3,4,5,6,7]),import.meta.url)},view:{label:"View",component:tt(()=>import("./ViewApiDocs-D09kZD3M.js"),__vite__mapDeps([8,4,5,6]),import.meta.url)},create:{label:"Create",component:tt(()=>import("./CreateApiDocs-n2O_YbPr.js"),__vite__mapDeps([9,4,5,6]),import.meta.url)},update:{label:"Update",component:tt(()=>import("./UpdateApiDocs-CYknfZa_.js"),__vite__mapDeps([10,4,5,6]),import.meta.url)},delete:{label:"Delete",component:tt(()=>import("./DeleteApiDocs-DninUosh.js"),__vite__mapDeps([11,4,5]),import.meta.url)},realtime:{label:"Realtime",component:tt(()=>import("./RealtimeApiDocs-Bz63T_FK.js"),__vite__mapDeps([12,4,5]),import.meta.url)}},l={"auth-with-password":{label:"Auth with password",component:tt(()=>import("./AuthWithPasswordDocs-B1auplF0.js"),__vite__mapDeps([13,4,5,6]),import.meta.url)},"auth-with-oauth2":{label:"Auth with OAuth2",component:tt(()=>import("./AuthWithOAuth2Docs-CtVYpHU-.js"),__vite__mapDeps([14,4,5,6]),import.meta.url)},refresh:{label:"Auth refresh",component:tt(()=>import("./AuthRefreshDocs-1UxU_c6D.js"),__vite__mapDeps([15,4,5,6]),import.meta.url)},"request-verification":{label:"Request verification",component:tt(()=>import("./RequestVerificationDocs-CmHx_pVy.js"),__vite__mapDeps([16,4,5]),import.meta.url)},"confirm-verification":{label:"Confirm verification",component:tt(()=>import("./ConfirmVerificationDocs-CzG7odGM.js"),__vite__mapDeps([17,4,5]),import.meta.url)},"request-password-reset":{label:"Request password reset",component:tt(()=>import("./RequestPasswordResetDocs-Ux0BhdtA.js"),__vite__mapDeps([18,4,5]),import.meta.url)},"confirm-password-reset":{label:"Confirm password reset",component:tt(()=>import("./ConfirmPasswordResetDocs-DZJDH7s9.js"),__vite__mapDeps([19,4,5]),import.meta.url)},"request-email-change":{label:"Request email change",component:tt(()=>import("./RequestEmailChangeDocs-OulvgXBH.js"),__vite__mapDeps([20,4,5]),import.meta.url)},"confirm-email-change":{label:"Confirm email change",component:tt(()=>import("./ConfirmEmailChangeDocs-DBFq8TK_.js"),__vite__mapDeps([21,4,5]),import.meta.url)},"list-auth-methods":{label:"List auth methods",component:tt(()=>import("./AuthMethodsDocs-Dsno-hdt.js"),__vite__mapDeps([22,4,5,6]),import.meta.url)},"list-linked-accounts":{label:"List OAuth2 accounts",component:tt(()=>import("./ListExternalAuthsDocs-DQacf2gi.js"),__vite__mapDeps([23,4,5,6]),import.meta.url)},"unlink-account":{label:"Unlink OAuth2 account",component:tt(()=>import("./UnlinkExternalAuthDocs-BcuOuUMj.js"),__vite__mapDeps([24,4,5]),import.meta.url)}};let s,o={},r,a=[];a.length&&(r=Object.keys(a)[0]);function f(y){return t(2,o=y),c(Object.keys(a)[0]),s==null?void 0:s.show()}function u(){return s==null?void 0:s.hide()}function c(y){t(5,r=y)}const d=()=>u(),m=y=>c(y);function h(y){ee[y?"unshift":"push"](()=>{s=y,t(4,s)})}function _(y){Ce.call(this,n,y)}function g(y){Ce.call(this,n,y)}return n.$$.update=()=>{n.$$.dirty&12&&(o.type==="auth"?(t(3,a=Object.assign({},i,l)),!o.options.allowUsernameAuth&&!o.options.allowEmailAuth&&delete a["auth-with-password"],o.options.allowOAuth2Auth||delete a["auth-with-oauth2"]):o.type==="view"?(t(3,a=Object.assign({},i)),delete a.create,delete a.update,delete a.delete,delete a.realtime):t(3,a=Object.assign({},i)))},[u,c,o,a,s,r,i,f,d,m,h,_,g]}class _6 extends ge{constructor(e){super(),_e(this,e,h6,m6,me,{show:7,hide:0,changeTab:1})}get show(){return this.$$.ctx[7]}get hide(){return this.$$.ctx[0]}get changeTab(){return this.$$.ctx[1]}}function g6(n){let e,t,i,l;return{c(){e=b("i"),p(e,"class","ri-calendar-event-line txt-disabled")},m(s,o){w(s,e,o),i||(l=Se(t=Pe.call(null,e,{text:n[0].join(` -`),position:"left"})),i=!0)},p(s,[o]){t&&Ct(t.update)&&o&1&&t.update.call(null,{text:s[0].join(` -`),position:"left"})},i:Q,o:Q,d(s){s&&v(e),i=!1,l()}}}const jp="yyyy-MM-dd HH:mm:ss.SSS";function b6(n,e,t){let{model:i}=e,l=[];function s(){t(0,l=[]),i.created&&l.push("Created: "+j.formatToLocalDate(i.created,jp)+" Local"),i.updated&&l.push("Updated: "+j.formatToLocalDate(i.updated,jp)+" Local")}return n.$$set=o=>{"model"in o&&t(1,i=o.model)},n.$$.update=()=>{n.$$.dirty&2&&i&&s()},[l,i]}class Kb extends ge{constructor(e){super(),_e(this,e,b6,g6,me,{model:1})}}function k6(n){let e,t,i,l,s,o,r,a,f,u;return s=new sl({props:{value:n[1]}}),{c(){e=b("div"),t=b("span"),i=K(n[1]),l=M(),B(s.$$.fragment),o=M(),r=b("i"),p(t,"class","secret svelte-1md8247"),p(r,"class","ri-refresh-line txt-sm link-hint"),p(r,"aria-label","Refresh"),p(e,"class","flex flex-gap-5 p-5")},m(c,d){w(c,e,d),k(e,t),k(t,i),n[6](t),k(e,l),z(s,e,null),k(e,o),k(e,r),a=!0,f||(u=[Se(Pe.call(null,r,"Refresh")),J(r,"click",n[4])],f=!0)},p(c,d){(!a||d&2)&&oe(i,c[1]);const m={};d&2&&(m.value=c[1]),s.$set(m)},i(c){a||(E(s.$$.fragment,c),a=!0)},o(c){A(s.$$.fragment,c),a=!1},d(c){c&&v(e),n[6](null),V(s),f=!1,$e(u)}}}function y6(n){let e,t,i,l,s,o,r,a,f,u;function c(m){n[7](m)}let d={class:"dropdown dropdown-upside dropdown-center dropdown-nowrap",$$slots:{default:[k6]},$$scope:{ctx:n}};return n[3]!==void 0&&(d.active=n[3]),l=new On({props:d}),ee.push(()=>be(l,"active",c)),l.$on("show",n[4]),{c(){e=b("button"),t=b("i"),i=M(),B(l.$$.fragment),p(t,"class","ri-sparkling-line"),p(t,"aria-hidden","true"),p(e,"tabindex","-1"),p(e,"type","button"),p(e,"aria-label","Generate"),p(e,"class",o="btn btn-circle "+n[0]+" svelte-1md8247")},m(m,h){w(m,e,h),k(e,t),k(e,i),z(l,e,null),a=!0,f||(u=Se(r=Pe.call(null,e,n[3]?"":"Generate")),f=!0)},p(m,[h]){const _={};h&518&&(_.$$scope={dirty:h,ctx:m}),!s&&h&8&&(s=!0,_.active=m[3],ke(()=>s=!1)),l.$set(_),(!a||h&1&&o!==(o="btn btn-circle "+m[0]+" svelte-1md8247"))&&p(e,"class",o),r&&Ct(r.update)&&h&8&&r.update.call(null,m[3]?"":"Generate")},i(m){a||(E(l.$$.fragment,m),a=!0)},o(m){A(l.$$.fragment,m),a=!1},d(m){m&&v(e),V(l),f=!1,u()}}}function v6(n,e,t){const i=lt();let{class:l="btn-sm btn-hint btn-transparent"}=e,{length:s=32}=e,o="",r,a=!1;async function f(){if(t(1,o=j.randomSecret(s)),i("generate",o),await Qt(),r){let d=document.createRange();d.selectNode(r),window.getSelection().removeAllRanges(),window.getSelection().addRange(d)}}function u(d){ee[d?"unshift":"push"](()=>{r=d,t(2,r)})}function c(d){a=d,t(3,a)}return n.$$set=d=>{"class"in d&&t(0,l=d.class),"length"in d&&t(5,s=d.length)},[l,o,r,a,f,s,u,c]}class Jb extends ge{constructor(e){super(),_e(this,e,v6,y6,me,{class:0,length:5})}}function w6(n){let e,t,i,l,s,o,r,a,f,u,c,d;return{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Username",o=M(),r=b("input"),p(t,"class",j.getFieldTypeIcon("user")),p(l,"class","txt"),p(e,"for",s=n[13]),p(r,"type","text"),p(r,"requried",a=!n[2]),p(r,"placeholder",f=n[2]?"Leave empty to auto generate...":n[4]),p(r,"id",u=n[13])},m(m,h){w(m,e,h),k(e,t),k(e,i),k(e,l),w(m,o,h),w(m,r,h),re(r,n[0].username),c||(d=J(r,"input",n[5]),c=!0)},p(m,h){h&8192&&s!==(s=m[13])&&p(e,"for",s),h&4&&a!==(a=!m[2])&&p(r,"requried",a),h&4&&f!==(f=m[2]?"Leave empty to auto generate...":m[4])&&p(r,"placeholder",f),h&8192&&u!==(u=m[13])&&p(r,"id",u),h&1&&r.value!==m[0].username&&re(r,m[0].username)},d(m){m&&(v(e),v(o),v(r)),c=!1,d()}}}function S6(n){let e,t,i,l,s,o,r,a,f,u,c=n[0].emailVisibility?"On":"Off",d,m,h,_,g,y,S,T;return{c(){var $;e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Email",o=M(),r=b("div"),a=b("button"),f=b("span"),u=K("Public: "),d=K(c),h=M(),_=b("input"),p(t,"class",j.getFieldTypeIcon("email")),p(l,"class","txt"),p(e,"for",s=n[13]),p(f,"class","txt"),p(a,"type","button"),p(a,"class",m="btn btn-sm btn-transparent "+(n[0].emailVisibility?"btn-success":"btn-hint")),p(r,"class","form-field-addon email-visibility-addon svelte-1751a4d"),p(_,"type","email"),_.autofocus=n[2],p(_,"autocomplete","off"),p(_,"id",g=n[13]),_.required=y=($=n[1].options)==null?void 0:$.requireEmail,p(_,"class","svelte-1751a4d")},m($,C){w($,e,C),k(e,t),k(e,i),k(e,l),w($,o,C),w($,r,C),k(r,a),k(a,f),k(f,u),k(f,d),w($,h,C),w($,_,C),re(_,n[0].email),n[2]&&_.focus(),S||(T=[Se(Pe.call(null,a,{text:"Make email public or private",position:"top-right"})),J(a,"click",Be(n[6])),J(_,"input",n[7])],S=!0)},p($,C){var O;C&8192&&s!==(s=$[13])&&p(e,"for",s),C&1&&c!==(c=$[0].emailVisibility?"On":"Off")&&oe(d,c),C&1&&m!==(m="btn btn-sm btn-transparent "+($[0].emailVisibility?"btn-success":"btn-hint"))&&p(a,"class",m),C&4&&(_.autofocus=$[2]),C&8192&&g!==(g=$[13])&&p(_,"id",g),C&2&&y!==(y=(O=$[1].options)==null?void 0:O.requireEmail)&&(_.required=y),C&1&&_.value!==$[0].email&&re(_,$[0].email)},d($){$&&(v(e),v(o),v(r),v(h),v(_)),S=!1,$e(T)}}}function Hp(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",name:"verified",$$slots:{default:[$6,({uniqueId:i})=>({13:i}),({uniqueId:i})=>i?8192:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&24584&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function $6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Change password"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(l,"for",o=n[13])},m(f,u){w(f,e,u),e.checked=n[3],w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[8]),r=!0)},p(f,u){u&8192&&t!==(t=f[13])&&p(e,"id",t),u&8&&(e.checked=f[3]),u&8192&&o!==(o=f[13])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function zp(n){let e,t,i,l,s,o,r,a,f;return l=new ce({props:{class:"form-field required",name:"password",$$slots:{default:[T6,({uniqueId:u})=>({13:u}),({uniqueId:u})=>u?8192:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[C6,({uniqueId:u})=>({13:u}),({uniqueId:u})=>u?8192:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),B(r.$$.fragment),p(i,"class","col-sm-6"),p(o,"class","col-sm-6"),p(t,"class","grid"),x(t,"p-t-xs",n[3]),p(e,"class","block")},m(u,c){w(u,e,c),k(e,t),k(t,i),z(l,i,null),k(t,s),k(t,o),z(r,o,null),f=!0},p(u,c){const d={};c&24579&&(d.$$scope={dirty:c,ctx:u}),l.$set(d);const m={};c&24577&&(m.$$scope={dirty:c,ctx:u}),r.$set(m),(!f||c&8)&&x(t,"p-t-xs",u[3])},i(u){f||(E(l.$$.fragment,u),E(r.$$.fragment,u),u&&Ke(()=>{f&&(a||(a=Fe(e,et,{duration:150},!0)),a.run(1))}),f=!0)},o(u){A(l.$$.fragment,u),A(r.$$.fragment,u),u&&(a||(a=Fe(e,et,{duration:150},!1)),a.run(0)),f=!1},d(u){u&&v(e),V(l),V(r),u&&a&&a.end()}}}function T6(n){var _,g;let e,t,i,l,s,o,r,a,f,u,c,d,m,h;return c=new Jb({props:{length:Math.max(15,((g=(_=n[1])==null?void 0:_.options)==null?void 0:g.minPasswordLength)||0)}}),{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Password",o=M(),r=b("input"),f=M(),u=b("div"),B(c.$$.fragment),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[13]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[13]),r.required=!0,p(u,"class","form-field-addon")},m(y,S){w(y,e,S),k(e,t),k(e,i),k(e,l),w(y,o,S),w(y,r,S),re(r,n[0].password),w(y,f,S),w(y,u,S),z(c,u,null),d=!0,m||(h=J(r,"input",n[9]),m=!0)},p(y,S){var $,C;(!d||S&8192&&s!==(s=y[13]))&&p(e,"for",s),(!d||S&8192&&a!==(a=y[13]))&&p(r,"id",a),S&1&&r.value!==y[0].password&&re(r,y[0].password);const T={};S&2&&(T.length=Math.max(15,((C=($=y[1])==null?void 0:$.options)==null?void 0:C.minPasswordLength)||0)),c.$set(T)},i(y){d||(E(c.$$.fragment,y),d=!0)},o(y){A(c.$$.fragment,y),d=!1},d(y){y&&(v(e),v(o),v(r),v(f),v(u)),V(c),m=!1,h()}}}function C6(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Password confirm",o=M(),r=b("input"),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[13]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[13]),r.required=!0},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,o,d),w(c,r,d),re(r,n[0].passwordConfirm),f||(u=J(r,"input",n[10]),f=!0)},p(c,d){d&8192&&s!==(s=c[13])&&p(e,"for",s),d&8192&&a!==(a=c[13])&&p(r,"id",a),d&1&&r.value!==c[0].passwordConfirm&&re(r,c[0].passwordConfirm)},d(c){c&&(v(e),v(o),v(r)),f=!1,u()}}}function O6(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Verified"),p(e,"type","checkbox"),p(e,"id",t=n[13]),p(l,"for",o=n[13])},m(f,u){w(f,e,u),e.checked=n[0].verified,w(f,i,u),w(f,l,u),k(l,s),r||(a=[J(e,"change",n[11]),J(e,"change",Be(n[12]))],r=!0)},p(f,u){u&8192&&t!==(t=f[13])&&p(e,"id",t),u&1&&(e.checked=f[0].verified),u&8192&&o!==(o=f[13])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,$e(a)}}}function M6(n){var g;let e,t,i,l,s,o,r,a,f,u,c,d,m;i=new ce({props:{class:"form-field "+(n[2]?"":"required"),name:"username",$$slots:{default:[w6,({uniqueId:y})=>({13:y}),({uniqueId:y})=>y?8192:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field "+((g=n[1].options)!=null&&g.requireEmail?"required":""),name:"email",$$slots:{default:[S6,({uniqueId:y})=>({13:y}),({uniqueId:y})=>y?8192:0]},$$scope:{ctx:n}}});let h=!n[2]&&Hp(n),_=(n[2]||n[3])&&zp(n);return d=new ce({props:{class:"form-field form-field-toggle",name:"verified",$$slots:{default:[O6,({uniqueId:y})=>({13:y}),({uniqueId:y})=>y?8192:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),a=b("div"),h&&h.c(),f=M(),_&&_.c(),u=M(),c=b("div"),B(d.$$.fragment),p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(a,"class","col-lg-12"),p(c,"class","col-lg-12"),p(e,"class","grid m-b-base")},m(y,S){w(y,e,S),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,r),k(e,a),h&&h.m(a,null),k(a,f),_&&_.m(a,null),k(e,u),k(e,c),z(d,c,null),m=!0},p(y,[S]){var O;const T={};S&4&&(T.class="form-field "+(y[2]?"":"required")),S&24581&&(T.$$scope={dirty:S,ctx:y}),i.$set(T);const $={};S&2&&($.class="form-field "+((O=y[1].options)!=null&&O.requireEmail?"required":"")),S&24583&&($.$$scope={dirty:S,ctx:y}),o.$set($),y[2]?h&&(le(),A(h,1,1,()=>{h=null}),se()):h?(h.p(y,S),S&4&&E(h,1)):(h=Hp(y),h.c(),E(h,1),h.m(a,f)),y[2]||y[3]?_?(_.p(y,S),S&12&&E(_,1)):(_=zp(y),_.c(),E(_,1),_.m(a,null)):_&&(le(),A(_,1,1,()=>{_=null}),se());const C={};S&24581&&(C.$$scope={dirty:S,ctx:y}),d.$set(C)},i(y){m||(E(i.$$.fragment,y),E(o.$$.fragment,y),E(h),E(_),E(d.$$.fragment,y),m=!0)},o(y){A(i.$$.fragment,y),A(o.$$.fragment,y),A(h),A(_),A(d.$$.fragment,y),m=!1},d(y){y&&v(e),V(i),V(o),h&&h.d(),_&&_.d(),V(d)}}}function D6(n,e,t){let{record:i}=e,{collection:l}=e,{isNew:s=!(i!=null&&i.id)}=e,o=i.username||null,r=!1;function a(){i.username=this.value,t(0,i),t(3,r)}const f=()=>t(0,i.emailVisibility=!i.emailVisibility,i);function u(){i.email=this.value,t(0,i),t(3,r)}function c(){r=this.checked,t(3,r)}function d(){i.password=this.value,t(0,i),t(3,r)}function m(){i.passwordConfirm=this.value,t(0,i),t(3,r)}function h(){i.verified=this.checked,t(0,i),t(3,r)}const _=g=>{s||fn("Do you really want to manually change the verified account state?",()=>{},()=>{t(0,i.verified=!g.target.checked,i)})};return n.$$set=g=>{"record"in g&&t(0,i=g.record),"collection"in g&&t(1,l=g.collection),"isNew"in g&&t(2,s=g.isNew)},n.$$.update=()=>{n.$$.dirty&1&&!i.username&&i.username!==null&&t(0,i.username=null,i),n.$$.dirty&8&&(r||(t(0,i.password=null,i),t(0,i.passwordConfirm=null,i),li("password"),li("passwordConfirm")))},[i,l,s,r,o,a,f,u,c,d,m,h,_]}class E6 extends ge{constructor(e){super(),_e(this,e,D6,M6,me,{record:0,collection:1,isNew:2})}}function I6(n){let e,t,i,l=[n[3]],s={};for(let o=0;o{r&&(t(1,r.style.height="",r),t(1,r.style.height=Math.min(r.scrollHeight,o)+"px",r))},0)}function u(m){if((m==null?void 0:m.code)==="Enter"&&!(m!=null&&m.shiftKey)&&!(m!=null&&m.isComposing)){m.preventDefault();const h=r.closest("form");h!=null&&h.requestSubmit&&h.requestSubmit()}}Ht(()=>(f(),()=>clearTimeout(a)));function c(m){ee[m?"unshift":"push"](()=>{r=m,t(1,r)})}function d(){s=this.value,t(0,s)}return n.$$set=m=>{e=Ie(Ie({},e),Yt(m)),t(3,l=Ge(e,i)),"value"in m&&t(0,s=m.value),"maxHeight"in m&&t(4,o=m.maxHeight)},n.$$.update=()=>{n.$$.dirty&1&&typeof s!==void 0&&f()},[s,r,u,l,o,c,d]}class L6 extends ge{constructor(e){super(),_e(this,e,A6,I6,me,{value:0,maxHeight:4})}}function N6(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d;function m(_){n[2](_)}let h={id:n[3],required:n[1].required};return n[0]!==void 0&&(h.value=n[0]),u=new L6({props:h}),ee.push(()=>be(u,"value",m)),{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),B(u.$$.fragment),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[3])},m(_,g){w(_,e,g),k(e,t),k(e,l),k(e,s),k(s,r),w(_,f,g),z(u,_,g),d=!0},p(_,g){(!d||g&2&&i!==(i=j.getFieldTypeIcon(_[1].type)))&&p(t,"class",i),(!d||g&2)&&o!==(o=_[1].name+"")&&oe(r,o),(!d||g&8&&a!==(a=_[3]))&&p(e,"for",a);const y={};g&8&&(y.id=_[3]),g&2&&(y.required=_[1].required),!c&&g&1&&(c=!0,y.value=_[0],ke(()=>c=!1)),u.$set(y)},i(_){d||(E(u.$$.fragment,_),d=!0)},o(_){A(u.$$.fragment,_),d=!1},d(_){_&&(v(e),v(f)),V(u,_)}}}function P6(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[N6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function F6(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(o){l=o,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class R6 extends ge{constructor(e){super(),_e(this,e,F6,P6,me,{field:1,value:0})}}function q6(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h,_,g;return{c(){var y,S;e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),u=b("input"),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[3]),p(u,"type","number"),p(u,"id",c=n[3]),u.required=d=n[1].required,p(u,"min",m=(y=n[1].options)==null?void 0:y.min),p(u,"max",h=(S=n[1].options)==null?void 0:S.max),p(u,"step","any")},m(y,S){w(y,e,S),k(e,t),k(e,l),k(e,s),k(s,r),w(y,f,S),w(y,u,S),re(u,n[0]),_||(g=J(u,"input",n[2]),_=!0)},p(y,S){var T,$;S&2&&i!==(i=j.getFieldTypeIcon(y[1].type))&&p(t,"class",i),S&2&&o!==(o=y[1].name+"")&&oe(r,o),S&8&&a!==(a=y[3])&&p(e,"for",a),S&8&&c!==(c=y[3])&&p(u,"id",c),S&2&&d!==(d=y[1].required)&&(u.required=d),S&2&&m!==(m=(T=y[1].options)==null?void 0:T.min)&&p(u,"min",m),S&2&&h!==(h=($=y[1].options)==null?void 0:$.max)&&p(u,"max",h),S&1&&it(u.value)!==y[0]&&re(u,y[0])},d(y){y&&(v(e),v(f),v(u)),_=!1,g()}}}function j6(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[q6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function H6(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=it(this.value),t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class z6 extends ge{constructor(e){super(),_e(this,e,H6,j6,me,{field:1,value:0})}}function V6(n){let e,t,i,l,s=n[1].name+"",o,r,a,f;return{c(){e=b("input"),i=M(),l=b("label"),o=K(s),p(e,"type","checkbox"),p(e,"id",t=n[3]),p(l,"for",r=n[3])},m(u,c){w(u,e,c),e.checked=n[0],w(u,i,c),w(u,l,c),k(l,o),a||(f=J(e,"change",n[2]),a=!0)},p(u,c){c&8&&t!==(t=u[3])&&p(e,"id",t),c&1&&(e.checked=u[0]),c&2&&s!==(s=u[1].name+"")&&oe(o,s),c&8&&r!==(r=u[3])&&p(l,"for",r)},d(u){u&&(v(e),v(i),v(l)),a=!1,f()}}}function B6(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[V6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field form-field-toggle "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function U6(n,e,t){let{field:i}=e,{value:l=!1}=e;function s(){l=this.checked,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class W6 extends ge{constructor(e){super(),_e(this,e,U6,B6,me,{field:1,value:0})}}function Y6(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h;return{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),u=b("input"),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[3]),p(u,"type","email"),p(u,"id",c=n[3]),u.required=d=n[1].required},m(_,g){w(_,e,g),k(e,t),k(e,l),k(e,s),k(s,r),w(_,f,g),w(_,u,g),re(u,n[0]),m||(h=J(u,"input",n[2]),m=!0)},p(_,g){g&2&&i!==(i=j.getFieldTypeIcon(_[1].type))&&p(t,"class",i),g&2&&o!==(o=_[1].name+"")&&oe(r,o),g&8&&a!==(a=_[3])&&p(e,"for",a),g&8&&c!==(c=_[3])&&p(u,"id",c),g&2&&d!==(d=_[1].required)&&(u.required=d),g&1&&u.value!==_[0]&&re(u,_[0])},d(_){_&&(v(e),v(f),v(u)),m=!1,h()}}}function K6(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[Y6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function J6(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=this.value,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class Z6 extends ge{constructor(e){super(),_e(this,e,J6,K6,me,{field:1,value:0})}}function G6(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h;return{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),u=b("input"),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[3]),p(u,"type","url"),p(u,"id",c=n[3]),u.required=d=n[1].required},m(_,g){w(_,e,g),k(e,t),k(e,l),k(e,s),k(s,r),w(_,f,g),w(_,u,g),re(u,n[0]),m||(h=J(u,"input",n[2]),m=!0)},p(_,g){g&2&&i!==(i=j.getFieldTypeIcon(_[1].type))&&p(t,"class",i),g&2&&o!==(o=_[1].name+"")&&oe(r,o),g&8&&a!==(a=_[3])&&p(e,"for",a),g&8&&c!==(c=_[3])&&p(u,"id",c),g&2&&d!==(d=_[1].required)&&(u.required=d),g&1&&u.value!==_[0]&&re(u,_[0])},d(_){_&&(v(e),v(f),v(u)),m=!1,h()}}}function X6(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[G6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&27&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Q6(n,e,t){let{field:i}=e,{value:l=void 0}=e;function s(){l=this.value,t(0,l)}return n.$$set=o=>{"field"in o&&t(1,i=o.field),"value"in o&&t(0,l=o.value)},[l,i,s]}class x6 extends ge{constructor(e){super(),_e(this,e,Q6,X6,me,{field:1,value:0})}}function Vp(n){let e,t,i,l;return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","link-hint clear-btn svelte-11df51y"),p(e,"class","form-field-addon")},m(s,o){w(s,e,o),k(e,t),i||(l=[Se(Pe.call(null,t,"Clear")),J(t,"click",n[5])],i=!0)},p:Q,d(s){s&&v(e),i=!1,$e(l)}}}function eO(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h,_,g=n[0]&&!n[1].required&&Vp(n);function y($){n[6]($)}function S($){n[7]($)}let T={id:n[8],options:j.defaultFlatpickrOptions()};return n[2]!==void 0&&(T.value=n[2]),n[0]!==void 0&&(T.formattedValue=n[0]),d=new Va({props:T}),ee.push(()=>be(d,"value",y)),ee.push(()=>be(d,"formattedValue",S)),d.$on("close",n[3]),{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),a=K(" (UTC)"),u=M(),g&&g.c(),c=M(),B(d.$$.fragment),p(t,"class",i=Un(j.getFieldTypeIcon(n[1].type))+" svelte-11df51y"),p(s,"class","txt"),p(e,"for",f=n[8])},m($,C){w($,e,C),k(e,t),k(e,l),k(e,s),k(s,r),k(s,a),w($,u,C),g&&g.m($,C),w($,c,C),z(d,$,C),_=!0},p($,C){(!_||C&2&&i!==(i=Un(j.getFieldTypeIcon($[1].type))+" svelte-11df51y"))&&p(t,"class",i),(!_||C&2)&&o!==(o=$[1].name+"")&&oe(r,o),(!_||C&256&&f!==(f=$[8]))&&p(e,"for",f),$[0]&&!$[1].required?g?g.p($,C):(g=Vp($),g.c(),g.m(c.parentNode,c)):g&&(g.d(1),g=null);const O={};C&256&&(O.id=$[8]),!m&&C&4&&(m=!0,O.value=$[2],ke(()=>m=!1)),!h&&C&1&&(h=!0,O.formattedValue=$[0],ke(()=>h=!1)),d.$set(O)},i($){_||(E(d.$$.fragment,$),_=!0)},o($){A(d.$$.fragment,$),_=!1},d($){$&&(v(e),v(u),v(c)),g&&g.d($),V(d,$)}}}function tO(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[eO,({uniqueId:i})=>({8:i}),({uniqueId:i})=>i?256:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&775&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function nO(n,e,t){let{field:i}=e,{value:l=void 0}=e,s=l;function o(c){c.detail&&c.detail.length==3&&t(0,l=c.detail[1])}function r(){t(0,l="")}const a=()=>r();function f(c){s=c,t(2,s),t(0,l)}function u(c){l=c,t(0,l)}return n.$$set=c=>{"field"in c&&t(1,i=c.field),"value"in c&&t(0,l=c.value)},n.$$.update=()=>{n.$$.dirty&1&&l&&l.length>19&&t(0,l=l.substring(0,19)),n.$$.dirty&5&&s!=l&&t(2,s=l)},[l,i,s,o,r,a,f,u]}class iO extends ge{constructor(e){super(),_e(this,e,nO,tO,me,{field:1,value:0})}}function Bp(n){let e,t,i=n[1].options.maxSelect+"",l,s;return{c(){e=b("div"),t=K("Select up to "),l=K(i),s=K(" items."),p(e,"class","help-block")},m(o,r){w(o,e,r),k(e,t),k(e,l),k(e,s)},p(o,r){r&2&&i!==(i=o[1].options.maxSelect+"")&&oe(l,i)},d(o){o&&v(e)}}}function lO(n){var S,T,$,C,O,D;let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h;function _(I){n[3](I)}let g={id:n[4],toggle:!n[1].required||n[2],multiple:n[2],closable:!n[2]||((S=n[0])==null?void 0:S.length)>=((T=n[1].options)==null?void 0:T.maxSelect),items:($=n[1].options)==null?void 0:$.values,searchable:((O=(C=n[1].options)==null?void 0:C.values)==null?void 0:O.length)>5};n[0]!==void 0&&(g.selected=n[0]),u=new Wb({props:g}),ee.push(()=>be(u,"selected",_));let y=((D=n[1].options)==null?void 0:D.maxSelect)>1&&Bp(n);return{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),B(u.$$.fragment),d=M(),y&&y.c(),m=ye(),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[4])},m(I,L){w(I,e,L),k(e,t),k(e,l),k(e,s),k(s,r),w(I,f,L),z(u,I,L),w(I,d,L),y&&y.m(I,L),w(I,m,L),h=!0},p(I,L){var F,N,P,q,H,W;(!h||L&2&&i!==(i=j.getFieldTypeIcon(I[1].type)))&&p(t,"class",i),(!h||L&2)&&o!==(o=I[1].name+"")&&oe(r,o),(!h||L&16&&a!==(a=I[4]))&&p(e,"for",a);const R={};L&16&&(R.id=I[4]),L&6&&(R.toggle=!I[1].required||I[2]),L&4&&(R.multiple=I[2]),L&7&&(R.closable=!I[2]||((F=I[0])==null?void 0:F.length)>=((N=I[1].options)==null?void 0:N.maxSelect)),L&2&&(R.items=(P=I[1].options)==null?void 0:P.values),L&2&&(R.searchable=((H=(q=I[1].options)==null?void 0:q.values)==null?void 0:H.length)>5),!c&&L&1&&(c=!0,R.selected=I[0],ke(()=>c=!1)),u.$set(R),((W=I[1].options)==null?void 0:W.maxSelect)>1?y?y.p(I,L):(y=Bp(I),y.c(),y.m(m.parentNode,m)):y&&(y.d(1),y=null)},i(I){h||(E(u.$$.fragment,I),h=!0)},o(I){A(u.$$.fragment,I),h=!1},d(I){I&&(v(e),v(f),v(d),v(m)),V(u,I),y&&y.d(I)}}}function sO(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[lO,({uniqueId:i})=>({4:i}),({uniqueId:i})=>i?16:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&55&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function oO(n,e,t){let i,{field:l}=e,{value:s=void 0}=e;function o(r){s=r,t(0,s),t(2,i),t(1,l)}return n.$$set=r=>{"field"in r&&t(1,l=r.field),"value"in r&&t(0,s=r.value)},n.$$.update=()=>{var r;n.$$.dirty&2&&t(2,i=((r=l.options)==null?void 0:r.maxSelect)>1),n.$$.dirty&5&&typeof s>"u"&&t(0,s=i?[]:""),n.$$.dirty&7&&i&&Array.isArray(s)&&s.length>l.options.maxSelect&&t(0,s=s.slice(s.length-l.options.maxSelect))},[s,l,i,o]}class rO extends ge{constructor(e){super(),_e(this,e,oO,sO,me,{field:1,value:0})}}function aO(n){let e;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function fO(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-fill txt-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function uO(n){let e;return{c(){e=b("input"),p(e,"type","text"),p(e,"class","txt-mono"),e.value="Loading...",e.disabled=!0},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function cO(n){let e,t,i;var l=n[3];function s(o,r){return{props:{id:o[6],maxHeight:"500",language:"json",value:o[2]}}}return l&&(e=Dt(l,s(n)),e.$on("change",n[5])),{c(){e&&B(e.$$.fragment),t=ye()},m(o,r){e&&z(e,o,r),w(o,t,r),i=!0},p(o,r){if(r&8&&l!==(l=o[3])){if(e){le();const a=e;A(a.$$.fragment,1,0,()=>{V(a,1)}),se()}l?(e=Dt(l,s(o)),e.$on("change",o[5]),B(e.$$.fragment),E(e.$$.fragment,1),z(e,t.parentNode,t)):e=null}else if(l){const a={};r&64&&(a.id=o[6]),r&4&&(a.value=o[2]),e.$set(a)}},i(o){i||(e&&E(e.$$.fragment,o),i=!0)},o(o){e&&A(e.$$.fragment,o),i=!1},d(o){o&&v(t),e&&V(e,o)}}}function dO(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m,h,_,g,y,S;function T(L,R){return L[4]?fO:aO}let $=T(n),C=$(n);const O=[cO,uO],D=[];function I(L,R){return L[3]?0:1}return m=I(n),h=D[m]=O[m](n),{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),a=M(),f=b("span"),C.c(),d=M(),h.c(),_=ye(),p(t,"class",i=Un(j.getFieldTypeIcon(n[1].type))+" svelte-p6ecb8"),p(s,"class","txt"),p(f,"class","json-state svelte-p6ecb8"),p(e,"for",c=n[6])},m(L,R){w(L,e,R),k(e,t),k(e,l),k(e,s),k(s,r),k(e,a),k(e,f),C.m(f,null),w(L,d,R),D[m].m(L,R),w(L,_,R),g=!0,y||(S=Se(u=Pe.call(null,f,{position:"left",text:n[4]?"Valid JSON":"Invalid JSON"})),y=!0)},p(L,R){(!g||R&2&&i!==(i=Un(j.getFieldTypeIcon(L[1].type))+" svelte-p6ecb8"))&&p(t,"class",i),(!g||R&2)&&o!==(o=L[1].name+"")&&oe(r,o),$!==($=T(L))&&(C.d(1),C=$(L),C&&(C.c(),C.m(f,null))),u&&Ct(u.update)&&R&16&&u.update.call(null,{position:"left",text:L[4]?"Valid JSON":"Invalid JSON"}),(!g||R&64&&c!==(c=L[6]))&&p(e,"for",c);let F=m;m=I(L),m===F?D[m].p(L,R):(le(),A(D[F],1,1,()=>{D[F]=null}),se(),h=D[m],h?h.p(L,R):(h=D[m]=O[m](L),h.c()),E(h,1),h.m(_.parentNode,_))},i(L){g||(E(h),g=!0)},o(L){A(h),g=!1},d(L){L&&(v(e),v(d),v(_)),C.d(),D[m].d(L),y=!1,S()}}}function pO(n){let e,t;return e=new ce({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[dO,({uniqueId:i})=>({6:i}),({uniqueId:i})=>i?64:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&2&&(s.class="form-field "+(i[1].required?"required":"")),l&2&&(s.name=i[1].name),l&223&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Up(n){return typeof n=="string"&&Zb(n)?n:JSON.stringify(typeof n>"u"?null:n,null,2)}function Zb(n){try{return JSON.parse(n===""?null:n),!0}catch{}return!1}function mO(n,e,t){let i,{field:l}=e,{value:s=void 0}=e,o,r=Up(s);Ht(async()=>{try{t(3,o=(await tt(async()=>{const{default:f}=await import("./CodeEditor-CZ0EgQcM.js");return{default:f}},__vite__mapDeps([2,1]),import.meta.url)).default)}catch(f){console.warn(f)}});const a=f=>{t(2,r=f.detail),t(0,s=r.trim())};return n.$$set=f=>{"field"in f&&t(1,l=f.field),"value"in f&&t(0,s=f.value)},n.$$.update=()=>{n.$$.dirty&5&&s!==(r==null?void 0:r.trim())&&(t(2,r=Up(s)),t(0,s=r)),n.$$.dirty&4&&t(4,i=Zb(r))},[s,l,r,o,i,a]}class hO extends ge{constructor(e){super(),_e(this,e,mO,pO,me,{field:1,value:0})}}function _O(n){let e,t;return{c(){e=b("i"),p(e,"class","ri-file-line"),p(e,"alt",t=n[0].name)},m(i,l){w(i,e,l)},p(i,l){l&1&&t!==(t=i[0].name)&&p(e,"alt",t)},d(i){i&&v(e)}}}function gO(n){let e,t,i;return{c(){e=b("img"),p(e,"draggable",!1),en(e.src,t=n[2])||p(e,"src",t),p(e,"width",n[1]),p(e,"height",n[1]),p(e,"alt",i=n[0].name)},m(l,s){w(l,e,s)},p(l,s){s&4&&!en(e.src,t=l[2])&&p(e,"src",t),s&2&&p(e,"width",l[1]),s&2&&p(e,"height",l[1]),s&1&&i!==(i=l[0].name)&&p(e,"alt",i)},d(l){l&&v(e)}}}function bO(n){let e;function t(s,o){return s[2]?gO:_O}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:Q,o:Q,d(s){s&&v(e),l.d(s)}}}function kO(n,e,t){let i,{file:l}=e,{size:s=50}=e;function o(){j.hasImageExtension(l==null?void 0:l.name)?j.generateThumb(l,s,s).then(r=>{t(2,i=r)}).catch(r=>{t(2,i=""),console.warn("Unable to generate thumb: ",r)}):t(2,i="")}return n.$$set=r=>{"file"in r&&t(0,l=r.file),"size"in r&&t(1,s=r.size)},n.$$.update=()=>{n.$$.dirty&1&&typeof l<"u"&&o()},t(2,i=""),[l,s,i]}class yO extends ge{constructor(e){super(),_e(this,e,kO,bO,me,{file:0,size:1})}}function Wp(n){let e;function t(s,o){return s[4]==="image"?wO:vO}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function vO(n){let e,t;return{c(){e=b("object"),t=K("Cannot preview the file."),p(e,"title",n[2]),p(e,"data",n[1])},m(i,l){w(i,e,l),k(e,t)},p(i,l){l&4&&p(e,"title",i[2]),l&2&&p(e,"data",i[1])},d(i){i&&v(e)}}}function wO(n){let e,t,i;return{c(){e=b("img"),en(e.src,t=n[1])||p(e,"src",t),p(e,"alt",i="Preview "+n[2])},m(l,s){w(l,e,s)},p(l,s){s&2&&!en(e.src,t=l[1])&&p(e,"src",t),s&4&&i!==(i="Preview "+l[2])&&p(e,"alt",i)},d(l){l&&v(e)}}}function SO(n){var l;let e=(l=n[3])==null?void 0:l.isActive(),t,i=e&&Wp(n);return{c(){i&&i.c(),t=ye()},m(s,o){i&&i.m(s,o),w(s,t,o)},p(s,o){var r;o&8&&(e=(r=s[3])==null?void 0:r.isActive()),e?i?i.p(s,o):(i=Wp(s),i.c(),i.m(t.parentNode,t)):i&&(i.d(1),i=null)},d(s){s&&v(t),i&&i.d(s)}}}function $O(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","overlay-close")},m(l,s){w(l,e,s),t||(i=J(e,"click",Be(n[0])),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function TO(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("a"),t=K(n[2]),i=M(),l=b("i"),s=M(),o=b("div"),r=M(),a=b("button"),a.textContent="Close",p(l,"class","ri-external-link-line"),p(e,"href",n[1]),p(e,"title",n[2]),p(e,"target","_blank"),p(e,"rel","noreferrer noopener"),p(e,"class","link-hint txt-ellipsis inline-flex"),p(o,"class","flex-fill"),p(a,"type","button"),p(a,"class","btn btn-transparent")},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,s,d),w(c,o,d),w(c,r,d),w(c,a,d),f||(u=J(a,"click",n[0]),f=!0)},p(c,d){d&4&&oe(t,c[2]),d&2&&p(e,"href",c[1]),d&4&&p(e,"title",c[2])},d(c){c&&(v(e),v(s),v(o),v(r),v(a)),f=!1,u()}}}function CO(n){let e,t,i={class:"preview preview-"+n[4],btnClose:!1,popup:!0,$$slots:{footer:[TO],header:[$O],default:[SO]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[7](e),e.$on("show",n[8]),e.$on("hide",n[9]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.class="preview preview-"+l[4]),s&1054&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[7](null),V(e,l)}}}function OO(n,e,t){let i,l,s,o,r="";function a(m){m!==""&&(t(1,r=m),o==null||o.show())}function f(){return o==null?void 0:o.hide()}function u(m){ee[m?"unshift":"push"](()=>{o=m,t(3,o)})}function c(m){Ce.call(this,n,m)}function d(m){Ce.call(this,n,m)}return n.$$.update=()=>{n.$$.dirty&2&&t(6,i=r.indexOf("?")),n.$$.dirty&66&&t(2,l=r.substring(r.lastIndexOf("/")+1,i>0?i:void 0)),n.$$.dirty&4&&t(4,s=j.getFileType(l))},[f,r,l,o,s,a,i,u,c,d]}class MO extends ge{constructor(e){super(),_e(this,e,OO,CO,me,{show:5,hide:0})}get show(){return this.$$.ctx[5]}get hide(){return this.$$.ctx[0]}}function DO(n){let e,t,i,l,s;function o(f,u){return f[3]==="image"?LO:f[3]==="video"||f[3]==="audio"?AO:IO}let r=o(n),a=r(n);return{c(){e=b("a"),a.c(),p(e,"draggable",!1),p(e,"class",t="thumb "+(n[1]?`thumb-${n[1]}`:"")),p(e,"href",n[6]),p(e,"target","_blank"),p(e,"rel","noreferrer"),p(e,"title",i=(n[7]?"Preview":"Download")+" "+n[0])},m(f,u){w(f,e,u),a.m(e,null),l||(s=J(e,"click",Tn(n[11])),l=!0)},p(f,u){r===(r=o(f))&&a?a.p(f,u):(a.d(1),a=r(f),a&&(a.c(),a.m(e,null))),u&2&&t!==(t="thumb "+(f[1]?`thumb-${f[1]}`:""))&&p(e,"class",t),u&64&&p(e,"href",f[6]),u&129&&i!==(i=(f[7]?"Preview":"Download")+" "+f[0])&&p(e,"title",i)},d(f){f&&v(e),a.d(),l=!1,s()}}}function EO(n){let e,t;return{c(){e=b("div"),p(e,"class",t="thumb "+(n[1]?`thumb-${n[1]}`:""))},m(i,l){w(i,e,l)},p(i,l){l&2&&t!==(t="thumb "+(i[1]?`thumb-${i[1]}`:""))&&p(e,"class",t)},d(i){i&&v(e)}}}function IO(n){let e;return{c(){e=b("i"),p(e,"class","ri-file-3-line")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function AO(n){let e;return{c(){e=b("i"),p(e,"class","ri-video-line")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function LO(n){let e,t,i,l,s;return{c(){e=b("img"),p(e,"draggable",!1),p(e,"loading","lazy"),en(e.src,t=n[5])||p(e,"src",t),p(e,"alt",n[0]),p(e,"title",i="Preview "+n[0])},m(o,r){w(o,e,r),l||(s=J(e,"error",n[8]),l=!0)},p(o,r){r&32&&!en(e.src,t=o[5])&&p(e,"src",t),r&1&&p(e,"alt",o[0]),r&1&&i!==(i="Preview "+o[0])&&p(e,"title",i)},d(o){o&&v(e),l=!1,s()}}}function Yp(n){let e,t,i={};return e=new MO({props:i}),n[12](e),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,s){const o={};e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[12](null),V(e,l)}}}function NO(n){let e,t,i;function l(a,f){return a[2]?EO:DO}let s=l(n),o=s(n),r=n[7]&&Yp(n);return{c(){o.c(),e=M(),r&&r.c(),t=ye()},m(a,f){o.m(a,f),w(a,e,f),r&&r.m(a,f),w(a,t,f),i=!0},p(a,[f]){s===(s=l(a))&&o?o.p(a,f):(o.d(1),o=s(a),o&&(o.c(),o.m(e.parentNode,e))),a[7]?r?(r.p(a,f),f&128&&E(r,1)):(r=Yp(a),r.c(),E(r,1),r.m(t.parentNode,t)):r&&(le(),A(r,1,1,()=>{r=null}),se())},i(a){i||(E(r),i=!0)},o(a){A(r),i=!1},d(a){a&&(v(e),v(t)),o.d(a),r&&r.d(a)}}}function PO(n,e,t){let i,l,{record:s=null}=e,{filename:o=""}=e,{size:r=""}=e,a,f="",u="",c="",d=!0;m();async function m(){t(2,d=!0);try{t(10,c=await ae.getAdminFileToken(s.collectionId))}catch(y){console.warn("File token failure:",y)}t(2,d=!1)}function h(){t(5,f="")}const _=y=>{l&&(y.preventDefault(),a==null||a.show(u))};function g(y){ee[y?"unshift":"push"](()=>{a=y,t(4,a)})}return n.$$set=y=>{"record"in y&&t(9,s=y.record),"filename"in y&&t(0,o=y.filename),"size"in y&&t(1,r=y.size)},n.$$.update=()=>{n.$$.dirty&1&&t(3,i=j.getFileType(o)),n.$$.dirty&9&&t(7,l=["image","audio","video"].includes(i)||o.endsWith(".pdf")),n.$$.dirty&1541&&t(6,u=d?"":ae.files.getUrl(s,o,{token:c})),n.$$.dirty&1541&&t(5,f=d?"":ae.files.getUrl(s,o,{thumb:"100x100",token:c}))},[o,r,d,i,a,f,u,l,h,s,c,_,g]}class Ua extends ge{constructor(e){super(),_e(this,e,PO,NO,me,{record:9,filename:0,size:1})}}function Kp(n,e,t){const i=n.slice();return i[29]=e[t],i[31]=t,i}function Jp(n,e,t){const i=n.slice();i[34]=e[t],i[31]=t;const l=i[2].includes(i[34]);return i[35]=l,i}function FO(n){let e,t,i;function l(){return n[17](n[34])}return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove")},m(s,o){w(s,e,o),t||(i=[Se(Pe.call(null,e,"Remove file")),J(e,"click",l)],t=!0)},p(s,o){n=s},d(s){s&&v(e),t=!1,$e(i)}}}function RO(n){let e,t,i;function l(){return n[16](n[34])}return{c(){e=b("button"),e.innerHTML='Restore',p(e,"type","button"),p(e,"class","btn btn-sm btn-danger btn-transparent")},m(s,o){w(s,e,o),t||(i=J(e,"click",l),t=!0)},p(s,o){n=s},d(s){s&&v(e),t=!1,i()}}}function qO(n){let e,t,i,l,s,o,r=n[34]+"",a,f,u,c,d,m;i=new Ua({props:{record:n[3],filename:n[34]}});function h(y,S){return y[35]?RO:FO}let _=h(n),g=_(n);return{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),o=b("a"),a=K(r),c=M(),d=b("div"),g.c(),x(t,"fade",n[35]),p(o,"draggable",!1),p(o,"href",f=ae.files.getUrl(n[3],n[34],{token:n[10]})),p(o,"class",u="txt-ellipsis "+(n[35]?"txt-strikethrough txt-hint":"link-primary")),p(o,"title","Download"),p(o,"target","_blank"),p(o,"rel","noopener noreferrer"),p(s,"class","content"),p(d,"class","actions"),p(e,"class","list-item"),x(e,"dragging",n[32]),x(e,"dragover",n[33])},m(y,S){w(y,e,S),k(e,t),z(i,t,null),k(e,l),k(e,s),k(s,o),k(o,a),k(e,c),k(e,d),g.m(d,null),m=!0},p(y,S){const T={};S[0]&8&&(T.record=y[3]),S[0]&32&&(T.filename=y[34]),i.$set(T),(!m||S[0]&36)&&x(t,"fade",y[35]),(!m||S[0]&32)&&r!==(r=y[34]+"")&&oe(a,r),(!m||S[0]&1064&&f!==(f=ae.files.getUrl(y[3],y[34],{token:y[10]})))&&p(o,"href",f),(!m||S[0]&36&&u!==(u="txt-ellipsis "+(y[35]?"txt-strikethrough txt-hint":"link-primary")))&&p(o,"class",u),_===(_=h(y))&&g?g.p(y,S):(g.d(1),g=_(y),g&&(g.c(),g.m(d,null))),(!m||S[1]&2)&&x(e,"dragging",y[32]),(!m||S[1]&4)&&x(e,"dragover",y[33])},i(y){m||(E(i.$$.fragment,y),m=!0)},o(y){A(i.$$.fragment,y),m=!1},d(y){y&&v(e),V(i),g.d()}}}function Zp(n,e){let t,i,l,s;function o(a){e[18](a)}let r={group:e[4].name+"_uploaded",index:e[31],disabled:!e[6],$$slots:{default:[qO,({dragging:a,dragover:f})=>({32:a,33:f}),({dragging:a,dragover:f})=>[0,(a?2:0)|(f?4:0)]]},$$scope:{ctx:e}};return e[0]!==void 0&&(r.list=e[0]),i=new Ms({props:r}),ee.push(()=>be(i,"list",o)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f[0]&16&&(u.group=e[4].name+"_uploaded"),f[0]&32&&(u.index=e[31]),f[0]&64&&(u.disabled=!e[6]),f[0]&1068|f[1]&70&&(u.$$scope={dirty:f,ctx:e}),!l&&f[0]&1&&(l=!0,u.list=e[0],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function jO(n){let e,t,i,l,s,o,r,a,f=n[29].name+"",u,c,d,m,h,_,g;i=new yO({props:{file:n[29]}});function y(){return n[19](n[31])}return{c(){e=b("div"),t=b("figure"),B(i.$$.fragment),l=M(),s=b("div"),o=b("small"),o.textContent="New",r=M(),a=b("span"),u=K(f),d=M(),m=b("button"),m.innerHTML='',p(t,"class","thumb"),p(o,"class","label label-success m-r-5"),p(a,"class","txt"),p(s,"class","filename m-r-auto"),p(s,"title",c=n[29].name),p(m,"type","button"),p(m,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove"),p(e,"class","list-item"),x(e,"dragging",n[32]),x(e,"dragover",n[33])},m(S,T){w(S,e,T),k(e,t),z(i,t,null),k(e,l),k(e,s),k(s,o),k(s,r),k(s,a),k(a,u),k(e,d),k(e,m),h=!0,_||(g=[Se(Pe.call(null,m,"Remove file")),J(m,"click",y)],_=!0)},p(S,T){n=S;const $={};T[0]&2&&($.file=n[29]),i.$set($),(!h||T[0]&2)&&f!==(f=n[29].name+"")&&oe(u,f),(!h||T[0]&2&&c!==(c=n[29].name))&&p(s,"title",c),(!h||T[1]&2)&&x(e,"dragging",n[32]),(!h||T[1]&4)&&x(e,"dragover",n[33])},i(S){h||(E(i.$$.fragment,S),h=!0)},o(S){A(i.$$.fragment,S),h=!1},d(S){S&&v(e),V(i),_=!1,$e(g)}}}function Gp(n,e){let t,i,l,s;function o(a){e[20](a)}let r={group:e[4].name+"_new",index:e[31],disabled:!e[6],$$slots:{default:[jO,({dragging:a,dragover:f})=>({32:a,33:f}),({dragging:a,dragover:f})=>[0,(a?2:0)|(f?4:0)]]},$$scope:{ctx:e}};return e[1]!==void 0&&(r.list=e[1]),i=new Ms({props:r}),ee.push(()=>be(i,"list",o)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f[0]&16&&(u.group=e[4].name+"_new"),f[0]&2&&(u.index=e[31]),f[0]&64&&(u.disabled=!e[6]),f[0]&2|f[1]&70&&(u.$$scope={dirty:f,ctx:e}),!l&&f[0]&2&&(l=!0,u.list=e[1],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function HO(n){let e,t,i,l,s,o=n[4].name+"",r,a,f,u,c=[],d=new Map,m,h=[],_=new Map,g,y,S,T,$,C,O,D,I,L,R,F,N=ue(n[5]);const P=W=>W[34]+W[3].id;for(let W=0;WW[29].name+W[31];for(let W=0;W({28:o}),({uniqueId:o})=>[o?268435456:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","block")},m(o,r){w(o,e,r),z(t,e,null),i=!0,l||(s=[J(e,"dragover",Be(n[25])),J(e,"dragleave",n[26]),J(e,"drop",n[15])],l=!0)},p(o,r){const a={};r[0]&528&&(a.class=` - form-field form-field-list form-field-file - `+(o[4].required?"required":"")+` - `+(o[9]?"dragover":"")+` - `),r[0]&16&&(a.name=o[4].name),r[0]&268439039|r[1]&64&&(a.$$scope={dirty:r,ctx:o}),t.$set(a)},i(o){i||(E(t.$$.fragment,o),i=!0)},o(o){A(t.$$.fragment,o),i=!1},d(o){o&&v(e),V(t),l=!1,$e(s)}}}function VO(n,e,t){let i,l,s,{record:o}=e,{field:r}=e,{value:a=""}=e,{uploadedFiles:f=[]}=e,{deletedFileNames:u=[]}=e,c,d,m=!1,h="";function _(H){j.removeByValue(u,H),t(2,u)}function g(H){j.pushUnique(u,H),t(2,u)}function y(H){j.isEmpty(f[H])||f.splice(H,1),t(1,f)}function S(){d==null||d.dispatchEvent(new CustomEvent("change",{detail:{value:a,uploadedFiles:f,deletedFileNames:u},bubbles:!0}))}function T(H){var G,U;H.preventDefault(),t(9,m=!1);const W=((G=H.dataTransfer)==null?void 0:G.files)||[];if(!(s||!W.length)){for(const Y of W){const ie=l.length+f.length-u.length;if(((U=r.options)==null?void 0:U.maxSelect)<=ie)break;f.push(Y)}t(1,f)}}Ht(async()=>{t(10,h=await ae.getAdminFileToken(o.collectionId))});const $=H=>_(H),C=H=>g(H);function O(H){a=H,t(0,a),t(6,i),t(4,r)}const D=H=>y(H);function I(H){f=H,t(1,f)}function L(H){ee[H?"unshift":"push"](()=>{c=H,t(7,c)})}const R=()=>{for(let H of c.files)f.push(H);t(1,f),t(7,c.value=null,c)},F=()=>c==null?void 0:c.click();function N(H){ee[H?"unshift":"push"](()=>{d=H,t(8,d)})}const P=()=>{t(9,m=!0)},q=()=>{t(9,m=!1)};return n.$$set=H=>{"record"in H&&t(3,o=H.record),"field"in H&&t(4,r=H.field),"value"in H&&t(0,a=H.value),"uploadedFiles"in H&&t(1,f=H.uploadedFiles),"deletedFileNames"in H&&t(2,u=H.deletedFileNames)},n.$$.update=()=>{var H,W;n.$$.dirty[0]&2&&(Array.isArray(f)||t(1,f=j.toArray(f))),n.$$.dirty[0]&4&&(Array.isArray(u)||t(2,u=j.toArray(u))),n.$$.dirty[0]&16&&t(6,i=((H=r.options)==null?void 0:H.maxSelect)>1),n.$$.dirty[0]&65&&j.isEmpty(a)&&t(0,a=i?[]:""),n.$$.dirty[0]&1&&t(5,l=j.toArray(a)),n.$$.dirty[0]&54&&t(11,s=(l.length||f.length)&&((W=r.options)==null?void 0:W.maxSelect)<=l.length+f.length-u.length),n.$$.dirty[0]&6&&(f!==-1||u!==-1)&&S()},[a,f,u,o,r,l,i,c,d,m,h,s,_,g,y,T,$,C,O,D,I,L,R,F,N,P,q]}class BO extends ge{constructor(e){super(),_e(this,e,VO,zO,me,{record:3,field:4,value:0,uploadedFiles:1,deletedFileNames:2},null,[-1,-1])}}function Xp(n){return typeof n=="function"?{threshold:100,callback:n}:n||{}}function UO(n,e){e=Xp(e),e!=null&&e.callback&&e.callback();function t(i){if(!(e!=null&&e.callback))return;i.target.scrollHeight-i.target.clientHeight-i.target.scrollTop<=e.threshold&&e.callback()}return n.addEventListener("scroll",t),n.addEventListener("resize",t),{update(i){e=Xp(i)},destroy(){n.removeEventListener("scroll",t),n.removeEventListener("resize",t)}}}function Qp(n,e,t){const i=n.slice();i[6]=e[t];const l=j.toArray(i[0][i[6]]).slice(0,5);return i[7]=l,i}function xp(n,e,t){const i=n.slice();return i[10]=e[t],i}function em(n){let e,t;return e=new Ua({props:{record:n[0],filename:n[10],size:"xs"}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&1&&(s.record=i[0]),l&3&&(s.filename=i[10]),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function tm(n){let e=!j.isEmpty(n[10]),t,i,l=e&&em(n);return{c(){l&&l.c(),t=ye()},m(s,o){l&&l.m(s,o),w(s,t,o),i=!0},p(s,o){o&3&&(e=!j.isEmpty(s[10])),e?l?(l.p(s,o),o&3&&E(l,1)):(l=em(s),l.c(),E(l,1),l.m(t.parentNode,t)):l&&(le(),A(l,1,1,()=>{l=null}),se())},i(s){i||(E(l),i=!0)},o(s){A(l),i=!1},d(s){s&&v(t),l&&l.d(s)}}}function nm(n){let e,t,i=ue(n[7]),l=[];for(let o=0;oA(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;oA(m[_],1,1,()=>{m[_]=null});return{c(){e=b("div"),t=b("i"),l=M();for(let _=0;_t(4,l=f));let{record:s}=e,o=[],r=[];function a(){const f=(i==null?void 0:i.schema)||[];if(t(1,o=f.filter(u=>u.presentable&&u.type=="file").map(u=>u.name)),t(2,r=f.filter(u=>u.presentable&&u.type!="file").map(u=>u.name)),!o.length&&!r.length){const u=f.find(c=>{var d,m,h;return c.type=="file"&&((d=c.options)==null?void 0:d.maxSelect)==1&&((h=(m=c.options)==null?void 0:m.mimeTypes)==null?void 0:h.find(_=>_.startsWith("image/")))});u&&o.push(u.name)}}return n.$$set=f=>{"record"in f&&t(0,s=f.record)},n.$$.update=()=>{n.$$.dirty&17&&t(3,i=l==null?void 0:l.find(f=>f.id==(s==null?void 0:s.collectionId))),n.$$.dirty&8&&i&&a()},[s,o,r,i,l]}class xo extends ge{constructor(e){super(),_e(this,e,YO,WO,me,{record:0})}}function im(n,e,t){const i=n.slice();return i[49]=e[t],i[51]=t,i}function lm(n,e,t){const i=n.slice();i[49]=e[t];const l=i[9](i[49]);return i[6]=l,i}function sm(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='
    New record
    ',p(e,"type","button"),p(e,"class","btn btn-pill btn-transparent btn-hint p-l-xs p-r-xs")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[31]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function om(n){let e,t=!n[13]&&rm(n);return{c(){t&&t.c(),e=ye()},m(i,l){t&&t.m(i,l),w(i,e,l)},p(i,l){i[13]?t&&(t.d(1),t=null):t?t.p(i,l):(t=rm(i),t.c(),t.m(e.parentNode,e))},d(i){i&&v(e),t&&t.d(i)}}}function rm(n){var s;let e,t,i,l=((s=n[2])==null?void 0:s.length)&&am(n);return{c(){e=b("div"),t=b("span"),t.textContent="No records found.",i=M(),l&&l.c(),p(t,"class","txt txt-hint"),p(e,"class","list-item")},m(o,r){w(o,e,r),k(e,t),k(e,i),l&&l.m(e,null)},p(o,r){var a;(a=o[2])!=null&&a.length?l?l.p(o,r):(l=am(o),l.c(),l.m(e,null)):l&&(l.d(1),l=null)},d(o){o&&v(e),l&&l.d()}}}function am(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[35]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function KO(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-blank-circle-line txt-disabled")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function JO(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-fill txt-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function fm(n){let e,t,i,l;function s(){return n[32](n[49])}return{c(){e=b("div"),t=b("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","btn btn-sm btn-circle btn-transparent btn-hint m-l-auto"),p(e,"class","actions nonintrusive")},m(o,r){w(o,e,r),k(e,t),i||(l=[Se(Pe.call(null,t,"Edit")),J(t,"keydown",Tn(n[27])),J(t,"click",Tn(s))],i=!0)},p(o,r){n=o},d(o){o&&v(e),i=!1,$e(l)}}}function um(n,e){let t,i,l,s,o,r,a,f;function u(g,y){return g[6]?JO:KO}let c=u(e),d=c(e);s=new xo({props:{record:e[49]}});let m=!e[11]&&fm(e);function h(){return e[33](e[49])}function _(...g){return e[34](e[49],...g)}return{key:n,first:null,c(){t=b("div"),d.c(),i=M(),l=b("div"),B(s.$$.fragment),o=M(),m&&m.c(),p(l,"class","content"),p(t,"tabindex","0"),p(t,"class","list-item handle"),x(t,"selected",e[6]),x(t,"disabled",!e[6]&&e[4]>1&&!e[10]),this.first=t},m(g,y){w(g,t,y),d.m(t,null),k(t,i),k(t,l),z(s,l,null),k(t,o),m&&m.m(t,null),r=!0,a||(f=[J(t,"click",h),J(t,"keydown",_)],a=!0)},p(g,y){e=g,c!==(c=u(e))&&(d.d(1),d=c(e),d&&(d.c(),d.m(t,i)));const S={};y[0]&256&&(S.record=e[49]),s.$set(S),e[11]?m&&(m.d(1),m=null):m?m.p(e,y):(m=fm(e),m.c(),m.m(t,null)),(!r||y[0]&768)&&x(t,"selected",e[6]),(!r||y[0]&1808)&&x(t,"disabled",!e[6]&&e[4]>1&&!e[10])},i(g){r||(E(s.$$.fragment,g),r=!0)},o(g){A(s.$$.fragment,g),r=!1},d(g){g&&v(t),d.d(),V(s),m&&m.d(),a=!1,$e(f)}}}function cm(n){let e;return{c(){e=b("div"),e.innerHTML='
    ',p(e,"class","list-item")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function dm(n){let e,t=n[6].length+"",i,l,s,o;return{c(){e=K("("),i=K(t),l=K(" of MAX "),s=K(n[4]),o=K(")")},m(r,a){w(r,e,a),w(r,i,a),w(r,l,a),w(r,s,a),w(r,o,a)},p(r,a){a[0]&64&&t!==(t=r[6].length+"")&&oe(i,t),a[0]&16&&oe(s,r[4])},d(r){r&&(v(e),v(i),v(l),v(s),v(o))}}}function ZO(n){let e;return{c(){e=b("p"),e.textContent="No selected records.",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function GO(n){let e,t,i=ue(n[6]),l=[];for(let o=0;oA(l[o],1,1,()=>{l[o]=null});return{c(){e=b("div");for(let o=0;o',s=M(),p(l,"type","button"),p(l,"title","Remove"),p(l,"class","btn btn-circle btn-transparent btn-hint btn-xs"),p(e,"class","label"),x(e,"label-danger",n[52]),x(e,"label-warning",n[53])},m(u,c){w(u,e,c),z(t,e,null),k(e,i),k(e,l),w(u,s,c),o=!0,r||(a=J(l,"click",f),r=!0)},p(u,c){n=u;const d={};c[0]&64&&(d.record=n[49]),t.$set(d),(!o||c[1]&2097152)&&x(e,"label-danger",n[52]),(!o||c[1]&4194304)&&x(e,"label-warning",n[53])},i(u){o||(E(t.$$.fragment,u),o=!0)},o(u){A(t.$$.fragment,u),o=!1},d(u){u&&(v(e),v(s)),V(t),r=!1,a()}}}function pm(n){let e,t,i;function l(o){n[38](o)}let s={index:n[51],$$slots:{default:[XO,({dragging:o,dragover:r})=>({52:o,53:r}),({dragging:o,dragover:r})=>[0,(o?2097152:0)|(r?4194304:0)]]},$$scope:{ctx:n}};return n[6]!==void 0&&(s.list=n[6]),e=new Ms({props:s}),ee.push(()=>be(e,"list",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[0]&64|r[1]&39845888&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&64&&(t=!0,a.list=o[6],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function QO(n){let e,t,i,l,s,o=[],r=new Map,a,f,u,c,d,m,h,_,g,y,S,T;t=new $s({props:{value:n[2],autocompleteCollection:n[5]}}),t.$on("submit",n[30]);let $=!n[11]&&sm(n),C=ue(n[8]);const O=P=>P[49].id;for(let P=0;P1&&dm(n);const R=[GO,ZO],F=[];function N(P,q){return P[6].length?0:1}return h=N(n),_=F[h]=R[h](n),{c(){e=b("div"),B(t.$$.fragment),i=M(),$&&$.c(),l=M(),s=b("div");for(let P=0;P1?L?L.p(P,q):(L=dm(P),L.c(),L.m(c,null)):L&&(L.d(1),L=null);let W=h;h=N(P),h===W?F[h].p(P,q):(le(),A(F[W],1,1,()=>{F[W]=null}),se(),_=F[h],_?_.p(P,q):(_=F[h]=R[h](P),_.c()),E(_,1),_.m(g.parentNode,g))},i(P){if(!y){E(t.$$.fragment,P);for(let q=0;qCancel',t=M(),i=b("button"),i.innerHTML='Set selection',p(e,"type","button"),p(e,"class","btn btn-transparent"),p(i,"type","button"),p(i,"class","btn")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),l||(s=[J(e,"click",n[28]),J(i,"click",n[29])],l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,$e(s)}}}function tM(n){let e,t,i,l;const s=[{popup:!0},{class:"overlay-panel-xl"},n[19]];let o={$$slots:{footer:[eM],header:[xO],default:[QO]},$$scope:{ctx:n}};for(let a=0;at(26,m=we));const h=lt(),_="picker_"+j.randomString(5);let{value:g}=e,{field:y}=e,S,T,$="",C=[],O=[],D=1,I=0,L=!1,R=!1;function F(){return t(2,$=""),t(8,C=[]),t(6,O=[]),P(),q(!0),S==null?void 0:S.show()}function N(){return S==null?void 0:S.hide()}async function P(){const we=j.toArray(g);if(!l||!we.length)return;t(24,R=!0);let Ye=[];const zt=we.slice(),cn=[];for(;zt.length>0;){const rn=[];for(const qn of zt.splice(0,no))rn.push(`id="${qn}"`);cn.push(ae.collection(l).getFullList({batch:no,filter:rn.join("||"),fields:"*:excerpt(200)",requestKey:null}))}try{await Promise.all(cn).then(rn=>{Ye=Ye.concat(...rn)}),t(6,O=[]);for(const rn of we){const qn=j.findByKey(Ye,"id",rn);qn&&O.push(qn)}$.trim()||t(8,C=j.filterDuplicatesByKey(O.concat(C))),t(24,R=!1)}catch(rn){rn.isAbort||(ae.error(rn),t(24,R=!1))}}async function q(we=!1){if(l){t(3,L=!0),we&&($.trim()?t(8,C=[]):t(8,C=j.toArray(O).slice()));try{const Ye=we?1:D+1,zt=j.getAllCollectionIdentifiers(s),cn=await ae.collection(l).getList(Ye,no,{filter:j.normalizeSearchFilter($,zt),sort:o?"":"-created",fields:"*:excerpt(200)",skipTotal:1,requestKey:_+"loadList"});t(8,C=j.filterDuplicatesByKey(C.concat(cn.items))),D=cn.page,t(23,I=cn.items.length),t(3,L=!1)}catch(Ye){Ye.isAbort||(ae.error(Ye),t(3,L=!1))}}}function H(we){i==1?t(6,O=[we]):f&&(j.pushOrReplaceByKey(O,we),t(6,O))}function W(we){j.removeByKey(O,"id",we.id),t(6,O)}function G(we){u(we)?W(we):H(we)}function U(){var we;i!=1?t(20,g=O.map(Ye=>Ye.id)):t(20,g=((we=O==null?void 0:O[0])==null?void 0:we.id)||""),h("save",O),N()}function Y(we){Ce.call(this,n,we)}const ie=()=>N(),te=()=>U(),pe=we=>t(2,$=we.detail),Ne=()=>T==null?void 0:T.show(),He=we=>T==null?void 0:T.show(we.id),Xe=we=>G(we),xe=(we,Ye)=>{(Ye.code==="Enter"||Ye.code==="Space")&&(Ye.preventDefault(),Ye.stopPropagation(),G(we))},Mt=()=>t(2,$=""),ft=()=>{a&&!L&&q()},mt=we=>W(we);function Gt(we){O=we,t(6,O)}function De(we){ee[we?"unshift":"push"](()=>{S=we,t(1,S)})}function Ae(we){Ce.call(this,n,we)}function ze(we){Ce.call(this,n,we)}function gt(we){ee[we?"unshift":"push"](()=>{T=we,t(7,T)})}const de=we=>{j.removeByKey(C,"id",we.detail.record.id),C.unshift(we.detail.record),t(8,C),H(we.detail.record)},ve=we=>{j.removeByKey(C,"id",we.detail.id),t(8,C),W(we.detail)};return n.$$set=we=>{e=Ie(Ie({},e),Yt(we)),t(19,d=Ge(e,c)),"value"in we&&t(20,g=we.value),"field"in we&&t(21,y=we.field)},n.$$.update=()=>{var we,Ye;n.$$.dirty[0]&2097152&&t(4,i=((we=y==null?void 0:y.options)==null?void 0:we.maxSelect)||null),n.$$.dirty[0]&2097152&&t(25,l=(Ye=y==null?void 0:y.options)==null?void 0:Ye.collectionId),n.$$.dirty[0]&100663296&&t(5,s=m.find(zt=>zt.id==l)||null),n.$$.dirty[0]&6&&typeof $<"u"&&S!=null&&S.isActive()&&q(!0),n.$$.dirty[0]&32&&t(11,o=(s==null?void 0:s.type)==="view"),n.$$.dirty[0]&16777224&&t(13,r=L||R),n.$$.dirty[0]&8388608&&t(12,a=I==no),n.$$.dirty[0]&80&&t(10,f=i===null||i>O.length),n.$$.dirty[0]&64&&t(9,u=function(zt){return j.findByKey(O,"id",zt.id)})},[N,S,$,L,i,s,O,T,C,u,f,o,a,r,q,H,W,G,U,d,g,y,F,I,R,l,m,Y,ie,te,pe,Ne,He,Xe,xe,Mt,ft,mt,Gt,De,Ae,ze,gt,de,ve]}class iM extends ge{constructor(e){super(),_e(this,e,nM,tM,me,{value:20,field:21,show:22,hide:0},null,[-1,-1])}get show(){return this.$$.ctx[22]}get hide(){return this.$$.ctx[0]}}function mm(n,e,t){const i=n.slice();return i[21]=e[t],i[23]=t,i}function hm(n,e,t){const i=n.slice();return i[26]=e[t],i}function _m(n){let e,t,i,l;return{c(){e=b("i"),p(e,"class","ri-error-warning-line link-hint m-l-auto flex-order-10")},m(s,o){w(s,e,o),i||(l=Se(t=Pe.call(null,e,{position:"left",text:"The following relation ids were removed from the list because they are missing or invalid: "+n[6].join(", ")})),i=!0)},p(s,o){t&&Ct(t.update)&&o&64&&t.update.call(null,{position:"left",text:"The following relation ids were removed from the list because they are missing or invalid: "+s[6].join(", ")})},d(s){s&&v(e),i=!1,l()}}}function gm(n){let e,t=n[5]&&bm(n);return{c(){t&&t.c(),e=ye()},m(i,l){t&&t.m(i,l),w(i,e,l)},p(i,l){i[5]?t?t.p(i,l):(t=bm(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){i&&v(e),t&&t.d(i)}}}function bm(n){let e,t=ue(j.toArray(n[0]).slice(0,10)),i=[];for(let l=0;l ',p(e,"class","list-item")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function lM(n){let e,t,i,l,s,o,r,a,f,u;i=new xo({props:{record:n[21]}});function c(){return n[11](n[21])}return{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),o=b("button"),o.innerHTML='',r=M(),p(t,"class","content"),p(o,"type","button"),p(o,"class","btn btn-transparent btn-hint btn-sm btn-circle btn-remove"),p(s,"class","actions"),p(e,"class","list-item"),x(e,"dragging",n[24]),x(e,"dragover",n[25])},m(d,m){w(d,e,m),k(e,t),z(i,t,null),k(e,l),k(e,s),k(s,o),w(d,r,m),a=!0,f||(u=[Se(Pe.call(null,o,"Remove")),J(o,"click",c)],f=!0)},p(d,m){n=d;const h={};m&16&&(h.record=n[21]),i.$set(h),(!a||m&16777216)&&x(e,"dragging",n[24]),(!a||m&33554432)&&x(e,"dragover",n[25])},i(d){a||(E(i.$$.fragment,d),a=!0)},o(d){A(i.$$.fragment,d),a=!1},d(d){d&&(v(e),v(r)),V(i),f=!1,$e(u)}}}function ym(n,e){let t,i,l,s;function o(a){e[12](a)}let r={group:e[2].name+"_relation",index:e[23],disabled:!e[7],$$slots:{default:[lM,({dragging:a,dragover:f})=>({24:a,25:f}),({dragging:a,dragover:f})=>(a?16777216:0)|(f?33554432:0)]},$$scope:{ctx:e}};return e[4]!==void 0&&(r.list=e[4]),i=new Ms({props:r}),ee.push(()=>be(i,"list",o)),i.$on("sort",e[13]),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f&4&&(u.group=e[2].name+"_relation"),f&16&&(u.index=e[23]),f&128&&(u.disabled=!e[7]),f&587202576&&(u.$$scope={dirty:f,ctx:e}),!l&&f&16&&(l=!0,u.list=e[4],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function sM(n){let e,t,i,l,s,o=n[2].name+"",r,a,f,u,c,d,m=[],h=new Map,_,g,y,S,T,$,C=n[6].length&&_m(n),O=ue(n[4]);const D=L=>L[21].id;for(let L=0;L Open picker',p(t,"class",i=Un(j.getFieldTypeIcon(n[2].type))+" svelte-1ynw0pc"),p(s,"class","txt"),p(e,"for",f=n[20]),p(d,"class","relations-list svelte-1ynw0pc"),p(y,"type","button"),p(y,"class","btn btn-transparent btn-sm btn-block"),p(g,"class","list-item list-item-btn"),p(c,"class","list")},m(L,R){w(L,e,R),k(e,t),k(e,l),k(e,s),k(s,r),k(e,a),C&&C.m(e,null),w(L,u,R),w(L,c,R),k(c,d);for(let F=0;F({20:r}),({uniqueId:r})=>r?1048576:0]},$$scope:{ctx:n}};e=new ce({props:s}),n[15](e);let o={value:n[0],field:n[2]};return i=new iM({props:o}),n[16](i),i.$on("save",n[17]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(r,a){z(e,r,a),w(r,t,a),z(i,r,a),l=!0},p(r,[a]){const f={};a&4&&(f.class="form-field form-field-list "+(r[2].required?"required":"")),a&4&&(f.name=r[2].name),a&537919735&&(f.$$scope={dirty:a,ctx:r}),e.$set(f);const u={};a&1&&(u.value=r[0]),a&4&&(u.field=r[2]),i.$set(u)},i(r){l||(E(e.$$.fragment,r),E(i.$$.fragment,r),l=!0)},o(r){A(e.$$.fragment,r),A(i.$$.fragment,r),l=!1},d(r){r&&v(t),n[15](null),V(e,r),n[16](null),V(i,r)}}}const vm=100;function rM(n,e,t){let i,{field:l}=e,{value:s}=e,{picker:o}=e,r,a=[],f=!1,u,c=[];function d(){if(f)return!1;const D=j.toArray(s);return t(4,a=a.filter(I=>D.includes(I.id))),D.length!=a.length}async function m(){var R,F;const D=j.toArray(s);if(t(4,a=[]),t(6,c=[]),!((R=l==null?void 0:l.options)!=null&&R.collectionId)||!D.length){t(5,f=!1);return}t(5,f=!0);const I=D.slice(),L=[];for(;I.length>0;){const N=[];for(const P of I.splice(0,vm))N.push(`id="${P}"`);L.push(ae.collection((F=l==null?void 0:l.options)==null?void 0:F.collectionId).getFullList(vm,{filter:N.join("||"),fields:"*:excerpt(200)",requestKey:null}))}try{let N=[];await Promise.all(L).then(P=>{N=N.concat(...P)});for(const P of D){const q=j.findByKey(N,"id",P);q?a.push(q):c.push(P)}t(4,a),_()}catch(N){ae.error(N)}t(5,f=!1)}function h(D){j.removeByKey(a,"id",D.id),t(4,a),_()}function _(){var D;i?t(0,s=a.map(I=>I.id)):t(0,s=((D=a[0])==null?void 0:D.id)||"")}ks(()=>{clearTimeout(u)});const g=D=>h(D);function y(D){a=D,t(4,a)}const S=()=>{_()},T=()=>o==null?void 0:o.show();function $(D){ee[D?"unshift":"push"](()=>{r=D,t(3,r)})}function C(D){ee[D?"unshift":"push"](()=>{o=D,t(1,o)})}const O=D=>{var I;t(4,a=D.detail||[]),t(0,s=i?a.map(L=>L.id):((I=a[0])==null?void 0:I.id)||"")};return n.$$set=D=>{"field"in D&&t(2,l=D.field),"value"in D&&t(0,s=D.value),"picker"in D&&t(1,o=D.picker)},n.$$.update=()=>{var D;n.$$.dirty&4&&t(7,i=((D=l.options)==null?void 0:D.maxSelect)!=1),n.$$.dirty&9&&typeof s<"u"&&(r==null||r.changed()),n.$$.dirty&1041&&d()&&(t(5,f=!0),clearTimeout(u),t(10,u=setTimeout(m,0)))},[s,o,l,r,a,f,c,i,h,_,u,g,y,S,T,$,C,O]}class aM extends ge{constructor(e){super(),_e(this,e,rM,oM,me,{field:2,value:0,picker:1})}}function fM(n){let e;return{c(){e=b("textarea"),p(e,"id",n[0]),b0(e,"visibility","hidden")},m(t,i){w(t,e,i),n[15](e)},p(t,i){i&1&&p(e,"id",t[0])},d(t){t&&v(e),n[15](null)}}}function uM(n){let e;return{c(){e=b("div"),p(e,"id",n[0])},m(t,i){w(t,e,i),n[14](e)},p(t,i){i&1&&p(e,"id",t[0])},d(t){t&&v(e),n[14](null)}}}function cM(n){let e;function t(s,o){return s[1]?uM:fM}let i=t(n),l=i(n);return{c(){e=b("div"),l.c(),p(e,"class",n[2])},m(s,o){w(s,e,o),l.m(e,null),n[16](e)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e,null))),o&4&&p(e,"class",s[2])},i:Q,o:Q,d(s){s&&v(e),l.d(),n[16](null)}}}function dM(){let n={listeners:[],scriptLoaded:!1,injected:!1};function e(i,l,s){n.injected=!0;const o=i.createElement("script");o.referrerPolicy="origin",o.type="application/javascript",o.src=l,o.onload=()=>{s()},i.head&&i.head.appendChild(o)}function t(i,l,s){n.scriptLoaded?s():(n.listeners.push(s),n.injected||e(i,l,()=>{n.listeners.forEach(o=>o()),n.scriptLoaded=!0}))}return{load:t}}let pM=dM();function Ir(){return window&&window.tinymce?window.tinymce:null}function mM(n,e,t){let{id:i="tinymce_svelte"+j.randomString(7)}=e,{inline:l=void 0}=e,{disabled:s=!1}=e,{scriptSrc:o="./libs/tinymce/tinymce.min.js"}=e,{conf:r={}}=e,{modelEvents:a="change input undo redo"}=e,{value:f=""}=e,{text:u=""}=e,{cssClass:c="tinymce-wrapper"}=e;const d=["Activate","AddUndo","BeforeAddUndo","BeforeExecCommand","BeforeGetContent","BeforeRenderUI","BeforeSetContent","BeforePaste","Blur","Change","ClearUndos","Click","ContextMenu","Copy","Cut","Dblclick","Deactivate","Dirty","Drag","DragDrop","DragEnd","DragGesture","DragOver","Drop","ExecCommand","Focus","FocusIn","FocusOut","GetContent","Hide","Init","KeyDown","KeyPress","KeyUp","LoadContent","MouseDown","MouseEnter","MouseLeave","MouseMove","MouseOut","MouseOver","MouseUp","NodeChange","ObjectResizeStart","ObjectResized","ObjectSelected","Paste","PostProcess","PostRender","PreProcess","ProgressState","Redo","Remove","Reset","ResizeEditor","SaveContent","SelectionChange","SetAttrib","SetContent","Show","Submit","Undo","VisualAid"],m=(I,L)=>{d.forEach(R=>{I.on(R,F=>{L(R.toLowerCase(),{eventName:R,event:F,editor:I})})})};let h,_,g,y=f,S=s;const T=lt();function $(){const I={...r,target:_,inline:l!==void 0?l:r.inline!==void 0?r.inline:!1,readonly:s,setup:L=>{t(11,g=L),L.on("init",()=>{L.setContent(f),L.on(a,()=>{t(12,y=L.getContent()),y!==f&&(t(5,f=y),t(6,u=L.getContent({format:"text"})))})}),m(L,T),typeof r.setup=="function"&&r.setup(L)}};t(4,_.style.visibility="",_),Ir().init(I)}Ht(()=>(Ir()!==null?$():pM.load(h.ownerDocument,o,()=>{h&&$()}),()=>{var I,L;try{g&&((I=g.dom)==null||I.unbind(document),(L=Ir())==null||L.remove(g))}catch{}}));function C(I){ee[I?"unshift":"push"](()=>{_=I,t(4,_)})}function O(I){ee[I?"unshift":"push"](()=>{_=I,t(4,_)})}function D(I){ee[I?"unshift":"push"](()=>{h=I,t(3,h)})}return n.$$set=I=>{"id"in I&&t(0,i=I.id),"inline"in I&&t(1,l=I.inline),"disabled"in I&&t(7,s=I.disabled),"scriptSrc"in I&&t(8,o=I.scriptSrc),"conf"in I&&t(9,r=I.conf),"modelEvents"in I&&t(10,a=I.modelEvents),"value"in I&&t(5,f=I.value),"text"in I&&t(6,u=I.text),"cssClass"in I&&t(2,c=I.cssClass)},n.$$.update=()=>{var I;if(n.$$.dirty&14496)try{g&&y!==f&&(g.setContent(f),t(6,u=g.getContent({format:"text"}))),g&&s!==S&&(t(13,S=s),typeof((I=g.mode)==null?void 0:I.set)=="function"?g.mode.set(s?"readonly":"design"):g.setMode(s?"readonly":"design"))}catch(L){console.warn("TinyMCE reactive error:",L)}},[i,l,c,h,_,f,u,s,o,r,a,g,y,S,C,O,D]}class Wa extends ge{constructor(e){super(),_e(this,e,mM,cM,me,{id:0,inline:1,disabled:7,scriptSrc:8,conf:9,modelEvents:10,value:5,text:6,cssClass:2})}}function wm(n,e,t){const i=n.slice();i[44]=e[t];const l=i[19](i[44]);return i[45]=l,i}function Sm(n,e,t){const i=n.slice();return i[48]=e[t],i}function $m(n,e,t){const i=n.slice();return i[51]=e[t],i}function hM(n){let e,t,i=[],l=new Map,s,o,r,a,f,u,c,d,m,h,_,g=ue(n[7]);const y=S=>S[51].id;for(let S=0;SNew record',c=M(),B(d.$$.fragment),p(t,"class","file-picker-sidebar"),p(u,"type","button"),p(u,"class","btn btn-pill btn-transparent btn-hint p-l-xs p-r-xs"),p(r,"class","flex m-b-base flex-gap-10"),p(o,"class","file-picker-content"),p(e,"class","file-picker")},m(S,T){w(S,e,T),k(e,t);for(let $=0;$file field.",p(e,"class","txt-center txt-hint")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Tm(n,e){let t,i=e[51].name+"",l,s,o,r;function a(){return e[29](e[51])}return{key:n,first:null,c(){var f;t=b("button"),l=K(i),s=M(),p(t,"type","button"),p(t,"class","sidebar-item"),x(t,"active",((f=e[8])==null?void 0:f.id)==e[51].id),this.first=t},m(f,u){w(f,t,u),k(t,l),k(t,s),o||(r=J(t,"click",Be(a)),o=!0)},p(f,u){var c;e=f,u[0]&128&&i!==(i=e[51].name+"")&&oe(l,i),u[0]&384&&x(t,"active",((c=e[8])==null?void 0:c.id)==e[51].id)},d(f){f&&v(t),o=!1,r()}}}function gM(n){var s;let e,t,i,l=((s=n[4])==null?void 0:s.length)&&Cm(n);return{c(){e=b("div"),t=b("span"),t.textContent="No records with images found.",i=M(),l&&l.c(),p(t,"class","txt txt-hint"),p(e,"class","inline-flex")},m(o,r){w(o,e,r),k(e,t),k(e,i),l&&l.m(e,null)},p(o,r){var a;(a=o[4])!=null&&a.length?l?l.p(o,r):(l=Cm(o),l.c(),l.m(e,null)):l&&(l.d(1),l=null)},d(o){o&&v(e),l&&l.d()}}}function bM(n){let e=[],t=new Map,i,l=ue(n[5]);const s=o=>o[44].id;for(let o=0;oClear filter',p(e,"type","button"),p(e,"class","btn btn-hint btn-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",Be(n[17])),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function kM(n){let e;return{c(){e=b("i"),p(e,"class","ri-file-3-line")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function yM(n){let e,t,i;return{c(){e=b("img"),p(e,"loading","lazy"),en(e.src,t=ae.files.getUrl(n[44],n[48],{thumb:"100x100"}))||p(e,"src",t),p(e,"alt",i=n[48])},m(l,s){w(l,e,s)},p(l,s){s[0]&32&&!en(e.src,t=ae.files.getUrl(l[44],l[48],{thumb:"100x100"}))&&p(e,"src",t),s[0]&32&&i!==(i=l[48])&&p(e,"alt",i)},d(l){l&&v(e)}}}function Om(n){let e,t,i,l,s,o;function r(u,c){return c[0]&32&&(t=null),t==null&&(t=!!j.hasImageExtension(u[48])),t?yM:kM}let a=r(n,[-1,-1]),f=a(n);return{c(){e=b("button"),f.c(),i=M(),p(e,"type","button"),p(e,"class","thumb handle"),x(e,"thumb-warning",n[16](n[44],n[48]))},m(u,c){w(u,e,c),f.m(e,null),k(e,i),s||(o=[Se(l=Pe.call(null,e,n[48]+` -(record: `+n[44].id+")")),J(e,"click",Be(function(){Ct(n[20](n[44],n[48]))&&n[20](n[44],n[48]).apply(this,arguments)}))],s=!0)},p(u,c){n=u,a===(a=r(n,c))&&f?f.p(n,c):(f.d(1),f=a(n),f&&(f.c(),f.m(e,i))),l&&Ct(l.update)&&c[0]&32&&l.update.call(null,n[48]+` -(record: `+n[44].id+")"),c[0]&589856&&x(e,"thumb-warning",n[16](n[44],n[48]))},d(u){u&&v(e),f.d(),s=!1,$e(o)}}}function Mm(n,e){let t,i,l=ue(e[45]),s=[];for(let o=0;o',p(e,"class","block txt-center")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function vM(n){let e,t;function i(r,a){if(r[15])return bM;if(!r[6])return gM}let l=i(n),s=l&&l(n),o=n[6]&&Dm();return{c(){s&&s.c(),e=M(),o&&o.c(),t=ye()},m(r,a){s&&s.m(r,a),w(r,e,a),o&&o.m(r,a),w(r,t,a)},p(r,a){l===(l=i(r))&&s?s.p(r,a):(s&&s.d(1),s=l&&l(r),s&&(s.c(),s.m(e.parentNode,e))),r[6]?o||(o=Dm(),o.c(),o.m(t.parentNode,t)):o&&(o.d(1),o=null)},d(r){r&&(v(e),v(t)),s&&s.d(r),o&&o.d(r)}}}function wM(n){let e,t,i,l;const s=[_M,hM],o=[];function r(a,f){return a[7].length?1:0}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,f){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function SM(n){let e,t;return{c(){e=b("h4"),t=K(n[0])},m(i,l){w(i,e,l),k(e,t)},p(i,l){l[0]&1&&oe(t,i[0])},d(i){i&&v(e)}}}function Em(n){let e,t;return e=new ce({props:{class:"form-field file-picker-size-select",$$slots:{default:[$M,({uniqueId:i})=>({23:i}),({uniqueId:i})=>[i?8388608:0]]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[0]&8402944|l[1]&8388608&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function $M(n){let e,t,i;function l(o){n[28](o)}let s={upside:!0,id:n[23],items:n[11],disabled:!n[13],selectPlaceholder:"Select size"};return n[12]!==void 0&&(s.keyOfSelected=n[12]),e=new hi({props:s}),ee.push(()=>be(e,"keyOfSelected",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[0]&8388608&&(a.id=o[23]),r[0]&2048&&(a.items=o[11]),r[0]&8192&&(a.disabled=!o[13]),!t&&r[0]&4096&&(t=!0,a.keyOfSelected=o[12],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function TM(n){var h;let e,t,i,l=j.hasImageExtension((h=n[9])==null?void 0:h.name),s,o,r,a,f,u,c,d,m=l&&Em(n);return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=M(),m&&m.c(),s=M(),o=b("button"),r=b("span"),a=K(n[1]),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent m-r-auto"),e.disabled=n[6],p(r,"class","txt"),p(o,"type","button"),p(o,"class","btn btn-expanded"),o.disabled=f=!n[13]},m(_,g){w(_,e,g),k(e,t),w(_,i,g),m&&m.m(_,g),w(_,s,g),w(_,o,g),k(o,r),k(r,a),u=!0,c||(d=[J(e,"click",n[2]),J(o,"click",n[21])],c=!0)},p(_,g){var y;(!u||g[0]&64)&&(e.disabled=_[6]),g[0]&512&&(l=j.hasImageExtension((y=_[9])==null?void 0:y.name)),l?m?(m.p(_,g),g[0]&512&&E(m,1)):(m=Em(_),m.c(),E(m,1),m.m(s.parentNode,s)):m&&(le(),A(m,1,1,()=>{m=null}),se()),(!u||g[0]&2)&&oe(a,_[1]),(!u||g[0]&8192&&f!==(f=!_[13]))&&(o.disabled=f)},i(_){u||(E(m),u=!0)},o(_){A(m),u=!1},d(_){_&&(v(e),v(i),v(s),v(o)),m&&m.d(_),c=!1,$e(d)}}}function CM(n){let e,t,i,l;const s=[{popup:!0},{class:"file-picker-popup"},n[22]];let o={$$slots:{footer:[TM],header:[SM],default:[wM]},$$scope:{ctx:n}};for(let a=0;at(27,f=Ae));const u=lt(),c="file_picker_"+j.randomString(5);let{title:d="Select a file"}=e,{submitText:m="Insert"}=e,{fileTypes:h=["image","document","video","audio","file"]}=e,_,g,y="",S=[],T=1,$=0,C=!1,O=[],D=[],I=[],L={},R={},F="";function N(){return W(!0),_==null?void 0:_.show()}function P(){return _==null?void 0:_.hide()}function q(){t(5,S=[]),t(9,R={}),t(12,F="")}function H(){t(4,y="")}async function W(Ae=!1){if(L!=null&&L.id){t(6,C=!0),Ae&&q();try{const ze=Ae?1:T+1,gt=j.getAllCollectionIdentifiers(L);let de=j.normalizeSearchFilter(y,gt)||"";de&&(de+=" && "),de+="("+D.map(we=>`${we.name}:length>0`).join("||")+")";const ve=await ae.collection(L.id).getList(ze,Im,{filter:de,sort:"-created",fields:"*:excerpt(100)",skipTotal:1,requestKey:c+"loadImagePicker"});t(5,S=j.filterDuplicatesByKey(S.concat(ve.items))),T=ve.page,t(26,$=ve.items.length),t(6,C=!1)}catch(ze){ze.isAbort||(ae.error(ze),t(6,C=!1))}}}function G(){var ze,gt;let Ae=["100x100"];if((ze=R==null?void 0:R.record)!=null&&ze.id){for(const de of D)if(j.toArray(R.record[de.name]).includes(R.name)){Ae=Ae.concat(j.toArray((gt=de.options)==null?void 0:gt.thumbs));break}}t(11,I=[{label:"Original size",value:""}]);for(const de of Ae)I.push({label:`${de} thumb`,value:de});F&&!Ae.includes(F)&&t(12,F="")}function U(Ae){let ze=[];for(const gt of D){const de=j.toArray(Ae[gt.name]);for(const ve of de)h.includes(j.getFileType(ve))&&ze.push(ve)}return ze}function Y(Ae,ze){t(9,R={record:Ae,name:ze})}function ie(){o&&(u("submit",Object.assign({size:F},R)),P())}function te(Ae){F=Ae,t(12,F)}const pe=Ae=>{t(8,L=Ae)},Ne=Ae=>t(4,y=Ae.detail),He=()=>g==null?void 0:g.show(),Xe=()=>{s&&W()};function xe(Ae){ee[Ae?"unshift":"push"](()=>{_=Ae,t(3,_)})}function Mt(Ae){Ce.call(this,n,Ae)}function ft(Ae){Ce.call(this,n,Ae)}function mt(Ae){ee[Ae?"unshift":"push"](()=>{g=Ae,t(10,g)})}const Gt=Ae=>{j.removeByKey(S,"id",Ae.detail.record.id),S.unshift(Ae.detail.record),t(5,S);const ze=U(Ae.detail.record);ze.length>0&&Y(Ae.detail.record,ze[0])},De=Ae=>{var ze;((ze=R==null?void 0:R.record)==null?void 0:ze.id)==Ae.detail.id&&t(9,R={}),j.removeByKey(S,"id",Ae.detail.id),t(5,S)};return n.$$set=Ae=>{e=Ie(Ie({},e),Yt(Ae)),t(22,a=Ge(e,r)),"title"in Ae&&t(0,d=Ae.title),"submitText"in Ae&&t(1,m=Ae.submitText),"fileTypes"in Ae&&t(24,h=Ae.fileTypes)},n.$$.update=()=>{var Ae;n.$$.dirty[0]&134217728&&t(7,O=f.filter(ze=>ze.type!=="view"&&!!j.toArray(ze.schema).find(gt=>{var de,ve,we,Ye,zt;return gt.type==="file"&&!((de=gt.options)!=null&&de.protected)&&(!((we=(ve=gt.options)==null?void 0:ve.mimeTypes)!=null&&we.length)||!!((zt=(Ye=gt.options)==null?void 0:Ye.mimeTypes)!=null&&zt.find(cn=>cn.startsWith("image/"))))}))),n.$$.dirty[0]&384&&!(L!=null&&L.id)&&O.length>0&&t(8,L=O[0]),n.$$.dirty[0]&256&&(D=(Ae=L==null?void 0:L.schema)==null?void 0:Ae.filter(ze=>{var gt;return ze.type==="file"&&!((gt=ze.options)!=null&>.protected)})),n.$$.dirty[0]&256&&L!=null&&L.id&&(H(),G()),n.$$.dirty[0]&512&&R!=null&&R.name&&G(),n.$$.dirty[0]&280&&typeof y<"u"&&L!=null&&L.id&&_!=null&&_.isActive()&&W(!0),n.$$.dirty[0]&512&&t(16,i=(ze,gt)=>{var de;return(R==null?void 0:R.name)==gt&&((de=R==null?void 0:R.record)==null?void 0:de.id)==ze.id}),n.$$.dirty[0]&32&&t(15,l=S.find(ze=>U(ze).length>0)),n.$$.dirty[0]&67108928&&t(14,s=!C&&$==Im),n.$$.dirty[0]&576&&t(13,o=!C&&!!(R!=null&&R.name))},[d,m,P,_,y,S,C,O,L,R,g,I,F,o,s,l,i,H,W,U,Y,ie,a,c,h,N,$,f,te,pe,Ne,He,Xe,xe,Mt,ft,mt,Gt,De]}class MM extends ge{constructor(e){super(),_e(this,e,OM,CM,me,{title:0,submitText:1,fileTypes:24,show:25,hide:2},null,[-1,-1])}get show(){return this.$$.ctx[25]}get hide(){return this.$$.ctx[2]}}function DM(n){let e;return{c(){e=b("div"),p(e,"class","tinymce-wrapper")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function EM(n){let e,t,i;function l(o){n[6](o)}let s={id:n[11],conf:n[5]};return n[0]!==void 0&&(s.value=n[0]),e=new Wa({props:s}),ee.push(()=>be(e,"value",l)),e.$on("init",n[7]),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r&2048&&(a.id=o[11]),r&32&&(a.conf=o[5]),!t&&r&1&&(t=!0,a.value=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function IM(n){let e,t,i,l,s,o=n[1].name+"",r,a,f,u,c,d,m;const h=[EM,DM],_=[];function g(y,S){return y[4]?0:1}return u=g(n),c=_[u]=h[u](n),{c(){e=b("label"),t=b("i"),l=M(),s=b("span"),r=K(o),f=M(),c.c(),d=ye(),p(t,"class",i=j.getFieldTypeIcon(n[1].type)),p(s,"class","txt"),p(e,"for",a=n[11])},m(y,S){w(y,e,S),k(e,t),k(e,l),k(e,s),k(s,r),w(y,f,S),_[u].m(y,S),w(y,d,S),m=!0},p(y,S){(!m||S&2&&i!==(i=j.getFieldTypeIcon(y[1].type)))&&p(t,"class",i),(!m||S&2)&&o!==(o=y[1].name+"")&&oe(r,o),(!m||S&2048&&a!==(a=y[11]))&&p(e,"for",a);let T=u;u=g(y),u===T?_[u].p(y,S):(le(),A(_[T],1,1,()=>{_[T]=null}),se(),c=_[u],c?c.p(y,S):(c=_[u]=h[u](y),c.c()),E(c,1),c.m(d.parentNode,d))},i(y){m||(E(c),m=!0)},o(y){A(c),m=!1},d(y){y&&(v(e),v(f),v(d)),_[u].d(y)}}}function AM(n){let e,t,i,l;e=new ce({props:{class:"form-field form-field-editor "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[IM,({uniqueId:o})=>({11:o}),({uniqueId:o})=>o?2048:0]},$$scope:{ctx:n}}});let s={title:"Select an image",fileTypes:["image"]};return i=new MM({props:s}),n[8](i),i.$on("submit",n[9]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(o,r){z(e,o,r),w(o,t,r),z(i,o,r),l=!0},p(o,[r]){const a={};r&2&&(a.class="form-field form-field-editor "+(o[1].required?"required":"")),r&2&&(a.name=o[1].name),r&6207&&(a.$$scope={dirty:r,ctx:o}),e.$set(a);const f={};i.$set(f)},i(o){l||(E(e.$$.fragment,o),E(i.$$.fragment,o),l=!0)},o(o){A(e.$$.fragment,o),A(i.$$.fragment,o),l=!1},d(o){o&&v(t),V(e,o),n[8](null),V(i,o)}}}function LM(n,e,t){let i,{field:l}=e,{value:s=""}=e,o,r,a=!1,f=null;Ht(async()=>(typeof s>"u"&&t(0,s=""),f=setTimeout(()=>{t(4,a=!0)},100),()=>{clearTimeout(f)}));function u(h){s=h,t(0,s)}const c=h=>{t(3,r=h.detail.editor),r.on("collections_file_picker",()=>{o==null||o.show()})};function d(h){ee[h?"unshift":"push"](()=>{o=h,t(2,o)})}const m=h=>{r==null||r.execCommand("InsertImage",!1,ae.files.getUrl(h.detail.record,h.detail.name,{thumb:h.detail.size}))};return n.$$set=h=>{"field"in h&&t(1,l=h.field),"value"in h&&t(0,s=h.value)},n.$$.update=()=>{var h;n.$$.dirty&2&&t(5,i=Object.assign(j.defaultEditorOptions(),{convert_urls:(h=l.options)==null?void 0:h.convertUrls,relative_urls:!1})),n.$$.dirty&1&&typeof s>"u"&&t(0,s="")},[s,l,o,r,a,i,u,c,d,m]}class NM extends ge{constructor(e){super(),_e(this,e,LM,AM,me,{field:1,value:0})}}function PM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Auth URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].authUrl),r||(a=J(s,"input",n[5]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&8&&(s.required=f[3]),u&1&&s.value!==f[0].authUrl&&re(s,f[0].authUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function FM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Token URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].tokenUrl),r||(a=J(s,"input",n[6]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&8&&(s.required=f[3]),u&1&&s.value!==f[0].tokenUrl&&re(s,f[0].tokenUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function RM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("User API URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[3]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].userApiUrl),r||(a=J(s,"input",n[7]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&8&&(s.required=f[3]),u&1&&s.value!==f[0].userApiUrl&&re(s,f[0].userApiUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function qM(n){let e,t,i,l,s,o,r,a,f;return l=new ce({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".authUrl",$$slots:{default:[PM,({uniqueId:u})=>({8:u}),({uniqueId:u})=>u?256:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".tokenUrl",$$slots:{default:[FM,({uniqueId:u})=>({8:u}),({uniqueId:u})=>u?256:0]},$$scope:{ctx:n}}}),a=new ce({props:{class:"form-field "+(n[3]?"required":""),name:n[1]+".userApiUrl",$$slots:{default:[RM,({uniqueId:u})=>({8:u}),({uniqueId:u})=>u?256:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=K(n[2]),i=M(),B(l.$$.fragment),s=M(),B(o.$$.fragment),r=M(),B(a.$$.fragment),p(e,"class","section-title")},m(u,c){w(u,e,c),k(e,t),w(u,i,c),z(l,u,c),w(u,s,c),z(o,u,c),w(u,r,c),z(a,u,c),f=!0},p(u,[c]){(!f||c&4)&&oe(t,u[2]);const d={};c&8&&(d.class="form-field "+(u[3]?"required":"")),c&2&&(d.name=u[1]+".authUrl"),c&777&&(d.$$scope={dirty:c,ctx:u}),l.$set(d);const m={};c&8&&(m.class="form-field "+(u[3]?"required":"")),c&2&&(m.name=u[1]+".tokenUrl"),c&777&&(m.$$scope={dirty:c,ctx:u}),o.$set(m);const h={};c&8&&(h.class="form-field "+(u[3]?"required":"")),c&2&&(h.name=u[1]+".userApiUrl"),c&777&&(h.$$scope={dirty:c,ctx:u}),a.$set(h)},i(u){f||(E(l.$$.fragment,u),E(o.$$.fragment,u),E(a.$$.fragment,u),f=!0)},o(u){A(l.$$.fragment,u),A(o.$$.fragment,u),A(a.$$.fragment,u),f=!1},d(u){u&&(v(e),v(i),v(s),v(r)),V(l,u),V(o,u),V(a,u)}}}function jM(n,e,t){let i,{key:l=""}=e,{config:s={}}=e,{required:o=!1}=e,{title:r="Provider endpoints"}=e;function a(){s.authUrl=this.value,t(0,s)}function f(){s.tokenUrl=this.value,t(0,s)}function u(){s.userApiUrl=this.value,t(0,s)}return n.$$set=c=>{"key"in c&&t(1,l=c.key),"config"in c&&t(0,s=c.config),"required"in c&&t(4,o=c.required),"title"in c&&t(2,r=c.title)},n.$$.update=()=>{n.$$.dirty&17&&t(3,i=o&&(s==null?void 0:s.enabled))},[s,l,r,i,o,a,f,u]}class Ar extends ge{constructor(e){super(),_e(this,e,jM,qM,me,{key:1,config:0,required:4,title:2})}}function HM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Display name"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","text"),p(s,"id",o=n[8]),s.required=n[2]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].displayName),r||(a=J(s,"input",n[3]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&4&&(s.required=f[2]),u&1&&s.value!==f[0].displayName&&re(s,f[0].displayName)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function zM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Auth URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[2]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].authUrl),r||(a=J(s,"input",n[4]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&4&&(s.required=f[2]),u&1&&s.value!==f[0].authUrl&&re(s,f[0].authUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function VM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Token URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[2]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].tokenUrl),r||(a=J(s,"input",n[5]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&4&&(s.required=f[2]),u&1&&s.value!==f[0].tokenUrl&&re(s,f[0].tokenUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function BM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("User API URL"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","url"),p(s,"id",o=n[8]),s.required=n[2]},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].userApiUrl),r||(a=J(s,"input",n[6]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&4&&(s.required=f[2]),u&1&&s.value!==f[0].userApiUrl&&re(s,f[0].userApiUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function UM(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Support PKCE",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[8]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[8])},m(c,d){w(c,e,d),e.checked=n[0].pkce,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[7]),Se(Pe.call(null,r,{text:"Usually it should be safe to be always enabled as most providers will just ignore the extra query parameters if they don't support PKCE.",position:"right"}))],f=!0)},p(c,d){d&256&&t!==(t=c[8])&&p(e,"id",t),d&1&&(e.checked=c[0].pkce),d&256&&a!==(a=c[8])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function WM(n){let e,t,i,l,s,o,r,a,f,u,c,d;return e=new ce({props:{class:"form-field "+(n[2]?"required":""),name:n[1]+".displayName",$$slots:{default:[HM,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),s=new ce({props:{class:"form-field "+(n[2]?"required":""),name:n[1]+".authUrl",$$slots:{default:[zM,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field "+(n[2]?"required":""),name:n[1]+".tokenUrl",$$slots:{default:[VM,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),f=new ce({props:{class:"form-field "+(n[2]?"required":""),name:n[1]+".userApiUrl",$$slots:{default:[BM,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),c=new ce({props:{class:"form-field",name:n[1]+".pkce",$$slots:{default:[UM,({uniqueId:m})=>({8:m}),({uniqueId:m})=>m?256:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),i=b("div"),i.textContent="Endpoints",l=M(),B(s.$$.fragment),o=M(),B(r.$$.fragment),a=M(),B(f.$$.fragment),u=M(),B(c.$$.fragment),p(i,"class","section-title")},m(m,h){z(e,m,h),w(m,t,h),w(m,i,h),w(m,l,h),z(s,m,h),w(m,o,h),z(r,m,h),w(m,a,h),z(f,m,h),w(m,u,h),z(c,m,h),d=!0},p(m,[h]){const _={};h&4&&(_.class="form-field "+(m[2]?"required":"")),h&2&&(_.name=m[1]+".displayName"),h&773&&(_.$$scope={dirty:h,ctx:m}),e.$set(_);const g={};h&4&&(g.class="form-field "+(m[2]?"required":"")),h&2&&(g.name=m[1]+".authUrl"),h&773&&(g.$$scope={dirty:h,ctx:m}),s.$set(g);const y={};h&4&&(y.class="form-field "+(m[2]?"required":"")),h&2&&(y.name=m[1]+".tokenUrl"),h&773&&(y.$$scope={dirty:h,ctx:m}),r.$set(y);const S={};h&4&&(S.class="form-field "+(m[2]?"required":"")),h&2&&(S.name=m[1]+".userApiUrl"),h&773&&(S.$$scope={dirty:h,ctx:m}),f.$set(S);const T={};h&2&&(T.name=m[1]+".pkce"),h&769&&(T.$$scope={dirty:h,ctx:m}),c.$set(T)},i(m){d||(E(e.$$.fragment,m),E(s.$$.fragment,m),E(r.$$.fragment,m),E(f.$$.fragment,m),E(c.$$.fragment,m),d=!0)},o(m){A(e.$$.fragment,m),A(s.$$.fragment,m),A(r.$$.fragment,m),A(f.$$.fragment,m),A(c.$$.fragment,m),d=!1},d(m){m&&(v(t),v(i),v(l),v(o),v(a),v(u)),V(e,m),V(s,m),V(r,m),V(f,m),V(c,m)}}}function YM(n,e,t){let i,{key:l=""}=e,{config:s={}}=e;j.isEmpty(s.pkce)&&(s.pkce=!0),s.displayName||(s.displayName="OIDC");function o(){s.displayName=this.value,t(0,s)}function r(){s.authUrl=this.value,t(0,s)}function a(){s.tokenUrl=this.value,t(0,s)}function f(){s.userApiUrl=this.value,t(0,s)}function u(){s.pkce=this.checked,t(0,s)}return n.$$set=c=>{"key"in c&&t(1,l=c.key),"config"in c&&t(0,s=c.config)},n.$$.update=()=>{n.$$.dirty&1&&t(2,i=!!s.enabled)},[s,l,i,o,r,a,f,u]}class Lr extends ge{constructor(e){super(),_e(this,e,YM,WM,me,{key:1,config:0})}}function KM(n){let e,t,i,l,s,o,r,a,f,u,c;return{c(){e=b("label"),t=K("Auth URL"),l=M(),s=b("input"),a=M(),f=b("div"),f.textContent="Eg. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize",p(e,"for",i=n[4]),p(s,"type","url"),p(s,"id",o=n[4]),s.required=r=n[0].enabled,p(f,"class","help-block")},m(d,m){w(d,e,m),k(e,t),w(d,l,m),w(d,s,m),re(s,n[0].authUrl),w(d,a,m),w(d,f,m),u||(c=J(s,"input",n[2]),u=!0)},p(d,m){m&16&&i!==(i=d[4])&&p(e,"for",i),m&16&&o!==(o=d[4])&&p(s,"id",o),m&1&&r!==(r=d[0].enabled)&&(s.required=r),m&1&&s.value!==d[0].authUrl&&re(s,d[0].authUrl)},d(d){d&&(v(e),v(l),v(s),v(a),v(f)),u=!1,c()}}}function JM(n){let e,t,i,l,s,o,r,a,f,u,c;return{c(){e=b("label"),t=K("Token URL"),l=M(),s=b("input"),a=M(),f=b("div"),f.textContent="Eg. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token",p(e,"for",i=n[4]),p(s,"type","url"),p(s,"id",o=n[4]),s.required=r=n[0].enabled,p(f,"class","help-block")},m(d,m){w(d,e,m),k(e,t),w(d,l,m),w(d,s,m),re(s,n[0].tokenUrl),w(d,a,m),w(d,f,m),u||(c=J(s,"input",n[3]),u=!0)},p(d,m){m&16&&i!==(i=d[4])&&p(e,"for",i),m&16&&o!==(o=d[4])&&p(s,"id",o),m&1&&r!==(r=d[0].enabled)&&(s.required=r),m&1&&s.value!==d[0].tokenUrl&&re(s,d[0].tokenUrl)},d(d){d&&(v(e),v(l),v(s),v(a),v(f)),u=!1,c()}}}function ZM(n){let e,t,i,l,s,o;return i=new ce({props:{class:"form-field "+(n[0].enabled?"required":""),name:n[1]+".authUrl",$$slots:{default:[KM,({uniqueId:r})=>({4:r}),({uniqueId:r})=>r?16:0]},$$scope:{ctx:n}}}),s=new ce({props:{class:"form-field "+(n[0].enabled?"required":""),name:n[1]+".tokenUrl",$$slots:{default:[JM,({uniqueId:r})=>({4:r}),({uniqueId:r})=>r?16:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.textContent="Azure AD endpoints",t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment),p(e,"class","section-title")},m(r,a){w(r,e,a),w(r,t,a),z(i,r,a),w(r,l,a),z(s,r,a),o=!0},p(r,[a]){const f={};a&1&&(f.class="form-field "+(r[0].enabled?"required":"")),a&2&&(f.name=r[1]+".authUrl"),a&49&&(f.$$scope={dirty:a,ctx:r}),i.$set(f);const u={};a&1&&(u.class="form-field "+(r[0].enabled?"required":"")),a&2&&(u.name=r[1]+".tokenUrl"),a&49&&(u.$$scope={dirty:a,ctx:r}),s.$set(u)},i(r){o||(E(i.$$.fragment,r),E(s.$$.fragment,r),o=!0)},o(r){A(i.$$.fragment,r),A(s.$$.fragment,r),o=!1},d(r){r&&(v(e),v(t),v(l)),V(i,r),V(s,r)}}}function GM(n,e,t){let{key:i=""}=e,{config:l={}}=e;function s(){l.authUrl=this.value,t(0,l)}function o(){l.tokenUrl=this.value,t(0,l)}return n.$$set=r=>{"key"in r&&t(1,i=r.key),"config"in r&&t(0,l=r.config)},[l,i,s,o]}class XM extends ge{constructor(e){super(),_e(this,e,GM,ZM,me,{key:1,config:0})}}function QM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Client ID"),l=M(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[2]),r||(a=J(s,"input",n[12]),r=!0)},p(f,u){u&8388608&&i!==(i=f[23])&&p(e,"for",i),u&8388608&&o!==(o=f[23])&&p(s,"id",o),u&4&&s.value!==f[2]&&re(s,f[2])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function xM(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Team ID"),l=M(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[3]),r||(a=J(s,"input",n[13]),r=!0)},p(f,u){u&8388608&&i!==(i=f[23])&&p(e,"for",i),u&8388608&&o!==(o=f[23])&&p(s,"id",o),u&8&&s.value!==f[3]&&re(s,f[3])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function e8(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Key ID"),l=M(),s=b("input"),p(e,"for",i=n[23]),p(s,"type","text"),p(s,"id",o=n[23]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[4]),r||(a=J(s,"input",n[14]),r=!0)},p(f,u){u&8388608&&i!==(i=f[23])&&p(e,"for",i),u&8388608&&o!==(o=f[23])&&p(s,"id",o),u&16&&s.value!==f[4]&&re(s,f[4])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function t8(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=b("span"),t.textContent="Duration (in seconds)",i=M(),l=b("i"),o=M(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[23]),p(r,"type","text"),p(r,"id",a=n[23]),p(r,"max",_o),r.required=!0},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,o,d),w(c,r,d),re(r,n[6]),f||(u=[Se(Pe.call(null,l,{text:`Max ${_o} seconds (~${_o/(60*60*24*30)<<0} months).`,position:"top"})),J(r,"input",n[15])],f=!0)},p(c,d){d&8388608&&s!==(s=c[23])&&p(e,"for",s),d&8388608&&a!==(a=c[23])&&p(r,"id",a),d&64&&r.value!==c[6]&&re(r,c[6])},d(c){c&&(v(e),v(o),v(r)),f=!1,$e(u)}}}function n8(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=K("Private key"),l=M(),s=b("textarea"),r=M(),a=b("div"),a.textContent="The key is not stored on the server and it is used only for generating the signed JWT.",p(e,"for",i=n[23]),p(s,"id",o=n[23]),s.required=!0,p(s,"rows","8"),p(s,"placeholder",`-----BEGIN PRIVATE KEY----- -... ------END PRIVATE KEY-----`),p(a,"class","help-block")},m(c,d){w(c,e,d),k(e,t),w(c,l,d),w(c,s,d),re(s,n[5]),w(c,r,d),w(c,a,d),f||(u=J(s,"input",n[16]),f=!0)},p(c,d){d&8388608&&i!==(i=c[23])&&p(e,"for",i),d&8388608&&o!==(o=c[23])&&p(s,"id",o),d&32&&re(s,c[5])},d(c){c&&(v(e),v(l),v(s),v(r),v(a)),f=!1,u()}}}function i8(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S;return l=new ce({props:{class:"form-field required",name:"clientId",$$slots:{default:[QM,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field required",name:"teamId",$$slots:{default:[xM,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),u=new ce({props:{class:"form-field required",name:"keyId",$$slots:{default:[e8,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),m=new ce({props:{class:"form-field required",name:"duration",$$slots:{default:[t8,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),_=new ce({props:{class:"form-field required",name:"privateKey",$$slots:{default:[n8,({uniqueId:T})=>({23:T}),({uniqueId:T})=>T?8388608:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),B(r.$$.fragment),a=M(),f=b("div"),B(u.$$.fragment),c=M(),d=b("div"),B(m.$$.fragment),h=M(),B(_.$$.fragment),p(i,"class","col-lg-6"),p(o,"class","col-lg-6"),p(f,"class","col-lg-6"),p(d,"class","col-lg-6"),p(t,"class","grid"),p(e,"id",n[9]),p(e,"autocomplete","off")},m(T,$){w(T,e,$),k(e,t),k(t,i),z(l,i,null),k(t,s),k(t,o),z(r,o,null),k(t,a),k(t,f),z(u,f,null),k(t,c),k(t,d),z(m,d,null),k(t,h),z(_,t,null),g=!0,y||(S=J(e,"submit",Be(n[17])),y=!0)},p(T,$){const C={};$&25165828&&(C.$$scope={dirty:$,ctx:T}),l.$set(C);const O={};$&25165832&&(O.$$scope={dirty:$,ctx:T}),r.$set(O);const D={};$&25165840&&(D.$$scope={dirty:$,ctx:T}),u.$set(D);const I={};$&25165888&&(I.$$scope={dirty:$,ctx:T}),m.$set(I);const L={};$&25165856&&(L.$$scope={dirty:$,ctx:T}),_.$set(L)},i(T){g||(E(l.$$.fragment,T),E(r.$$.fragment,T),E(u.$$.fragment,T),E(m.$$.fragment,T),E(_.$$.fragment,T),g=!0)},o(T){A(l.$$.fragment,T),A(r.$$.fragment,T),A(u.$$.fragment,T),A(m.$$.fragment,T),A(_.$$.fragment,T),g=!1},d(T){T&&v(e),V(l),V(r),V(u),V(m),V(_),y=!1,S()}}}function l8(n){let e;return{c(){e=b("h4"),e.textContent="Generate Apple client secret",p(e,"class","center txt-break")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function s8(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("button"),t=K("Close"),i=M(),l=b("button"),s=b("i"),o=M(),r=b("span"),r.textContent="Generate and set secret",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[7],p(s,"class","ri-key-line"),p(r,"class","txt"),p(l,"type","submit"),p(l,"form",n[9]),p(l,"class","btn btn-expanded"),l.disabled=a=!n[8]||n[7],x(l,"btn-loading",n[7])},m(c,d){w(c,e,d),k(e,t),w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=J(e,"click",n[0]),f=!0)},p(c,d){d&128&&(e.disabled=c[7]),d&384&&a!==(a=!c[8]||c[7])&&(l.disabled=a),d&128&&x(l,"btn-loading",c[7])},d(c){c&&(v(e),v(i),v(l)),f=!1,u()}}}function o8(n){let e,t,i={overlayClose:!n[7],escClose:!n[7],beforeHide:n[18],popup:!0,$$slots:{footer:[s8],header:[l8],default:[i8]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[19](e),e.$on("show",n[20]),e.$on("hide",n[21]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&128&&(o.overlayClose=!l[7]),s&128&&(o.escClose=!l[7]),s&128&&(o.beforeHide=l[18]),s&16777724&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[19](null),V(e,l)}}}const _o=15777e3;function r8(n,e,t){let i;const l=lt(),s="apple_secret_"+j.randomString(5);let o,r,a,f,u,c,d=!1;function m(R={}){t(2,r=R.clientId||""),t(3,a=R.teamId||""),t(4,f=R.keyId||""),t(5,u=R.privateKey||""),t(6,c=R.duration||_o),Jt({}),o==null||o.show()}function h(){return o==null?void 0:o.hide()}async function _(){t(7,d=!0);try{const R=await ae.settings.generateAppleClientSecret(r,a,f,u.trim(),c);t(7,d=!1),Lt("Successfully generated client secret."),l("submit",R),o==null||o.hide()}catch(R){ae.error(R)}t(7,d=!1)}function g(){r=this.value,t(2,r)}function y(){a=this.value,t(3,a)}function S(){f=this.value,t(4,f)}function T(){c=this.value,t(6,c)}function $(){u=this.value,t(5,u)}const C=()=>_(),O=()=>!d;function D(R){ee[R?"unshift":"push"](()=>{o=R,t(1,o)})}function I(R){Ce.call(this,n,R)}function L(R){Ce.call(this,n,R)}return t(8,i=!0),[h,o,r,a,f,u,c,d,i,s,_,m,g,y,S,T,$,C,O,D,I,L]}class a8 extends ge{constructor(e){super(),_e(this,e,r8,o8,me,{show:11,hide:0})}get show(){return this.$$.ctx[11]}get hide(){return this.$$.ctx[0]}}function f8(n){let e,t,i,l,s,o,r,a,f,u,c={};return r=new a8({props:c}),n[4](r),r.$on("submit",n[5]),{c(){e=b("button"),t=b("i"),i=M(),l=b("span"),l.textContent="Generate secret",o=M(),B(r.$$.fragment),p(t,"class","ri-key-line"),p(l,"class","txt"),p(e,"type","button"),p(e,"class",s="btn btn-sm btn-secondary btn-provider-"+n[1])},m(d,m){w(d,e,m),k(e,t),k(e,i),k(e,l),w(d,o,m),z(r,d,m),a=!0,f||(u=J(e,"click",n[3]),f=!0)},p(d,[m]){(!a||m&2&&s!==(s="btn btn-sm btn-secondary btn-provider-"+d[1]))&&p(e,"class",s);const h={};r.$set(h)},i(d){a||(E(r.$$.fragment,d),a=!0)},o(d){A(r.$$.fragment,d),a=!1},d(d){d&&(v(e),v(o)),n[4](null),V(r,d),f=!1,u()}}}function u8(n,e,t){let{key:i=""}=e,{config:l={}}=e,s;const o=()=>s==null?void 0:s.show({clientId:l.clientId});function r(f){ee[f?"unshift":"push"](()=>{s=f,t(2,s)})}const a=f=>{var u;t(0,l.clientSecret=((u=f.detail)==null?void 0:u.secret)||"",l)};return n.$$set=f=>{"key"in f&&t(1,i=f.key),"config"in f&&t(0,l=f.config)},[l,i,s,o,r,a]}class c8 extends ge{constructor(e){super(),_e(this,e,u8,f8,me,{key:1,config:0})}}const go=[{key:"appleAuth",title:"Apple",logo:"apple.svg",optionsComponent:c8},{key:"googleAuth",title:"Google",logo:"google.svg"},{key:"microsoftAuth",title:"Microsoft",logo:"microsoft.svg",optionsComponent:XM},{key:"yandexAuth",title:"Yandex",logo:"yandex.svg"},{key:"facebookAuth",title:"Facebook",logo:"facebook.svg"},{key:"instagramAuth",title:"Instagram",logo:"instagram.svg"},{key:"githubAuth",title:"GitHub",logo:"github.svg"},{key:"gitlabAuth",title:"GitLab",logo:"gitlab.svg",optionsComponent:Ar,optionsComponentProps:{title:"Self-hosted endpoints (optional)"}},{key:"bitbucketAuth",title:"Bitbucket",logo:"bitbucket.svg"},{key:"giteeAuth",title:"Gitee",logo:"gitee.svg"},{key:"giteaAuth",title:"Gitea",logo:"gitea.svg",optionsComponent:Ar,optionsComponentProps:{title:"Self-hosted endpoints (optional)"}},{key:"discordAuth",title:"Discord",logo:"discord.svg"},{key:"twitterAuth",title:"Twitter",logo:"twitter.svg"},{key:"kakaoAuth",title:"Kakao",logo:"kakao.svg"},{key:"vkAuth",title:"VK",logo:"vk.svg"},{key:"spotifyAuth",title:"Spotify",logo:"spotify.svg"},{key:"twitchAuth",title:"Twitch",logo:"twitch.svg"},{key:"patreonAuth",title:"Patreon (v2)",logo:"patreon.svg"},{key:"stravaAuth",title:"Strava",logo:"strava.svg"},{key:"livechatAuth",title:"LiveChat",logo:"livechat.svg"},{key:"mailcowAuth",title:"mailcow",logo:"mailcow.svg",optionsComponent:Ar,optionsComponentProps:{required:!0}},{key:"planningcenterAuth",title:"Planning Center",logo:"planningcenter.svg"},{key:"oidcAuth",title:"OpenID Connect",logo:"oidc.svg",optionsComponent:Lr},{key:"oidc2Auth",title:"(2) OpenID Connect",logo:"oidc.svg",optionsComponent:Lr},{key:"oidc3Auth",title:"(3) OpenID Connect",logo:"oidc.svg",optionsComponent:Lr}];function Am(n,e,t){const i=n.slice();return i[9]=e[t],i}function d8(n){let e;return{c(){e=b("h6"),e.textContent="No linked OAuth2 providers.",p(e,"class","txt-hint txt-center m-t-sm m-b-sm")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function p8(n){let e,t=ue(n[1]),i=[];for(let l=0;l',p(e,"class","block txt-center")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Lm(n){let e,t,i,l,s,o,r=n[4](n[9].provider)+"",a,f,u,c,d=n[9].providerId+"",m,h,_,g,y,S;function T(){return n[6](n[9])}return{c(){var $;e=b("div"),t=b("figure"),i=b("img"),s=M(),o=b("span"),a=K(r),f=M(),u=b("div"),c=K("ID: "),m=K(d),h=M(),_=b("button"),_.innerHTML='',g=M(),en(i.src,l="./images/oauth2/"+(($=n[3](n[9].provider))==null?void 0:$.logo))||p(i,"src",l),p(i,"alt","Provider logo"),p(t,"class","provider-logo"),p(o,"class","txt"),p(u,"class","txt-hint"),p(_,"type","button"),p(_,"class","btn btn-transparent link-hint btn-circle btn-sm m-l-auto"),p(e,"class","list-item")},m($,C){w($,e,C),k(e,t),k(t,i),k(e,s),k(e,o),k(o,a),k(e,f),k(e,u),k(u,c),k(u,m),k(e,h),k(e,_),k(e,g),y||(S=J(_,"click",T),y=!0)},p($,C){var O;n=$,C&2&&!en(i.src,l="./images/oauth2/"+((O=n[3](n[9].provider))==null?void 0:O.logo))&&p(i,"src",l),C&2&&r!==(r=n[4](n[9].provider)+"")&&oe(a,r),C&2&&d!==(d=n[9].providerId+"")&&oe(m,d)},d($){$&&v(e),y=!1,S()}}}function h8(n){let e;function t(s,o){var r;return s[2]?m8:(r=s[0])!=null&&r.id&&s[1].length?p8:d8}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:Q,o:Q,d(s){s&&v(e),l.d(s)}}}function _8(n,e,t){const i=lt();let{record:l}=e,s=[],o=!1;function r(d){return go.find(m=>m.key==d+"Auth")||{}}function a(d){var m;return((m=r(d))==null?void 0:m.title)||j.sentenize(d,!1)}async function f(){if(!(l!=null&&l.id)){t(1,s=[]),t(2,o=!1);return}t(2,o=!0);try{t(1,s=await ae.collection(l.collectionId).listExternalAuths(l.id))}catch(d){ae.error(d)}t(2,o=!1)}function u(d){!(l!=null&&l.id)||!d||fn(`Do you really want to unlink the ${a(d)} provider?`,()=>ae.collection(l.collectionId).unlinkExternalAuth(l.id,d).then(()=>{Lt(`Successfully unlinked the ${a(d)} provider.`),i("unlink",d),f()}).catch(m=>{ae.error(m)}))}f();const c=d=>u(d.provider);return n.$$set=d=>{"record"in d&&t(0,l=d.record)},[l,s,o,r,a,u,c]}class g8 extends ge{constructor(e){super(),_e(this,e,_8,h8,me,{record:0})}}function Nm(n,e,t){const i=n.slice();return i[71]=e[t],i[72]=e,i[73]=t,i}function Pm(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_;return{c(){e=b("div"),t=b("div"),i=b("div"),i.innerHTML='',l=M(),s=b("div"),o=K(`The record has previous unsaved changes. - `),r=b("button"),r.textContent="Restore draft",a=M(),f=b("button"),f.innerHTML='',u=M(),c=b("div"),p(i,"class","icon"),p(r,"type","button"),p(r,"class","btn btn-sm btn-secondary"),p(s,"class","flex flex-gap-xs"),p(f,"type","button"),p(f,"class","close"),p(f,"aria-label","Discard draft"),p(t,"class","alert alert-info m-0"),p(c,"class","clearfix p-b-base"),p(e,"class","block")},m(g,y){w(g,e,y),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),k(s,r),k(t,a),k(t,f),k(e,u),k(e,c),m=!0,h||(_=[J(r,"click",n[39]),Se(Pe.call(null,f,"Discard draft")),J(f,"click",Be(n[40]))],h=!0)},p:Q,i(g){m||(d&&d.end(1),m=!0)},o(g){g&&(d=ca(e,et,{duration:150})),m=!1},d(g){g&&v(e),g&&d&&d.end(),h=!1,$e(_)}}}function Fm(n){let e,t,i;return t=new Kb({props:{model:n[3]}}),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","form-field-addon")},m(l,s){w(l,e,s),z(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.model=l[3]),t.$set(o)},i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function b8(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y=!n[6]&&Fm(n);return{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="id",s=M(),o=b("span"),a=M(),y&&y.c(),f=M(),u=b("input"),p(t,"class",Un(j.getFieldTypeIcon("primary"))+" svelte-qc5ngu"),p(l,"class","txt"),p(o,"class","flex-fill"),p(e,"for",r=n[74]),p(u,"type","text"),p(u,"id",c=n[74]),p(u,"placeholder",d=n[7]?"":"Leave empty to auto generate..."),p(u,"minlength","15"),u.readOnly=m=!n[6]},m(S,T){w(S,e,T),k(e,t),k(e,i),k(e,l),k(e,s),k(e,o),w(S,a,T),y&&y.m(S,T),w(S,f,T),w(S,u,T),re(u,n[3].id),h=!0,_||(g=J(u,"input",n[41]),_=!0)},p(S,T){(!h||T[2]&4096&&r!==(r=S[74]))&&p(e,"for",r),S[6]?y&&(le(),A(y,1,1,()=>{y=null}),se()):y?(y.p(S,T),T[0]&64&&E(y,1)):(y=Fm(S),y.c(),E(y,1),y.m(f.parentNode,f)),(!h||T[2]&4096&&c!==(c=S[74]))&&p(u,"id",c),(!h||T[0]&128&&d!==(d=S[7]?"":"Leave empty to auto generate..."))&&p(u,"placeholder",d),(!h||T[0]&64&&m!==(m=!S[6]))&&(u.readOnly=m),T[0]&8&&u.value!==S[3].id&&re(u,S[3].id)},i(S){h||(E(y),h=!0)},o(S){A(y),h=!1},d(S){S&&(v(e),v(a),v(f),v(u)),y&&y.d(S),_=!1,g()}}}function Rm(n){var f,u;let e,t,i,l,s;function o(c){n[42](c)}let r={isNew:n[6],collection:n[0]};n[3]!==void 0&&(r.record=n[3]),e=new E6({props:r}),ee.push(()=>be(e,"record",o));let a=((u=(f=n[0])==null?void 0:f.schema)==null?void 0:u.length)&&qm();return{c(){B(e.$$.fragment),i=M(),a&&a.c(),l=ye()},m(c,d){z(e,c,d),w(c,i,d),a&&a.m(c,d),w(c,l,d),s=!0},p(c,d){var h,_;const m={};d[0]&64&&(m.isNew=c[6]),d[0]&1&&(m.collection=c[0]),!t&&d[0]&8&&(t=!0,m.record=c[3],ke(()=>t=!1)),e.$set(m),(_=(h=c[0])==null?void 0:h.schema)!=null&&_.length?a||(a=qm(),a.c(),a.m(l.parentNode,l)):a&&(a.d(1),a=null)},i(c){s||(E(e.$$.fragment,c),s=!0)},o(c){A(e.$$.fragment,c),s=!1},d(c){c&&(v(i),v(l)),V(e,c),a&&a.d(c)}}}function qm(n){let e;return{c(){e=b("hr")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function k8(n){let e,t,i;function l(o){n[55](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new aM({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function y8(n){let e,t,i,l,s;function o(u){n[52](u,n[71])}function r(u){n[53](u,n[71])}function a(u){n[54](u,n[71])}let f={field:n[71],record:n[3]};return n[3][n[71].name]!==void 0&&(f.value=n[3][n[71].name]),n[4][n[71].name]!==void 0&&(f.uploadedFiles=n[4][n[71].name]),n[5][n[71].name]!==void 0&&(f.deletedFileNames=n[5][n[71].name]),e=new BO({props:f}),ee.push(()=>be(e,"value",o)),ee.push(()=>be(e,"uploadedFiles",r)),ee.push(()=>be(e,"deletedFileNames",a)),{c(){B(e.$$.fragment)},m(u,c){z(e,u,c),s=!0},p(u,c){n=u;const d={};c[0]&1&&(d.field=n[71]),c[0]&8&&(d.record=n[3]),!t&&c[0]&9&&(t=!0,d.value=n[3][n[71].name],ke(()=>t=!1)),!i&&c[0]&17&&(i=!0,d.uploadedFiles=n[4][n[71].name],ke(()=>i=!1)),!l&&c[0]&33&&(l=!0,d.deletedFileNames=n[5][n[71].name],ke(()=>l=!1)),e.$set(d)},i(u){s||(E(e.$$.fragment,u),s=!0)},o(u){A(e.$$.fragment,u),s=!1},d(u){V(e,u)}}}function v8(n){let e,t,i;function l(o){n[51](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new hO({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function w8(n){let e,t,i;function l(o){n[50](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new rO({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function S8(n){let e,t,i;function l(o){n[49](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new iO({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function $8(n){let e,t,i;function l(o){n[48](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new NM({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function T8(n){let e,t,i;function l(o){n[47](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new x6({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function C8(n){let e,t,i;function l(o){n[46](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new Z6({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function O8(n){let e,t,i;function l(o){n[45](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new W6({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function M8(n){let e,t,i;function l(o){n[44](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new z6({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function D8(n){let e,t,i;function l(o){n[43](o,n[71])}let s={field:n[71]};return n[3][n[71].name]!==void 0&&(s.value=n[3][n[71].name]),e=new R6({props:s}),ee.push(()=>be(e,"value",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){n=o;const a={};r[0]&1&&(a.field=n[71]),!t&&r[0]&9&&(t=!0,a.value=n[3][n[71].name],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function jm(n,e){let t,i,l,s,o;const r=[D8,M8,O8,C8,T8,$8,S8,w8,v8,y8,k8],a=[];function f(u,c){return u[71].type==="text"?0:u[71].type==="number"?1:u[71].type==="bool"?2:u[71].type==="email"?3:u[71].type==="url"?4:u[71].type==="editor"?5:u[71].type==="date"?6:u[71].type==="select"?7:u[71].type==="json"?8:u[71].type==="file"?9:u[71].type==="relation"?10:-1}return~(i=f(e))&&(l=a[i]=r[i](e)),{key:n,first:null,c(){t=ye(),l&&l.c(),s=ye(),this.first=t},m(u,c){w(u,t,c),~i&&a[i].m(u,c),w(u,s,c),o=!0},p(u,c){e=u;let d=i;i=f(e),i===d?~i&&a[i].p(e,c):(l&&(le(),A(a[d],1,1,()=>{a[d]=null}),se()),~i?(l=a[i],l?l.p(e,c):(l=a[i]=r[i](e),l.c()),E(l,1),l.m(s.parentNode,s)):l=null)},i(u){o||(E(l),o=!0)},o(u){A(l),o=!1},d(u){u&&(v(t),v(s)),~i&&a[i].d(u)}}}function Hm(n){let e,t,i;return t=new g8({props:{record:n[3]}}),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","tab-item"),x(e,"active",n[13]===_s)},m(l,s){w(l,e,s),z(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.record=l[3]),t.$set(o),(!i||s[0]&8192)&&x(e,"active",l[13]===_s)},i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function E8(n){var S;let e,t,i,l,s,o,r=[],a=new Map,f,u,c,d,m=!n[8]&&n[10]&&!n[7]&&Pm(n);l=new ce({props:{class:"form-field "+(n[6]?"":"readonly"),name:"id",$$slots:{default:[b8,({uniqueId:T})=>({74:T}),({uniqueId:T})=>[0,0,T?4096:0]]},$$scope:{ctx:n}}});let h=n[14]&&Rm(n),_=ue(((S=n[0])==null?void 0:S.schema)||[]);const g=T=>T[71].name;for(let T=0;T<_.length;T+=1){let $=Nm(n,_,T),C=g($);a.set(C,r[T]=jm(C,$))}let y=n[14]&&!n[6]&&Hm(n);return{c(){e=b("div"),t=b("form"),m&&m.c(),i=M(),B(l.$$.fragment),s=M(),h&&h.c(),o=M();for(let T=0;T{m=null}),se());const C={};$[0]&64&&(C.class="form-field "+(T[6]?"":"readonly")),$[0]&200|$[2]&12288&&(C.$$scope={dirty:$,ctx:T}),l.$set(C),T[14]?h?(h.p(T,$),$[0]&16384&&E(h,1)):(h=Rm(T),h.c(),E(h,1),h.m(t,o)):h&&(le(),A(h,1,1,()=>{h=null}),se()),$[0]&57&&(_=ue(((O=T[0])==null?void 0:O.schema)||[]),le(),r=at(r,$,g,1,T,_,a,t,Et,jm,null,Nm),se()),(!u||$[0]&128)&&x(t,"no-pointer-events",T[7]),(!u||$[0]&8192)&&x(t,"active",T[13]===Gi),T[14]&&!T[6]?y?(y.p(T,$),$[0]&16448&&E(y,1)):(y=Hm(T),y.c(),E(y,1),y.m(e,null)):y&&(le(),A(y,1,1,()=>{y=null}),se())},i(T){if(!u){E(m),E(l.$$.fragment,T),E(h);for(let $=0;$<_.length;$+=1)E(r[$]);E(y),u=!0}},o(T){A(m),A(l.$$.fragment,T),A(h);for(let $=0;${d=null}),se()):d?(d.p(h,_),_[0]&64&&E(d,1)):(d=zm(h),d.c(),E(d,1),d.m(u.parentNode,u))},i(h){c||(E(d),c=!0)},o(h){A(d),c=!1},d(h){h&&(v(e),v(f),v(u)),d&&d.d(h)}}}function A8(n){let e,t,i;return{c(){e=b("span"),t=M(),i=b("h4"),i.textContent="Loading...",p(e,"class","loader loader-sm"),p(i,"class","panel-title txt-hint svelte-qc5ngu")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},p:Q,i:Q,o:Q,d(l){l&&(v(e),v(t),v(i))}}}function zm(n){let e,t,i,l,s,o,r;return o=new On({props:{class:"dropdown dropdown-right dropdown-nowrap",$$slots:{default:[L8]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=M(),i=b("div"),l=b("i"),s=M(),B(o.$$.fragment),p(e,"class","flex-fill"),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(i,"tabindex","0"),p(i,"role","button"),p(i,"aria-label","More record options"),p(i,"class","btn btn-sm btn-circle btn-transparent flex-gap-0")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),k(i,l),k(i,s),z(o,i,null),r=!0},p(a,f){const u={};f[0]&16388|f[2]&8192&&(u.$$scope={dirty:f,ctx:a}),o.$set(u)},i(a){r||(E(o.$$.fragment,a),r=!0)},o(a){A(o.$$.fragment,a),r=!1},d(a){a&&(v(e),v(t),v(i)),V(o)}}}function Vm(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send verification email',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[33]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function Bm(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send password reset email',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[34]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function L8(n){let e,t,i,l,s,o,r,a=n[14]&&!n[2].verified&&n[2].email&&Vm(n),f=n[14]&&n[2].email&&Bm(n);return{c(){a&&a.c(),e=M(),f&&f.c(),t=M(),i=b("button"),i.innerHTML=' Duplicate',l=M(),s=b("button"),s.innerHTML=' Delete',p(i,"type","button"),p(i,"class","dropdown-item closable"),p(i,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item txt-danger closable"),p(s,"role","menuitem")},m(u,c){a&&a.m(u,c),w(u,e,c),f&&f.m(u,c),w(u,t,c),w(u,i,c),w(u,l,c),w(u,s,c),o||(r=[J(i,"click",n[35]),J(s,"click",Tn(Be(n[36])))],o=!0)},p(u,c){u[14]&&!u[2].verified&&u[2].email?a?a.p(u,c):(a=Vm(u),a.c(),a.m(e.parentNode,e)):a&&(a.d(1),a=null),u[14]&&u[2].email?f?f.p(u,c):(f=Bm(u),f.c(),f.m(t.parentNode,t)):f&&(f.d(1),f=null)},d(u){u&&(v(e),v(t),v(i),v(l),v(s)),a&&a.d(u),f&&f.d(u),o=!1,$e(r)}}}function Um(n){let e,t,i,l,s,o;return{c(){e=b("div"),t=b("button"),t.textContent="Account",i=M(),l=b("button"),l.textContent="Authorized providers",p(t,"type","button"),p(t,"class","tab-item"),x(t,"active",n[13]===Gi),p(l,"type","button"),p(l,"class","tab-item"),x(l,"active",n[13]===_s),p(e,"class","tabs-header stretched")},m(r,a){w(r,e,a),k(e,t),k(e,i),k(e,l),s||(o=[J(t,"click",n[37]),J(l,"click",n[38])],s=!0)},p(r,a){a[0]&8192&&x(t,"active",r[13]===Gi),a[0]&8192&&x(l,"active",r[13]===_s)},d(r){r&&v(e),s=!1,$e(o)}}}function N8(n){let e,t,i,l,s;const o=[A8,I8],r=[];function a(u,c){return u[7]?0:1}e=a(n),t=r[e]=o[e](n);let f=n[14]&&!n[6]&&Um(n);return{c(){t.c(),i=M(),f&&f.c(),l=ye()},m(u,c){r[e].m(u,c),w(u,i,c),f&&f.m(u,c),w(u,l,c),s=!0},p(u,c){let d=e;e=a(u),e===d?r[e].p(u,c):(le(),A(r[d],1,1,()=>{r[d]=null}),se(),t=r[e],t?t.p(u,c):(t=r[e]=o[e](u),t.c()),E(t,1),t.m(i.parentNode,i)),u[14]&&!u[6]?f?f.p(u,c):(f=Um(u),f.c(),f.m(l.parentNode,l)):f&&(f.d(1),f=null)},i(u){s||(E(t),s=!0)},o(u){A(t),s=!1},d(u){u&&(v(i),v(l)),r[e].d(u),f&&f.d(u)}}}function P8(n){let e,t,i,l,s,o,r=n[6]?"Create":"Save changes",a,f,u,c;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",l=M(),s=b("button"),o=b("span"),a=K(r),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=i=n[11]||n[7],p(o,"class","txt"),p(s,"type","submit"),p(s,"form",n[17]),p(s,"class","btn btn-expanded"),s.disabled=f=!n[15]||n[11],x(s,"btn-loading",n[11]||n[7])},m(d,m){w(d,e,m),k(e,t),w(d,l,m),w(d,s,m),k(s,o),k(o,a),u||(c=J(e,"click",n[32]),u=!0)},p(d,m){m[0]&2176&&i!==(i=d[11]||d[7])&&(e.disabled=i),m[0]&64&&r!==(r=d[6]?"Create":"Save changes")&&oe(a,r),m[0]&34816&&f!==(f=!d[15]||d[11])&&(s.disabled=f),m[0]&2176&&x(s,"btn-loading",d[11]||d[7])},d(d){d&&(v(e),v(l),v(s)),u=!1,c()}}}function F8(n){let e,t,i={class:` - record-panel - `+(n[16]?"overlay-panel-xl":"overlay-panel-lg")+` - `+(n[14]&&!n[6]?"colored-header":"")+` - `,btnClose:!n[7],escClose:!n[7],overlayClose:!n[7],beforeHide:n[56],$$slots:{footer:[P8],header:[N8],default:[E8]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[57](e),e.$on("hide",n[58]),e.$on("show",n[59]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,s){const o={};s[0]&81984&&(o.class=` - record-panel - `+(l[16]?"overlay-panel-xl":"overlay-panel-lg")+` - `+(l[14]&&!l[6]?"colored-header":"")+` - `),s[0]&128&&(o.btnClose=!l[7]),s[0]&128&&(o.escClose=!l[7]),s[0]&128&&(o.overlayClose=!l[7]),s[0]&4352&&(o.beforeHide=l[56]),s[0]&60925|s[2]&8192&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[57](null),V(e,l)}}}const Gi="form",_s="providers";function R8(n,e,t){let i,l,s,o,r;const a=lt(),f="record_"+j.randomString(5);let{collection:u}=e,c,d={},m={},h=null,_=!1,g=!1,y={},S={},T=JSON.stringify(d),$=T,C=Gi,O=!0,D=!0,I=u;function L(he){return q(he),t(12,g=!0),t(13,C=Gi),c==null?void 0:c.show()}function R(){return c==null?void 0:c.hide()}function F(){t(12,g=!1),R()}function N(){t(30,I=u),c!=null&&c.isActive()&&(U(JSON.stringify(m)),F())}async function P(he){if(he&&typeof he=="string"){try{return await ae.collection(u.id).getOne(he)}catch(Oe){Oe.isAbort||(F(),console.warn("resolveModel:",Oe),ii(`Unable to load record with id "${he}"`))}return null}return he}async function q(he){t(7,D=!0),Jt({}),t(4,y={}),t(5,S={}),t(2,d=typeof he=="string"?{id:he,collectionId:u==null?void 0:u.id,collectionName:u==null?void 0:u.name}:he||{}),t(3,m=structuredClone(d)),t(2,d=await P(he)||{}),t(3,m=structuredClone(d)),await Qt(),t(10,h=G()),!h||ie(m,h)?t(10,h=null):(delete h.password,delete h.passwordConfirm),t(28,T=JSON.stringify(m)),t(7,D=!1)}async function H(he){var ht,Kt;Jt({}),t(2,d=he||{}),t(4,y={}),t(5,S={});const Oe=((Kt=(ht=u==null?void 0:u.schema)==null?void 0:ht.filter(ut=>ut.type!="file"))==null?void 0:Kt.map(ut=>ut.name))||[];for(let ut in he)Oe.includes(ut)||t(3,m[ut]=he[ut],m);await Qt(),t(28,T=JSON.stringify(m)),te()}function W(){return"record_draft_"+((u==null?void 0:u.id)||"")+"_"+((d==null?void 0:d.id)||"")}function G(he){try{const Oe=window.localStorage.getItem(W());if(Oe)return JSON.parse(Oe)}catch{}return he}function U(he){try{window.localStorage.setItem(W(),he)}catch(Oe){console.warn("updateDraft failure:",Oe),window.localStorage.removeItem(W())}}function Y(){h&&(t(3,m=h),t(10,h=null))}function ie(he,Oe){var Z;const ht=structuredClone(he||{}),Kt=structuredClone(Oe||{}),ut=(Z=u==null?void 0:u.schema)==null?void 0:Z.filter(X=>X.type==="file");for(let X of ut)delete ht[X.name],delete Kt[X.name];const oi=["expand","password","passwordConfirm"];for(let X of oi)delete ht[X],delete Kt[X];return JSON.stringify(ht)==JSON.stringify(Kt)}function te(){t(10,h=null),window.localStorage.removeItem(W())}async function pe(he=!0){if(!(_||!r||!(u!=null&&u.id))){t(11,_=!0);try{const Oe=He();let ht;O?ht=await ae.collection(u.id).create(Oe):ht=await ae.collection(u.id).update(m.id,Oe),Lt(O?"Successfully created record.":"Successfully updated record."),te(),he?F():H(ht),a("save",{isNew:O,record:ht})}catch(Oe){ae.error(Oe)}t(11,_=!1)}}function Ne(){d!=null&&d.id&&fn("Do you really want to delete the selected record?",()=>ae.collection(d.collectionId).delete(d.id).then(()=>{R(),Lt("Successfully deleted record."),a("delete",d)}).catch(he=>{ae.error(he)}))}function He(){const he=structuredClone(m||{}),Oe=new FormData,ht={id:he.id},Kt={};for(const ut of(u==null?void 0:u.schema)||[])ht[ut.name]=!0,ut.type=="json"&&(Kt[ut.name]=!0);i&&(ht.username=!0,ht.email=!0,ht.emailVisibility=!0,ht.password=!0,ht.passwordConfirm=!0,ht.verified=!0);for(const ut in he)if(ht[ut]){if(typeof he[ut]>"u"&&(he[ut]=null),Kt[ut]&&he[ut]!=="")try{JSON.parse(he[ut])}catch(oi){const Z={};throw Z[ut]={code:"invalid_json",message:oi.toString()},new gn({status:400,response:{data:Z}})}j.addValueToFormData(Oe,ut,he[ut])}for(const ut in y){const oi=j.toArray(y[ut]);for(const Z of oi)Oe.append(ut,Z)}for(const ut in S){const oi=j.toArray(S[ut]);for(const Z of oi)Oe.append(ut+"."+Z,"")}return Oe}function Xe(){!(u!=null&&u.id)||!(d!=null&&d.email)||fn(`Do you really want to sent verification email to ${d.email}?`,()=>ae.collection(u.id).requestVerification(d.email).then(()=>{Lt(`Successfully sent verification email to ${d.email}.`)}).catch(he=>{ae.error(he)}))}function xe(){!(u!=null&&u.id)||!(d!=null&&d.email)||fn(`Do you really want to sent password reset email to ${d.email}?`,()=>ae.collection(u.id).requestPasswordReset(d.email).then(()=>{Lt(`Successfully sent password reset email to ${d.email}.`)}).catch(he=>{ae.error(he)}))}function Mt(){o?fn("You have unsaved changes. Do you really want to discard them?",()=>{ft()}):ft()}async function ft(){let he=d?structuredClone(d):null;if(he){he.id="",he.created="",he.updated="";const Oe=(u==null?void 0:u.schema)||[];for(const ht of Oe)ht.type==="file"&&delete he[ht.name]}te(),L(he),await Qt(),t(28,T="")}function mt(he){(he.ctrlKey||he.metaKey)&&he.code=="KeyS"&&(he.preventDefault(),he.stopPropagation(),pe(!1))}const Gt=()=>R(),De=()=>Xe(),Ae=()=>xe(),ze=()=>Mt(),gt=()=>Ne(),de=()=>t(13,C=Gi),ve=()=>t(13,C=_s),we=()=>Y(),Ye=()=>te();function zt(){m.id=this.value,t(3,m)}function cn(he){m=he,t(3,m)}function rn(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function qn(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Ai(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function ol(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function gi(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Ee(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Nt(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Li(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Kn(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function rl(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}function Pl(he,Oe){n.$$.not_equal(y[Oe.name],he)&&(y[Oe.name]=he,t(4,y))}function bi(he,Oe){n.$$.not_equal(S[Oe.name],he)&&(S[Oe.name]=he,t(5,S))}function al(he,Oe){n.$$.not_equal(m[Oe.name],he)&&(m[Oe.name]=he,t(3,m))}const fl=()=>o&&g?(fn("You have unsaved changes. Do you really want to close the panel?",()=>{F()}),!1):(Jt({}),te(),!0);function bt(he){ee[he?"unshift":"push"](()=>{c=he,t(9,c)})}function rt(he){Ce.call(this,n,he)}function Mn(he){Ce.call(this,n,he)}return n.$$set=he=>{"collection"in he&&t(0,u=he.collection)},n.$$.update=()=>{var he;n.$$.dirty[0]&1&&t(14,i=(u==null?void 0:u.type)==="auth"),n.$$.dirty[0]&1&&t(16,l=!!((he=u==null?void 0:u.schema)!=null&&he.find(Oe=>Oe.type==="editor"))),n.$$.dirty[0]&48&&t(31,s=j.hasNonEmptyProps(y)||j.hasNonEmptyProps(S)),n.$$.dirty[0]&8&&t(29,$=JSON.stringify(m)),n.$$.dirty[0]&805306368|n.$$.dirty[1]&1&&t(8,o=s||T!=$),n.$$.dirty[0]&4&&t(6,O=!d||!d.id),n.$$.dirty[0]&448&&t(15,r=!D&&(O||o)),n.$$.dirty[0]&536871040&&(D||U($)),n.$$.dirty[0]&1073741825&&u&&(I==null?void 0:I.id)!=(u==null?void 0:u.id)&&N()},[u,R,d,m,y,S,O,D,o,c,h,_,g,C,i,r,l,f,F,Y,te,pe,Ne,Xe,xe,Mt,mt,L,T,$,I,s,Gt,De,Ae,ze,gt,de,ve,we,Ye,zt,cn,rn,qn,Ai,ol,gi,Ee,Nt,Li,Kn,rl,Pl,bi,al,fl,bt,rt,Mn]}class Ya extends ge{constructor(e){super(),_e(this,e,R8,F8,me,{collection:0,show:27,hide:1},null,[-1,-1,-1])}get show(){return this.$$.ctx[27]}get hide(){return this.$$.ctx[1]}}function q8(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function j8(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("div"),t=b("div"),i=K(n[2]),l=M(),s=b("div"),o=K(n[1]),r=K(" UTC"),p(t,"class","date"),p(s,"class","time svelte-5pjd03"),p(e,"class","datetime svelte-5pjd03")},m(u,c){w(u,e,c),k(e,t),k(t,i),k(e,l),k(e,s),k(s,o),k(s,r),a||(f=Se(Pe.call(null,e,n[3])),a=!0)},p(u,c){c&4&&oe(i,u[2]),c&2&&oe(o,u[1])},d(u){u&&v(e),a=!1,f()}}}function H8(n){let e;function t(s,o){return s[0]?j8:q8}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:Q,o:Q,d(s){s&&v(e),l.d(s)}}}function z8(n,e,t){let i,l,{date:s=""}=e;const o={get text(){return j.formatToLocalDate(s)+" Local"}};return n.$$set=r=>{"date"in r&&t(0,s=r.date)},n.$$.update=()=>{n.$$.dirty&1&&t(2,i=s?s.substring(0,10):null),n.$$.dirty&1&&t(1,l=s?s.substring(10,19):null)},[s,l,i,o]}class el extends ge{constructor(e){super(),_e(this,e,z8,H8,me,{date:0})}}function Wm(n,e,t){const i=n.slice();return i[18]=e[t],i[8]=t,i}function Ym(n,e,t){const i=n.slice();return i[13]=e[t],i}function Km(n,e,t){const i=n.slice();return i[6]=e[t],i[8]=t,i}function Jm(n,e,t){const i=n.slice();return i[6]=e[t],i[8]=t,i}function V8(n){const e=n.slice(),t=j.toArray(e[3]);e[16]=t;const i=e[2]?10:500;return e[17]=i,e}function B8(n){var s,o;const e=n.slice(),t=j.toArray(e[3]);e[9]=t;const i=j.toArray((o=(s=e[0])==null?void 0:s.expand)==null?void 0:o[e[1].name]);e[10]=i;const l=e[2]?20:500;return e[11]=l,e}function U8(n){const e=n.slice(),t=j.trimQuotedValue(JSON.stringify(e[3]))||'""';return e[5]=t,e}function W8(n){let e,t;return{c(){e=b("div"),t=K(n[3]),p(e,"class","block txt-break fallback-block svelte-jdf51v")},m(i,l){w(i,e,l),k(e,t)},p(i,l){l&8&&oe(t,i[3])},i:Q,o:Q,d(i){i&&v(e)}}}function Y8(n){let e,t=j.truncate(n[3])+"",i,l;return{c(){e=b("span"),i=K(t),p(e,"class","txt txt-ellipsis"),p(e,"title",l=j.truncate(n[3]))},m(s,o){w(s,e,o),k(e,i)},p(s,o){o&8&&t!==(t=j.truncate(s[3])+"")&&oe(i,t),o&8&&l!==(l=j.truncate(s[3]))&&p(e,"title",l)},i:Q,o:Q,d(s){s&&v(e)}}}function K8(n){let e,t=[],i=new Map,l,s,o=ue(n[16].slice(0,n[17]));const r=f=>f[8]+f[18];for(let f=0;fn[17]&&Gm();return{c(){var f;e=b("div");for(let u=0;uf[17]?a||(a=Gm(),a.c(),a.m(e,null)):a&&(a.d(1),a=null),(!s||u&2)&&x(e,"multiple",((c=f[1].options)==null?void 0:c.maxSelect)!=1)},i(f){if(!s){for(let u=0;un[11]&&xm();return{c(){e=b("div"),i.c(),l=M(),f&&f.c(),p(e,"class","inline-flex")},m(u,c){w(u,e,c),r[t].m(e,null),k(e,l),f&&f.m(e,null),s=!0},p(u,c){let d=t;t=a(u),t===d?r[t].p(u,c):(le(),A(r[d],1,1,()=>{r[d]=null}),se(),i=r[t],i?i.p(u,c):(i=r[t]=o[t](u),i.c()),E(i,1),i.m(e,l)),u[9].length>u[11]?f||(f=xm(),f.c(),f.m(e,null)):f&&(f.d(1),f=null)},i(u){s||(E(i),s=!0)},o(u){A(i),s=!1},d(u){u&&v(e),r[t].d(),f&&f.d()}}}function Z8(n){let e,t=[],i=new Map,l=ue(j.toArray(n[3]));const s=o=>o[8]+o[6];for(let o=0;o{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function Q8(n){let e,t=j.truncate(n[3])+"",i,l,s;return{c(){e=b("a"),i=K(t),p(e,"class","txt-ellipsis"),p(e,"href",n[3]),p(e,"target","_blank"),p(e,"rel","noopener noreferrer")},m(o,r){w(o,e,r),k(e,i),l||(s=[Se(Pe.call(null,e,"Open in new tab")),J(e,"click",Tn(n[4]))],l=!0)},p(o,r){r&8&&t!==(t=j.truncate(o[3])+"")&&oe(i,t),r&8&&p(e,"href",o[3])},i:Q,o:Q,d(o){o&&v(e),l=!1,$e(s)}}}function x8(n){let e,t;return{c(){e=b("span"),t=K(n[3]),p(e,"class","txt")},m(i,l){w(i,e,l),k(e,t)},p(i,l){l&8&&oe(t,i[3])},i:Q,o:Q,d(i){i&&v(e)}}}function eD(n){let e,t=n[3]?"True":"False",i;return{c(){e=b("span"),i=K(t),p(e,"class","txt")},m(l,s){w(l,e,s),k(e,i)},p(l,s){s&8&&t!==(t=l[3]?"True":"False")&&oe(i,t)},i:Q,o:Q,d(l){l&&v(e)}}}function tD(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function nD(n){let e,t,i,l;const s=[aD,rD],o=[];function r(a,f){return a[2]?0:1}return e=r(n),t=o[e]=s[e](n),{c(){t.c(),i=ye()},m(a,f){o[e].m(a,f),w(a,i,f),l=!0},p(a,f){let u=e;e=r(a),e===u?o[e].p(a,f):(le(),A(o[u],1,1,()=>{o[u]=null}),se(),t=o[e],t?t.p(a,f):(t=o[e]=s[e](a),t.c()),E(t,1),t.m(i.parentNode,i))},i(a){l||(E(t),l=!0)},o(a){A(t),l=!1},d(a){a&&v(i),o[e].d(a)}}}function Zm(n,e){let t,i,l;return i=new Ua({props:{record:e[0],filename:e[18],size:"sm"}}),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(s,o){w(s,t,o),z(i,s,o),l=!0},p(s,o){e=s;const r={};o&1&&(r.record=e[0]),o&12&&(r.filename=e[18]),i.$set(r)},i(s){l||(E(i.$$.fragment,s),l=!0)},o(s){A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(i,s)}}}function Gm(n){let e;return{c(){e=K("...")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function iD(n){let e,t=ue(n[9].slice(0,n[11])),i=[];for(let l=0;lr[8]+r[6];for(let r=0;r500&&th(n);return{c(){e=b("span"),i=K(t),l=M(),r&&r.c(),s=ye(),p(e,"class","txt")},m(a,f){w(a,e,f),k(e,i),w(a,l,f),r&&r.m(a,f),w(a,s,f),o=!0},p(a,f){(!o||f&8)&&t!==(t=j.truncate(a[5],500,!0)+"")&&oe(i,t),a[5].length>500?r?(r.p(a,f),f&8&&E(r,1)):(r=th(a),r.c(),E(r,1),r.m(s.parentNode,s)):r&&(le(),A(r,1,1,()=>{r=null}),se())},i(a){o||(E(r),o=!0)},o(a){A(r),o=!1},d(a){a&&(v(e),v(l),v(s)),r&&r.d(a)}}}function aD(n){let e,t=j.truncate(n[5])+"",i;return{c(){e=b("span"),i=K(t),p(e,"class","txt txt-ellipsis")},m(l,s){w(l,e,s),k(e,i)},p(l,s){s&8&&t!==(t=j.truncate(l[5])+"")&&oe(i,t)},i:Q,o:Q,d(l){l&&v(e)}}}function th(n){let e,t;return e=new sl({props:{value:JSON.stringify(n[3],null,2)}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&8&&(s.value=JSON.stringify(i[3],null,2)),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function fD(n){let e,t,i,l,s;const o=[nD,tD,eD,x8,Q8,X8,G8,Z8,J8,K8,Y8,W8],r=[];function a(u,c){return c&8&&(e=null),u[1].type==="json"?0:(e==null&&(e=!!j.isEmpty(u[3])),e?1:u[1].type==="bool"?2:u[1].type==="number"?3:u[1].type==="url"?4:u[1].type==="editor"?5:u[1].type==="date"?6:u[1].type==="select"?7:u[1].type==="relation"?8:u[1].type==="file"?9:u[2]?10:11)}function f(u,c){return c===0?U8(u):c===8?B8(u):c===9?V8(u):u}return t=a(n,-1),i=r[t]=o[t](f(n,t)),{c(){i.c(),l=ye()},m(u,c){r[t].m(u,c),w(u,l,c),s=!0},p(u,[c]){let d=t;t=a(u,c),t===d?r[t].p(f(u,t),c):(le(),A(r[d],1,1,()=>{r[d]=null}),se(),i=r[t],i?i.p(f(u,t),c):(i=r[t]=o[t](f(u,t)),i.c()),E(i,1),i.m(l.parentNode,l))},i(u){s||(E(i),s=!0)},o(u){A(i),s=!1},d(u){u&&v(l),r[t].d(u)}}}function uD(n,e,t){let i,{record:l}=e,{field:s}=e,{short:o=!1}=e;function r(a){Ce.call(this,n,a)}return n.$$set=a=>{"record"in a&&t(0,l=a.record),"field"in a&&t(1,s=a.field),"short"in a&&t(2,o=a.short)},n.$$.update=()=>{n.$$.dirty&3&&t(3,i=l==null?void 0:l[s.name])},[l,s,o,i,r]}class Gb extends ge{constructor(e){super(),_e(this,e,uD,fD,me,{record:0,field:1,short:2})}}function nh(n,e,t){const i=n.slice();return i[13]=e[t],i}function ih(n){let e,t,i=n[13].name+"",l,s,o,r,a;return r=new Gb({props:{field:n[13],record:n[3]}}),{c(){e=b("tr"),t=b("td"),l=K(i),s=M(),o=b("td"),B(r.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(o,"class","col-field svelte-1nt58f7")},m(f,u){w(f,e,u),k(e,t),k(t,l),k(e,s),k(e,o),z(r,o,null),a=!0},p(f,u){(!a||u&1)&&i!==(i=f[13].name+"")&&oe(l,i);const c={};u&1&&(c.field=f[13]),u&8&&(c.record=f[3]),r.$set(c)},i(f){a||(E(r.$$.fragment,f),a=!0)},o(f){A(r.$$.fragment,f),a=!1},d(f){f&&v(e),V(r)}}}function lh(n){let e,t,i,l,s,o;return s=new el({props:{date:n[3].created}}),{c(){e=b("tr"),t=b("td"),t.textContent="created",i=M(),l=b("td"),B(s.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(l,"class","col-field svelte-1nt58f7")},m(r,a){w(r,e,a),k(e,t),k(e,i),k(e,l),z(s,l,null),o=!0},p(r,a){const f={};a&8&&(f.date=r[3].created),s.$set(f)},i(r){o||(E(s.$$.fragment,r),o=!0)},o(r){A(s.$$.fragment,r),o=!1},d(r){r&&v(e),V(s)}}}function sh(n){let e,t,i,l,s,o;return s=new el({props:{date:n[3].updated}}),{c(){e=b("tr"),t=b("td"),t.textContent="updated",i=M(),l=b("td"),B(s.$$.fragment),p(t,"class","min-width txt-hint txt-bold"),p(l,"class","col-field svelte-1nt58f7")},m(r,a){w(r,e,a),k(e,t),k(e,i),k(e,l),z(s,l,null),o=!0},p(r,a){const f={};a&8&&(f.date=r[3].updated),s.$set(f)},i(r){o||(E(s.$$.fragment,r),o=!0)},o(r){A(s.$$.fragment,r),o=!1},d(r){r&&v(e),V(s)}}}function cD(n){var O;let e,t,i,l,s,o,r,a,f,u,c=(n[3].id||"...")+"",d,m,h,_,g;a=new sl({props:{value:n[3].id}});let y=ue((O=n[0])==null?void 0:O.schema),S=[];for(let D=0;DA(S[D],1,1,()=>{S[D]=null});let $=n[3].created&&lh(n),C=n[3].updated&&sh(n);return{c(){e=b("table"),t=b("tbody"),i=b("tr"),l=b("td"),l.textContent="id",s=M(),o=b("td"),r=b("div"),B(a.$$.fragment),f=M(),u=b("span"),d=K(c),m=M();for(let D=0;D{$=null}),se()),D[3].updated?C?(C.p(D,I),I&8&&E(C,1)):(C=sh(D),C.c(),E(C,1),C.m(t,null)):C&&(le(),A(C,1,1,()=>{C=null}),se()),(!g||I&16)&&x(e,"table-loading",D[4])},i(D){if(!g){E(a.$$.fragment,D);for(let I=0;IClose',p(e,"type","button"),p(e,"class","btn btn-transparent")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[7]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function mD(n){let e,t,i={class:"record-preview-panel "+(n[5]?"overlay-panel-xl":"overlay-panel-lg"),$$slots:{footer:[pD],header:[dD],default:[cD]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[8](e),e.$on("hide",n[9]),e.$on("show",n[10]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&32&&(o.class="record-preview-panel "+(l[5]?"overlay-panel-xl":"overlay-panel-lg")),s&65561&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[8](null),V(e,l)}}}function hD(n,e,t){let i,{collection:l}=e,s,o={},r=!1;function a(g){return u(g),s==null?void 0:s.show()}function f(){return t(4,r=!1),s==null?void 0:s.hide()}async function u(g){t(3,o={}),t(4,r=!0),t(3,o=await c(g)||{}),t(4,r=!1)}async function c(g){if(g&&typeof g=="string"){try{return await ae.collection(l.id).getOne(g)}catch(y){y.isAbort||(f(),console.warn("resolveModel:",y),ii(`Unable to load record with id "${g}"`))}return null}return g}const d=()=>f();function m(g){ee[g?"unshift":"push"](()=>{s=g,t(2,s)})}function h(g){Ce.call(this,n,g)}function _(g){Ce.call(this,n,g)}return n.$$set=g=>{"collection"in g&&t(0,l=g.collection)},n.$$.update=()=>{var g;n.$$.dirty&1&&t(5,i=!!((g=l==null?void 0:l.schema)!=null&&g.find(y=>y.type==="editor")))},[l,f,s,o,r,i,a,d,m,h,_]}class _D extends ge{constructor(e){super(),_e(this,e,hD,mD,me,{collection:0,show:6,hide:1})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[1]}}function oh(n,e,t){const i=n.slice();return i[63]=e[t],i}function rh(n,e,t){const i=n.slice();return i[66]=e[t],i}function ah(n,e,t){const i=n.slice();return i[66]=e[t],i}function fh(n,e,t){const i=n.slice();return i[59]=e[t],i}function uh(n){let e;function t(s,o){return s[13]?bD:gD}let i=t(n),l=i(n);return{c(){e=b("th"),l.c(),p(e,"class","bulk-select-col min-width")},m(s,o){w(s,e,o),l.m(e,null)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e,null)))},d(s){s&&v(e),l.d()}}}function gD(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),t=b("input"),l=M(),s=b("label"),p(t,"type","checkbox"),p(t,"id","checkbox_0"),t.disabled=i=!n[3].length,t.checked=n[17],p(s,"for","checkbox_0"),p(e,"class","form-field")},m(a,f){w(a,e,f),k(e,t),k(e,l),k(e,s),o||(r=J(t,"change",n[32]),o=!0)},p(a,f){f[0]&8&&i!==(i=!a[3].length)&&(t.disabled=i),f[0]&131072&&(t.checked=a[17])},d(a){a&&v(e),o=!1,r()}}}function bD(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function ch(n){let e,t,i;function l(o){n[33](o)}let s={class:"col-type-text col-field-id",name:"id",$$slots:{default:[kD]},$$scope:{ctx:n}};return n[0]!==void 0&&(s.sort=n[0]),e=new Sn({props:s}),ee.push(()=>be(e,"sort",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[2]&512&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&1&&(t=!0,a.sort=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function kD(n){let e;return{c(){e=b("div"),e.innerHTML=` id`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function dh(n){let e=!n[5].includes("@username"),t,i=!n[5].includes("@email"),l,s,o=e&&ph(n),r=i&&mh(n);return{c(){o&&o.c(),t=M(),r&&r.c(),l=ye()},m(a,f){o&&o.m(a,f),w(a,t,f),r&&r.m(a,f),w(a,l,f),s=!0},p(a,f){f[0]&32&&(e=!a[5].includes("@username")),e?o?(o.p(a,f),f[0]&32&&E(o,1)):(o=ph(a),o.c(),E(o,1),o.m(t.parentNode,t)):o&&(le(),A(o,1,1,()=>{o=null}),se()),f[0]&32&&(i=!a[5].includes("@email")),i?r?(r.p(a,f),f[0]&32&&E(r,1)):(r=mh(a),r.c(),E(r,1),r.m(l.parentNode,l)):r&&(le(),A(r,1,1,()=>{r=null}),se())},i(a){s||(E(o),E(r),s=!0)},o(a){A(o),A(r),s=!1},d(a){a&&(v(t),v(l)),o&&o.d(a),r&&r.d(a)}}}function ph(n){let e,t,i;function l(o){n[34](o)}let s={class:"col-type-text col-field-id",name:"username",$$slots:{default:[yD]},$$scope:{ctx:n}};return n[0]!==void 0&&(s.sort=n[0]),e=new Sn({props:s}),ee.push(()=>be(e,"sort",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[2]&512&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&1&&(t=!0,a.sort=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function yD(n){let e;return{c(){e=b("div"),e.innerHTML=` username`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function mh(n){let e,t,i;function l(o){n[35](o)}let s={class:"col-type-email col-field-email",name:"email",$$slots:{default:[vD]},$$scope:{ctx:n}};return n[0]!==void 0&&(s.sort=n[0]),e=new Sn({props:s}),ee.push(()=>be(e,"sort",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[2]&512&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&1&&(t=!0,a.sort=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function vD(n){let e;return{c(){e=b("div"),e.innerHTML=` email`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function wD(n){let e,t,i,l,s,o=n[66].name+"",r;return{c(){e=b("div"),t=b("i"),l=M(),s=b("span"),r=K(o),p(t,"class",i=j.getFieldTypeIcon(n[66].type)),p(s,"class","txt"),p(e,"class","col-header-content")},m(a,f){w(a,e,f),k(e,t),k(e,l),k(e,s),k(s,r)},p(a,f){f[0]&524288&&i!==(i=j.getFieldTypeIcon(a[66].type))&&p(t,"class",i),f[0]&524288&&o!==(o=a[66].name+"")&&oe(r,o)},d(a){a&&v(e)}}}function hh(n,e){let t,i,l,s;function o(a){e[36](a)}let r={class:"col-type-"+e[66].type+" col-field-"+e[66].name,name:e[66].name,$$slots:{default:[wD]},$$scope:{ctx:e}};return e[0]!==void 0&&(r.sort=e[0]),i=new Sn({props:r}),ee.push(()=>be(i,"sort",o)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),z(i,a,f),s=!0},p(a,f){e=a;const u={};f[0]&524288&&(u.class="col-type-"+e[66].type+" col-field-"+e[66].name),f[0]&524288&&(u.name=e[66].name),f[0]&524288|f[2]&512&&(u.$$scope={dirty:f,ctx:e}),!l&&f[0]&1&&(l=!0,u.sort=e[0],ke(()=>l=!1)),i.$set(u)},i(a){s||(E(i.$$.fragment,a),s=!0)},o(a){A(i.$$.fragment,a),s=!1},d(a){a&&v(t),V(i,a)}}}function _h(n){let e,t,i;function l(o){n[37](o)}let s={class:"col-type-date col-field-created",name:"created",$$slots:{default:[SD]},$$scope:{ctx:n}};return n[0]!==void 0&&(s.sort=n[0]),e=new Sn({props:s}),ee.push(()=>be(e,"sort",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[2]&512&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&1&&(t=!0,a.sort=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function SD(n){let e;return{c(){e=b("div"),e.innerHTML=` created`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function gh(n){let e,t,i;function l(o){n[38](o)}let s={class:"col-type-date col-field-updated",name:"updated",$$slots:{default:[$D]},$$scope:{ctx:n}};return n[0]!==void 0&&(s.sort=n[0]),e=new Sn({props:s}),ee.push(()=>be(e,"sort",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[2]&512&&(a.$$scope={dirty:r,ctx:o}),!t&&r[0]&1&&(t=!0,a.sort=o[0],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function $D(n){let e;return{c(){e=b("div"),e.innerHTML=` updated`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function bh(n){let e;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Toggle columns"),p(e,"class","btn btn-sm btn-transparent p-0")},m(t,i){w(t,e,i),n[39](e)},p:Q,d(t){t&&v(e),n[39](null)}}}function kh(n){let e;function t(s,o){return s[13]?CD:TD}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function TD(n){let e,t,i,l;function s(a,f){var u;if((u=a[1])!=null&&u.length)return MD;if(!a[10])return OD}let o=s(n),r=o&&o(n);return{c(){e=b("tr"),t=b("td"),i=b("h6"),i.textContent="No records found.",l=M(),r&&r.c(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),k(e,t),k(t,i),k(t,l),r&&r.m(t,null)},p(a,f){o===(o=s(a))&&r?r.p(a,f):(r&&r.d(1),r=o&&o(a),r&&(r.c(),r.m(t,null)))},d(a){a&&v(e),r&&r.d()}}}function CD(n){let e;return{c(){e=b("tr"),e.innerHTML=''},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function OD(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' New record',p(e,"type","button"),p(e,"class","btn btn-secondary btn-expanded m-t-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[44]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function MD(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[43]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function yh(n){let e,t,i,l,s,o,r,a,f,u;function c(){return n[40](n[63])}return{c(){e=b("td"),t=b("div"),i=b("input"),o=M(),r=b("label"),p(i,"type","checkbox"),p(i,"id",l="checkbox_"+n[63].id),i.checked=s=n[4][n[63].id],p(r,"for",a="checkbox_"+n[63].id),p(t,"class","form-field"),p(e,"class","bulk-select-col min-width")},m(d,m){w(d,e,m),k(e,t),k(t,i),k(t,o),k(t,r),f||(u=[J(i,"change",c),J(t,"click",Tn(n[30]))],f=!0)},p(d,m){n=d,m[0]&8&&l!==(l="checkbox_"+n[63].id)&&p(i,"id",l),m[0]&24&&s!==(s=n[4][n[63].id])&&(i.checked=s),m[0]&8&&a!==(a="checkbox_"+n[63].id)&&p(r,"for",a)},d(d){d&&v(e),f=!1,$e(u)}}}function vh(n){let e,t,i,l,s,o,r=n[63].id+"",a,f,u;l=new sl({props:{value:n[63].id}});let c=n[9]&&wh(n);return{c(){e=b("td"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),a=K(r),f=M(),c&&c.c(),p(o,"class","txt txt-ellipsis"),p(i,"class","label"),p(t,"class","flex flex-gap-5"),p(e,"class","col-type-text col-field-id")},m(d,m){w(d,e,m),k(e,t),k(t,i),z(l,i,null),k(i,s),k(i,o),k(o,a),k(t,f),c&&c.m(t,null),u=!0},p(d,m){const h={};m[0]&8&&(h.value=d[63].id),l.$set(h),(!u||m[0]&8)&&r!==(r=d[63].id+"")&&oe(a,r),d[9]?c?c.p(d,m):(c=wh(d),c.c(),c.m(t,null)):c&&(c.d(1),c=null)},i(d){u||(E(l.$$.fragment,d),u=!0)},o(d){A(l.$$.fragment,d),u=!1},d(d){d&&v(e),V(l),c&&c.d()}}}function wh(n){let e;function t(s,o){return s[63].verified?ED:DD}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i!==(i=t(s))&&(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function DD(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-sm txt-hint")},m(l,s){w(l,e,s),t||(i=Se(Pe.call(null,e,"Unverified")),t=!0)},d(l){l&&v(e),t=!1,i()}}}function ED(n){let e,t,i;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-fill txt-sm txt-success")},m(l,s){w(l,e,s),t||(i=Se(Pe.call(null,e,"Verified")),t=!0)},d(l){l&&v(e),t=!1,i()}}}function Sh(n){let e=!n[5].includes("@username"),t,i=!n[5].includes("@email"),l,s=e&&$h(n),o=i&&Th(n);return{c(){s&&s.c(),t=M(),o&&o.c(),l=ye()},m(r,a){s&&s.m(r,a),w(r,t,a),o&&o.m(r,a),w(r,l,a)},p(r,a){a[0]&32&&(e=!r[5].includes("@username")),e?s?s.p(r,a):(s=$h(r),s.c(),s.m(t.parentNode,t)):s&&(s.d(1),s=null),a[0]&32&&(i=!r[5].includes("@email")),i?o?o.p(r,a):(o=Th(r),o.c(),o.m(l.parentNode,l)):o&&(o.d(1),o=null)},d(r){r&&(v(t),v(l)),s&&s.d(r),o&&o.d(r)}}}function $h(n){let e,t;function i(o,r){return r[0]&8&&(t=null),t==null&&(t=!!j.isEmpty(o[63].username)),t?AD:ID}let l=i(n,[-1,-1,-1]),s=l(n);return{c(){e=b("td"),s.c(),p(e,"class","col-type-text col-field-username")},m(o,r){w(o,e,r),s.m(e,null)},p(o,r){l===(l=i(o,r))&&s?s.p(o,r):(s.d(1),s=l(o),s&&(s.c(),s.m(e,null)))},d(o){o&&v(e),s.d()}}}function ID(n){let e,t=n[63].username+"",i,l;return{c(){e=b("span"),i=K(t),p(e,"class","txt txt-ellipsis"),p(e,"title",l=n[63].username)},m(s,o){w(s,e,o),k(e,i)},p(s,o){o[0]&8&&t!==(t=s[63].username+"")&&oe(i,t),o[0]&8&&l!==(l=s[63].username)&&p(e,"title",l)},d(s){s&&v(e)}}}function AD(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Th(n){let e,t;function i(o,r){return r[0]&8&&(t=null),t==null&&(t=!!j.isEmpty(o[63].email)),t?ND:LD}let l=i(n,[-1,-1,-1]),s=l(n);return{c(){e=b("td"),s.c(),p(e,"class","col-type-text col-field-email")},m(o,r){w(o,e,r),s.m(e,null)},p(o,r){l===(l=i(o,r))&&s?s.p(o,r):(s.d(1),s=l(o),s&&(s.c(),s.m(e,null)))},d(o){o&&v(e),s.d()}}}function LD(n){let e,t=n[63].email+"",i,l;return{c(){e=b("span"),i=K(t),p(e,"class","txt txt-ellipsis"),p(e,"title",l=n[63].email)},m(s,o){w(s,e,o),k(e,i)},p(s,o){o[0]&8&&t!==(t=s[63].email+"")&&oe(i,t),o[0]&8&&l!==(l=s[63].email)&&p(e,"title",l)},d(s){s&&v(e)}}}function ND(n){let e;return{c(){e=b("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Ch(n,e){let t,i,l,s;return i=new Gb({props:{short:!0,record:e[63],field:e[66]}}),{key:n,first:null,c(){t=b("td"),B(i.$$.fragment),p(t,"class",l="col-type-"+e[66].type+" col-field-"+e[66].name),this.first=t},m(o,r){w(o,t,r),z(i,t,null),s=!0},p(o,r){e=o;const a={};r[0]&8&&(a.record=e[63]),r[0]&524288&&(a.field=e[66]),i.$set(a),(!s||r[0]&524288&&l!==(l="col-type-"+e[66].type+" col-field-"+e[66].name))&&p(t,"class",l)},i(o){s||(E(i.$$.fragment,o),s=!0)},o(o){A(i.$$.fragment,o),s=!1},d(o){o&&v(t),V(i)}}}function Oh(n){let e,t,i;return t=new el({props:{date:n[63].created}}),{c(){e=b("td"),B(t.$$.fragment),p(e,"class","col-type-date col-field-created")},m(l,s){w(l,e,s),z(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.date=l[63].created),t.$set(o)},i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function Mh(n){let e,t,i;return t=new el({props:{date:n[63].updated}}),{c(){e=b("td"),B(t.$$.fragment),p(e,"class","col-type-date col-field-updated")},m(l,s){w(l,e,s),z(t,e,null),i=!0},p(l,s){const o={};s[0]&8&&(o.date=l[63].updated),t.$set(o)},i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function Dh(n,e){let t,i,l=!e[5].includes("@id"),s,o,r=[],a=new Map,f,u=e[8]&&!e[5].includes("@created"),c,d=e[7]&&!e[5].includes("@updated"),m,h,_,g,y,S=!e[10]&&yh(e),T=l&&vh(e),$=e[9]&&Sh(e),C=ue(e[19]);const O=F=>F[66].name;for(let F=0;F',p(h,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(F,N){w(F,t,N),S&&S.m(t,null),k(t,i),T&&T.m(t,null),k(t,s),$&&$.m(t,null),k(t,o);for(let P=0;P{T=null}),se()),e[9]?$?$.p(e,N):($=Sh(e),$.c(),$.m(t,o)):$&&($.d(1),$=null),N[0]&524296&&(C=ue(e[19]),le(),r=at(r,N,O,1,e,C,a,t,Et,Ch,f,rh),se()),N[0]&288&&(u=e[8]&&!e[5].includes("@created")),u?D?(D.p(e,N),N[0]&288&&E(D,1)):(D=Oh(e),D.c(),E(D,1),D.m(t,c)):D&&(le(),A(D,1,1,()=>{D=null}),se()),N[0]&160&&(d=e[7]&&!e[5].includes("@updated")),d?I?(I.p(e,N),N[0]&160&&E(I,1)):(I=Mh(e),I.c(),E(I,1),I.m(t,m)):I&&(le(),A(I,1,1,()=>{I=null}),se())},i(F){if(!_){E(T);for(let N=0;NU[66].name;for(let U=0;UU[10]?U[63]:U[63].id;for(let U=0;U{D=null}),se()),U[9]?I?(I.p(U,Y),Y[0]&512&&E(I,1)):(I=dh(U),I.c(),E(I,1),I.m(i,r)):I&&(le(),A(I,1,1,()=>{I=null}),se()),Y[0]&524289&&(L=ue(U[19]),le(),a=at(a,Y,R,1,U,L,f,i,Et,hh,u,ah),se()),Y[0]&288&&(c=U[8]&&!U[5].includes("@created")),c?F?(F.p(U,Y),Y[0]&288&&E(F,1)):(F=_h(U),F.c(),E(F,1),F.m(i,d)):F&&(le(),A(F,1,1,()=>{F=null}),se()),Y[0]&160&&(m=U[7]&&!U[5].includes("@updated")),m?N?(N.p(U,Y),Y[0]&160&&E(N,1)):(N=gh(U),N.c(),E(N,1),N.m(i,h)):N&&(le(),A(N,1,1,()=>{N=null}),se()),U[16].length?P?P.p(U,Y):(P=bh(U),P.c(),P.m(_,null)):P&&(P.d(1),P=null),Y[0]&9971642&&(q=ue(U[3]),le(),S=at(S,Y,H,1,U,q,T,y,Et,Dh,$,oh),se(),!q.length&&W?W.p(U,Y):q.length?W&&(W.d(1),W=null):(W=kh(U),W.c(),W.m(y,$))),U[3].length&&U[18]?G?G.p(U,Y):(G=Eh(U),G.c(),G.m(y,null)):G&&(G.d(1),G=null),(!C||Y[0]&8192)&&x(e,"table-loading",U[13])},i(U){if(!C){E(D),E(I);for(let Y=0;Y({62:s}),({uniqueId:s})=>[0,0,s?1:0]]},$$scope:{ctx:e}}}),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(s,o){w(s,t,o),z(i,s,o),l=!0},p(s,o){e=s;const r={};o[0]&65568|o[2]&513&&(r.$$scope={dirty:o,ctx:e}),i.$set(r)},i(s){l||(E(i.$$.fragment,s),l=!0)},o(s){A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(i,s)}}}function RD(n){let e,t,i=[],l=new Map,s,o,r=ue(n[16]);const a=f=>f[59].id+f[59].name;for(let f=0;f{i=null}),se())},i(l){t||(E(i),t=!0)},o(l){A(i),t=!1},d(l){l&&v(e),i&&i.d(l)}}}function Lh(n){let e,t,i,l,s,o,r=n[6]===1?"record":"records",a,f,u,c,d,m,h,_,g,y,S;return{c(){e=b("div"),t=b("div"),i=K("Selected "),l=b("strong"),s=K(n[6]),o=M(),a=K(r),f=M(),u=b("button"),u.innerHTML='Reset',c=M(),d=b("div"),m=M(),h=b("button"),h.innerHTML='Delete selected',p(t,"class","txt"),p(u,"type","button"),p(u,"class","btn btn-xs btn-transparent btn-outline p-l-5 p-r-5"),x(u,"btn-disabled",n[14]),p(d,"class","flex-fill"),p(h,"type","button"),p(h,"class","btn btn-sm btn-transparent btn-danger"),x(h,"btn-loading",n[14]),x(h,"btn-disabled",n[14]),p(e,"class","bulkbar")},m(T,$){w(T,e,$),k(e,t),k(t,i),k(t,l),k(l,s),k(t,o),k(t,a),k(e,f),k(e,u),k(e,c),k(e,d),k(e,m),k(e,h),g=!0,y||(S=[J(u,"click",n[47]),J(h,"click",n[48])],y=!0)},p(T,$){(!g||$[0]&64)&&oe(s,T[6]),(!g||$[0]&64)&&r!==(r=T[6]===1?"record":"records")&&oe(a,r),(!g||$[0]&16384)&&x(u,"btn-disabled",T[14]),(!g||$[0]&16384)&&x(h,"btn-loading",T[14]),(!g||$[0]&16384)&&x(h,"btn-disabled",T[14])},i(T){g||(T&&Ke(()=>{g&&(_||(_=Fe(e,Fn,{duration:150,y:5},!0)),_.run(1))}),g=!0)},o(T){T&&(_||(_=Fe(e,Fn,{duration:150,y:5},!1)),_.run(0)),g=!1},d(T){T&&v(e),T&&_&&_.end(),y=!1,$e(S)}}}function jD(n){let e,t,i,l,s={class:"table-wrapper",$$slots:{before:[qD],default:[PD]},$$scope:{ctx:n}};e=new Go({props:s}),n[46](e);let o=n[6]&&Lh(n);return{c(){B(e.$$.fragment),t=M(),o&&o.c(),i=ye()},m(r,a){z(e,r,a),w(r,t,a),o&&o.m(r,a),w(r,i,a),l=!0},p(r,a){const f={};a[0]&1030075|a[2]&512&&(f.$$scope={dirty:a,ctx:r}),e.$set(f),r[6]?o?(o.p(r,a),a[0]&64&&E(o,1)):(o=Lh(r),o.c(),E(o,1),o.m(i.parentNode,i)):o&&(le(),A(o,1,1,()=>{o=null}),se())},i(r){l||(E(e.$$.fragment,r),E(o),l=!0)},o(r){A(e.$$.fragment,r),A(o),l=!1},d(r){r&&(v(t),v(i)),n[46](null),V(e,r),o&&o.d(r)}}}const HD=/^([\+\-])?(\w+)$/,Nh=40;function zD(n,e,t){let i,l,s,o,r,a,f,u,c,d,m,h;Ue(n,Rn,Ee=>t(53,h=Ee));const _=lt();let{collection:g}=e,{sort:y=""}=e,{filter:S=""}=e,T,$=[],C=1,O=0,D={},I=!0,L=!1,R=0,F,N=[],P=[],q="";function H(){g!=null&&g.id&&(N.length?localStorage.setItem(q,JSON.stringify(N)):localStorage.removeItem(q))}function W(){if(t(5,N=[]),!!(g!=null&&g.id))try{const Ee=localStorage.getItem(q);Ee&&t(5,N=JSON.parse(Ee)||[])}catch{}}function G(Ee){return!!$.find(Nt=>Nt.id)}async function U(){const Ee=C;for(let Nt=1;Nt<=Ee;Nt++)(Nt===1||f)&&await Y(Nt,!1)}async function Y(Ee=1,Nt=!0){var al,fl,bt;if(!(g!=null&&g.id))return;t(13,I=!0);let Li=y;const Kn=Li.match(HD),rl=Kn?r.find(rt=>rt.name===Kn[2]):null;if(Kn&&rl){const rt=((bt=(fl=(al=h==null?void 0:h.find(he=>{var Oe;return he.id==((Oe=rl.options)==null?void 0:Oe.collectionId)}))==null?void 0:al.schema)==null?void 0:fl.filter(he=>he.presentable))==null?void 0:bt.map(he=>he.name))||[],Mn=[];for(const he of rt)Mn.push((Kn[1]||"")+Kn[2]+"."+he);Mn.length>0&&(Li=Mn.join(","))}const Pl=j.getAllCollectionIdentifiers(g),bi=o.map(rt=>rt.name+":excerpt(200)").concat(r.map(rt=>"expand."+rt.name+".*:excerpt(200)"));return bi.length&&bi.unshift("*"),ae.collection(g.id).getList(Ee,Nh,{sort:Li,skipTotal:1,filter:j.normalizeSearchFilter(S,Pl),expand:r.map(rt=>rt.name).join(","),fields:bi.join(","),requestKey:"records_list"}).then(async rt=>{var Mn;if(Ee<=1&&ie(),t(13,I=!1),t(12,C=rt.page),t(28,O=rt.items.length),_("load",$.concat(rt.items)),o.length)for(let he of rt.items)he._partial=!0;if(Nt){const he=++R;for(;(Mn=rt.items)!=null&&Mn.length&&R==he;){const Oe=rt.items.splice(0,20);for(let ht of Oe)j.pushOrReplaceByKey($,ht);t(3,$),await j.yieldToMain()}}else{for(let he of rt.items)j.pushOrReplaceByKey($,he);t(3,$)}}).catch(rt=>{rt!=null&&rt.isAbort||(t(13,I=!1),console.warn(rt),ie(),ae.error(rt,!S||(rt==null?void 0:rt.status)!=400))})}function ie(){T==null||T.resetVerticalScroll(),t(3,$=[]),t(12,C=1),t(28,O=0),t(4,D={})}function te(){c?pe():Ne()}function pe(){t(4,D={})}function Ne(){for(const Ee of $)t(4,D[Ee.id]=Ee,D);t(4,D)}function He(Ee){D[Ee.id]?delete D[Ee.id]:t(4,D[Ee.id]=Ee,D),t(4,D)}function Xe(){fn(`Do you really want to delete the selected ${u===1?"record":"records"}?`,xe)}async function xe(){if(L||!u||!(g!=null&&g.id))return;let Ee=[];for(const Nt of Object.keys(D))Ee.push(ae.collection(g.id).delete(Nt));return t(14,L=!0),Promise.all(Ee).then(()=>{Lt(`Successfully deleted the selected ${u===1?"record":"records"}.`),_("delete",D),pe()}).catch(Nt=>{ae.error(Nt)}).finally(()=>(t(14,L=!1),U()))}function Mt(Ee){Ce.call(this,n,Ee)}const ft=(Ee,Nt)=>{Nt.target.checked?j.removeByValue(N,Ee.id):j.pushUnique(N,Ee.id),t(5,N)},mt=()=>te();function Gt(Ee){y=Ee,t(0,y)}function De(Ee){y=Ee,t(0,y)}function Ae(Ee){y=Ee,t(0,y)}function ze(Ee){y=Ee,t(0,y)}function gt(Ee){y=Ee,t(0,y)}function de(Ee){y=Ee,t(0,y)}function ve(Ee){ee[Ee?"unshift":"push"](()=>{F=Ee,t(15,F)})}const we=Ee=>He(Ee),Ye=Ee=>_("select",Ee),zt=(Ee,Nt)=>{Nt.code==="Enter"&&(Nt.preventDefault(),_("select",Ee))},cn=()=>t(1,S=""),rn=()=>_("new"),qn=()=>Y(C+1);function Ai(Ee){ee[Ee?"unshift":"push"](()=>{T=Ee,t(11,T)})}const ol=()=>pe(),gi=()=>Xe();return n.$$set=Ee=>{"collection"in Ee&&t(25,g=Ee.collection),"sort"in Ee&&t(0,y=Ee.sort),"filter"in Ee&&t(1,S=Ee.filter)},n.$$.update=()=>{n.$$.dirty[0]&33554432&&g!=null&&g.id&&(q=g.id+"@hiddenColumns",W(),ie()),n.$$.dirty[0]&33554432&&t(10,i=(g==null?void 0:g.type)==="view"),n.$$.dirty[0]&33554432&&t(9,l=(g==null?void 0:g.type)==="auth"),n.$$.dirty[0]&33554432&&t(29,s=(g==null?void 0:g.schema)||[]),n.$$.dirty[0]&536870912&&(o=s.filter(Ee=>Ee.type==="editor")),n.$$.dirty[0]&536870912&&(r=s.filter(Ee=>Ee.type==="relation")),n.$$.dirty[0]&536870944&&t(19,a=s.filter(Ee=>!N.includes(Ee.id))),n.$$.dirty[0]&33554435&&g!=null&&g.id&&y!==-1&&S!==-1&&Y(1),n.$$.dirty[0]&268435456&&t(18,f=O>=Nh),n.$$.dirty[0]&16&&t(6,u=Object.keys(D).length),n.$$.dirty[0]&72&&t(17,c=$.length&&u===$.length),n.$$.dirty[0]&32&&N!==-1&&H(),n.$$.dirty[0]&1032&&t(8,d=!i||$.length>0&&typeof $[0].created<"u"),n.$$.dirty[0]&1032&&t(7,m=!i||$.length>0&&typeof $[0].updated<"u"),n.$$.dirty[0]&536871808&&t(16,P=[].concat(l?[{id:"@username",name:"username"},{id:"@email",name:"email"}]:[],s.map(Ee=>({id:Ee.id,name:Ee.name})),d?{id:"@created",name:"created"}:[],m?{id:"@updated",name:"updated"}:[]))},[y,S,Y,$,D,N,u,m,d,l,i,T,C,I,L,F,P,c,f,a,_,te,pe,He,Xe,g,G,U,O,s,Mt,ft,mt,Gt,De,Ae,ze,gt,de,ve,we,Ye,zt,cn,rn,qn,Ai,ol,gi]}class VD extends ge{constructor(e){super(),_e(this,e,zD,jD,me,{collection:25,sort:0,filter:1,hasRecord:26,reloadLoadedPages:27,load:2},null,[-1,-1,-1])}get hasRecord(){return this.$$.ctx[26]}get reloadLoadedPages(){return this.$$.ctx[27]}get load(){return this.$$.ctx[2]}}function BD(n){let e,t,i,l,s=(n[2]?"...":n[0])+"",o,r;return{c(){e=b("div"),t=b("span"),t.textContent="Total found:",i=M(),l=b("span"),o=K(s),p(t,"class","txt"),p(l,"class","txt"),p(e,"class",r="inline-flex flex-gap-5 records-counter "+n[1])},m(a,f){w(a,e,f),k(e,t),k(e,i),k(e,l),k(l,o)},p(a,[f]){f&5&&s!==(s=(a[2]?"...":a[0])+"")&&oe(o,s),f&2&&r!==(r="inline-flex flex-gap-5 records-counter "+a[1])&&p(e,"class",r)},i:Q,o:Q,d(a){a&&v(e)}}}function UD(n,e,t){const i=lt();let{collection:l}=e,{filter:s=""}=e,{totalCount:o=0}=e,{class:r=void 0}=e,a=!1;async function f(){if(l!=null&&l.id){t(2,a=!0),t(0,o=0);try{const u=j.getAllCollectionIdentifiers(l),c=await ae.collection(l.id).getList(1,1,{filter:j.normalizeSearchFilter(s,u),fields:"id",requestKey:"records_count"});t(0,o=c.totalItems),i("count",o),t(2,a=!1)}catch(u){u!=null&&u.isAbort||(t(2,a=!1),console.warn(u))}}}return n.$$set=u=>{"collection"in u&&t(3,l=u.collection),"filter"in u&&t(4,s=u.filter),"totalCount"in u&&t(0,o=u.totalCount),"class"in u&&t(1,r=u.class)},n.$$.update=()=>{n.$$.dirty&24&&l!=null&&l.id&&s!==-1&&f()},[o,r,a,l,s,f]}class WD extends ge{constructor(e){super(),_e(this,e,UD,BD,me,{collection:3,filter:4,totalCount:0,class:1,reload:5})}get reload(){return this.$$.ctx[5]}}function YD(n){let e,t,i,l;return e=new a6({}),i=new bn({props:{class:"flex-content",$$slots:{footer:[GD],default:[ZD]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,o){const r={};o[0]&6135|o[1]&8192&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function KD(n){let e,t;return e=new bn({props:{center:!0,$$slots:{default:[xD]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[0]&4112|l[1]&8192&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function JD(n){let e,t;return e=new bn({props:{center:!0,$$slots:{default:[eE]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[1]&8192&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function Ph(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='',p(e,"type","button"),p(e,"aria-label","Edit collection"),p(e,"class","btn btn-transparent btn-circle")},m(l,s){w(l,e,s),t||(i=[Se(Pe.call(null,e,{text:"Edit collection",position:"right"})),J(e,"click",n[20])],t=!0)},p:Q,d(l){l&&v(e),t=!1,$e(i)}}}function Fh(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' New record',p(e,"type","button"),p(e,"class","btn btn-expanded")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[23]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function ZD(n){let e,t,i,l,s,o=n[2].name+"",r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I,L,R,F=!n[12]&&Ph(n);c=new Zo({}),c.$on("refresh",n[21]);let N=n[2].type!=="view"&&Fh(n);y=new $s({props:{value:n[0],autocompleteCollection:n[2]}}),y.$on("submit",n[24]);function P(W){n[26](W)}function q(W){n[27](W)}let H={collection:n[2]};return n[0]!==void 0&&(H.filter=n[0]),n[1]!==void 0&&(H.sort=n[1]),C=new VD({props:H}),n[25](C),ee.push(()=>be(C,"filter",P)),ee.push(()=>be(C,"sort",q)),C.$on("select",n[28]),C.$on("delete",n[29]),C.$on("new",n[30]),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Collections",l=M(),s=b("div"),r=K(o),a=M(),f=b("div"),F&&F.c(),u=M(),B(c.$$.fragment),d=M(),m=b("div"),h=b("button"),h.innerHTML=' API Preview',_=M(),N&&N.c(),g=M(),B(y.$$.fragment),S=M(),T=b("div"),$=M(),B(C.$$.fragment),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(f,"class","inline-flex gap-5"),p(h,"type","button"),p(h,"class","btn btn-outline"),p(m,"class","btns-group"),p(e,"class","page-header"),p(T,"class","clearfix m-b-sm")},m(W,G){w(W,e,G),k(e,t),k(t,i),k(t,l),k(t,s),k(s,r),k(e,a),k(e,f),F&&F.m(f,null),k(f,u),z(c,f,null),k(e,d),k(e,m),k(m,h),k(m,_),N&&N.m(m,null),w(W,g,G),z(y,W,G),w(W,S,G),w(W,T,G),w(W,$,G),z(C,W,G),I=!0,L||(R=J(h,"click",n[22]),L=!0)},p(W,G){(!I||G[0]&4)&&o!==(o=W[2].name+"")&&oe(r,o),W[12]?F&&(F.d(1),F=null):F?F.p(W,G):(F=Ph(W),F.c(),F.m(f,u)),W[2].type!=="view"?N?N.p(W,G):(N=Fh(W),N.c(),N.m(m,null)):N&&(N.d(1),N=null);const U={};G[0]&1&&(U.value=W[0]),G[0]&4&&(U.autocompleteCollection=W[2]),y.$set(U);const Y={};G[0]&4&&(Y.collection=W[2]),!O&&G[0]&1&&(O=!0,Y.filter=W[0],ke(()=>O=!1)),!D&&G[0]&2&&(D=!0,Y.sort=W[1],ke(()=>D=!1)),C.$set(Y)},i(W){I||(E(c.$$.fragment,W),E(y.$$.fragment,W),E(C.$$.fragment,W),I=!0)},o(W){A(c.$$.fragment,W),A(y.$$.fragment,W),A(C.$$.fragment,W),I=!1},d(W){W&&(v(e),v(g),v(S),v(T),v($)),F&&F.d(),V(c),N&&N.d(),V(y,W),n[25](null),V(C,W),L=!1,R()}}}function GD(n){let e,t,i;function l(o){n[19](o)}let s={class:"m-r-auto txt-sm txt-hint",collection:n[2],filter:n[0]};return n[10]!==void 0&&(s.totalCount=n[10]),e=new WD({props:s}),n[18](e),ee.push(()=>be(e,"totalCount",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};r[0]&4&&(a.collection=o[2]),r[0]&1&&(a.filter=o[0]),!t&&r[0]&1024&&(t=!0,a.totalCount=o[10],ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){n[18](null),V(e,o)}}}function XD(n){let e,t,i,l,s;return{c(){e=b("h1"),e.textContent="Create your first collection to add records!",t=M(),i=b("button"),i.innerHTML=' Create new collection',p(e,"class","m-b-10"),p(i,"type","button"),p(i,"class","btn btn-expanded-lg btn-lg")},m(o,r){w(o,e,r),w(o,t,r),w(o,i,r),l||(s=J(i,"click",n[17]),l=!0)},p:Q,d(o){o&&(v(e),v(t),v(i)),l=!1,s()}}}function QD(n){let e;return{c(){e=b("h1"),e.textContent="You don't have any collections yet.",p(e,"class","m-b-10")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function xD(n){let e,t,i;function l(r,a){return r[12]?QD:XD}let s=l(n),o=s(n);return{c(){e=b("div"),t=b("div"),t.innerHTML='',i=M(),o.c(),p(t,"class","icon"),p(e,"class","placeholder-section m-b-base")},m(r,a){w(r,e,a),k(e,t),k(e,i),o.m(e,null)},p(r,a){s===(s=l(r))&&o?o.p(r,a):(o.d(1),o=s(r),o&&(o.c(),o.m(e,null)))},d(r){r&&v(e),o.d()}}}function eE(n){let e;return{c(){e=b("div"),e.innerHTML='

    Loading collections...

    ',p(e,"class","placeholder-section m-b-base")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function tE(n){let e,t,i,l,s,o,r,a,f,u,c;const d=[JD,KD,YD],m=[];function h(T,$){return T[3]&&!T[11].length?0:T[11].length?2:1}e=h(n),t=m[e]=d[e](n);let _={};l=new Ba({props:_}),n[31](l);let g={};o=new _6({props:g}),n[32](o);let y={collection:n[2]};a=new Ya({props:y}),n[33](a),a.$on("hide",n[34]),a.$on("save",n[35]),a.$on("delete",n[36]);let S={collection:n[2]};return u=new _D({props:S}),n[37](u),u.$on("hide",n[38]),{c(){t.c(),i=M(),B(l.$$.fragment),s=M(),B(o.$$.fragment),r=M(),B(a.$$.fragment),f=M(),B(u.$$.fragment)},m(T,$){m[e].m(T,$),w(T,i,$),z(l,T,$),w(T,s,$),z(o,T,$),w(T,r,$),z(a,T,$),w(T,f,$),z(u,T,$),c=!0},p(T,$){let C=e;e=h(T),e===C?m[e].p(T,$):(le(),A(m[C],1,1,()=>{m[C]=null}),se(),t=m[e],t?t.p(T,$):(t=m[e]=d[e](T),t.c()),E(t,1),t.m(i.parentNode,i));const O={};l.$set(O);const D={};o.$set(D);const I={};$[0]&4&&(I.collection=T[2]),a.$set(I);const L={};$[0]&4&&(L.collection=T[2]),u.$set(L)},i(T){c||(E(t),E(l.$$.fragment,T),E(o.$$.fragment,T),E(a.$$.fragment,T),E(u.$$.fragment,T),c=!0)},o(T){A(t),A(l.$$.fragment,T),A(o.$$.fragment,T),A(a.$$.fragment,T),A(u.$$.fragment,T),c=!1},d(T){T&&(v(i),v(s),v(r),v(f)),m[e].d(T),n[31](null),V(l,T),n[32](null),V(o,T),n[33](null),V(a,T),n[37](null),V(u,T)}}}function nE(n,e,t){let i,l,s,o,r,a,f;Ue(n,Yn,De=>t(2,l=De)),Ue(n,It,De=>t(39,s=De)),Ue(n,To,De=>t(3,o=De)),Ue(n,jo,De=>t(16,r=De)),Ue(n,Rn,De=>t(11,a=De)),Ue(n,Xi,De=>t(12,f=De));const u=new URLSearchParams(r);let c,d,m,h,_,g,y=u.get("filter")||"",S=u.get("sort")||"-created",T=u.get("collectionId")||(l==null?void 0:l.id),$=0;J1(T);async function C(De){await Qt(),(l==null?void 0:l.type)==="view"?h.show(De):m==null||m.show(De)}function O(){t(14,T=l==null?void 0:l.id),t(0,y=""),t(1,S="-created"),I({recordId:null}),D(),c==null||c.forceHide(),d==null||d.hide()}async function D(){if(!S)return;const De=j.getAllCollectionIdentifiers(l),Ae=S.split(",").map(ze=>ze.startsWith("+")||ze.startsWith("-")?ze.substring(1):ze);Ae.filter(ze=>De.includes(ze)).length!=Ae.length&&(De.includes("created")?t(1,S="-created"):t(1,S=""))}function I(De={}){const Ae=Object.assign({collectionId:(l==null?void 0:l.id)||"",filter:y,sort:S},De);j.replaceHashQueryParams(Ae)}const L=()=>c==null?void 0:c.show();function R(De){ee[De?"unshift":"push"](()=>{g=De,t(9,g)})}function F(De){$=De,t(10,$)}const N=()=>c==null?void 0:c.show(l),P=()=>{_==null||_.load(),g==null||g.reload()},q=()=>d==null?void 0:d.show(l),H=()=>m==null?void 0:m.show(),W=De=>t(0,y=De.detail);function G(De){ee[De?"unshift":"push"](()=>{_=De,t(8,_)})}function U(De){y=De,t(0,y)}function Y(De){S=De,t(1,S)}const ie=De=>{I({recordId:De.detail.id});let Ae=De.detail._partial?De.detail.id:De.detail;l.type==="view"?h==null||h.show(Ae):m==null||m.show(Ae)},te=()=>{g==null||g.reload()},pe=()=>m==null?void 0:m.show();function Ne(De){ee[De?"unshift":"push"](()=>{c=De,t(4,c)})}function He(De){ee[De?"unshift":"push"](()=>{d=De,t(5,d)})}function Xe(De){ee[De?"unshift":"push"](()=>{m=De,t(6,m)})}const xe=()=>{I({recordId:null})},Mt=De=>{y?g==null||g.reload():De.detail.isNew&&t(10,$++,$),_==null||_.reloadLoadedPages()},ft=De=>{(!y||_!=null&&_.hasRecord(De.detail.id))&&t(10,$--,$),_==null||_.reloadLoadedPages()};function mt(De){ee[De?"unshift":"push"](()=>{h=De,t(7,h)})}const Gt=()=>{I({recordId:null})};return n.$$.update=()=>{n.$$.dirty[0]&65536&&t(15,i=new URLSearchParams(r)),n.$$.dirty[0]&49160&&!o&&i.get("collectionId")&&i.get("collectionId")!=T&&iv(i.get("collectionId")),n.$$.dirty[0]&16388&&l!=null&&l.id&&T!=l.id&&O(),n.$$.dirty[0]&4&&l!=null&&l.id&&D(),n.$$.dirty[0]&8&&!o&&u.get("recordId")&&C(u.get("recordId")),n.$$.dirty[0]&15&&!o&&(S||y||l!=null&&l.id)&&I(),n.$$.dirty[0]&4&&xt(It,s=(l==null?void 0:l.name)||"Collections",s)},[y,S,l,o,c,d,m,h,_,g,$,a,f,I,T,i,r,L,R,F,N,P,q,H,W,G,U,Y,ie,te,pe,Ne,He,Xe,xe,Mt,ft,mt,Gt]}class iE extends ge{constructor(e){super(),_e(this,e,nE,tE,me,{},null,[-1,-1])}}function Rh(n){let e,t,i,l,s,o,r;return{c(){e=b("div"),e.innerHTML='Sync',t=M(),i=b("a"),i.innerHTML=' Export collections',l=M(),s=b("a"),s.innerHTML=' Import collections',p(e,"class","sidebar-title"),p(i,"href","/settings/export-collections"),p(i,"class","sidebar-list-item"),p(s,"href","/settings/import-collections"),p(s,"class","sidebar-list-item")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),w(a,l,f),w(a,s,f),o||(r=[Se(Ln.call(null,i,{path:"/settings/export-collections/?.*"})),Se(nn.call(null,i)),Se(Ln.call(null,s,{path:"/settings/import-collections/?.*"})),Se(nn.call(null,s))],o=!0)},d(a){a&&(v(e),v(t),v(i),v(l),v(s)),o=!1,$e(r)}}}function lE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O=!n[0]&&Rh();return{c(){e=b("div"),t=b("div"),t.textContent="System",i=M(),l=b("a"),l.innerHTML=' Application',s=M(),o=b("a"),o.innerHTML=' Mail settings',r=M(),a=b("a"),a.innerHTML=' Files storage',f=M(),u=b("a"),u.innerHTML=' Backups',c=M(),O&&O.c(),d=M(),m=b("div"),m.textContent="Authentication",h=M(),_=b("a"),_.innerHTML=' Auth providers',g=M(),y=b("a"),y.innerHTML=' Token options',S=M(),T=b("a"),T.innerHTML=' Admins',p(t,"class","sidebar-title"),p(l,"href","/settings"),p(l,"class","sidebar-list-item"),p(o,"href","/settings/mail"),p(o,"class","sidebar-list-item"),p(a,"href","/settings/storage"),p(a,"class","sidebar-list-item"),p(u,"href","/settings/backups"),p(u,"class","sidebar-list-item"),p(m,"class","sidebar-title"),p(_,"href","/settings/auth-providers"),p(_,"class","sidebar-list-item"),p(y,"href","/settings/tokens"),p(y,"class","sidebar-list-item"),p(T,"href","/settings/admins"),p(T,"class","sidebar-list-item"),p(e,"class","sidebar-content")},m(D,I){w(D,e,I),k(e,t),k(e,i),k(e,l),k(e,s),k(e,o),k(e,r),k(e,a),k(e,f),k(e,u),k(e,c),O&&O.m(e,null),k(e,d),k(e,m),k(e,h),k(e,_),k(e,g),k(e,y),k(e,S),k(e,T),$||(C=[Se(Ln.call(null,l,{path:"/settings"})),Se(nn.call(null,l)),Se(Ln.call(null,o,{path:"/settings/mail/?.*"})),Se(nn.call(null,o)),Se(Ln.call(null,a,{path:"/settings/storage/?.*"})),Se(nn.call(null,a)),Se(Ln.call(null,u,{path:"/settings/backups/?.*"})),Se(nn.call(null,u)),Se(Ln.call(null,_,{path:"/settings/auth-providers/?.*"})),Se(nn.call(null,_)),Se(Ln.call(null,y,{path:"/settings/tokens/?.*"})),Se(nn.call(null,y)),Se(Ln.call(null,T,{path:"/settings/admins/?.*"})),Se(nn.call(null,T))],$=!0)},p(D,I){D[0]?O&&(O.d(1),O=null):O||(O=Rh(),O.c(),O.m(e,d))},d(D){D&&v(e),O&&O.d(),$=!1,$e(C)}}}function sE(n){let e,t;return e=new Hb({props:{class:"settings-sidebar",$$slots:{default:[lE]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&3&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function oE(n,e,t){let i;return Ue(n,Xi,l=>t(0,i=l)),[i]}class _i extends ge{constructor(e){super(),_e(this,e,oE,sE,me,{})}}function qh(n,e,t){const i=n.slice();return i[31]=e[t],i}function jh(n){let e,t;return e=new ce({props:{class:"form-field readonly",name:"id",$$slots:{default:[rE,({uniqueId:i})=>({30:i}),({uniqueId:i})=>[i?1073741824:0]]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[0]&1073741826|l[1]&8&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function rE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;return a=new Kb({props:{model:n[1]}}),{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="id",o=M(),r=b("div"),B(a.$$.fragment),f=M(),u=b("input"),p(t,"class",j.getFieldTypeIcon("primary")),p(l,"class","txt"),p(e,"for",s=n[30]),p(r,"class","form-field-addon"),p(u,"type","text"),p(u,"id",c=n[30]),u.value=d=n[1].id,u.readOnly=!0},m(h,_){w(h,e,_),k(e,t),k(e,i),k(e,l),w(h,o,_),w(h,r,_),z(a,r,null),w(h,f,_),w(h,u,_),m=!0},p(h,_){(!m||_[0]&1073741824&&s!==(s=h[30]))&&p(e,"for",s);const g={};_[0]&2&&(g.model=h[1]),a.$set(g),(!m||_[0]&1073741824&&c!==(c=h[30]))&&p(u,"id",c),(!m||_[0]&2&&d!==(d=h[1].id)&&u.value!==d)&&(u.value=d)},i(h){m||(E(a.$$.fragment,h),m=!0)},o(h){A(a.$$.fragment,h),m=!1},d(h){h&&(v(e),v(o),v(r),v(f),v(u)),V(a)}}}function Hh(n){let e,t,i,l,s,o,r;function a(){return n[18](n[31])}return{c(){e=b("button"),t=b("img"),l=M(),en(t.src,i="./images/avatars/avatar"+n[31]+".svg")||p(t,"src",i),p(t,"alt","Avatar "+n[31]),p(e,"type","button"),p(e,"class",s="link-fade thumb thumb-circle "+(n[31]==n[2]?"thumb-primary":"thumb-sm"))},m(f,u){w(f,e,u),k(e,t),k(e,l),o||(r=J(e,"click",a),o=!0)},p(f,u){n=f,u[0]&4&&s!==(s="link-fade thumb thumb-circle "+(n[31]==n[2]?"thumb-primary":"thumb-sm"))&&p(e,"class",s)},d(f){f&&v(e),o=!1,r()}}}function aE(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Email",o=M(),r=b("input"),p(t,"class",j.getFieldTypeIcon("email")),p(l,"class","txt"),p(e,"for",s=n[30]),p(r,"type","email"),p(r,"autocomplete","off"),p(r,"id",a=n[30]),r.required=!0},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,o,d),w(c,r,d),re(r,n[3]),f||(u=J(r,"input",n[19]),f=!0)},p(c,d){d[0]&1073741824&&s!==(s=c[30])&&p(e,"for",s),d[0]&1073741824&&a!==(a=c[30])&&p(r,"id",a),d[0]&8&&r.value!==c[3]&&re(r,c[3])},d(c){c&&(v(e),v(o),v(r)),f=!1,u()}}}function zh(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",$$slots:{default:[fE,({uniqueId:i})=>({30:i}),({uniqueId:i})=>[i?1073741824:0]]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[0]&1073741840|l[1]&8&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function fE(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Change password"),p(e,"type","checkbox"),p(e,"id",t=n[30]),p(l,"for",o=n[30])},m(f,u){w(f,e,u),e.checked=n[4],w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[20]),r=!0)},p(f,u){u[0]&1073741824&&t!==(t=f[30])&&p(e,"id",t),u[0]&16&&(e.checked=f[4]),u[0]&1073741824&&o!==(o=f[30])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function Vh(n){let e,t,i,l,s,o,r,a,f;return l=new ce({props:{class:"form-field required",name:"password",$$slots:{default:[uE,({uniqueId:u})=>({30:u}),({uniqueId:u})=>[u?1073741824:0]]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[cE,({uniqueId:u})=>({30:u}),({uniqueId:u})=>[u?1073741824:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),B(r.$$.fragment),p(i,"class","col-sm-6"),p(o,"class","col-sm-6"),p(t,"class","grid"),p(e,"class","col-12")},m(u,c){w(u,e,c),k(e,t),k(t,i),z(l,i,null),k(t,s),k(t,o),z(r,o,null),f=!0},p(u,c){const d={};c[0]&1073742336|c[1]&8&&(d.$$scope={dirty:c,ctx:u}),l.$set(d);const m={};c[0]&1073742848|c[1]&8&&(m.$$scope={dirty:c,ctx:u}),r.$set(m)},i(u){f||(E(l.$$.fragment,u),E(r.$$.fragment,u),u&&Ke(()=>{f&&(a||(a=Fe(t,et,{duration:150},!0)),a.run(1))}),f=!0)},o(u){A(l.$$.fragment,u),A(r.$$.fragment,u),u&&(a||(a=Fe(t,et,{duration:150},!1)),a.run(0)),f=!1},d(u){u&&v(e),V(l),V(r),u&&a&&a.end()}}}function uE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h;return c=new Jb({}),{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Password",o=M(),r=b("input"),f=M(),u=b("div"),B(c.$$.fragment),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[30]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[30]),r.required=!0,p(u,"class","form-field-addon")},m(_,g){w(_,e,g),k(e,t),k(e,i),k(e,l),w(_,o,g),w(_,r,g),re(r,n[9]),w(_,f,g),w(_,u,g),z(c,u,null),d=!0,m||(h=J(r,"input",n[21]),m=!0)},p(_,g){(!d||g[0]&1073741824&&s!==(s=_[30]))&&p(e,"for",s),(!d||g[0]&1073741824&&a!==(a=_[30]))&&p(r,"id",a),g[0]&512&&r.value!==_[9]&&re(r,_[9])},i(_){d||(E(c.$$.fragment,_),d=!0)},o(_){A(c.$$.fragment,_),d=!1},d(_){_&&(v(e),v(o),v(r),v(f),v(u)),V(c),m=!1,h()}}}function cE(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=b("i"),i=M(),l=b("span"),l.textContent="Password confirm",o=M(),r=b("input"),p(t,"class","ri-lock-line"),p(l,"class","txt"),p(e,"for",s=n[30]),p(r,"type","password"),p(r,"autocomplete","new-password"),p(r,"id",a=n[30]),r.required=!0},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,o,d),w(c,r,d),re(r,n[10]),f||(u=J(r,"input",n[22]),f=!0)},p(c,d){d[0]&1073741824&&s!==(s=c[30])&&p(e,"for",s),d[0]&1073741824&&a!==(a=c[30])&&p(r,"id",a),d[0]&1024&&r.value!==c[10]&&re(r,c[10])},d(c){c&&(v(e),v(o),v(r)),f=!1,u()}}}function dE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h=!n[5]&&jh(n),_=ue([0,1,2,3,4,5,6,7,8,9]),g=[];for(let T=0;T<10;T+=1)g[T]=Hh(qh(n,_,T));a=new ce({props:{class:"form-field required",name:"email",$$slots:{default:[aE,({uniqueId:T})=>({30:T}),({uniqueId:T})=>[T?1073741824:0]]},$$scope:{ctx:n}}});let y=!n[5]&&zh(n),S=(n[5]||n[4])&&Vh(n);return{c(){e=b("form"),h&&h.c(),t=M(),i=b("div"),l=b("p"),l.textContent="Avatar",s=M(),o=b("div");for(let T=0;T<10;T+=1)g[T].c();r=M(),B(a.$$.fragment),f=M(),y&&y.c(),u=M(),S&&S.c(),p(l,"class","section-title"),p(o,"class","flex flex-gap-xs flex-wrap"),p(i,"class","content"),p(e,"id",n[12]),p(e,"class","grid"),p(e,"autocomplete","off")},m(T,$){w(T,e,$),h&&h.m(e,null),k(e,t),k(e,i),k(i,l),k(i,s),k(i,o);for(let C=0;C<10;C+=1)g[C]&&g[C].m(o,null);k(e,r),z(a,e,null),k(e,f),y&&y.m(e,null),k(e,u),S&&S.m(e,null),c=!0,d||(m=J(e,"submit",Be(n[13])),d=!0)},p(T,$){if(T[5]?h&&(le(),A(h,1,1,()=>{h=null}),se()):h?(h.p(T,$),$[0]&32&&E(h,1)):(h=jh(T),h.c(),E(h,1),h.m(e,t)),$[0]&4){_=ue([0,1,2,3,4,5,6,7,8,9]);let O;for(O=0;O<10;O+=1){const D=qh(T,_,O);g[O]?g[O].p(D,$):(g[O]=Hh(D),g[O].c(),g[O].m(o,null))}for(;O<10;O+=1)g[O].d(1)}const C={};$[0]&1073741832|$[1]&8&&(C.$$scope={dirty:$,ctx:T}),a.$set(C),T[5]?y&&(le(),A(y,1,1,()=>{y=null}),se()):y?(y.p(T,$),$[0]&32&&E(y,1)):(y=zh(T),y.c(),E(y,1),y.m(e,u)),T[5]||T[4]?S?(S.p(T,$),$[0]&48&&E(S,1)):(S=Vh(T),S.c(),E(S,1),S.m(e,null)):S&&(le(),A(S,1,1,()=>{S=null}),se())},i(T){c||(E(h),E(a.$$.fragment,T),E(y),E(S),c=!0)},o(T){A(h),A(a.$$.fragment,T),A(y),A(S),c=!1},d(T){T&&v(e),h&&h.d(),ot(g,T),V(a),y&&y.d(),S&&S.d(),d=!1,m()}}}function pE(n){let e,t=n[5]?"New admin":"Edit admin",i;return{c(){e=b("h4"),i=K(t)},m(l,s){w(l,e,s),k(e,i)},p(l,s){s[0]&32&&t!==(t=l[5]?"New admin":"Edit admin")&&oe(i,t)},d(l){l&&v(e)}}}function Bh(n){let e,t,i,l,s,o,r,a,f;return o=new On({props:{class:"dropdown dropdown-upside dropdown-left dropdown-nowrap",$$slots:{default:[mE]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("span"),i=M(),l=b("i"),s=M(),B(o.$$.fragment),r=M(),a=b("div"),p(t,"aria-hidden","true"),p(l,"class","ri-more-line"),p(l,"aria-hidden","true"),p(e,"tabindex","0"),p(e,"role","button"),p(e,"aria-label","More admin options"),p(e,"class","btn btn-sm btn-circle btn-transparent"),p(a,"class","flex-fill")},m(u,c){w(u,e,c),k(e,t),k(e,i),k(e,l),k(e,s),z(o,e,null),w(u,r,c),w(u,a,c),f=!0},p(u,c){const d={};c[1]&8&&(d.$$scope={dirty:c,ctx:u}),o.$set(d)},i(u){f||(E(o.$$.fragment,u),f=!0)},o(u){A(o.$$.fragment,u),f=!1},d(u){u&&(v(e),v(r),v(a)),V(o)}}}function mE(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Delete',p(e,"type","button"),p(e,"class","dropdown-item txt-danger"),p(e,"role","menuitem")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[16]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function hE(n){let e,t,i,l,s,o,r=n[5]?"Create":"Save changes",a,f,u,c,d,m=!n[5]&&Bh(n);return{c(){m&&m.c(),e=M(),t=b("button"),i=b("span"),i.textContent="Cancel",l=M(),s=b("button"),o=b("span"),a=K(r),p(i,"class","txt"),p(t,"type","button"),p(t,"class","btn btn-transparent"),t.disabled=n[7],p(o,"class","txt"),p(s,"type","submit"),p(s,"form",n[12]),p(s,"class","btn btn-expanded"),s.disabled=f=!n[11]||n[7],x(s,"btn-loading",n[7])},m(h,_){m&&m.m(h,_),w(h,e,_),w(h,t,_),k(t,i),w(h,l,_),w(h,s,_),k(s,o),k(o,a),u=!0,c||(d=J(t,"click",n[17]),c=!0)},p(h,_){h[5]?m&&(le(),A(m,1,1,()=>{m=null}),se()):m?(m.p(h,_),_[0]&32&&E(m,1)):(m=Bh(h),m.c(),E(m,1),m.m(e.parentNode,e)),(!u||_[0]&128)&&(t.disabled=h[7]),(!u||_[0]&32)&&r!==(r=h[5]?"Create":"Save changes")&&oe(a,r),(!u||_[0]&2176&&f!==(f=!h[11]||h[7]))&&(s.disabled=f),(!u||_[0]&128)&&x(s,"btn-loading",h[7])},i(h){u||(E(m),u=!0)},o(h){A(m),u=!1},d(h){h&&(v(e),v(t),v(l),v(s)),m&&m.d(h),c=!1,d()}}}function _E(n){let e,t,i={popup:!0,class:"admin-panel",beforeHide:n[23],$$slots:{footer:[hE],header:[pE],default:[dE]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[24](e),e.$on("hide",n[25]),e.$on("show",n[26]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,s){const o={};s[0]&2304&&(o.beforeHide=l[23]),s[0]&3774|s[1]&8&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[24](null),V(e,l)}}}function gE(n,e,t){let i,l;const s=lt(),o="admin_"+j.randomString(5);let r,a={},f=!1,u=!1,c=0,d="",m="",h="",_=!1;function g(G){return S(G),t(8,u=!0),r==null?void 0:r.show()}function y(){return r==null?void 0:r.hide()}function S(G){t(1,a=structuredClone(G||{})),T()}function T(){t(4,_=!1),t(3,d=(a==null?void 0:a.email)||""),t(2,c=(a==null?void 0:a.avatar)||0),t(9,m=""),t(10,h=""),Jt({})}function $(){if(f||!l)return;t(7,f=!0);const G={email:d,avatar:c};(i||_)&&(G.password=m,G.passwordConfirm=h);let U;i?U=ae.admins.create(G):U=ae.admins.update(a.id,G),U.then(async Y=>{var ie;t(8,u=!1),y(),Lt(i?"Successfully created admin.":"Successfully updated admin."),((ie=ae.authStore.model)==null?void 0:ie.id)===Y.id&&ae.authStore.save(ae.authStore.token,Y),s("save",Y)}).catch(Y=>{ae.error(Y)}).finally(()=>{t(7,f=!1)})}function C(){a!=null&&a.id&&fn("Do you really want to delete the selected admin?",()=>ae.admins.delete(a.id).then(()=>{t(8,u=!1),y(),Lt("Successfully deleted admin."),s("delete",a)}).catch(G=>{ae.error(G)}))}const O=()=>C(),D=()=>y(),I=G=>t(2,c=G);function L(){d=this.value,t(3,d)}function R(){_=this.checked,t(4,_)}function F(){m=this.value,t(9,m)}function N(){h=this.value,t(10,h)}const P=()=>l&&u?(fn("You have unsaved changes. Do you really want to close the panel?",()=>{t(8,u=!1),y()}),!1):!0;function q(G){ee[G?"unshift":"push"](()=>{r=G,t(6,r)})}function H(G){Ce.call(this,n,G)}function W(G){Ce.call(this,n,G)}return n.$$.update=()=>{n.$$.dirty[0]&2&&t(5,i=!(a!=null&&a.id)),n.$$.dirty[0]&62&&t(11,l=i&&d!=""||_||d!==a.email||c!==a.avatar)},[y,a,c,d,_,i,r,f,u,m,h,l,o,$,C,g,O,D,I,L,R,F,N,P,q,H,W]}class bE extends ge{constructor(e){super(),_e(this,e,gE,_E,me,{show:15,hide:0},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[0]}}function Uh(n,e,t){const i=n.slice();return i[24]=e[t],i}function kE(n){let e;return{c(){e=b("div"),e.innerHTML=` id`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function yE(n){let e;return{c(){e=b("div"),e.innerHTML=` email`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function vE(n){let e;return{c(){e=b("div"),e.innerHTML=` created`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function wE(n){let e;return{c(){e=b("div"),e.innerHTML=` updated`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Wh(n){let e;function t(s,o){return s[5]?$E:SE}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function SE(n){var r;let e,t,i,l,s,o=((r=n[1])==null?void 0:r.length)&&Yh(n);return{c(){e=b("tr"),t=b("td"),i=b("h6"),i.textContent="No admins found.",l=M(),o&&o.c(),s=M(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),k(e,t),k(t,i),k(t,l),o&&o.m(t,null),k(e,s)},p(a,f){var u;(u=a[1])!=null&&u.length?o?o.p(a,f):(o=Yh(a),o.c(),o.m(t,null)):o&&(o.d(1),o=null)},d(a){a&&v(e),o&&o.d()}}}function $E(n){let e;return{c(){e=b("tr"),e.innerHTML=' '},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function Yh(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[17]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function Kh(n){let e;return{c(){e=b("span"),e.textContent="You",p(e,"class","label label-warning m-l-5")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Jh(n,e){let t,i,l,s,o,r,a,f,u,c,d,m=e[24].id+"",h,_,g,y,S,T=e[24].email+"",$,C,O,D,I,L,R,F,N,P,q,H,W,G;u=new sl({props:{value:e[24].id}});let U=e[24].id===e[7].id&&Kh();I=new el({props:{date:e[24].created}}),F=new el({props:{date:e[24].updated}});function Y(){return e[15](e[24])}function ie(...te){return e[16](e[24],...te)}return{key:n,first:null,c(){t=b("tr"),i=b("td"),l=b("figure"),s=b("img"),r=M(),a=b("td"),f=b("div"),B(u.$$.fragment),c=M(),d=b("span"),h=K(m),_=M(),U&&U.c(),g=M(),y=b("td"),S=b("span"),$=K(T),O=M(),D=b("td"),B(I.$$.fragment),L=M(),R=b("td"),B(F.$$.fragment),N=M(),P=b("td"),P.innerHTML='',q=M(),en(s.src,o="./images/avatars/avatar"+(e[24].avatar||0)+".svg")||p(s,"src",o),p(s,"alt","Admin avatar"),p(l,"class","thumb thumb-sm thumb-circle"),p(i,"class","min-width"),p(d,"class","txt"),p(f,"class","label"),p(a,"class","col-type-text col-field-id"),p(S,"class","txt txt-ellipsis"),p(S,"title",C=e[24].email),p(y,"class","col-type-email col-field-email"),p(D,"class","col-type-date col-field-created"),p(R,"class","col-type-date col-field-updated"),p(P,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(te,pe){w(te,t,pe),k(t,i),k(i,l),k(l,s),k(t,r),k(t,a),k(a,f),z(u,f,null),k(f,c),k(f,d),k(d,h),k(a,_),U&&U.m(a,null),k(t,g),k(t,y),k(y,S),k(S,$),k(t,O),k(t,D),z(I,D,null),k(t,L),k(t,R),z(F,R,null),k(t,N),k(t,P),k(t,q),H=!0,W||(G=[J(t,"click",Y),J(t,"keydown",ie)],W=!0)},p(te,pe){e=te,(!H||pe&16&&!en(s.src,o="./images/avatars/avatar"+(e[24].avatar||0)+".svg"))&&p(s,"src",o);const Ne={};pe&16&&(Ne.value=e[24].id),u.$set(Ne),(!H||pe&16)&&m!==(m=e[24].id+"")&&oe(h,m),e[24].id===e[7].id?U||(U=Kh(),U.c(),U.m(a,null)):U&&(U.d(1),U=null),(!H||pe&16)&&T!==(T=e[24].email+"")&&oe($,T),(!H||pe&16&&C!==(C=e[24].email))&&p(S,"title",C);const He={};pe&16&&(He.date=e[24].created),I.$set(He);const Xe={};pe&16&&(Xe.date=e[24].updated),F.$set(Xe)},i(te){H||(E(u.$$.fragment,te),E(I.$$.fragment,te),E(F.$$.fragment,te),H=!0)},o(te){A(u.$$.fragment,te),A(I.$$.fragment,te),A(F.$$.fragment,te),H=!1},d(te){te&&v(t),V(u),U&&U.d(),V(I),V(F),W=!1,$e(G)}}}function TE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C=[],O=new Map,D;function I(Y){n[11](Y)}let L={class:"col-type-text",name:"id",$$slots:{default:[kE]},$$scope:{ctx:n}};n[2]!==void 0&&(L.sort=n[2]),o=new Sn({props:L}),ee.push(()=>be(o,"sort",I));function R(Y){n[12](Y)}let F={class:"col-type-email col-field-email",name:"email",$$slots:{default:[yE]},$$scope:{ctx:n}};n[2]!==void 0&&(F.sort=n[2]),f=new Sn({props:F}),ee.push(()=>be(f,"sort",R));function N(Y){n[13](Y)}let P={class:"col-type-date col-field-created",name:"created",$$slots:{default:[vE]},$$scope:{ctx:n}};n[2]!==void 0&&(P.sort=n[2]),d=new Sn({props:P}),ee.push(()=>be(d,"sort",N));function q(Y){n[14](Y)}let H={class:"col-type-date col-field-updated",name:"updated",$$slots:{default:[wE]},$$scope:{ctx:n}};n[2]!==void 0&&(H.sort=n[2]),_=new Sn({props:H}),ee.push(()=>be(_,"sort",q));let W=ue(n[4]);const G=Y=>Y[24].id;for(let Y=0;Yr=!1)),o.$set(te);const pe={};ie&134217728&&(pe.$$scope={dirty:ie,ctx:Y}),!u&&ie&4&&(u=!0,pe.sort=Y[2],ke(()=>u=!1)),f.$set(pe);const Ne={};ie&134217728&&(Ne.$$scope={dirty:ie,ctx:Y}),!m&&ie&4&&(m=!0,Ne.sort=Y[2],ke(()=>m=!1)),d.$set(Ne);const He={};ie&134217728&&(He.$$scope={dirty:ie,ctx:Y}),!g&&ie&4&&(g=!0,He.sort=Y[2],ke(()=>g=!1)),_.$set(He),ie&186&&(W=ue(Y[4]),le(),C=at(C,ie,G,1,Y,W,O,$,Et,Jh,null,Uh),se(),!W.length&&U?U.p(Y,ie):W.length?U&&(U.d(1),U=null):(U=Wh(Y),U.c(),U.m($,null))),(!D||ie&32)&&x(e,"table-loading",Y[5])},i(Y){if(!D){E(o.$$.fragment,Y),E(f.$$.fragment,Y),E(d.$$.fragment,Y),E(_.$$.fragment,Y);for(let ie=0;ie New admin',h=M(),B(_.$$.fragment),g=M(),y=b("div"),S=M(),B(T.$$.fragment),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(u,"class","flex-fill"),p(m,"type","button"),p(m,"class","btn btn-expanded"),p(d,"class","btns-group"),p(e,"class","page-header"),p(y,"class","clearfix m-b-base")},m(D,I){w(D,e,I),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),k(e,r),z(a,e,null),k(e,f),k(e,u),k(e,c),k(e,d),k(d,m),w(D,h,I),z(_,D,I),w(D,g,I),w(D,y,I),w(D,S,I),z(T,D,I),$=!0,C||(O=J(m,"click",n[9]),C=!0)},p(D,I){(!$||I&64)&&oe(o,D[6]);const L={};I&2&&(L.value=D[1]),_.$set(L);const R={};I&134217918&&(R.$$scope={dirty:I,ctx:D}),T.$set(R)},i(D){$||(E(a.$$.fragment,D),E(_.$$.fragment,D),E(T.$$.fragment,D),$=!0)},o(D){A(a.$$.fragment,D),A(_.$$.fragment,D),A(T.$$.fragment,D),$=!1},d(D){D&&(v(e),v(h),v(g),v(y),v(S)),V(a),V(_,D),V(T,D),C=!1,O()}}}function OE(n){let e,t,i=n[4].length+"",l;return{c(){e=b("div"),t=K("Total found: "),l=K(i),p(e,"class","m-r-auto txt-sm txt-hint")},m(s,o){w(s,e,o),k(e,t),k(e,l)},p(s,o){o&16&&i!==(i=s[4].length+"")&&oe(l,i)},d(s){s&&v(e)}}}function ME(n){let e,t,i,l,s,o;e=new _i({}),i=new bn({props:{$$slots:{footer:[OE],default:[CE]},$$scope:{ctx:n}}});let r={};return s=new bE({props:r}),n[18](s),s.$on("save",n[19]),s.$on("delete",n[20]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment)},m(a,f){z(e,a,f),w(a,t,f),z(i,a,f),w(a,l,f),z(s,a,f),o=!0},p(a,[f]){const u={};f&134217982&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};s.$set(c)},i(a){o||(E(e.$$.fragment,a),E(i.$$.fragment,a),E(s.$$.fragment,a),o=!0)},o(a){A(e.$$.fragment,a),A(i.$$.fragment,a),A(s.$$.fragment,a),o=!1},d(a){a&&(v(t),v(l)),V(e,a),V(i,a),n[18](null),V(s,a)}}}function DE(n,e,t){let i,l,s;Ue(n,jo,F=>t(21,i=F)),Ue(n,It,F=>t(6,l=F)),Ue(n,$a,F=>t(7,s=F)),xt(It,l="Admins",l);const o=new URLSearchParams(i);let r,a=[],f=!1,u=o.get("filter")||"",c=o.get("sort")||"-created";function d(){t(5,f=!0),t(4,a=[]);const F=j.normalizeSearchFilter(u,["id","email","created","updated"]);return ae.admins.getFullList(100,{sort:c||"-created",filter:F}).then(N=>{t(4,a=N),t(5,f=!1)}).catch(N=>{N!=null&&N.isAbort||(t(5,f=!1),console.warn(N),m(),ae.error(N,!F||(N==null?void 0:N.status)!=400))})}function m(){t(4,a=[])}const h=()=>d(),_=()=>r==null?void 0:r.show(),g=F=>t(1,u=F.detail);function y(F){c=F,t(2,c)}function S(F){c=F,t(2,c)}function T(F){c=F,t(2,c)}function $(F){c=F,t(2,c)}const C=F=>r==null?void 0:r.show(F),O=(F,N)=>{(N.code==="Enter"||N.code==="Space")&&(N.preventDefault(),r==null||r.show(F))},D=()=>t(1,u="");function I(F){ee[F?"unshift":"push"](()=>{r=F,t(3,r)})}const L=()=>d(),R=()=>d();return n.$$.update=()=>{if(n.$$.dirty&6&&c!==-1&&u!==-1){const F=new URLSearchParams({filter:u,sort:c}).toString();tl("/settings/admins?"+F),d()}},[d,u,c,r,a,f,l,s,h,_,g,y,S,T,$,C,O,D,I,L,R]}class EE extends ge{constructor(e){super(),_e(this,e,DE,ME,me,{loadAdmins:0})}get loadAdmins(){return this.$$.ctx[0]}}function IE(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Email"),l=M(),s=b("input"),p(e,"for",i=n[8]),p(s,"type","email"),p(s,"id",o=n[8]),s.required=!0,s.autofocus=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0]),s.focus(),r||(a=J(s,"input",n[4]),r=!0)},p(f,u){u&256&&i!==(i=f[8])&&p(e,"for",i),u&256&&o!==(o=f[8])&&p(s,"id",o),u&1&&s.value!==f[0]&&re(s,f[0])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function AE(n){let e,t,i,l,s,o,r,a,f,u,c;return{c(){e=b("label"),t=K("Password"),l=M(),s=b("input"),r=M(),a=b("div"),f=b("a"),f.textContent="Forgotten password?",p(e,"for",i=n[8]),p(s,"type","password"),p(s,"id",o=n[8]),s.required=!0,p(f,"href","/request-password-reset"),p(f,"class","link-hint"),p(a,"class","help-block")},m(d,m){w(d,e,m),k(e,t),w(d,l,m),w(d,s,m),re(s,n[1]),w(d,r,m),w(d,a,m),k(a,f),u||(c=[J(s,"input",n[5]),Se(nn.call(null,f))],u=!0)},p(d,m){m&256&&i!==(i=d[8])&&p(e,"for",i),m&256&&o!==(o=d[8])&&p(s,"id",o),m&2&&s.value!==d[1]&&re(s,d[1])},d(d){d&&(v(e),v(l),v(s),v(r),v(a)),u=!1,$e(c)}}}function LE(n){let e,t,i,l,s,o,r,a,f,u,c;return l=new ce({props:{class:"form-field required",name:"identity",$$slots:{default:[IE,({uniqueId:d})=>({8:d}),({uniqueId:d})=>d?256:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field required",name:"password",$$slots:{default:[AE,({uniqueId:d})=>({8:d}),({uniqueId:d})=>d?256:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),t=b("div"),t.innerHTML="

    Admin sign in

    ",i=M(),B(l.$$.fragment),s=M(),B(o.$$.fragment),r=M(),a=b("button"),a.innerHTML='Login ',p(t,"class","content txt-center m-b-base"),p(a,"type","submit"),p(a,"class","btn btn-lg btn-block btn-next"),x(a,"btn-disabled",n[2]),x(a,"btn-loading",n[2]),p(e,"class","block")},m(d,m){w(d,e,m),k(e,t),k(e,i),z(l,e,null),k(e,s),z(o,e,null),k(e,r),k(e,a),f=!0,u||(c=J(e,"submit",Be(n[3])),u=!0)},p(d,m){const h={};m&769&&(h.$$scope={dirty:m,ctx:d}),l.$set(h);const _={};m&770&&(_.$$scope={dirty:m,ctx:d}),o.$set(_),(!f||m&4)&&x(a,"btn-disabled",d[2]),(!f||m&4)&&x(a,"btn-loading",d[2])},i(d){f||(E(l.$$.fragment,d),E(o.$$.fragment,d),f=!0)},o(d){A(l.$$.fragment,d),A(o.$$.fragment,d),f=!1},d(d){d&&v(e),V(l),V(o),u=!1,c()}}}function NE(n){let e,t;return e=new Z1({props:{$$slots:{default:[LE]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&519&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function PE(n,e,t){let i;Ue(n,jo,c=>t(6,i=c));const l=new URLSearchParams(i);let s=l.get("demoEmail")||"",o=l.get("demoPassword")||"",r=!1;function a(){if(!r)return t(2,r=!0),ae.admins.authWithPassword(s,o).then(()=>{wa(),tl("/")}).catch(()=>{ii("Invalid login credentials.")}).finally(()=>{t(2,r=!1)})}function f(){s=this.value,t(0,s)}function u(){o=this.value,t(1,o)}return[s,o,r,a,f,u]}class FE extends ge{constructor(e){super(),_e(this,e,PE,NE,me,{})}}function RE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T;i=new ce({props:{class:"form-field required",name:"meta.appName",$$slots:{default:[jE,({uniqueId:C})=>({18:C}),({uniqueId:C})=>C?262144:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field required",name:"meta.appUrl",$$slots:{default:[HE,({uniqueId:C})=>({18:C}),({uniqueId:C})=>C?262144:0]},$$scope:{ctx:n}}}),a=new ce({props:{class:"form-field form-field-toggle",name:"meta.hideControls",$$slots:{default:[zE,({uniqueId:C})=>({18:C}),({uniqueId:C})=>C?262144:0]},$$scope:{ctx:n}}});let $=n[3]&&Zh(n);return{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),B(a.$$.fragment),f=M(),u=b("div"),c=b("div"),d=M(),$&&$.c(),m=M(),h=b("button"),_=b("span"),_.textContent="Save changes",p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(c,"class","flex-fill"),p(_,"class","txt"),p(h,"type","submit"),p(h,"class","btn btn-expanded"),h.disabled=g=!n[3]||n[2],x(h,"btn-loading",n[2]),p(u,"class","col-lg-12 flex"),p(e,"class","grid")},m(C,O){w(C,e,O),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,r),z(a,e,null),k(e,f),k(e,u),k(u,c),k(u,d),$&&$.m(u,null),k(u,m),k(u,h),k(h,_),y=!0,S||(T=J(h,"click",n[12]),S=!0)},p(C,O){const D={};O&786433&&(D.$$scope={dirty:O,ctx:C}),i.$set(D);const I={};O&786433&&(I.$$scope={dirty:O,ctx:C}),o.$set(I);const L={};O&786433&&(L.$$scope={dirty:O,ctx:C}),a.$set(L),C[3]?$?$.p(C,O):($=Zh(C),$.c(),$.m(u,m)):$&&($.d(1),$=null),(!y||O&12&&g!==(g=!C[3]||C[2]))&&(h.disabled=g),(!y||O&4)&&x(h,"btn-loading",C[2])},i(C){y||(E(i.$$.fragment,C),E(o.$$.fragment,C),E(a.$$.fragment,C),y=!0)},o(C){A(i.$$.fragment,C),A(o.$$.fragment,C),A(a.$$.fragment,C),y=!1},d(C){C&&v(e),V(i),V(o),V(a),$&&$.d(),S=!1,T()}}}function qE(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function jE(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Application name"),l=M(),s=b("input"),p(e,"for",i=n[18]),p(s,"type","text"),p(s,"id",o=n[18]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].meta.appName),r||(a=J(s,"input",n[8]),r=!0)},p(f,u){u&262144&&i!==(i=f[18])&&p(e,"for",i),u&262144&&o!==(o=f[18])&&p(s,"id",o),u&1&&s.value!==f[0].meta.appName&&re(s,f[0].meta.appName)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function HE(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Application URL"),l=M(),s=b("input"),p(e,"for",i=n[18]),p(s,"type","text"),p(s,"id",o=n[18]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].meta.appUrl),r||(a=J(s,"input",n[9]),r=!0)},p(f,u){u&262144&&i!==(i=f[18])&&p(e,"for",i),u&262144&&o!==(o=f[18])&&p(s,"id",o),u&1&&s.value!==f[0].meta.appUrl&&re(s,f[0].meta.appUrl)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function zE(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Hide collection create and edit controls",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[18]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[18])},m(c,d){w(c,e,d),e.checked=n[0].meta.hideControls,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[10]),Se(Pe.call(null,r,{text:"This could prevent making accidental schema changes when in production environment.",position:"right"}))],f=!0)},p(c,d){d&262144&&t!==(t=c[18])&&p(e,"id",t),d&1&&(e.checked=c[0].meta.hideControls),d&262144&&a!==(a=c[18])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function Zh(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[2]},m(s,o){w(s,e,o),k(e,t),i||(l=J(e,"click",n[11]),i=!0)},p(s,o){o&4&&(e.disabled=s[2])},d(s){s&&v(e),i=!1,l()}}}function VE(n){let e,t,i,l,s,o,r,a,f;const u=[qE,RE],c=[];function d(m,h){return m[1]?0:1}return s=d(n),o=c[s]=u[s](n),{c(){e=b("header"),e.innerHTML='',t=M(),i=b("div"),l=b("form"),o.c(),p(e,"class","page-header"),p(l,"class","panel"),p(l,"autocomplete","off"),p(i,"class","wrapper")},m(m,h){w(m,e,h),w(m,t,h),w(m,i,h),k(i,l),c[s].m(l,null),r=!0,a||(f=J(l,"submit",Be(n[4])),a=!0)},p(m,h){let _=s;s=d(m),s===_?c[s].p(m,h):(le(),A(c[_],1,1,()=>{c[_]=null}),se(),o=c[s],o?o.p(m,h):(o=c[s]=u[s](m),o.c()),E(o,1),o.m(l,null))},i(m){r||(E(o),r=!0)},o(m){A(o),r=!1},d(m){m&&(v(e),v(t),v(i)),c[s].d(),a=!1,f()}}}function BE(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[VE]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,[o]){const r={};o&524303&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function UE(n,e,t){let i,l,s,o;Ue(n,Xi,C=>t(13,l=C)),Ue(n,Oo,C=>t(14,s=C)),Ue(n,It,C=>t(15,o=C)),xt(It,o="Application settings",o);let r={},a={},f=!1,u=!1,c="";d();async function d(){t(1,f=!0);try{const C=await ae.settings.getAll()||{};h(C)}catch(C){ae.error(C)}t(1,f=!1)}async function m(){if(!(u||!i)){t(2,u=!0);try{const C=await ae.settings.update(j.filterRedactedProps(a));h(C),Lt("Successfully saved application settings.")}catch(C){ae.error(C)}t(2,u=!1)}}function h(C={}){var O,D;xt(Oo,s=(O=C==null?void 0:C.meta)==null?void 0:O.appName,s),xt(Xi,l=!!((D=C==null?void 0:C.meta)!=null&&D.hideControls),l),t(0,a={meta:(C==null?void 0:C.meta)||{}}),t(6,r=JSON.parse(JSON.stringify(a)))}function _(){t(0,a=JSON.parse(JSON.stringify(r||{})))}function g(){a.meta.appName=this.value,t(0,a)}function y(){a.meta.appUrl=this.value,t(0,a)}function S(){a.meta.hideControls=this.checked,t(0,a)}const T=()=>_(),$=()=>m();return n.$$.update=()=>{n.$$.dirty&64&&t(7,c=JSON.stringify(r)),n.$$.dirty&129&&t(3,i=c!=JSON.stringify(a))},[a,f,u,i,m,_,r,c,g,y,S,T,$]}class WE extends ge{constructor(e){super(),_e(this,e,UE,BE,me,{})}}function YE(n){let e,t,i,l=[{type:"password"},{autocomplete:"new-password"},n[5]],s={};for(let o=0;o',i=M(),l=b("input"),p(t,"type","button"),p(t,"class","btn btn-transparent btn-circle"),p(e,"class","form-field-addon"),ni(l,a)},m(f,u){w(f,e,u),k(e,t),w(f,i,u),w(f,l,u),l.autofocus&&l.focus(),s||(o=[Se(Pe.call(null,t,{position:"left",text:"Set new value"})),J(t,"click",n[6])],s=!0)},p(f,u){ni(l,a=pt(r,[{readOnly:!0},{type:"text"},u&2&&{placeholder:f[1]},u&32&&f[5]]))},d(f){f&&(v(e),v(i),v(l)),s=!1,$e(o)}}}function JE(n){let e;function t(s,o){return s[3]?KE:YE}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,[o]){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},i:Q,o:Q,d(s){s&&v(e),l.d(s)}}}function ZE(n,e,t){const i=["value","mask"];let l=Ge(e,i),{value:s=""}=e,{mask:o="******"}=e,r,a=!1;async function f(){t(0,s=""),t(3,a=!1),await Qt(),r==null||r.focus()}const u=()=>f();function c(m){ee[m?"unshift":"push"](()=>{r=m,t(2,r)})}function d(){s=this.value,t(0,s)}return n.$$set=m=>{e=Ie(Ie({},e),Yt(m)),t(5,l=Ge(e,i)),"value"in m&&t(0,s=m.value),"mask"in m&&t(1,o=m.mask)},n.$$.update=()=>{n.$$.dirty&3&&t(3,a=s===o)},[s,o,r,a,f,l,u,c,d]}class Ka extends ge{constructor(e){super(),_e(this,e,ZE,JE,me,{value:0,mask:1})}}function GE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_;return{c(){e=b("label"),t=K("Subject"),l=M(),s=b("input"),r=M(),a=b("div"),f=K(`Available placeholder parameters: - `),u=b("button"),u.textContent="{APP_NAME} ",c=K(`, - `),d=b("button"),d.textContent="{APP_URL} ",m=K("."),p(e,"for",i=n[31]),p(s,"type","text"),p(s,"id",o=n[31]),p(s,"spellcheck","false"),s.required=!0,p(u,"type","button"),p(u,"class","label label-sm link-primary txt-mono"),p(d,"type","button"),p(d,"class","label label-sm link-primary txt-mono"),p(a,"class","help-block")},m(g,y){w(g,e,y),k(e,t),w(g,l,y),w(g,s,y),re(s,n[0].subject),w(g,r,y),w(g,a,y),k(a,f),k(a,u),k(a,c),k(a,d),k(a,m),h||(_=[J(s,"input",n[13]),J(u,"click",n[14]),J(d,"click",n[15])],h=!0)},p(g,y){y[1]&1&&i!==(i=g[31])&&p(e,"for",i),y[1]&1&&o!==(o=g[31])&&p(s,"id",o),y[0]&1&&s.value!==g[0].subject&&re(s,g[0].subject)},d(g){g&&(v(e),v(l),v(s),v(r),v(a)),h=!1,$e(_)}}}function XE(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y;return{c(){e=b("label"),t=K("Action URL"),l=M(),s=b("input"),r=M(),a=b("div"),f=K(`Available placeholder parameters: - `),u=b("button"),u.textContent="{APP_NAME} ",c=K(`, - `),d=b("button"),d.textContent="{APP_URL} ",m=K(`, - `),h=b("button"),h.textContent="{TOKEN} ",_=K("."),p(e,"for",i=n[31]),p(s,"type","text"),p(s,"id",o=n[31]),p(s,"spellcheck","false"),s.required=!0,p(u,"type","button"),p(u,"class","label label-sm link-primary txt-mono"),p(d,"type","button"),p(d,"class","label label-sm link-primary txt-mono"),p(h,"type","button"),p(h,"class","label label-sm link-primary txt-mono"),p(h,"title","Required parameter"),p(a,"class","help-block")},m(S,T){w(S,e,T),k(e,t),w(S,l,T),w(S,s,T),re(s,n[0].actionUrl),w(S,r,T),w(S,a,T),k(a,f),k(a,u),k(a,c),k(a,d),k(a,m),k(a,h),k(a,_),g||(y=[J(s,"input",n[16]),J(u,"click",n[17]),J(d,"click",n[18]),J(h,"click",n[19])],g=!0)},p(S,T){T[1]&1&&i!==(i=S[31])&&p(e,"for",i),T[1]&1&&o!==(o=S[31])&&p(s,"id",o),T[0]&1&&s.value!==S[0].actionUrl&&re(s,S[0].actionUrl)},d(S){S&&(v(e),v(l),v(s),v(r),v(a)),g=!1,$e(y)}}}function QE(n){let e,t,i,l;return{c(){e=b("textarea"),p(e,"id",t=n[31]),p(e,"class","txt-mono"),p(e,"spellcheck","false"),p(e,"rows","14"),e.required=!0},m(s,o){w(s,e,o),re(e,n[0].body),i||(l=J(e,"input",n[21]),i=!0)},p(s,o){o[1]&1&&t!==(t=s[31])&&p(e,"id",t),o[0]&1&&re(e,s[0].body)},i:Q,o:Q,d(s){s&&v(e),i=!1,l()}}}function xE(n){let e,t,i,l;function s(a){n[20](a)}var o=n[4];function r(a,f){let u={id:a[31],language:"html"};return a[0].body!==void 0&&(u.value=a[0].body),{props:u}}return o&&(e=Dt(o,r(n)),ee.push(()=>be(e,"value",s))),{c(){e&&B(e.$$.fragment),i=ye()},m(a,f){e&&z(e,a,f),w(a,i,f),l=!0},p(a,f){if(f[0]&16&&o!==(o=a[4])){if(e){le();const u=e;A(u.$$.fragment,1,0,()=>{V(u,1)}),se()}o?(e=Dt(o,r(a)),ee.push(()=>be(e,"value",s)),B(e.$$.fragment),E(e.$$.fragment,1),z(e,i.parentNode,i)):e=null}else if(o){const u={};f[1]&1&&(u.id=a[31]),!t&&f[0]&1&&(t=!0,u.value=a[0].body,ke(()=>t=!1)),e.$set(u)}},i(a){l||(e&&E(e.$$.fragment,a),l=!0)},o(a){e&&A(e.$$.fragment,a),l=!1},d(a){a&&v(i),e&&V(e,a)}}}function eI(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$;const C=[xE,QE],O=[];function D(I,L){return I[4]&&!I[5]?0:1}return s=D(n),o=O[s]=C[s](n),{c(){e=b("label"),t=K("Body (HTML)"),l=M(),o.c(),r=M(),a=b("div"),f=K(`Available placeholder parameters: - `),u=b("button"),u.textContent="{APP_NAME} ",c=K(`, - `),d=b("button"),d.textContent="{APP_URL} ",m=K(`, - `),h=b("button"),h.textContent="{TOKEN} ",_=K(`, - `),g=b("button"),g.textContent="{ACTION_URL} ",y=K("."),p(e,"for",i=n[31]),p(u,"type","button"),p(u,"class","label label-sm link-primary txt-mono"),p(d,"type","button"),p(d,"class","label label-sm link-primary txt-mono"),p(h,"type","button"),p(h,"class","label label-sm link-primary txt-mono"),p(g,"type","button"),p(g,"class","label label-sm link-primary txt-mono"),p(g,"title","Required parameter"),p(a,"class","help-block")},m(I,L){w(I,e,L),k(e,t),w(I,l,L),O[s].m(I,L),w(I,r,L),w(I,a,L),k(a,f),k(a,u),k(a,c),k(a,d),k(a,m),k(a,h),k(a,_),k(a,g),k(a,y),S=!0,T||($=[J(u,"click",n[22]),J(d,"click",n[23]),J(h,"click",n[24]),J(g,"click",n[25])],T=!0)},p(I,L){(!S||L[1]&1&&i!==(i=I[31]))&&p(e,"for",i);let R=s;s=D(I),s===R?O[s].p(I,L):(le(),A(O[R],1,1,()=>{O[R]=null}),se(),o=O[s],o?o.p(I,L):(o=O[s]=C[s](I),o.c()),E(o,1),o.m(r.parentNode,r))},i(I){S||(E(o),S=!0)},o(I){A(o),S=!1},d(I){I&&(v(e),v(l),v(r),v(a)),O[s].d(I),T=!1,$e($)}}}function tI(n){let e,t,i,l,s,o;return e=new ce({props:{class:"form-field required",name:n[1]+".subject",$$slots:{default:[GE,({uniqueId:r})=>({31:r}),({uniqueId:r})=>[0,r?1:0]]},$$scope:{ctx:n}}}),i=new ce({props:{class:"form-field required",name:n[1]+".actionUrl",$$slots:{default:[XE,({uniqueId:r})=>({31:r}),({uniqueId:r})=>[0,r?1:0]]},$$scope:{ctx:n}}}),s=new ce({props:{class:"form-field m-0 required",name:n[1]+".body",$$slots:{default:[eI,({uniqueId:r})=>({31:r}),({uniqueId:r})=>[0,r?1:0]]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment)},m(r,a){z(e,r,a),w(r,t,a),z(i,r,a),w(r,l,a),z(s,r,a),o=!0},p(r,a){const f={};a[0]&2&&(f.name=r[1]+".subject"),a[0]&1|a[1]&3&&(f.$$scope={dirty:a,ctx:r}),e.$set(f);const u={};a[0]&2&&(u.name=r[1]+".actionUrl"),a[0]&1|a[1]&3&&(u.$$scope={dirty:a,ctx:r}),i.$set(u);const c={};a[0]&2&&(c.name=r[1]+".body"),a[0]&49|a[1]&3&&(c.$$scope={dirty:a,ctx:r}),s.$set(c)},i(r){o||(E(e.$$.fragment,r),E(i.$$.fragment,r),E(s.$$.fragment,r),o=!0)},o(r){A(e.$$.fragment,r),A(i.$$.fragment,r),A(s.$$.fragment,r),o=!1},d(r){r&&(v(t),v(l)),V(e,r),V(i,r),V(s,r)}}}function Gh(n){let e,t,i,l,s;return{c(){e=b("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(o,r){w(o,e,r),i=!0,l||(s=Se(Pe.call(null,e,{text:"Has errors",position:"left"})),l=!0)},i(o){i||(o&&Ke(()=>{i&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!0)),t.run(1))}),i=!0)},o(o){o&&(t||(t=Fe(e,Wt,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(o){o&&v(e),o&&t&&t.end(),l=!1,s()}}}function nI(n){let e,t,i,l,s,o,r,a,f,u=n[6]&&Gh();return{c(){e=b("div"),t=b("i"),i=M(),l=b("span"),s=K(n[2]),o=M(),r=b("div"),a=M(),u&&u.c(),f=ye(),p(t,"class","ri-draft-line"),p(l,"class","txt"),p(e,"class","inline-flex"),p(r,"class","flex-fill")},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),k(l,s),w(c,o,d),w(c,r,d),w(c,a,d),u&&u.m(c,d),w(c,f,d)},p(c,d){d[0]&4&&oe(s,c[2]),c[6]?u?d[0]&64&&E(u,1):(u=Gh(),u.c(),E(u,1),u.m(f.parentNode,f)):u&&(le(),A(u,1,1,()=>{u=null}),se())},d(c){c&&(v(e),v(o),v(r),v(a),v(f)),u&&u.d(c)}}}function iI(n){let e,t;const i=[n[8]];let l={$$slots:{header:[nI],default:[tI]},$$scope:{ctx:n}};for(let s=0;st(12,o=Y));let{key:r}=e,{title:a}=e,{config:f={}}=e,u,c=Xh,d=!1;function m(){u==null||u.expand()}function h(){u==null||u.collapse()}function _(){u==null||u.collapseSiblings()}async function g(){c||d||(t(5,d=!0),t(4,c=(await tt(async()=>{const{default:Y}=await import("./CodeEditor-CZ0EgQcM.js");return{default:Y}},__vite__mapDeps([2,1]),import.meta.url)).default),Xh=c,t(5,d=!1))}function y(Y){j.copyToClipboard(Y),$o(`Copied ${Y} to clipboard`,2e3)}g();function S(){f.subject=this.value,t(0,f)}const T=()=>y("{APP_NAME}"),$=()=>y("{APP_URL}");function C(){f.actionUrl=this.value,t(0,f)}const O=()=>y("{APP_NAME}"),D=()=>y("{APP_URL}"),I=()=>y("{TOKEN}");function L(Y){n.$$.not_equal(f.body,Y)&&(f.body=Y,t(0,f))}function R(){f.body=this.value,t(0,f)}const F=()=>y("{APP_NAME}"),N=()=>y("{APP_URL}"),P=()=>y("{TOKEN}"),q=()=>y("{ACTION_URL}");function H(Y){ee[Y?"unshift":"push"](()=>{u=Y,t(3,u)})}function W(Y){Ce.call(this,n,Y)}function G(Y){Ce.call(this,n,Y)}function U(Y){Ce.call(this,n,Y)}return n.$$set=Y=>{e=Ie(Ie({},e),Yt(Y)),t(8,s=Ge(e,l)),"key"in Y&&t(1,r=Y.key),"title"in Y&&t(2,a=Y.title),"config"in Y&&t(0,f=Y.config)},n.$$.update=()=>{n.$$.dirty[0]&4098&&t(6,i=!j.isEmpty(j.getNestedVal(o,r))),n.$$.dirty[0]&3&&(f.enabled||li(r))},[f,r,a,u,c,d,i,y,s,m,h,_,o,S,T,$,C,O,D,I,L,R,F,N,P,q,H,W,G,U]}class Ja extends ge{constructor(e){super(),_e(this,e,lI,iI,me,{key:1,title:2,config:0,expand:9,collapse:10,collapseSiblings:11},null,[-1,-1])}get expand(){return this.$$.ctx[9]}get collapse(){return this.$$.ctx[10]}get collapseSiblings(){return this.$$.ctx[11]}}function Qh(n,e,t){const i=n.slice();return i[21]=e[t],i}function xh(n,e){let t,i,l,s,o,r=e[21].label+"",a,f,u,c,d,m;return c=_0(e[11][0]),{key:n,first:null,c(){t=b("div"),i=b("input"),s=M(),o=b("label"),a=K(r),u=M(),p(i,"type","radio"),p(i,"name","template"),p(i,"id",l=e[20]+e[21].value),i.__value=e[21].value,re(i,i.__value),p(o,"for",f=e[20]+e[21].value),p(t,"class","form-field-block"),c.p(i),this.first=t},m(h,_){w(h,t,_),k(t,i),i.checked=i.__value===e[2],k(t,s),k(t,o),k(o,a),k(t,u),d||(m=J(i,"change",e[10]),d=!0)},p(h,_){e=h,_&1048576&&l!==(l=e[20]+e[21].value)&&p(i,"id",l),_&4&&(i.checked=i.__value===e[2]),_&1048576&&f!==(f=e[20]+e[21].value)&&p(o,"for",f)},d(h){h&&v(t),c.r(),d=!1,m()}}}function sI(n){let e=[],t=new Map,i,l=ue(n[7]);const s=o=>o[21].value;for(let o=0;o({20:a}),({uniqueId:a})=>a?1048576:0]},$$scope:{ctx:n}}}),l=new ce({props:{class:"form-field required m-0",name:"email",$$slots:{default:[oI,({uniqueId:a})=>({20:a}),({uniqueId:a})=>a?1048576:0]},$$scope:{ctx:n}}}),{c(){e=b("form"),B(t.$$.fragment),i=M(),B(l.$$.fragment),p(e,"id",n[6]),p(e,"autocomplete","off")},m(a,f){w(a,e,f),z(t,e,null),k(e,i),z(l,e,null),s=!0,o||(r=J(e,"submit",Be(n[13])),o=!0)},p(a,f){const u={};f&17825796&&(u.$$scope={dirty:f,ctx:a}),t.$set(u);const c={};f&17825794&&(c.$$scope={dirty:f,ctx:a}),l.$set(c)},i(a){s||(E(t.$$.fragment,a),E(l.$$.fragment,a),s=!0)},o(a){A(t.$$.fragment,a),A(l.$$.fragment,a),s=!1},d(a){a&&v(e),V(t),V(l),o=!1,r()}}}function aI(n){let e;return{c(){e=b("h4"),e.textContent="Send test email",p(e,"class","center txt-break")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function fI(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("button"),t=K("Close"),i=M(),l=b("button"),s=b("i"),o=M(),r=b("span"),r.textContent="Send",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[4],p(s,"class","ri-mail-send-line"),p(r,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=a=!n[5]||n[4],x(l,"btn-loading",n[4])},m(c,d){w(c,e,d),k(e,t),w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=J(e,"click",n[0]),f=!0)},p(c,d){d&16&&(e.disabled=c[4]),d&48&&a!==(a=!c[5]||c[4])&&(l.disabled=a),d&16&&x(l,"btn-loading",c[4])},d(c){c&&(v(e),v(i),v(l)),f=!1,u()}}}function uI(n){let e,t,i={class:"overlay-panel-sm email-test-popup",overlayClose:!n[4],escClose:!n[4],beforeHide:n[14],popup:!0,$$slots:{footer:[fI],header:[aI],default:[rI]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[15](e),e.$on("show",n[16]),e.$on("hide",n[17]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.overlayClose=!l[4]),s&16&&(o.escClose=!l[4]),s&16&&(o.beforeHide=l[14]),s&16777270&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[15](null),V(e,l)}}}const Nr="last_email_test",e_="email_test_request";function cI(n,e,t){let i;const l=lt(),s="email_test_"+j.randomString(5),o=[{label:'"Verification" template',value:"verification"},{label:'"Password reset" template',value:"password-reset"},{label:'"Confirm email change" template',value:"email-change"}];let r,a=localStorage.getItem(Nr),f=o[0].value,u=!1,c=null;function d(D="",I=""){t(1,a=D||localStorage.getItem(Nr)),t(2,f=I||o[0].value),Jt({}),r==null||r.show()}function m(){return clearTimeout(c),r==null?void 0:r.hide()}async function h(){if(!(!i||u)){t(4,u=!0),localStorage==null||localStorage.setItem(Nr,a),clearTimeout(c),c=setTimeout(()=>{ae.cancelRequest(e_),ii("Test email send timeout.")},3e4);try{await ae.settings.testEmail(a,f,{$cancelKey:e_}),Lt("Successfully sent test email."),l("submit"),t(4,u=!1),await Qt(),m()}catch(D){t(4,u=!1),ae.error(D)}clearTimeout(c)}}const _=[[]];function g(){f=this.__value,t(2,f)}function y(){a=this.value,t(1,a)}const S=()=>h(),T=()=>!u;function $(D){ee[D?"unshift":"push"](()=>{r=D,t(3,r)})}function C(D){Ce.call(this,n,D)}function O(D){Ce.call(this,n,D)}return n.$$.update=()=>{n.$$.dirty&6&&t(5,i=!!a&&!!f)},[m,a,f,r,u,i,s,o,h,d,g,_,y,S,T,$,C,O]}class dI extends ge{constructor(e){super(),_e(this,e,cI,uI,me,{show:9,hide:0})}get show(){return this.$$.ctx[9]}get hide(){return this.$$.ctx[0]}}function pI(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$;i=new ce({props:{class:"form-field required",name:"meta.senderName",$$slots:{default:[hI,({uniqueId:N})=>({34:N}),({uniqueId:N})=>[0,N?8:0]]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field required",name:"meta.senderAddress",$$slots:{default:[_I,({uniqueId:N})=>({34:N}),({uniqueId:N})=>[0,N?8:0]]},$$scope:{ctx:n}}});let C=!n[0].meta.verificationTemplate.hidden&&t_(n),O=!n[0].meta.resetPasswordTemplate.hidden&&n_(n),D=!n[0].meta.confirmEmailChangeTemplate.hidden&&i_(n);h=new ce({props:{class:"form-field form-field-toggle m-b-sm",$$slots:{default:[gI,({uniqueId:N})=>({34:N}),({uniqueId:N})=>[0,N?8:0]]},$$scope:{ctx:n}}});let I=n[0].smtp.enabled&&l_(n);function L(N,P){return N[5]?MI:OI}let R=L(n),F=R(n);return{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),a=b("div"),C&&C.c(),f=M(),O&&O.c(),u=M(),D&&D.c(),c=M(),d=b("hr"),m=M(),B(h.$$.fragment),_=M(),I&&I.c(),g=M(),y=b("div"),S=b("div"),T=M(),F.c(),p(t,"class","col-lg-6"),p(s,"class","col-lg-6"),p(e,"class","grid m-b-base"),p(a,"class","accordions"),p(S,"class","flex-fill"),p(y,"class","flex")},m(N,P){w(N,e,P),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),w(N,r,P),w(N,a,P),C&&C.m(a,null),k(a,f),O&&O.m(a,null),k(a,u),D&&D.m(a,null),w(N,c,P),w(N,d,P),w(N,m,P),z(h,N,P),w(N,_,P),I&&I.m(N,P),w(N,g,P),w(N,y,P),k(y,S),k(y,T),F.m(y,null),$=!0},p(N,P){const q={};P[0]&1|P[1]&24&&(q.$$scope={dirty:P,ctx:N}),i.$set(q);const H={};P[0]&1|P[1]&24&&(H.$$scope={dirty:P,ctx:N}),o.$set(H),N[0].meta.verificationTemplate.hidden?C&&(le(),A(C,1,1,()=>{C=null}),se()):C?(C.p(N,P),P[0]&1&&E(C,1)):(C=t_(N),C.c(),E(C,1),C.m(a,f)),N[0].meta.resetPasswordTemplate.hidden?O&&(le(),A(O,1,1,()=>{O=null}),se()):O?(O.p(N,P),P[0]&1&&E(O,1)):(O=n_(N),O.c(),E(O,1),O.m(a,u)),N[0].meta.confirmEmailChangeTemplate.hidden?D&&(le(),A(D,1,1,()=>{D=null}),se()):D?(D.p(N,P),P[0]&1&&E(D,1)):(D=i_(N),D.c(),E(D,1),D.m(a,null));const W={};P[0]&1|P[1]&24&&(W.$$scope={dirty:P,ctx:N}),h.$set(W),N[0].smtp.enabled?I?(I.p(N,P),P[0]&1&&E(I,1)):(I=l_(N),I.c(),E(I,1),I.m(g.parentNode,g)):I&&(le(),A(I,1,1,()=>{I=null}),se()),R===(R=L(N))&&F?F.p(N,P):(F.d(1),F=R(N),F&&(F.c(),F.m(y,null)))},i(N){$||(E(i.$$.fragment,N),E(o.$$.fragment,N),E(C),E(O),E(D),E(h.$$.fragment,N),E(I),$=!0)},o(N){A(i.$$.fragment,N),A(o.$$.fragment,N),A(C),A(O),A(D),A(h.$$.fragment,N),A(I),$=!1},d(N){N&&(v(e),v(r),v(a),v(c),v(d),v(m),v(_),v(g),v(y)),V(i),V(o),C&&C.d(),O&&O.d(),D&&D.d(),V(h,N),I&&I.d(N),F.d()}}}function mI(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function hI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Sender name"),l=M(),s=b("input"),p(e,"for",i=n[34]),p(s,"type","text"),p(s,"id",o=n[34]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].meta.senderName),r||(a=J(s,"input",n[13]),r=!0)},p(f,u){u[1]&8&&i!==(i=f[34])&&p(e,"for",i),u[1]&8&&o!==(o=f[34])&&p(s,"id",o),u[0]&1&&s.value!==f[0].meta.senderName&&re(s,f[0].meta.senderName)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function _I(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Sender address"),l=M(),s=b("input"),p(e,"for",i=n[34]),p(s,"type","email"),p(s,"id",o=n[34]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].meta.senderAddress),r||(a=J(s,"input",n[14]),r=!0)},p(f,u){u[1]&8&&i!==(i=f[34])&&p(e,"for",i),u[1]&8&&o!==(o=f[34])&&p(s,"id",o),u[0]&1&&s.value!==f[0].meta.senderAddress&&re(s,f[0].meta.senderAddress)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function t_(n){let e,t,i;function l(o){n[15](o)}let s={single:!0,key:"meta.verificationTemplate",title:'Default "Verification" email template'};return n[0].meta.verificationTemplate!==void 0&&(s.config=n[0].meta.verificationTemplate),e=new Ja({props:s}),ee.push(()=>be(e,"config",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&1&&(t=!0,a.config=o[0].meta.verificationTemplate,ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function n_(n){let e,t,i;function l(o){n[16](o)}let s={single:!0,key:"meta.resetPasswordTemplate",title:'Default "Password reset" email template'};return n[0].meta.resetPasswordTemplate!==void 0&&(s.config=n[0].meta.resetPasswordTemplate),e=new Ja({props:s}),ee.push(()=>be(e,"config",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&1&&(t=!0,a.config=o[0].meta.resetPasswordTemplate,ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function i_(n){let e,t,i;function l(o){n[17](o)}let s={single:!0,key:"meta.confirmEmailChangeTemplate",title:'Default "Confirm email change" email template'};return n[0].meta.confirmEmailChangeTemplate!==void 0&&(s.config=n[0].meta.confirmEmailChangeTemplate),e=new Ja({props:s}),ee.push(()=>be(e,"config",l)),{c(){B(e.$$.fragment)},m(o,r){z(e,o,r),i=!0},p(o,r){const a={};!t&&r[0]&1&&(t=!0,a.config=o[0].meta.confirmEmailChangeTemplate,ke(()=>t=!1)),e.$set(a)},i(o){i||(E(e.$$.fragment,o),i=!0)},o(o){A(e.$$.fragment,o),i=!1},d(o){V(e,o)}}}function gI(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.innerHTML="Use SMTP mail server (recommended)",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[34]),e.required=!0,p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[34])},m(c,d){w(c,e,d),e.checked=n[0].smtp.enabled,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[18]),Se(Pe.call(null,r,{text:'By default PocketBase uses the unix "sendmail" command for sending emails. For better emails deliverability it is recommended to use a SMTP mail server.',position:"top"}))],f=!0)},p(c,d){d[1]&8&&t!==(t=c[34])&&p(e,"id",t),d[0]&1&&(e.checked=c[0].smtp.enabled),d[1]&8&&a!==(a=c[34])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function l_(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$;l=new ce({props:{class:"form-field required",name:"smtp.host",$$slots:{default:[bI,({uniqueId:L})=>({34:L}),({uniqueId:L})=>[0,L?8:0]]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field required",name:"smtp.port",$$slots:{default:[kI,({uniqueId:L})=>({34:L}),({uniqueId:L})=>[0,L?8:0]]},$$scope:{ctx:n}}}),u=new ce({props:{class:"form-field",name:"smtp.username",$$slots:{default:[yI,({uniqueId:L})=>({34:L}),({uniqueId:L})=>[0,L?8:0]]},$$scope:{ctx:n}}}),m=new ce({props:{class:"form-field",name:"smtp.password",$$slots:{default:[vI,({uniqueId:L})=>({34:L}),({uniqueId:L})=>[0,L?8:0]]},$$scope:{ctx:n}}});function C(L,R){return L[4]?SI:wI}let O=C(n),D=O(n),I=n[4]&&s_(n);return{c(){e=b("div"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),B(r.$$.fragment),a=M(),f=b("div"),B(u.$$.fragment),c=M(),d=b("div"),B(m.$$.fragment),h=M(),_=b("button"),D.c(),g=M(),I&&I.c(),p(i,"class","col-lg-4"),p(o,"class","col-lg-2"),p(f,"class","col-lg-3"),p(d,"class","col-lg-3"),p(t,"class","grid"),p(_,"type","button"),p(_,"class","btn btn-sm btn-secondary m-t-sm m-b-sm")},m(L,R){w(L,e,R),k(e,t),k(t,i),z(l,i,null),k(t,s),k(t,o),z(r,o,null),k(t,a),k(t,f),z(u,f,null),k(t,c),k(t,d),z(m,d,null),k(e,h),k(e,_),D.m(_,null),k(e,g),I&&I.m(e,null),S=!0,T||($=J(_,"click",Be(n[23])),T=!0)},p(L,R){const F={};R[0]&1|R[1]&24&&(F.$$scope={dirty:R,ctx:L}),l.$set(F);const N={};R[0]&1|R[1]&24&&(N.$$scope={dirty:R,ctx:L}),r.$set(N);const P={};R[0]&1|R[1]&24&&(P.$$scope={dirty:R,ctx:L}),u.$set(P);const q={};R[0]&1|R[1]&24&&(q.$$scope={dirty:R,ctx:L}),m.$set(q),O!==(O=C(L))&&(D.d(1),D=O(L),D&&(D.c(),D.m(_,null))),L[4]?I?(I.p(L,R),R[0]&16&&E(I,1)):(I=s_(L),I.c(),E(I,1),I.m(e,null)):I&&(le(),A(I,1,1,()=>{I=null}),se())},i(L){S||(E(l.$$.fragment,L),E(r.$$.fragment,L),E(u.$$.fragment,L),E(m.$$.fragment,L),E(I),L&&Ke(()=>{S&&(y||(y=Fe(e,et,{duration:150},!0)),y.run(1))}),S=!0)},o(L){A(l.$$.fragment,L),A(r.$$.fragment,L),A(u.$$.fragment,L),A(m.$$.fragment,L),A(I),L&&(y||(y=Fe(e,et,{duration:150},!1)),y.run(0)),S=!1},d(L){L&&v(e),V(l),V(r),V(u),V(m),D.d(),I&&I.d(),L&&y&&y.end(),T=!1,$()}}}function bI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("SMTP server host"),l=M(),s=b("input"),p(e,"for",i=n[34]),p(s,"type","text"),p(s,"id",o=n[34]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].smtp.host),r||(a=J(s,"input",n[19]),r=!0)},p(f,u){u[1]&8&&i!==(i=f[34])&&p(e,"for",i),u[1]&8&&o!==(o=f[34])&&p(s,"id",o),u[0]&1&&s.value!==f[0].smtp.host&&re(s,f[0].smtp.host)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function kI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Port"),l=M(),s=b("input"),p(e,"for",i=n[34]),p(s,"type","number"),p(s,"id",o=n[34]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].smtp.port),r||(a=J(s,"input",n[20]),r=!0)},p(f,u){u[1]&8&&i!==(i=f[34])&&p(e,"for",i),u[1]&8&&o!==(o=f[34])&&p(s,"id",o),u[0]&1&&it(s.value)!==f[0].smtp.port&&re(s,f[0].smtp.port)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function yI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Username"),l=M(),s=b("input"),p(e,"for",i=n[34]),p(s,"type","text"),p(s,"id",o=n[34])},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].smtp.username),r||(a=J(s,"input",n[21]),r=!0)},p(f,u){u[1]&8&&i!==(i=f[34])&&p(e,"for",i),u[1]&8&&o!==(o=f[34])&&p(s,"id",o),u[0]&1&&s.value!==f[0].smtp.username&&re(s,f[0].smtp.username)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function vI(n){let e,t,i,l,s,o,r;function a(u){n[22](u)}let f={id:n[34]};return n[0].smtp.password!==void 0&&(f.value=n[0].smtp.password),s=new Ka({props:f}),ee.push(()=>be(s,"value",a)),{c(){e=b("label"),t=K("Password"),l=M(),B(s.$$.fragment),p(e,"for",i=n[34])},m(u,c){w(u,e,c),k(e,t),w(u,l,c),z(s,u,c),r=!0},p(u,c){(!r||c[1]&8&&i!==(i=u[34]))&&p(e,"for",i);const d={};c[1]&8&&(d.id=u[34]),!o&&c[0]&1&&(o=!0,d.value=u[0].smtp.password,ke(()=>o=!1)),s.$set(d)},i(u){r||(E(s.$$.fragment,u),r=!0)},o(u){A(s.$$.fragment,u),r=!1},d(u){u&&(v(e),v(l)),V(s,u)}}}function wI(n){let e,t,i;return{c(){e=b("span"),e.textContent="Show more options",t=M(),i=b("i"),p(e,"class","txt"),p(i,"class","ri-arrow-down-s-line")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},d(l){l&&(v(e),v(t),v(i))}}}function SI(n){let e,t,i;return{c(){e=b("span"),e.textContent="Hide more options",t=M(),i=b("i"),p(e,"class","txt"),p(i,"class","ri-arrow-up-s-line")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},d(l){l&&(v(e),v(t),v(i))}}}function s_(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;return i=new ce({props:{class:"form-field",name:"smtp.tls",$$slots:{default:[$I,({uniqueId:h})=>({34:h}),({uniqueId:h})=>[0,h?8:0]]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field",name:"smtp.authMethod",$$slots:{default:[TI,({uniqueId:h})=>({34:h}),({uniqueId:h})=>[0,h?8:0]]},$$scope:{ctx:n}}}),f=new ce({props:{class:"form-field",name:"smtp.localName",$$slots:{default:[CI,({uniqueId:h})=>({34:h}),({uniqueId:h})=>[0,h?8:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),a=b("div"),B(f.$$.fragment),u=M(),c=b("div"),p(t,"class","col-lg-3"),p(s,"class","col-lg-3"),p(a,"class","col-lg-6"),p(c,"class","col-lg-12"),p(e,"class","grid")},m(h,_){w(h,e,_),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,r),k(e,a),z(f,a,null),k(e,u),k(e,c),m=!0},p(h,_){const g={};_[0]&1|_[1]&24&&(g.$$scope={dirty:_,ctx:h}),i.$set(g);const y={};_[0]&1|_[1]&24&&(y.$$scope={dirty:_,ctx:h}),o.$set(y);const S={};_[0]&1|_[1]&24&&(S.$$scope={dirty:_,ctx:h}),f.$set(S)},i(h){m||(E(i.$$.fragment,h),E(o.$$.fragment,h),E(f.$$.fragment,h),h&&Ke(()=>{m&&(d||(d=Fe(e,et,{duration:150},!0)),d.run(1))}),m=!0)},o(h){A(i.$$.fragment,h),A(o.$$.fragment,h),A(f.$$.fragment,h),h&&(d||(d=Fe(e,et,{duration:150},!1)),d.run(0)),m=!1},d(h){h&&v(e),V(i),V(o),V(f),h&&d&&d.end()}}}function $I(n){let e,t,i,l,s,o,r;function a(u){n[24](u)}let f={id:n[34],items:n[7]};return n[0].smtp.tls!==void 0&&(f.keyOfSelected=n[0].smtp.tls),s=new hi({props:f}),ee.push(()=>be(s,"keyOfSelected",a)),{c(){e=b("label"),t=K("TLS encryption"),l=M(),B(s.$$.fragment),p(e,"for",i=n[34])},m(u,c){w(u,e,c),k(e,t),w(u,l,c),z(s,u,c),r=!0},p(u,c){(!r||c[1]&8&&i!==(i=u[34]))&&p(e,"for",i);const d={};c[1]&8&&(d.id=u[34]),!o&&c[0]&1&&(o=!0,d.keyOfSelected=u[0].smtp.tls,ke(()=>o=!1)),s.$set(d)},i(u){r||(E(s.$$.fragment,u),r=!0)},o(u){A(s.$$.fragment,u),r=!1},d(u){u&&(v(e),v(l)),V(s,u)}}}function TI(n){let e,t,i,l,s,o,r;function a(u){n[25](u)}let f={id:n[34],items:n[8]};return n[0].smtp.authMethod!==void 0&&(f.keyOfSelected=n[0].smtp.authMethod),s=new hi({props:f}),ee.push(()=>be(s,"keyOfSelected",a)),{c(){e=b("label"),t=K("AUTH method"),l=M(),B(s.$$.fragment),p(e,"for",i=n[34])},m(u,c){w(u,e,c),k(e,t),w(u,l,c),z(s,u,c),r=!0},p(u,c){(!r||c[1]&8&&i!==(i=u[34]))&&p(e,"for",i);const d={};c[1]&8&&(d.id=u[34]),!o&&c[0]&1&&(o=!0,d.keyOfSelected=u[0].smtp.authMethod,ke(()=>o=!1)),s.$set(d)},i(u){r||(E(s.$$.fragment,u),r=!0)},o(u){A(s.$$.fragment,u),r=!1},d(u){u&&(v(e),v(l)),V(s,u)}}}function CI(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=b("span"),t.textContent="EHLO/HELO domain",i=M(),l=b("i"),o=M(),r=b("input"),p(t,"class","txt"),p(l,"class","ri-information-line link-hint"),p(e,"for",s=n[34]),p(r,"type","text"),p(r,"id",a=n[34]),p(r,"placeholder","Default to localhost")},m(c,d){w(c,e,d),k(e,t),k(e,i),k(e,l),w(c,o,d),w(c,r,d),re(r,n[0].smtp.localName),f||(u=[Se(Pe.call(null,l,{text:"Some SMTP servers, such as the Gmail SMTP-relay, requires a proper domain name in the inital EHLO/HELO exchange and will reject attempts to use localhost.",position:"top"})),J(r,"input",n[26])],f=!0)},p(c,d){d[1]&8&&s!==(s=c[34])&&p(e,"for",s),d[1]&8&&a!==(a=c[34])&&p(r,"id",a),d[0]&1&&r.value!==c[0].smtp.localName&&re(r,c[0].smtp.localName)},d(c){c&&(v(e),v(o),v(r)),f=!1,$e(u)}}}function OI(n){let e,t,i;return{c(){e=b("button"),e.innerHTML=' Send test email',p(e,"type","button"),p(e,"class","btn btn-expanded btn-outline")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[29]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function MI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=M(),l=b("button"),s=b("span"),s.textContent="Save changes",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[3],x(l,"btn-loading",n[3])},m(f,u){w(f,e,u),k(e,t),w(f,i,u),w(f,l,u),k(l,s),r||(a=[J(e,"click",n[27]),J(l,"click",n[28])],r=!0)},p(f,u){u[0]&8&&(e.disabled=f[3]),u[0]&40&&o!==(o=!f[5]||f[3])&&(l.disabled=o),u[0]&8&&x(l,"btn-loading",f[3])},d(f){f&&(v(e),v(i),v(l)),r=!1,$e(a)}}}function DI(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g;const y=[mI,pI],S=[];function T($,C){return $[2]?0:1}return d=T(n),m=S[d]=y[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[6]),r=M(),a=b("div"),f=b("form"),u=b("div"),u.innerHTML="

    Configure common settings for sending emails.

    ",c=M(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","content txt-xl m-b-base"),p(f,"class","panel"),p(f,"autocomplete","off"),p(a,"class","wrapper")},m($,C){w($,e,C),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w($,r,C),w($,a,C),k(a,f),k(f,u),k(f,c),S[d].m(f,null),h=!0,_||(g=J(f,"submit",Be(n[30])),_=!0)},p($,C){(!h||C[0]&64)&&oe(o,$[6]);let O=d;d=T($),d===O?S[d].p($,C):(le(),A(S[O],1,1,()=>{S[O]=null}),se(),m=S[d],m?m.p($,C):(m=S[d]=y[d]($),m.c()),E(m,1),m.m(f,null))},i($){h||(E(m),h=!0)},o($){A(m),h=!1},d($){$&&(v(e),v(r),v(a)),S[d].d(),_=!1,g()}}}function EI(n){let e,t,i,l,s,o;e=new _i({}),i=new bn({props:{$$slots:{default:[DI]},$$scope:{ctx:n}}});let r={};return s=new dI({props:r}),n[31](s),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment)},m(a,f){z(e,a,f),w(a,t,f),z(i,a,f),w(a,l,f),z(s,a,f),o=!0},p(a,f){const u={};f[0]&127|f[1]&16&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};s.$set(c)},i(a){o||(E(e.$$.fragment,a),E(i.$$.fragment,a),E(s.$$.fragment,a),o=!0)},o(a){A(e.$$.fragment,a),A(i.$$.fragment,a),A(s.$$.fragment,a),o=!1},d(a){a&&(v(t),v(l)),V(e,a),V(i,a),n[31](null),V(s,a)}}}function II(n,e,t){let i,l,s;Ue(n,It,te=>t(6,s=te));const o=[{label:"Auto (StartTLS)",value:!1},{label:"Always",value:!0}],r=[{label:"PLAIN (default)",value:"PLAIN"},{label:"LOGIN",value:"LOGIN"}];xt(It,s="Mail settings",s);let a,f={},u={},c=!1,d=!1,m=!1;h();async function h(){t(2,c=!0);try{const te=await ae.settings.getAll()||{};g(te)}catch(te){ae.error(te)}t(2,c=!1)}async function _(){if(!(d||!l)){t(3,d=!0);try{const te=await ae.settings.update(j.filterRedactedProps(u));g(te),Jt({}),Lt("Successfully saved mail settings.")}catch(te){ae.error(te)}t(3,d=!1)}}function g(te={}){t(0,u={meta:(te==null?void 0:te.meta)||{},smtp:(te==null?void 0:te.smtp)||{}}),u.smtp.authMethod||t(0,u.smtp.authMethod=r[0].value,u),t(11,f=JSON.parse(JSON.stringify(u)))}function y(){t(0,u=JSON.parse(JSON.stringify(f||{})))}function S(){u.meta.senderName=this.value,t(0,u)}function T(){u.meta.senderAddress=this.value,t(0,u)}function $(te){n.$$.not_equal(u.meta.verificationTemplate,te)&&(u.meta.verificationTemplate=te,t(0,u))}function C(te){n.$$.not_equal(u.meta.resetPasswordTemplate,te)&&(u.meta.resetPasswordTemplate=te,t(0,u))}function O(te){n.$$.not_equal(u.meta.confirmEmailChangeTemplate,te)&&(u.meta.confirmEmailChangeTemplate=te,t(0,u))}function D(){u.smtp.enabled=this.checked,t(0,u)}function I(){u.smtp.host=this.value,t(0,u)}function L(){u.smtp.port=it(this.value),t(0,u)}function R(){u.smtp.username=this.value,t(0,u)}function F(te){n.$$.not_equal(u.smtp.password,te)&&(u.smtp.password=te,t(0,u))}const N=()=>{t(4,m=!m)};function P(te){n.$$.not_equal(u.smtp.tls,te)&&(u.smtp.tls=te,t(0,u))}function q(te){n.$$.not_equal(u.smtp.authMethod,te)&&(u.smtp.authMethod=te,t(0,u))}function H(){u.smtp.localName=this.value,t(0,u)}const W=()=>y(),G=()=>_(),U=()=>a==null?void 0:a.show(),Y=()=>_();function ie(te){ee[te?"unshift":"push"](()=>{a=te,t(1,a)})}return n.$$.update=()=>{n.$$.dirty[0]&2048&&t(12,i=JSON.stringify(f)),n.$$.dirty[0]&4097&&t(5,l=i!=JSON.stringify(u))},[u,a,c,d,m,l,s,o,r,_,y,f,i,S,T,$,C,O,D,I,L,R,F,N,P,q,H,W,G,U,Y,ie]}class AI extends ge{constructor(e){super(),_e(this,e,II,EI,me,{},null,[-1,-1])}}const LI=n=>({isTesting:n&4,testError:n&2,enabled:n&1}),o_=n=>({isTesting:n[2],testError:n[1],enabled:n[0].enabled});function NI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K(n[4]),p(e,"type","checkbox"),p(e,"id",t=n[20]),e.required=!0,p(l,"for",o=n[20])},m(f,u){w(f,e,u),e.checked=n[0].enabled,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[8]),r=!0)},p(f,u){u&1048576&&t!==(t=f[20])&&p(e,"id",t),u&1&&(e.checked=f[0].enabled),u&16&&oe(s,f[4]),u&1048576&&o!==(o=f[20])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function r_(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O;return i=new ce({props:{class:"form-field required",name:n[3]+".endpoint",$$slots:{default:[PI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),o=new ce({props:{class:"form-field required",name:n[3]+".bucket",$$slots:{default:[FI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),f=new ce({props:{class:"form-field required",name:n[3]+".region",$$slots:{default:[RI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),d=new ce({props:{class:"form-field required",name:n[3]+".accessKey",$$slots:{default:[qI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),_=new ce({props:{class:"form-field required",name:n[3]+".secret",$$slots:{default:[jI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),S=new ce({props:{class:"form-field",name:n[3]+".forcePathStyle",$$slots:{default:[HI,({uniqueId:D})=>({20:D}),({uniqueId:D})=>D?1048576:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),B(i.$$.fragment),l=M(),s=b("div"),B(o.$$.fragment),r=M(),a=b("div"),B(f.$$.fragment),u=M(),c=b("div"),B(d.$$.fragment),m=M(),h=b("div"),B(_.$$.fragment),g=M(),y=b("div"),B(S.$$.fragment),T=M(),$=b("div"),p(t,"class","col-lg-6"),p(s,"class","col-lg-3"),p(a,"class","col-lg-3"),p(c,"class","col-lg-6"),p(h,"class","col-lg-6"),p(y,"class","col-lg-12"),p($,"class","col-lg-12"),p(e,"class","grid")},m(D,I){w(D,e,I),k(e,t),z(i,t,null),k(e,l),k(e,s),z(o,s,null),k(e,r),k(e,a),z(f,a,null),k(e,u),k(e,c),z(d,c,null),k(e,m),k(e,h),z(_,h,null),k(e,g),k(e,y),z(S,y,null),k(e,T),k(e,$),O=!0},p(D,I){const L={};I&8&&(L.name=D[3]+".endpoint"),I&1081345&&(L.$$scope={dirty:I,ctx:D}),i.$set(L);const R={};I&8&&(R.name=D[3]+".bucket"),I&1081345&&(R.$$scope={dirty:I,ctx:D}),o.$set(R);const F={};I&8&&(F.name=D[3]+".region"),I&1081345&&(F.$$scope={dirty:I,ctx:D}),f.$set(F);const N={};I&8&&(N.name=D[3]+".accessKey"),I&1081345&&(N.$$scope={dirty:I,ctx:D}),d.$set(N);const P={};I&8&&(P.name=D[3]+".secret"),I&1081345&&(P.$$scope={dirty:I,ctx:D}),_.$set(P);const q={};I&8&&(q.name=D[3]+".forcePathStyle"),I&1081345&&(q.$$scope={dirty:I,ctx:D}),S.$set(q)},i(D){O||(E(i.$$.fragment,D),E(o.$$.fragment,D),E(f.$$.fragment,D),E(d.$$.fragment,D),E(_.$$.fragment,D),E(S.$$.fragment,D),D&&Ke(()=>{O&&(C||(C=Fe(e,et,{duration:150},!0)),C.run(1))}),O=!0)},o(D){A(i.$$.fragment,D),A(o.$$.fragment,D),A(f.$$.fragment,D),A(d.$$.fragment,D),A(_.$$.fragment,D),A(S.$$.fragment,D),D&&(C||(C=Fe(e,et,{duration:150},!1)),C.run(0)),O=!1},d(D){D&&v(e),V(i),V(o),V(f),V(d),V(_),V(S),D&&C&&C.end()}}}function PI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Endpoint"),l=M(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","text"),p(s,"id",o=n[20]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].endpoint),r||(a=J(s,"input",n[9]),r=!0)},p(f,u){u&1048576&&i!==(i=f[20])&&p(e,"for",i),u&1048576&&o!==(o=f[20])&&p(s,"id",o),u&1&&s.value!==f[0].endpoint&&re(s,f[0].endpoint)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function FI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Bucket"),l=M(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","text"),p(s,"id",o=n[20]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].bucket),r||(a=J(s,"input",n[10]),r=!0)},p(f,u){u&1048576&&i!==(i=f[20])&&p(e,"for",i),u&1048576&&o!==(o=f[20])&&p(s,"id",o),u&1&&s.value!==f[0].bucket&&re(s,f[0].bucket)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function RI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Region"),l=M(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","text"),p(s,"id",o=n[20]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].region),r||(a=J(s,"input",n[11]),r=!0)},p(f,u){u&1048576&&i!==(i=f[20])&&p(e,"for",i),u&1048576&&o!==(o=f[20])&&p(s,"id",o),u&1&&s.value!==f[0].region&&re(s,f[0].region)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function qI(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Access key"),l=M(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","text"),p(s,"id",o=n[20]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[0].accessKey),r||(a=J(s,"input",n[12]),r=!0)},p(f,u){u&1048576&&i!==(i=f[20])&&p(e,"for",i),u&1048576&&o!==(o=f[20])&&p(s,"id",o),u&1&&s.value!==f[0].accessKey&&re(s,f[0].accessKey)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function jI(n){let e,t,i,l,s,o,r;function a(u){n[13](u)}let f={id:n[20],required:!0};return n[0].secret!==void 0&&(f.value=n[0].secret),s=new Ka({props:f}),ee.push(()=>be(s,"value",a)),{c(){e=b("label"),t=K("Secret"),l=M(),B(s.$$.fragment),p(e,"for",i=n[20])},m(u,c){w(u,e,c),k(e,t),w(u,l,c),z(s,u,c),r=!0},p(u,c){(!r||c&1048576&&i!==(i=u[20]))&&p(e,"for",i);const d={};c&1048576&&(d.id=u[20]),!o&&c&1&&(o=!0,d.value=u[0].secret,ke(()=>o=!1)),s.$set(d)},i(u){r||(E(s.$$.fragment,u),r=!0)},o(u){A(s.$$.fragment,u),r=!1},d(u){u&&(v(e),v(l)),V(s,u)}}}function HI(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("input"),i=M(),l=b("label"),s=b("span"),s.textContent="Force path-style addressing",o=M(),r=b("i"),p(e,"type","checkbox"),p(e,"id",t=n[20]),p(s,"class","txt"),p(r,"class","ri-information-line link-hint"),p(l,"for",a=n[20])},m(c,d){w(c,e,d),e.checked=n[0].forcePathStyle,w(c,i,d),w(c,l,d),k(l,s),k(l,o),k(l,r),f||(u=[J(e,"change",n[14]),Se(Pe.call(null,r,{text:'Forces the request to use path-style addressing, eg. "https://s3.amazonaws.com/BUCKET/KEY" instead of the default "https://BUCKET.s3.amazonaws.com/KEY".',position:"top"}))],f=!0)},p(c,d){d&1048576&&t!==(t=c[20])&&p(e,"id",t),d&1&&(e.checked=c[0].forcePathStyle),d&1048576&&a!==(a=c[20])&&p(l,"for",a)},d(c){c&&(v(e),v(i),v(l)),f=!1,$e(u)}}}function zI(n){let e,t,i,l,s;e=new ce({props:{class:"form-field form-field-toggle",$$slots:{default:[NI,({uniqueId:f})=>({20:f}),({uniqueId:f})=>f?1048576:0]},$$scope:{ctx:n}}});const o=n[7].default,r=wt(o,n,n[15],o_);let a=n[0].enabled&&r_(n);return{c(){B(e.$$.fragment),t=M(),r&&r.c(),i=M(),a&&a.c(),l=ye()},m(f,u){z(e,f,u),w(f,t,u),r&&r.m(f,u),w(f,i,u),a&&a.m(f,u),w(f,l,u),s=!0},p(f,[u]){const c={};u&1081361&&(c.$$scope={dirty:u,ctx:f}),e.$set(c),r&&r.p&&(!s||u&32775)&&$t(r,o,f,f[15],s?St(o,f[15],u,LI):Tt(f[15]),o_),f[0].enabled?a?(a.p(f,u),u&1&&E(a,1)):(a=r_(f),a.c(),E(a,1),a.m(l.parentNode,l)):a&&(le(),A(a,1,1,()=>{a=null}),se())},i(f){s||(E(e.$$.fragment,f),E(r,f),E(a),s=!0)},o(f){A(e.$$.fragment,f),A(r,f),A(a),s=!1},d(f){f&&(v(t),v(i),v(l)),V(e,f),r&&r.d(f),a&&a.d(f)}}}const Pr="s3_test_request";function VI(n,e,t){let{$$slots:i={},$$scope:l}=e,{originalConfig:s={}}=e,{config:o={}}=e,{configKey:r="s3"}=e,{toggleLabel:a="Enable S3"}=e,{testFilesystem:f="storage"}=e,{testError:u=null}=e,{isTesting:c=!1}=e,d=null,m=null;function h(D){t(2,c=!0),clearTimeout(m),m=setTimeout(()=>{_()},D)}async function _(){if(t(1,u=null),!o.enabled)return t(2,c=!1),u;ae.cancelRequest(Pr),clearTimeout(d),d=setTimeout(()=>{ae.cancelRequest(Pr),t(1,u=new Error("S3 test connection timeout.")),t(2,c=!1)},3e4),t(2,c=!0);let D;try{await ae.settings.testS3(f,{$cancelKey:Pr})}catch(I){D=I}return D!=null&&D.isAbort||(t(1,u=D),t(2,c=!1),clearTimeout(d)),u}Ht(()=>()=>{clearTimeout(d),clearTimeout(m)});function g(){o.enabled=this.checked,t(0,o)}function y(){o.endpoint=this.value,t(0,o)}function S(){o.bucket=this.value,t(0,o)}function T(){o.region=this.value,t(0,o)}function $(){o.accessKey=this.value,t(0,o)}function C(D){n.$$.not_equal(o.secret,D)&&(o.secret=D,t(0,o))}function O(){o.forcePathStyle=this.checked,t(0,o)}return n.$$set=D=>{"originalConfig"in D&&t(5,s=D.originalConfig),"config"in D&&t(0,o=D.config),"configKey"in D&&t(3,r=D.configKey),"toggleLabel"in D&&t(4,a=D.toggleLabel),"testFilesystem"in D&&t(6,f=D.testFilesystem),"testError"in D&&t(1,u=D.testError),"isTesting"in D&&t(2,c=D.isTesting),"$$scope"in D&&t(15,l=D.$$scope)},n.$$.update=()=>{n.$$.dirty&32&&s!=null&&s.enabled&&h(100),n.$$.dirty&9&&(o.enabled||li(r))},[o,u,c,r,a,s,f,i,g,y,S,T,$,C,O,l]}class Xb extends ge{constructor(e){super(),_e(this,e,VI,zI,me,{originalConfig:5,config:0,configKey:3,toggleLabel:4,testFilesystem:6,testError:1,isTesting:2})}}function BI(n){var D;let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g;function y(I){n[11](I)}function S(I){n[12](I)}function T(I){n[13](I)}let $={toggleLabel:"Use S3 storage",originalConfig:n[0].s3,$$slots:{default:[WI]},$$scope:{ctx:n}};n[1].s3!==void 0&&($.config=n[1].s3),n[4]!==void 0&&($.isTesting=n[4]),n[5]!==void 0&&($.testError=n[5]),e=new Xb({props:$}),ee.push(()=>be(e,"config",y)),ee.push(()=>be(e,"isTesting",S)),ee.push(()=>be(e,"testError",T));let C=((D=n[1].s3)==null?void 0:D.enabled)&&!n[6]&&!n[3]&&f_(n),O=n[6]&&u_(n);return{c(){B(e.$$.fragment),s=M(),o=b("div"),r=b("div"),a=M(),C&&C.c(),f=M(),O&&O.c(),u=M(),c=b("button"),d=b("span"),d.textContent="Save changes",p(r,"class","flex-fill"),p(d,"class","txt"),p(c,"type","submit"),p(c,"class","btn btn-expanded"),c.disabled=m=!n[6]||n[3],x(c,"btn-loading",n[3]),p(o,"class","flex")},m(I,L){z(e,I,L),w(I,s,L),w(I,o,L),k(o,r),k(o,a),C&&C.m(o,null),k(o,f),O&&O.m(o,null),k(o,u),k(o,c),k(c,d),h=!0,_||(g=J(c,"click",n[15]),_=!0)},p(I,L){var F;const R={};L&1&&(R.originalConfig=I[0].s3),L&524291&&(R.$$scope={dirty:L,ctx:I}),!t&&L&2&&(t=!0,R.config=I[1].s3,ke(()=>t=!1)),!i&&L&16&&(i=!0,R.isTesting=I[4],ke(()=>i=!1)),!l&&L&32&&(l=!0,R.testError=I[5],ke(()=>l=!1)),e.$set(R),(F=I[1].s3)!=null&&F.enabled&&!I[6]&&!I[3]?C?C.p(I,L):(C=f_(I),C.c(),C.m(o,f)):C&&(C.d(1),C=null),I[6]?O?O.p(I,L):(O=u_(I),O.c(),O.m(o,u)):O&&(O.d(1),O=null),(!h||L&72&&m!==(m=!I[6]||I[3]))&&(c.disabled=m),(!h||L&8)&&x(c,"btn-loading",I[3])},i(I){h||(E(e.$$.fragment,I),h=!0)},o(I){A(e.$$.fragment,I),h=!1},d(I){I&&(v(s),v(o)),V(e,I),C&&C.d(),O&&O.d(),_=!1,g()}}}function UI(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function a_(n){var L;let e,t,i,l,s,o,r,a=(L=n[0].s3)!=null&&L.enabled?"S3 storage":"local file system",f,u,c,d=n[1].s3.enabled?"S3 storage":"local file system",m,h,_,g,y,S,T,$,C,O,D,I;return{c(){e=b("div"),t=b("div"),i=b("div"),i.innerHTML='',l=M(),s=b("div"),o=K(`If you have existing uploaded files, you'll have to migrate them manually - from the - `),r=b("strong"),f=K(a),u=K(` - to the - `),c=b("strong"),m=K(d),h=K(`. - `),_=b("br"),g=K(` - There are numerous command line tools that can help you, such as: - `),y=b("a"),y.textContent=`rclone - `,S=K(`, - `),T=b("a"),T.textContent=`s5cmd - `,$=K(", etc."),C=M(),O=b("div"),p(i,"class","icon"),p(y,"href","https://github.com/rclone/rclone"),p(y,"target","_blank"),p(y,"rel","noopener noreferrer"),p(y,"class","txt-bold"),p(T,"href","https://github.com/peak/s5cmd"),p(T,"target","_blank"),p(T,"rel","noopener noreferrer"),p(T,"class","txt-bold"),p(s,"class","content"),p(t,"class","alert alert-warning m-0"),p(O,"class","clearfix m-t-base")},m(R,F){w(R,e,F),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),k(s,r),k(r,f),k(s,u),k(s,c),k(c,m),k(s,h),k(s,_),k(s,g),k(s,y),k(s,S),k(s,T),k(s,$),k(e,C),k(e,O),I=!0},p(R,F){var N;(!I||F&1)&&a!==(a=(N=R[0].s3)!=null&&N.enabled?"S3 storage":"local file system")&&oe(f,a),(!I||F&2)&&d!==(d=R[1].s3.enabled?"S3 storage":"local file system")&&oe(m,d)},i(R){I||(R&&Ke(()=>{I&&(D||(D=Fe(e,et,{duration:150},!0)),D.run(1))}),I=!0)},o(R){R&&(D||(D=Fe(e,et,{duration:150},!1)),D.run(0)),I=!1},d(R){R&&v(e),R&&D&&D.end()}}}function WI(n){var i;let e,t=((i=n[0].s3)==null?void 0:i.enabled)!=n[1].s3.enabled&&a_(n);return{c(){t&&t.c(),e=ye()},m(l,s){t&&t.m(l,s),w(l,e,s)},p(l,s){var o;((o=l[0].s3)==null?void 0:o.enabled)!=l[1].s3.enabled?t?(t.p(l,s),s&3&&E(t,1)):(t=a_(l),t.c(),E(t,1),t.m(e.parentNode,e)):t&&(le(),A(t,1,1,()=>{t=null}),se())},d(l){l&&v(e),t&&t.d(l)}}}function f_(n){let e;function t(s,o){return s[4]?JI:s[5]?KI:YI}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function YI(n){let e;return{c(){e=b("div"),e.innerHTML=' S3 connected successfully',p(e,"class","label label-sm label-success entrance-right")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function KI(n){let e,t,i,l;return{c(){e=b("div"),e.innerHTML=' Failed to establish S3 connection',p(e,"class","label label-sm label-warning entrance-right")},m(s,o){var r;w(s,e,o),i||(l=Se(t=Pe.call(null,e,(r=n[5].data)==null?void 0:r.message)),i=!0)},p(s,o){var r;t&&Ct(t.update)&&o&32&&t.update.call(null,(r=s[5].data)==null?void 0:r.message)},d(s){s&&v(e),i=!1,l()}}}function JI(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function u_(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Reset",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[3]},m(s,o){w(s,e,o),k(e,t),i||(l=J(e,"click",n[14]),i=!0)},p(s,o){o&8&&(e.disabled=s[3])},d(s){s&&v(e),i=!1,l()}}}function ZI(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g;const y=[UI,BI],S=[];function T($,C){return $[2]?0:1}return d=T(n),m=S[d]=y[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[7]),r=M(),a=b("div"),f=b("form"),u=b("div"),u.innerHTML="

    By default PocketBase uses the local file system to store uploaded files.

    If you have limited disk space, you could optionally connect to an S3 compatible storage.

    ",c=M(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","content txt-xl m-b-base"),p(f,"class","panel"),p(f,"autocomplete","off"),p(a,"class","wrapper")},m($,C){w($,e,C),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w($,r,C),w($,a,C),k(a,f),k(f,u),k(f,c),S[d].m(f,null),h=!0,_||(g=J(f,"submit",Be(n[16])),_=!0)},p($,C){(!h||C&128)&&oe(o,$[7]);let O=d;d=T($),d===O?S[d].p($,C):(le(),A(S[O],1,1,()=>{S[O]=null}),se(),m=S[d],m?m.p($,C):(m=S[d]=y[d]($),m.c()),E(m,1),m.m(f,null))},i($){h||(E(m),h=!0)},o($){A(m),h=!1},d($){$&&(v(e),v(r),v(a)),S[d].d(),_=!1,g()}}}function GI(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[ZI]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,[o]){const r={};o&524543&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}const XI="s3_test_request";function QI(n,e,t){let i,l,s;Ue(n,It,O=>t(7,s=O)),xt(It,s="Files storage",s);let o={},r={},a=!1,f=!1,u=!1,c=null;d();async function d(){t(2,a=!0);try{const O=await ae.settings.getAll()||{};h(O)}catch(O){ae.error(O)}t(2,a=!1)}async function m(){if(!(f||!l)){t(3,f=!0);try{ae.cancelRequest(XI);const O=await ae.settings.update(j.filterRedactedProps(r));Jt({}),await h(O),wa(),c?nv("Successfully saved but failed to establish S3 connection."):Lt("Successfully saved files storage settings.")}catch(O){ae.error(O)}t(3,f=!1)}}async function h(O={}){t(1,r={s3:(O==null?void 0:O.s3)||{}}),t(0,o=JSON.parse(JSON.stringify(r)))}async function _(){t(1,r=JSON.parse(JSON.stringify(o||{})))}function g(O){n.$$.not_equal(r.s3,O)&&(r.s3=O,t(1,r))}function y(O){u=O,t(4,u)}function S(O){c=O,t(5,c)}const T=()=>_(),$=()=>m(),C=()=>m();return n.$$.update=()=>{n.$$.dirty&1&&t(10,i=JSON.stringify(o)),n.$$.dirty&1026&&t(6,l=i!=JSON.stringify(r))},[o,r,a,f,u,c,l,s,m,_,i,g,y,S,T,$,C]}class xI extends ge{constructor(e){super(),_e(this,e,QI,GI,me,{})}}function eA(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable"),p(e,"type","checkbox"),p(e,"id",t=n[20]),p(l,"for",o=n[20])},m(f,u){w(f,e,u),e.checked=n[1].enabled,w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[11]),r=!0)},p(f,u){u&1048576&&t!==(t=f[20])&&p(e,"id",t),u&2&&(e.checked=f[1].enabled),u&1048576&&o!==(o=f[20])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function tA(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("label"),t=K("Client ID"),l=M(),s=b("input"),p(e,"for",i=n[20]),p(s,"type","text"),p(s,"id",o=n[20]),s.required=r=n[1].enabled},m(u,c){w(u,e,c),k(e,t),w(u,l,c),w(u,s,c),re(s,n[1].clientId),a||(f=J(s,"input",n[12]),a=!0)},p(u,c){c&1048576&&i!==(i=u[20])&&p(e,"for",i),c&1048576&&o!==(o=u[20])&&p(s,"id",o),c&2&&r!==(r=u[1].enabled)&&(s.required=r),c&2&&s.value!==u[1].clientId&&re(s,u[1].clientId)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function nA(n){let e,t,i,l,s,o,r;function a(u){n[13](u)}let f={id:n[20],required:n[1].enabled};return n[1].clientSecret!==void 0&&(f.value=n[1].clientSecret),s=new Ka({props:f}),ee.push(()=>be(s,"value",a)),{c(){e=b("label"),t=K("Client secret"),l=M(),B(s.$$.fragment),p(e,"for",i=n[20])},m(u,c){w(u,e,c),k(e,t),w(u,l,c),z(s,u,c),r=!0},p(u,c){(!r||c&1048576&&i!==(i=u[20]))&&p(e,"for",i);const d={};c&1048576&&(d.id=u[20]),c&2&&(d.required=u[1].enabled),!o&&c&2&&(o=!0,d.value=u[1].clientSecret,ke(()=>o=!1)),s.$set(d)},i(u){r||(E(s.$$.fragment,u),r=!0)},o(u){A(s.$$.fragment,u),r=!1},d(u){u&&(v(e),v(l)),V(s,u)}}}function c_(n){let e,t,i,l;const s=[{key:n[3].key},n[3].optionsComponentProps||{}];function o(f){n[14](f)}var r=n[3].optionsComponent;function a(f,u){let c={};for(let d=0;dbe(t,"config",o))),{c(){e=b("div"),t&&B(t.$$.fragment),p(e,"class","col-lg-12")},m(f,u){w(f,e,u),t&&z(t,e,null),l=!0},p(f,u){if(u&8&&r!==(r=f[3].optionsComponent)){if(t){le();const c=t;A(c.$$.fragment,1,0,()=>{V(c,1)}),se()}r?(t=Dt(r,a(f,u)),ee.push(()=>be(t,"config",o)),B(t.$$.fragment),E(t.$$.fragment,1),z(t,e,null)):t=null}else if(r){const c=u&8?pt(s,[{key:f[3].key},Ot(f[3].optionsComponentProps||{})]):{};!i&&u&2&&(i=!0,c.config=f[1],ke(()=>i=!1)),t.$set(c)}},i(f){l||(t&&E(t.$$.fragment,f),l=!0)},o(f){t&&A(t.$$.fragment,f),l=!1},d(f){f&&v(e),t&&V(t)}}}function iA(n){let e,t,i,l,s,o,r,a,f,u,c,d,m;i=new ce({props:{class:"form-field form-field-toggle m-b-0",name:n[3].key+".enabled",$$slots:{default:[eA,({uniqueId:_})=>({20:_}),({uniqueId:_})=>_?1048576:0]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field "+(n[1].enabled?"required":""),name:n[3].key+".clientId",$$slots:{default:[tA,({uniqueId:_})=>({20:_}),({uniqueId:_})=>_?1048576:0]},$$scope:{ctx:n}}}),f=new ce({props:{class:"form-field "+(n[1].enabled?"required":""),name:n[3].key+".clientSecret",$$slots:{default:[nA,({uniqueId:_})=>({20:_}),({uniqueId:_})=>_?1048576:0]},$$scope:{ctx:n}}});let h=n[3].optionsComponent&&c_(n);return{c(){e=b("form"),t=b("div"),B(i.$$.fragment),l=M(),s=b("button"),s.innerHTML='Clear all fields',o=M(),B(r.$$.fragment),a=M(),B(f.$$.fragment),u=M(),h&&h.c(),p(s,"type","button"),p(s,"class","btn btn-sm btn-transparent btn-hint m-l-auto"),p(t,"class","flex m-b-base"),p(e,"id",n[6]),p(e,"autocomplete","off")},m(_,g){w(_,e,g),k(e,t),z(i,t,null),k(t,l),k(t,s),k(e,o),z(r,e,null),k(e,a),z(f,e,null),k(e,u),h&&h.m(e,null),c=!0,d||(m=[J(s,"click",n[8]),J(e,"submit",Be(n[15]))],d=!0)},p(_,g){const y={};g&8&&(y.name=_[3].key+".enabled"),g&3145730&&(y.$$scope={dirty:g,ctx:_}),i.$set(y);const S={};g&2&&(S.class="form-field "+(_[1].enabled?"required":"")),g&8&&(S.name=_[3].key+".clientId"),g&3145730&&(S.$$scope={dirty:g,ctx:_}),r.$set(S);const T={};g&2&&(T.class="form-field "+(_[1].enabled?"required":"")),g&8&&(T.name=_[3].key+".clientSecret"),g&3145730&&(T.$$scope={dirty:g,ctx:_}),f.$set(T),_[3].optionsComponent?h?(h.p(_,g),g&8&&E(h,1)):(h=c_(_),h.c(),E(h,1),h.m(e,null)):h&&(le(),A(h,1,1,()=>{h=null}),se())},i(_){c||(E(i.$$.fragment,_),E(r.$$.fragment,_),E(f.$$.fragment,_),E(h),c=!0)},o(_){A(i.$$.fragment,_),A(r.$$.fragment,_),A(f.$$.fragment,_),A(h),c=!1},d(_){_&&v(e),V(i),V(r),V(f),h&&h.d(),d=!1,$e(m)}}}function lA(n){let e,t=(n[3].title||n[3].key)+"",i,l;return{c(){e=b("h4"),i=K(t),l=K(" provider"),p(e,"class","center txt-break")},m(s,o){w(s,e,o),k(e,i),k(e,l)},p(s,o){o&8&&t!==(t=(s[3].title||s[3].key)+"")&&oe(i,t)},d(s){s&&v(e)}}}function sA(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=K("Close"),i=M(),l=b("button"),s=b("span"),s.textContent="Save changes",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[4],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[4],x(l,"btn-loading",n[4])},m(f,u){w(f,e,u),k(e,t),w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"click",n[0]),r=!0)},p(f,u){u&16&&(e.disabled=f[4]),u&48&&o!==(o=!f[5]||f[4])&&(l.disabled=o),u&16&&x(l,"btn-loading",f[4])},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function oA(n){let e,t,i={overlayClose:!n[4],escClose:!n[4],$$slots:{footer:[sA],header:[lA],default:[iA]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[16](e),e.$on("show",n[17]),e.$on("hide",n[18]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.overlayClose=!l[4]),s&16&&(o.escClose=!l[4]),s&2097210&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[16](null),V(e,l)}}}function rA(n,e,t){let i;const l=lt(),s="provider_popup_"+j.randomString(5);let o,r={},a={},f=!1,u="";function c(D,I){Jt({}),t(3,r=Object.assign({},D)),t(1,a=Object.assign({enabled:!0},I)),t(10,u=JSON.stringify(a)),o==null||o.show()}function d(){return o==null?void 0:o.hide()}async function m(){t(4,f=!0);try{const D={};D[r.key]=j.filterRedactedProps(a);const I=await ae.settings.update(D);Jt({}),Lt("Successfully updated provider settings."),l("submit",I),d()}catch(D){ae.error(D)}t(4,f=!1)}function h(){var D;for(let I in a)t(1,a[I]=j.zeroValue(a[I]),a);(D=r.key)!=null&&D.startsWith("oidc")?t(1,a.pkce=!1,a):t(1,a.pkce=null,a)}function _(){a.enabled=this.checked,t(1,a)}function g(){a.clientId=this.value,t(1,a)}function y(D){n.$$.not_equal(a.clientSecret,D)&&(a.clientSecret=D,t(1,a))}function S(D){a=D,t(1,a)}const T=()=>m();function $(D){ee[D?"unshift":"push"](()=>{o=D,t(2,o)})}function C(D){Ce.call(this,n,D)}function O(D){Ce.call(this,n,D)}return n.$$.update=()=>{n.$$.dirty&1026&&t(5,i=JSON.stringify(a)!=u)},[d,a,o,r,f,i,s,m,h,c,u,_,g,y,S,T,$,C,O]}class aA extends ge{constructor(e){super(),_e(this,e,rA,oA,me,{show:9,hide:0})}get show(){return this.$$.ctx[9]}get hide(){return this.$$.ctx[0]}}function d_(n){let e,t,i;return{c(){e=b("img"),en(e.src,t="./images/oauth2/"+n[1].logo)||p(e,"src",t),p(e,"alt",i=n[1].title+" logo")},m(l,s){w(l,e,s)},p(l,s){s&2&&!en(e.src,t="./images/oauth2/"+l[1].logo)&&p(e,"src",t),s&2&&i!==(i=l[1].title+" logo")&&p(e,"alt",i)},d(l){l&&v(e)}}}function p_(n){let e;return{c(){e=b("div"),e.textContent="Enabled",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function fA(n){let e,t,i,l,s=n[1].title+"",o,r,a,f,u=n[1].key.slice(0,-4)+"",c,d,m,h,_,g,y,S,T,$,C=n[1].logo&&d_(n),O=n[0].enabled&&p_(),D={};return y=new aA({props:D}),n[4](y),y.$on("submit",n[5]),{c(){e=b("div"),t=b("figure"),C&&C.c(),i=M(),l=b("div"),o=K(s),r=M(),a=b("em"),f=K("("),c=K(u),d=K(")"),m=M(),O&&O.c(),h=M(),_=b("button"),_.innerHTML='',g=M(),B(y.$$.fragment),p(t,"class","provider-logo"),p(l,"class","title"),p(a,"class","txt-hint txt-sm m-r-auto"),p(_,"type","button"),p(_,"class","btn btn-circle btn-hint btn-transparent"),p(_,"aria-label","Provider settings"),p(e,"class","provider-card")},m(I,L){w(I,e,L),k(e,t),C&&C.m(t,null),k(e,i),k(e,l),k(l,o),k(e,r),k(e,a),k(a,f),k(a,c),k(a,d),k(e,m),O&&O.m(e,null),k(e,h),k(e,_),w(I,g,L),z(y,I,L),S=!0,T||($=J(_,"click",n[3]),T=!0)},p(I,[L]){I[1].logo?C?C.p(I,L):(C=d_(I),C.c(),C.m(t,null)):C&&(C.d(1),C=null),(!S||L&2)&&s!==(s=I[1].title+"")&&oe(o,s),(!S||L&2)&&u!==(u=I[1].key.slice(0,-4)+"")&&oe(c,u),I[0].enabled?O||(O=p_(),O.c(),O.m(e,h)):O&&(O.d(1),O=null);const R={};y.$set(R)},i(I){S||(E(y.$$.fragment,I),S=!0)},o(I){A(y.$$.fragment,I),S=!1},d(I){I&&(v(e),v(g)),C&&C.d(),O&&O.d(),n[4](null),V(y,I),T=!1,$()}}}function uA(n,e,t){let{provider:i={}}=e,{config:l={}}=e,s;const o=()=>{s==null||s.show(i,Object.assign({},l,{enabled:l.clientId?l.enabled:!0,pkce:l.clientId?l.pkce:null}))};function r(f){ee[f?"unshift":"push"](()=>{s=f,t(2,s)})}const a=f=>{f.detail[i.key]&&t(0,l=f.detail[i.key])};return n.$$set=f=>{"provider"in f&&t(1,i=f.provider),"config"in f&&t(0,l=f.config)},[l,i,s,o,r,a]}class Qb extends ge{constructor(e){super(),_e(this,e,uA,fA,me,{provider:1,config:0})}}function m_(n,e,t){const i=n.slice();return i[9]=e[t],i[10]=e,i[11]=t,i}function h_(n,e,t){const i=n.slice();return i[9]=e[t],i[12]=e,i[13]=t,i}function cA(n){let e,t=[],i=new Map,l,s,o,r=[],a=new Map,f,u=ue(n[3]);const c=_=>_[9].key;for(let _=0;_0&&n[2].length>0&&g_(),m=ue(n[2]);const h=_=>_[9].key;for(let _=0;_0&&_[2].length>0?d||(d=g_(),d.c(),d.m(s.parentNode,s)):d&&(d.d(1),d=null),g&5&&(m=ue(_[2]),le(),r=at(r,g,h,1,_,m,a,o,Et,b_,null,m_),se())},i(_){if(!f){for(let g=0;gbe(i,"config",r)),{key:n,first:null,c(){t=b("div"),B(i.$$.fragment),s=M(),p(t,"class","col-lg-6"),this.first=t},m(f,u){w(f,t,u),z(i,t,null),k(t,s),o=!0},p(f,u){e=f;const c={};u&8&&(c.provider=e[9]),!l&&u&9&&(l=!0,c.config=e[0][e[9].key],ke(()=>l=!1)),i.$set(c)},i(f){o||(E(i.$$.fragment,f),o=!0)},o(f){A(i.$$.fragment,f),o=!1},d(f){f&&v(t),V(i)}}}function g_(n){let e;return{c(){e=b("hr")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function b_(n,e){let t,i,l,s,o;function r(f){e[6](f,e[9])}let a={provider:e[9]};return e[0][e[9].key]!==void 0&&(a.config=e[0][e[9].key]),i=new Qb({props:a}),ee.push(()=>be(i,"config",r)),{key:n,first:null,c(){t=b("div"),B(i.$$.fragment),s=M(),p(t,"class","col-lg-6"),this.first=t},m(f,u){w(f,t,u),z(i,t,null),k(t,s),o=!0},p(f,u){e=f;const c={};u&4&&(c.provider=e[9]),!l&&u&5&&(l=!0,c.config=e[0][e[9].key],ke(()=>l=!1)),i.$set(c)},i(f){o||(E(i.$$.fragment,f),o=!0)},o(f){A(i.$$.fragment,f),o=!1},d(f){f&&v(t),V(i)}}}function pA(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h;const _=[dA,cA],g=[];function y(S,T){return S[1]?0:1}return d=y(n),m=g[d]=_[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[4]),r=M(),a=b("div"),f=b("div"),u=b("h6"),u.textContent="Manage the allowed users OAuth2 sign-in/sign-up methods.",c=M(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","m-b-base"),p(f,"class","panel"),p(a,"class","wrapper")},m(S,T){w(S,e,T),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w(S,r,T),w(S,a,T),k(a,f),k(f,u),k(f,c),g[d].m(f,null),h=!0},p(S,T){(!h||T&16)&&oe(o,S[4]);let $=d;d=y(S),d===$?g[d].p(S,T):(le(),A(g[$],1,1,()=>{g[$]=null}),se(),m=g[d],m?m.p(S,T):(m=g[d]=_[d](S),m.c()),E(m,1),m.m(f,null))},i(S){h||(E(m),h=!0)},o(S){A(m),h=!1},d(S){S&&(v(e),v(r),v(a)),g[d].d()}}}function mA(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[pA]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,[o]){const r={};o&16415&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function hA(n,e,t){let i,l,s;Ue(n,It,d=>t(4,s=d)),xt(It,s="Auth providers",s);let o=!1,r={};a();async function a(){t(1,o=!0);try{const d=await ae.settings.getAll()||{};f(d)}catch(d){ae.error(d)}t(1,o=!1)}function f(d){d=d||{},t(0,r={});for(const m of go)t(0,r[m.key]=Object.assign({enabled:!1},d[m.key]),r)}function u(d,m){n.$$.not_equal(r[m.key],d)&&(r[m.key]=d,t(0,r))}function c(d,m){n.$$.not_equal(r[m.key],d)&&(r[m.key]=d,t(0,r))}return n.$$.update=()=>{n.$$.dirty&1&&t(3,i=go.filter(d=>{var m;return(m=r[d.key])==null?void 0:m.enabled})),n.$$.dirty&1&&t(2,l=go.filter(d=>{var m;return!((m=r[d.key])!=null&&m.enabled)}))},[r,o,l,i,s,u,c]}class _A extends ge{constructor(e){super(),_e(this,e,hA,mA,me,{})}}function gA(n){let e,t,i,l,s,o,r,a,f,u,c,d;return{c(){e=b("label"),t=K(n[3]),i=K(" duration (in seconds)"),s=M(),o=b("input"),a=M(),f=b("div"),u=b("span"),u.textContent="Invalidate all previously issued tokens",p(e,"for",l=n[6]),p(o,"type","number"),p(o,"id",r=n[6]),o.required=!0,p(u,"class","link-primary"),x(u,"txt-success",!!n[1]),p(f,"class","help-block")},m(m,h){w(m,e,h),k(e,t),k(e,i),w(m,s,h),w(m,o,h),re(o,n[0]),w(m,a,h),w(m,f,h),k(f,u),c||(d=[J(o,"input",n[4]),J(u,"click",n[5])],c=!0)},p(m,h){h&8&&oe(t,m[3]),h&64&&l!==(l=m[6])&&p(e,"for",l),h&64&&r!==(r=m[6])&&p(o,"id",r),h&1&&it(o.value)!==m[0]&&re(o,m[0]),h&2&&x(u,"txt-success",!!m[1])},d(m){m&&(v(e),v(s),v(o),v(a),v(f)),c=!1,$e(d)}}}function bA(n){let e,t;return e=new ce({props:{class:"form-field required",name:n[2]+".duration",$$slots:{default:[gA,({uniqueId:i})=>({6:i}),({uniqueId:i})=>i?64:0]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,[l]){const s={};l&4&&(s.name=i[2]+".duration"),l&203&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function kA(n,e,t){let{key:i}=e,{label:l}=e,{duration:s}=e,{secret:o}=e;function r(){s=it(this.value),t(0,s)}const a=()=>{o?t(1,o=void 0):t(1,o=j.randomSecret(50))};return n.$$set=f=>{"key"in f&&t(2,i=f.key),"label"in f&&t(3,l=f.label),"duration"in f&&t(0,s=f.duration),"secret"in f&&t(1,o=f.secret)},[s,o,i,l,r,a]}class xb extends ge{constructor(e){super(),_e(this,e,kA,bA,me,{key:2,label:3,duration:0,secret:1})}}function k_(n,e,t){const i=n.slice();return i[19]=e[t],i[20]=e,i[21]=t,i}function y_(n,e,t){const i=n.slice();return i[19]=e[t],i[22]=e,i[23]=t,i}function yA(n){let e,t,i=[],l=new Map,s,o,r,a,f,u=[],c=new Map,d,m,h,_,g,y,S,T,$,C,O,D=ue(n[5]);const I=N=>N[19].key;for(let N=0;NN[19].key;for(let N=0;Nbe(i,"duration",r)),ee.push(()=>be(i,"secret",a)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(u,c){w(u,t,c),z(i,u,c),o=!0},p(u,c){e=u;const d={};!l&&c&33&&(l=!0,d.duration=e[0][e[19].key].duration,ke(()=>l=!1)),!s&&c&33&&(s=!0,d.secret=e[0][e[19].key].secret,ke(()=>s=!1)),i.$set(d)},i(u){o||(E(i.$$.fragment,u),o=!0)},o(u){A(i.$$.fragment,u),o=!1},d(u){u&&v(t),V(i,u)}}}function w_(n,e){let t,i,l,s,o;function r(u){e[13](u,e[19])}function a(u){e[14](u,e[19])}let f={key:e[19].key,label:e[19].label};return e[0][e[19].key].duration!==void 0&&(f.duration=e[0][e[19].key].duration),e[0][e[19].key].secret!==void 0&&(f.secret=e[0][e[19].key].secret),i=new xb({props:f}),ee.push(()=>be(i,"duration",r)),ee.push(()=>be(i,"secret",a)),{key:n,first:null,c(){t=ye(),B(i.$$.fragment),this.first=t},m(u,c){w(u,t,c),z(i,u,c),o=!0},p(u,c){e=u;const d={};!l&&c&65&&(l=!0,d.duration=e[0][e[19].key].duration,ke(()=>l=!1)),!s&&c&65&&(s=!0,d.secret=e[0][e[19].key].secret,ke(()=>s=!1)),i.$set(d)},i(u){o||(E(i.$$.fragment,u),o=!0)},o(u){A(i.$$.fragment,u),o=!1},d(u){u&&v(t),V(i,u)}}}function S_(n){let e,t,i,l;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent btn-hint"),e.disabled=n[2]},m(s,o){w(s,e,o),k(e,t),i||(l=J(e,"click",n[15]),i=!0)},p(s,o){o&4&&(e.disabled=s[2])},d(s){s&&v(e),i=!1,l()}}}function wA(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g;const y=[vA,yA],S=[];function T($,C){return $[1]?0:1}return d=T(n),m=S[d]=y[d](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[4]),r=M(),a=b("div"),f=b("form"),u=b("div"),u.innerHTML="

    Adjust common token options.

    ",c=M(),m.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(u,"class","content m-b-sm txt-xl"),p(f,"class","panel"),p(f,"autocomplete","off"),p(a,"class","wrapper")},m($,C){w($,e,C),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w($,r,C),w($,a,C),k(a,f),k(f,u),k(f,c),S[d].m(f,null),h=!0,_||(g=J(f,"submit",Be(n[7])),_=!0)},p($,C){(!h||C&16)&&oe(o,$[4]);let O=d;d=T($),d===O?S[d].p($,C):(le(),A(S[O],1,1,()=>{S[O]=null}),se(),m=S[d],m?m.p($,C):(m=S[d]=y[d]($),m.c()),E(m,1),m.m(f,null))},i($){h||(E(m),h=!0)},o($){A(m),h=!1},d($){$&&(v(e),v(r),v(a)),S[d].d(),_=!1,g()}}}function SA(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[wA]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,[o]){const r={};o&16777247&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function $A(n,e,t){let i,l,s;Ue(n,It,O=>t(4,s=O));const o=[{key:"recordAuthToken",label:"Auth record authentication token"},{key:"recordVerificationToken",label:"Auth record email verification token"},{key:"recordPasswordResetToken",label:"Auth record password reset token"},{key:"recordEmailChangeToken",label:"Auth record email change token"},{key:"recordFileToken",label:"Records protected file access token"}],r=[{key:"adminAuthToken",label:"Admins auth token"},{key:"adminPasswordResetToken",label:"Admins password reset token"},{key:"adminFileToken",label:"Admins protected file access token"}];xt(It,s="Token options",s);let a={},f={},u=!1,c=!1;d();async function d(){t(1,u=!0);try{const O=await ae.settings.getAll()||{};h(O)}catch(O){ae.error(O)}t(1,u=!1)}async function m(){if(!(c||!l)){t(2,c=!0);try{const O=await ae.settings.update(j.filterRedactedProps(f));h(O),Lt("Successfully saved tokens options.")}catch(O){ae.error(O)}t(2,c=!1)}}function h(O){var I;O=O||{},t(0,f={});const D=o.concat(r);for(const L of D)t(0,f[L.key]={duration:((I=O[L.key])==null?void 0:I.duration)||0},f);t(9,a=JSON.parse(JSON.stringify(f)))}function _(){t(0,f=JSON.parse(JSON.stringify(a||{})))}function g(O,D){n.$$.not_equal(f[D.key].duration,O)&&(f[D.key].duration=O,t(0,f))}function y(O,D){n.$$.not_equal(f[D.key].secret,O)&&(f[D.key].secret=O,t(0,f))}function S(O,D){n.$$.not_equal(f[D.key].duration,O)&&(f[D.key].duration=O,t(0,f))}function T(O,D){n.$$.not_equal(f[D.key].secret,O)&&(f[D.key].secret=O,t(0,f))}const $=()=>_(),C=()=>m();return n.$$.update=()=>{n.$$.dirty&512&&t(10,i=JSON.stringify(a)),n.$$.dirty&1025&&t(3,l=i!=JSON.stringify(f))},[f,u,c,l,s,o,r,m,_,a,i,g,y,S,T,$,C]}class TA extends ge{constructor(e){super(),_e(this,e,$A,SA,me,{})}}function $_(n,e,t){const i=n.slice();return i[22]=e[t],i}function CA(n){let e,t,i,l,s,o,r,a=[],f=new Map,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I,L,R,F,N;o=new ce({props:{class:"form-field",$$slots:{default:[MA,({uniqueId:H})=>({12:H}),({uniqueId:H})=>H?4096:0]},$$scope:{ctx:n}}});let P=ue(n[0]);const q=H=>H[22].id;for(let H=0;HBelow you'll find your current collections configuration that you could import in - another PocketBase environment.

    `,t=M(),i=b("div"),l=b("div"),s=b("div"),B(o.$$.fragment),r=M();for(let H=0;H({12:o}),({uniqueId:o})=>o?4096:0]},$$scope:{ctx:e}}}),{key:n,first:null,c(){t=b("div"),B(i.$$.fragment),l=M(),p(t,"class","list-item list-item-collection"),this.first=t},m(o,r){w(o,t,r),z(i,t,null),k(t,l),s=!0},p(o,r){e=o;const a={};r&33558531&&(a.$$scope={dirty:r,ctx:e}),i.$set(a)},i(o){s||(E(i.$$.fragment,o),s=!0)},o(o){A(i.$$.fragment,o),s=!1},d(o){o&&v(t),V(i)}}}function EA(n){let e,t,i,l,s,o,r,a,f,u,c,d;const m=[OA,CA],h=[];function _(g,y){return g[4]?0:1}return u=_(n),c=h[u]=m[u](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[7]),r=M(),a=b("div"),f=b("div"),c.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(f,"class","panel"),p(a,"class","wrapper")},m(g,y){w(g,e,y),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w(g,r,y),w(g,a,y),k(a,f),h[u].m(f,null),d=!0},p(g,y){(!d||y&128)&&oe(o,g[7]);let S=u;u=_(g),u===S?h[u].p(g,y):(le(),A(h[S],1,1,()=>{h[S]=null}),se(),c=h[u],c?c.p(g,y):(c=h[u]=m[u](g),c.c()),E(c,1),c.m(f,null))},i(g){d||(E(c),d=!0)},o(g){A(c),d=!1},d(g){g&&(v(e),v(r),v(a)),h[u].d()}}}function IA(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[EA]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,[o]){const r={};o&33554687&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function AA(n,e,t){let i,l,s,o;Ue(n,It,L=>t(7,o=L)),xt(It,o="Export collections",o);const r="export_"+j.randomString(5);let a,f=[],u={},c=!1;d();async function d(){t(4,c=!0);try{t(0,f=await ae.collections.getFullList({batch:100,$cancelKey:r})),t(0,f=j.sortCollections(f));for(let L of f)delete L.created,delete L.updated;y()}catch(L){ae.error(L)}t(4,c=!1)}function m(){j.downloadJson(Object.values(u),"pb_schema")}function h(){j.copyToClipboard(i),$o("The configuration was copied to your clipboard!",3e3)}function _(){s?g():y()}function g(){t(1,u={})}function y(){t(1,u={});for(const L of f)t(1,u[L.id]=L,u)}function S(L){u[L.id]?delete u[L.id]:t(1,u[L.id]=L,u),t(1,u)}const T=()=>_(),$=L=>S(L),C=()=>h();function O(L){ee[L?"unshift":"push"](()=>{a=L,t(3,a)})}const D=L=>{if(L.ctrlKey&&L.code==="KeyA"){L.preventDefault();const R=window.getSelection(),F=document.createRange();F.selectNodeContents(a),R.removeAllRanges(),R.addRange(F)}},I=()=>m();return n.$$.update=()=>{n.$$.dirty&2&&t(6,i=JSON.stringify(Object.values(u),null,4)),n.$$.dirty&2&&t(2,l=Object.keys(u).length),n.$$.dirty&5&&t(5,s=f.length&&l===f.length)},[f,u,l,a,c,s,i,o,m,h,_,S,r,T,$,C,O,D,I]}class LA extends ge{constructor(e){super(),_e(this,e,AA,IA,me,{})}}function C_(n,e,t){const i=n.slice();return i[14]=e[t],i}function O_(n,e,t){const i=n.slice();return i[17]=e[t][0],i[18]=e[t][1],i}function M_(n,e,t){const i=n.slice();return i[14]=e[t],i}function D_(n,e,t){const i=n.slice();return i[17]=e[t][0],i[23]=e[t][1],i}function E_(n,e,t){const i=n.slice();return i[14]=e[t],i}function I_(n,e,t){const i=n.slice();return i[17]=e[t][0],i[18]=e[t][1],i}function A_(n,e,t){const i=n.slice();return i[30]=e[t],i}function NA(n){let e,t,i,l,s=n[1].name+"",o,r=n[9]&&L_(),a=n[0].name!==n[1].name&&N_(n);return{c(){e=b("div"),r&&r.c(),t=M(),a&&a.c(),i=M(),l=b("strong"),o=K(s),p(l,"class","txt"),p(e,"class","inline-flex fleg-gap-5")},m(f,u){w(f,e,u),r&&r.m(e,null),k(e,t),a&&a.m(e,null),k(e,i),k(e,l),k(l,o)},p(f,u){f[9]?r||(r=L_(),r.c(),r.m(e,t)):r&&(r.d(1),r=null),f[0].name!==f[1].name?a?a.p(f,u):(a=N_(f),a.c(),a.m(e,i)):a&&(a.d(1),a=null),u[0]&2&&s!==(s=f[1].name+"")&&oe(o,s)},d(f){f&&v(e),r&&r.d(),a&&a.d()}}}function PA(n){var o;let e,t,i,l=((o=n[0])==null?void 0:o.name)+"",s;return{c(){e=b("span"),e.textContent="Deleted",t=M(),i=b("strong"),s=K(l),p(e,"class","label label-danger")},m(r,a){w(r,e,a),w(r,t,a),w(r,i,a),k(i,s)},p(r,a){var f;a[0]&1&&l!==(l=((f=r[0])==null?void 0:f.name)+"")&&oe(s,l)},d(r){r&&(v(e),v(t),v(i))}}}function FA(n){var o;let e,t,i,l=((o=n[1])==null?void 0:o.name)+"",s;return{c(){e=b("span"),e.textContent="Added",t=M(),i=b("strong"),s=K(l),p(e,"class","label label-success")},m(r,a){w(r,e,a),w(r,t,a),w(r,i,a),k(i,s)},p(r,a){var f;a[0]&2&&l!==(l=((f=r[1])==null?void 0:f.name)+"")&&oe(s,l)},d(r){r&&(v(e),v(t),v(i))}}}function L_(n){let e;return{c(){e=b("span"),e.textContent="Changed",p(e,"class","label label-warning")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function N_(n){let e,t=n[0].name+"",i,l,s;return{c(){e=b("strong"),i=K(t),l=M(),s=b("i"),p(e,"class","txt-strikethrough txt-hint"),p(s,"class","ri-arrow-right-line txt-sm")},m(o,r){w(o,e,r),k(e,i),w(o,l,r),w(o,s,r)},p(o,r){r[0]&1&&t!==(t=o[0].name+"")&&oe(i,t)},d(o){o&&(v(e),v(l),v(s))}}}function P_(n){var h,_;let e,t,i,l,s,o,r=n[12]((h=n[0])==null?void 0:h[n[30]])+"",a,f,u,c,d=n[12]((_=n[1])==null?void 0:_[n[30]])+"",m;return{c(){var g,y,S,T,$,C;e=b("tr"),t=b("td"),i=b("span"),i.textContent=`${n[30]}`,l=M(),s=b("td"),o=b("pre"),a=K(r),f=M(),u=b("td"),c=b("pre"),m=K(d),p(t,"class","min-width svelte-lmkr38"),p(o,"class","txt"),p(s,"class","svelte-lmkr38"),x(s,"changed-old-col",!n[10]&&hn((g=n[0])==null?void 0:g[n[30]],(y=n[1])==null?void 0:y[n[30]])),x(s,"changed-none-col",n[10]),p(c,"class","txt"),p(u,"class","svelte-lmkr38"),x(u,"changed-new-col",!n[5]&&hn((S=n[0])==null?void 0:S[n[30]],(T=n[1])==null?void 0:T[n[30]])),x(u,"changed-none-col",n[5]),p(e,"class","svelte-lmkr38"),x(e,"txt-primary",hn(($=n[0])==null?void 0:$[n[30]],(C=n[1])==null?void 0:C[n[30]]))},m(g,y){w(g,e,y),k(e,t),k(t,i),k(e,l),k(e,s),k(s,o),k(o,a),k(e,f),k(e,u),k(u,c),k(c,m)},p(g,y){var S,T,$,C,O,D,I,L;y[0]&1&&r!==(r=g[12]((S=g[0])==null?void 0:S[g[30]])+"")&&oe(a,r),y[0]&3075&&x(s,"changed-old-col",!g[10]&&hn((T=g[0])==null?void 0:T[g[30]],($=g[1])==null?void 0:$[g[30]])),y[0]&1024&&x(s,"changed-none-col",g[10]),y[0]&2&&d!==(d=g[12]((C=g[1])==null?void 0:C[g[30]])+"")&&oe(m,d),y[0]&2083&&x(u,"changed-new-col",!g[5]&&hn((O=g[0])==null?void 0:O[g[30]],(D=g[1])==null?void 0:D[g[30]])),y[0]&32&&x(u,"changed-none-col",g[5]),y[0]&2051&&x(e,"txt-primary",hn((I=g[0])==null?void 0:I[g[30]],(L=g[1])==null?void 0:L[g[30]]))},d(g){g&&v(e)}}}function F_(n){let e,t=ue(n[6]),i=[];for(let l=0;lProps Old New',s=M(),o=b("tbody");for(let $=0;$!["schema","created","updated"].includes(y));function _(){t(4,u=Array.isArray(r==null?void 0:r.schema)?r==null?void 0:r.schema.concat():[]),a||t(4,u=u.concat(f.filter(y=>!u.find(S=>y.id==S.id))))}function g(y){return typeof y>"u"?"":j.isObject(y)?JSON.stringify(y,null,4):y}return n.$$set=y=>{"collectionA"in y&&t(0,o=y.collectionA),"collectionB"in y&&t(1,r=y.collectionB),"deleteMissing"in y&&t(2,a=y.deleteMissing)},n.$$.update=()=>{n.$$.dirty[0]&2&&t(5,i=!(r!=null&&r.id)&&!(r!=null&&r.name)),n.$$.dirty[0]&33&&t(10,l=!i&&!(o!=null&&o.id)),n.$$.dirty[0]&1&&t(3,f=Array.isArray(o==null?void 0:o.schema)?o==null?void 0:o.schema.concat():[]),n.$$.dirty[0]&7&&(typeof(o==null?void 0:o.schema)<"u"||typeof(r==null?void 0:r.schema)<"u"||typeof a<"u")&&_(),n.$$.dirty[0]&24&&t(6,c=f.filter(y=>!u.find(S=>y.id==S.id))),n.$$.dirty[0]&24&&t(7,d=u.filter(y=>f.find(S=>S.id==y.id))),n.$$.dirty[0]&24&&t(8,m=u.filter(y=>!f.find(S=>S.id==y.id))),n.$$.dirty[0]&7&&t(9,s=j.hasCollectionChanges(o,r,a))},[o,r,a,f,u,i,c,d,m,s,l,h,g]}class jA extends ge{constructor(e){super(),_e(this,e,qA,RA,me,{collectionA:0,collectionB:1,deleteMissing:2},null,[-1,-1])}}function U_(n,e,t){const i=n.slice();return i[17]=e[t],i}function W_(n){let e,t;return e=new jA({props:{collectionA:n[17].old,collectionB:n[17].new,deleteMissing:n[3]}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l&4&&(s.collectionA=i[17].old),l&4&&(s.collectionB=i[17].new),l&8&&(s.deleteMissing=i[3]),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function HA(n){let e,t,i=ue(n[2]),l=[];for(let o=0;oA(l[o],1,1,()=>{l[o]=null});return{c(){for(let o=0;o{h()}):h()}async function h(){if(!f){t(4,f=!0);try{await ae.collections.import(o,a),Lt("Successfully imported collections configuration."),i("submit")}catch($){ae.error($)}t(4,f=!1),c()}}const _=()=>m(),g=()=>!f;function y($){ee[$?"unshift":"push"](()=>{l=$,t(1,l)})}function S($){Ce.call(this,n,$)}function T($){Ce.call(this,n,$)}return n.$$.update=()=>{n.$$.dirty&384&&Array.isArray(s)&&Array.isArray(o)&&d()},[c,l,r,a,f,m,u,s,o,_,g,y,S,T]}class WA extends ge{constructor(e){super(),_e(this,e,UA,BA,me,{show:6,hide:0})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[0]}}function Y_(n,e,t){const i=n.slice();return i[33]=e[t],i}function K_(n,e,t){const i=n.slice();return i[36]=e[t],i}function J_(n,e,t){const i=n.slice();return i[33]=e[t],i}function YA(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I;a=new ce({props:{class:"form-field "+(n[6]?"":"field-error"),name:"collections",$$slots:{default:[JA,({uniqueId:H})=>({41:H}),({uniqueId:H})=>[0,H?1024:0]]},$$scope:{ctx:n}}});let L=n[1].length&&G_(n),R=!1,F=n[6]&&n[1].length&&!n[7]&&X_(),N=n[6]&&n[1].length&&n[7]&&Q_(n),P=n[13].length&&fg(n),q=!!n[0]&&ug(n);return{c(){e=b("input"),t=M(),i=b("div"),l=b("p"),s=K(`Paste below the collections configuration you want to import or - `),o=b("button"),o.innerHTML='Load from JSON file',r=M(),B(a.$$.fragment),f=M(),L&&L.c(),u=M(),c=M(),F&&F.c(),d=M(),N&&N.c(),m=M(),P&&P.c(),h=M(),_=b("div"),q&&q.c(),g=M(),y=b("div"),S=M(),T=b("button"),$=b("span"),$.textContent="Review",p(e,"type","file"),p(e,"class","hidden"),p(e,"accept",".json"),p(o,"class","btn btn-outline btn-sm m-l-5"),x(o,"btn-loading",n[12]),p(i,"class","content txt-xl m-b-base"),p(y,"class","flex-fill"),p($,"class","txt"),p(T,"type","button"),p(T,"class","btn btn-expanded btn-warning m-l-auto"),T.disabled=C=!n[14],p(_,"class","flex m-t-base")},m(H,W){w(H,e,W),n[21](e),w(H,t,W),w(H,i,W),k(i,l),k(l,s),k(l,o),w(H,r,W),z(a,H,W),w(H,f,W),L&&L.m(H,W),w(H,u,W),w(H,c,W),F&&F.m(H,W),w(H,d,W),N&&N.m(H,W),w(H,m,W),P&&P.m(H,W),w(H,h,W),w(H,_,W),q&&q.m(_,null),k(_,g),k(_,y),k(_,S),k(_,T),k(T,$),O=!0,D||(I=[J(e,"change",n[22]),J(o,"click",n[23]),J(T,"click",n[19])],D=!0)},p(H,W){(!O||W[0]&4096)&&x(o,"btn-loading",H[12]);const G={};W[0]&64&&(G.class="form-field "+(H[6]?"":"field-error")),W[0]&65|W[1]&3072&&(G.$$scope={dirty:W,ctx:H}),a.$set(G),H[1].length?L?(L.p(H,W),W[0]&2&&E(L,1)):(L=G_(H),L.c(),E(L,1),L.m(u.parentNode,u)):L&&(le(),A(L,1,1,()=>{L=null}),se()),H[6]&&H[1].length&&!H[7]?F||(F=X_(),F.c(),F.m(d.parentNode,d)):F&&(F.d(1),F=null),H[6]&&H[1].length&&H[7]?N?N.p(H,W):(N=Q_(H),N.c(),N.m(m.parentNode,m)):N&&(N.d(1),N=null),H[13].length?P?P.p(H,W):(P=fg(H),P.c(),P.m(h.parentNode,h)):P&&(P.d(1),P=null),H[0]?q?q.p(H,W):(q=ug(H),q.c(),q.m(_,g)):q&&(q.d(1),q=null),(!O||W[0]&16384&&C!==(C=!H[14]))&&(T.disabled=C)},i(H){O||(E(a.$$.fragment,H),E(L),E(R),O=!0)},o(H){A(a.$$.fragment,H),A(L),A(R),O=!1},d(H){H&&(v(e),v(t),v(i),v(r),v(f),v(u),v(c),v(d),v(m),v(h),v(_)),n[21](null),V(a,H),L&&L.d(H),F&&F.d(H),N&&N.d(H),P&&P.d(H),q&&q.d(),D=!1,$e(I)}}}function KA(n){let e;return{c(){e=b("div"),p(e,"class","loader")},m(t,i){w(t,e,i)},p:Q,i:Q,o:Q,d(t){t&&v(e)}}}function Z_(n){let e;return{c(){e=b("div"),e.textContent="Invalid collections configuration.",p(e,"class","help-block help-block-error")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function JA(n){let e,t,i,l,s,o,r,a,f,u,c=!!n[0]&&!n[6]&&Z_();return{c(){e=b("label"),t=K("Collections"),l=M(),s=b("textarea"),r=M(),c&&c.c(),a=ye(),p(e,"for",i=n[41]),p(e,"class","p-b-10"),p(s,"id",o=n[41]),p(s,"class","code"),p(s,"spellcheck","false"),p(s,"rows","15"),s.required=!0},m(d,m){w(d,e,m),k(e,t),w(d,l,m),w(d,s,m),re(s,n[0]),w(d,r,m),c&&c.m(d,m),w(d,a,m),f||(u=J(s,"input",n[24]),f=!0)},p(d,m){m[1]&1024&&i!==(i=d[41])&&p(e,"for",i),m[1]&1024&&o!==(o=d[41])&&p(s,"id",o),m[0]&1&&re(s,d[0]),d[0]&&!d[6]?c||(c=Z_(),c.c(),c.m(a.parentNode,a)):c&&(c.d(1),c=null)},d(d){d&&(v(e),v(l),v(s),v(r),v(a)),c&&c.d(d),f=!1,u()}}}function G_(n){let e,t;return e=new ce({props:{class:"form-field form-field-toggle",$$slots:{default:[ZA,({uniqueId:i})=>({41:i}),({uniqueId:i})=>[0,i?1024:0]]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment)},m(i,l){z(e,i,l),t=!0},p(i,l){const s={};l[0]&96|l[1]&3072&&(s.$$scope={dirty:l,ctx:i}),e.$set(s)},i(i){t||(E(e.$$.fragment,i),t=!0)},o(i){A(e.$$.fragment,i),t=!1},d(i){V(e,i)}}}function ZA(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("input"),l=M(),s=b("label"),o=K("Merge with the existing collections"),p(e,"type","checkbox"),p(e,"id",t=n[41]),e.disabled=i=!n[6],p(s,"for",r=n[41])},m(u,c){w(u,e,c),e.checked=n[5],w(u,l,c),w(u,s,c),k(s,o),a||(f=J(e,"change",n[25]),a=!0)},p(u,c){c[1]&1024&&t!==(t=u[41])&&p(e,"id",t),c[0]&64&&i!==(i=!u[6])&&(e.disabled=i),c[0]&32&&(e.checked=u[5]),c[1]&1024&&r!==(r=u[41])&&p(s,"for",r)},d(u){u&&(v(e),v(l),v(s)),a=!1,f()}}}function X_(n){let e;return{c(){e=b("div"),e.innerHTML='
    Your collections configuration is already up-to-date!
    ',p(e,"class","alert alert-info")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function Q_(n){let e,t,i,l,s,o=n[9].length&&x_(n),r=n[3].length&&ng(n),a=n[8].length&&og(n);return{c(){e=b("h5"),e.textContent="Detected changes",t=M(),i=b("div"),o&&o.c(),l=M(),r&&r.c(),s=M(),a&&a.c(),p(e,"class","section-title"),p(i,"class","list")},m(f,u){w(f,e,u),w(f,t,u),w(f,i,u),o&&o.m(i,null),k(i,l),r&&r.m(i,null),k(i,s),a&&a.m(i,null)},p(f,u){f[9].length?o?o.p(f,u):(o=x_(f),o.c(),o.m(i,l)):o&&(o.d(1),o=null),f[3].length?r?r.p(f,u):(r=ng(f),r.c(),r.m(i,s)):r&&(r.d(1),r=null),f[8].length?a?a.p(f,u):(a=og(f),a.c(),a.m(i,null)):a&&(a.d(1),a=null)},d(f){f&&(v(e),v(t),v(i)),o&&o.d(),r&&r.d(),a&&a.d()}}}function x_(n){let e=[],t=new Map,i,l=ue(n[9]);const s=o=>o[33].id;for(let o=0;oo[36].old.id+o[36].new.id;for(let o=0;oo[33].id;for(let o=0;o',i=M(),l=b("div"),l.innerHTML=`Some of the imported collections share the same name and/or fields but are - imported with different IDs. You can replace them in the import if you want - to.`,s=M(),o=b("button"),o.innerHTML='Replace with original ids',p(t,"class","icon"),p(l,"class","content"),p(o,"type","button"),p(o,"class","btn btn-warning btn-sm btn-outline"),p(e,"class","alert alert-warning m-t-base")},m(f,u){w(f,e,u),k(e,t),k(e,i),k(e,l),k(e,s),k(e,o),r||(a=J(o,"click",n[27]),r=!0)},p:Q,d(f){f&&v(e),r=!1,a()}}}function ug(n){let e,t,i;return{c(){e=b("button"),e.innerHTML='Clear',p(e,"type","button"),p(e,"class","btn btn-transparent link-hint")},m(l,s){w(l,e,s),t||(i=J(e,"click",n[28]),t=!0)},p:Q,d(l){l&&v(e),t=!1,i()}}}function GA(n){let e,t,i,l,s,o,r,a,f,u,c,d;const m=[KA,YA],h=[];function _(g,y){return g[4]?0:1}return u=_(n),c=h[u]=m[u](n),{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[15]),r=M(),a=b("div"),f=b("div"),c.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(f,"class","panel"),p(a,"class","wrapper")},m(g,y){w(g,e,y),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w(g,r,y),w(g,a,y),k(a,f),h[u].m(f,null),d=!0},p(g,y){(!d||y[0]&32768)&&oe(o,g[15]);let S=u;u=_(g),u===S?h[u].p(g,y):(le(),A(h[S],1,1,()=>{h[S]=null}),se(),c=h[u],c?c.p(g,y):(c=h[u]=m[u](g),c.c()),E(c,1),c.m(f,null))},i(g){d||(E(c),d=!0)},o(g){A(c),d=!1},d(g){g&&(v(e),v(r),v(a)),h[u].d()}}}function XA(n){let e,t,i,l,s,o;e=new _i({}),i=new bn({props:{$$slots:{default:[GA]},$$scope:{ctx:n}}});let r={};return s=new WA({props:r}),n[29](s),s.$on("submit",n[18]),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment),l=M(),B(s.$$.fragment)},m(a,f){z(e,a,f),w(a,t,f),z(i,a,f),w(a,l,f),z(s,a,f),o=!0},p(a,f){const u={};f[0]&63487|f[1]&2048&&(u.$$scope={dirty:f,ctx:a}),i.$set(u);const c={};s.$set(c)},i(a){o||(E(e.$$.fragment,a),E(i.$$.fragment,a),E(s.$$.fragment,a),o=!0)},o(a){A(e.$$.fragment,a),A(i.$$.fragment,a),A(s.$$.fragment,a),o=!1},d(a){a&&(v(t),v(l)),V(e,a),V(i,a),n[29](null),V(s,a)}}}function QA(n,e,t){let i,l,s,o,r,a,f;Ue(n,It,ie=>t(15,f=ie)),xt(It,f="Import collections",f);let u,c,d="",m=!1,h=[],_=[],g=!0,y=[],S=!1,T=!1;$();async function $(){t(4,S=!0);try{t(20,_=await ae.collections.getFullList(200));for(let ie of _)delete ie.created,delete ie.updated}catch(ie){ae.error(ie)}t(4,S=!1)}function C(){if(t(3,y=[]),!!i)for(let ie of h){const te=j.findByKey(_,"id",ie.id);!(te!=null&&te.id)||!j.hasCollectionChanges(te,ie,g)||y.push({new:ie,old:te})}}function O(){t(1,h=[]);try{t(1,h=JSON.parse(d))}catch{}Array.isArray(h)?t(1,h=j.filterDuplicatesByKey(h)):t(1,h=[]);for(let ie of h)delete ie.created,delete ie.updated,ie.schema=j.filterDuplicatesByKey(ie.schema)}function D(){var ie,te;for(let pe of h){const Ne=j.findByKey(_,"name",pe.name)||j.findByKey(_,"id",pe.id);if(!Ne)continue;const He=pe.id,Xe=Ne.id;pe.id=Xe;const xe=Array.isArray(Ne.schema)?Ne.schema:[],Mt=Array.isArray(pe.schema)?pe.schema:[];for(const ft of Mt){const mt=j.findByKey(xe,"name",ft.name);mt&&mt.id&&(ft.id=mt.id)}for(let ft of h)if(Array.isArray(ft.schema))for(let mt of ft.schema)(ie=mt.options)!=null&&ie.collectionId&&((te=mt.options)==null?void 0:te.collectionId)===He&&(mt.options.collectionId=Xe)}t(0,d=JSON.stringify(h,null,4))}function I(ie){t(12,m=!0);const te=new FileReader;te.onload=async pe=>{t(12,m=!1),t(10,u.value="",u),t(0,d=pe.target.result),await Qt(),h.length||(ii("Invalid collections configuration."),L())},te.onerror=pe=>{console.warn(pe),ii("Failed to load the imported JSON."),t(12,m=!1),t(10,u.value="",u)},te.readAsText(ie)}function L(){t(0,d=""),t(10,u.value="",u),Jt({})}function R(){const ie=T?j.filterDuplicatesByKey(_.concat(h)):h;c==null||c.show(_,ie,g)}function F(ie){ee[ie?"unshift":"push"](()=>{u=ie,t(10,u)})}const N=()=>{u.files.length&&I(u.files[0])},P=()=>{u.click()};function q(){d=this.value,t(0,d)}function H(){T=this.checked,t(5,T)}function W(){g=this.checked,t(2,g)}const G=()=>D(),U=()=>L();function Y(ie){ee[ie?"unshift":"push"](()=>{c=ie,t(11,c)})}return n.$$.update=()=>{n.$$.dirty[0]&33&&typeof d<"u"&&T!==null&&O(),n.$$.dirty[0]&3&&t(6,i=!!d&&h.length&&h.length===h.filter(ie=>!!ie.id&&!!ie.name).length),n.$$.dirty[0]&1048678&&t(9,l=_.filter(ie=>i&&!T&&g&&!j.findByKey(h,"id",ie.id))),n.$$.dirty[0]&1048642&&t(8,s=h.filter(ie=>i&&!j.findByKey(_,"id",ie.id))),n.$$.dirty[0]&6&&(typeof h<"u"||typeof g<"u")&&C(),n.$$.dirty[0]&777&&t(7,o=!!d&&(l.length||s.length||y.length)),n.$$.dirty[0]&208&&t(14,r=!S&&i&&o),n.$$.dirty[0]&1048578&&t(13,a=h.filter(ie=>{let te=j.findByKey(_,"name",ie.name)||j.findByKey(_,"id",ie.id);if(!te)return!1;if(te.id!=ie.id)return!0;const pe=Array.isArray(te.schema)?te.schema:[],Ne=Array.isArray(ie.schema)?ie.schema:[];for(const He of Ne){if(j.findByKey(pe,"id",He.id))continue;const xe=j.findByKey(pe,"name",He.name);if(xe&&He.id!=xe.id)return!0}return!1}))},[d,h,g,y,S,T,i,o,s,l,u,c,m,a,r,f,D,I,L,R,_,F,N,P,q,H,W,G,U,Y]}class xA extends ge{constructor(e){super(),_e(this,e,QA,XA,me,{},null,[-1,-1])}}function e7(n){let e,t,i,l,s,o,r,a,f,u;return{c(){e=b("label"),t=K("Backup name"),l=M(),s=b("input"),r=M(),a=b("em"),a.textContent="Must be in the format [a-z0-9_-].zip",p(e,"for",i=n[15]),p(s,"type","text"),p(s,"id",o=n[15]),p(s,"placeholder","Leave empty to autogenerate"),p(s,"pattern","^[a-z0-9_-]+\\.zip$"),p(a,"class","help-block")},m(c,d){w(c,e,d),k(e,t),w(c,l,d),w(c,s,d),re(s,n[2]),w(c,r,d),w(c,a,d),f||(u=J(s,"input",n[7]),f=!0)},p(c,d){d&32768&&i!==(i=c[15])&&p(e,"for",i),d&32768&&o!==(o=c[15])&&p(s,"id",o),d&4&&s.value!==c[2]&&re(s,c[2])},d(c){c&&(v(e),v(l),v(s),v(r),v(a)),f=!1,u()}}}function t7(n){let e,t,i,l,s,o,r;return l=new ce({props:{class:"form-field m-0",name:"name",$$slots:{default:[e7,({uniqueId:a})=>({15:a}),({uniqueId:a})=>a?32768:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.innerHTML=`

    Please note that during the backup other concurrent write requests may fail since the - database will be temporary "locked" (this usually happens only during the ZIP generation).

    If you are using S3 storage for the collections file upload, you'll have to backup them - separately since they are not locally stored and will not be included in the final backup!

    `,t=M(),i=b("form"),B(l.$$.fragment),p(e,"class","alert alert-info"),p(i,"id",n[4]),p(i,"autocomplete","off")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),z(l,i,null),s=!0,o||(r=J(i,"submit",Be(n[5])),o=!0)},p(a,f){const u={};f&98308&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(E(l.$$.fragment,a),s=!0)},o(a){A(l.$$.fragment,a),s=!1},d(a){a&&(v(e),v(t),v(i)),V(l),o=!1,r()}}}function n7(n){let e;return{c(){e=b("h4"),e.textContent="Initialize new backup",p(e,"class","center txt-break")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function i7(n){let e,t,i,l,s,o,r;return{c(){e=b("button"),t=b("span"),t.textContent="Cancel",i=M(),l=b("button"),s=b("span"),s.textContent="Start backup",p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[3],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[4]),p(l,"class","btn btn-expanded"),l.disabled=n[3],x(l,"btn-loading",n[3])},m(a,f){w(a,e,f),k(e,t),w(a,i,f),w(a,l,f),k(l,s),o||(r=J(e,"click",n[0]),o=!0)},p(a,f){f&8&&(e.disabled=a[3]),f&8&&(l.disabled=a[3]),f&8&&x(l,"btn-loading",a[3])},d(a){a&&(v(e),v(i),v(l)),o=!1,r()}}}function l7(n){let e,t,i={class:"backup-create-panel",beforeOpen:n[8],beforeHide:n[9],popup:!0,$$slots:{footer:[i7],header:[n7],default:[t7]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[10](e),e.$on("show",n[11]),e.$on("hide",n[12]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&8&&(o.beforeOpen=l[8]),s&8&&(o.beforeHide=l[9]),s&65548&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[10](null),V(e,l)}}}function s7(n,e,t){const i=lt(),l="backup_create_"+j.randomString(5);let s,o="",r=!1,a;function f(S){Jt({}),t(3,r=!1),t(2,o=S||""),s==null||s.show()}function u(){return s==null?void 0:s.hide()}async function c(){if(!r){t(3,r=!0),clearTimeout(a),a=setTimeout(()=>{u()},1500);try{await ae.backups.create(o,{$cancelKey:l}),t(3,r=!1),u(),i("submit"),Lt("Successfully generated new backup.")}catch(S){S.isAbort||ae.error(S)}clearTimeout(a),t(3,r=!1)}}ks(()=>{clearTimeout(a)});function d(){o=this.value,t(2,o)}const m=()=>r?($o("A backup has already been started, please wait."),!1):!0,h=()=>(r&&$o("The backup was started but may take a while to complete. You can come back later.",4500),!0);function _(S){ee[S?"unshift":"push"](()=>{s=S,t(1,s)})}function g(S){Ce.call(this,n,S)}function y(S){Ce.call(this,n,S)}return[u,s,o,r,l,c,f,d,m,h,_,g,y]}class o7 extends ge{constructor(e){super(),_e(this,e,s7,l7,me,{show:6,hide:0})}get show(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[0]}}function r7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Backup name"),l=M(),s=b("input"),p(e,"for",i=n[15]),p(s,"type","text"),p(s,"id",o=n[15]),s.required=!0},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[2]),r||(a=J(s,"input",n[9]),r=!0)},p(f,u){u&32768&&i!==(i=f[15])&&p(e,"for",i),u&32768&&o!==(o=f[15])&&p(s,"id",o),u&4&&s.value!==f[2]&&re(s,f[2])},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function a7(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g;return f=new sl({props:{value:n[1]}}),m=new ce({props:{class:"form-field required m-0",name:"name",$$slots:{default:[r7,({uniqueId:y})=>({15:y}),({uniqueId:y})=>y?32768:0]},$$scope:{ctx:n}}}),{c(){e=b("div"),e.innerHTML=`

    Please proceed with caution and use it only with trusted backups!

    Backup restore is experimental and works only on UNIX based systems.

    The restore operation will attempt to replace your existing pb_data with the one from - the backup and will restart the application process.

    This means that on success all of your data (including app settings, users, admins, etc.) will - be replaced with the ones from the backup.

    Nothing will happen if the backup is invalid or incompatible (ex. missing - data.db file).

    `,t=M(),i=b("div"),l=K(`Type the backup name - `),s=b("div"),o=b("span"),r=K(n[1]),a=M(),B(f.$$.fragment),u=K(` - to confirm:`),c=M(),d=b("form"),B(m.$$.fragment),p(e,"class","alert alert-danger"),p(o,"class","txt"),p(s,"class","label"),p(i,"class","content m-b-xs"),p(d,"id",n[6]),p(d,"autocomplete","off")},m(y,S){w(y,e,S),w(y,t,S),w(y,i,S),k(i,l),k(i,s),k(s,o),k(o,r),k(s,a),z(f,s,null),k(i,u),w(y,c,S),w(y,d,S),z(m,d,null),h=!0,_||(g=J(d,"submit",Be(n[7])),_=!0)},p(y,S){(!h||S&2)&&oe(r,y[1]);const T={};S&2&&(T.value=y[1]),f.$set(T);const $={};S&98308&&($.$$scope={dirty:S,ctx:y}),m.$set($)},i(y){h||(E(f.$$.fragment,y),E(m.$$.fragment,y),h=!0)},o(y){A(f.$$.fragment,y),A(m.$$.fragment,y),h=!1},d(y){y&&(v(e),v(t),v(i),v(c),v(d)),V(f),V(m),_=!1,g()}}}function f7(n){let e,t,i,l;return{c(){e=b("h4"),t=K("Restore "),i=b("strong"),l=K(n[1]),p(e,"class","popup-title txt-ellipsis svelte-1fcgldh")},m(s,o){w(s,e,o),k(e,t),k(e,i),k(i,l)},p(s,o){o&2&&oe(l,s[1])},d(s){s&&v(e)}}}function u7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("button"),t=K("Cancel"),i=M(),l=b("button"),s=b("span"),s.textContent="Restore backup",p(e,"type","button"),p(e,"class","btn btn-transparent"),e.disabled=n[4],p(s,"class","txt"),p(l,"type","submit"),p(l,"form",n[6]),p(l,"class","btn btn-expanded"),l.disabled=o=!n[5]||n[4],x(l,"btn-loading",n[4])},m(f,u){w(f,e,u),k(e,t),w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"click",n[0]),r=!0)},p(f,u){u&16&&(e.disabled=f[4]),u&48&&o!==(o=!f[5]||f[4])&&(l.disabled=o),u&16&&x(l,"btn-loading",f[4])},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function c7(n){let e,t,i={class:"backup-restore-panel",overlayClose:!n[4],escClose:!n[4],beforeHide:n[10],popup:!0,$$slots:{footer:[u7],header:[f7],default:[a7]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[11](e),e.$on("show",n[12]),e.$on("hide",n[13]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&16&&(o.overlayClose=!l[4]),s&16&&(o.escClose=!l[4]),s&16&&(o.beforeHide=l[10]),s&65590&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[11](null),V(e,l)}}}function d7(n,e,t){let i;const l="backup_restore_"+j.randomString(5);let s,o="",r="",a=!1,f=null;function u(S){Jt({}),t(2,r=""),t(1,o=S),t(4,a=!1),s==null||s.show()}function c(){return s==null?void 0:s.hide()}async function d(){var S;if(!(!i||a)){clearTimeout(f),t(4,a=!0);try{await ae.backups.restore(o),f=setTimeout(()=>{window.location.reload()},2e3)}catch(T){clearTimeout(f),T!=null&&T.isAbort||(t(4,a=!1),ii(((S=T.response)==null?void 0:S.message)||T.message))}}}ks(()=>{clearTimeout(f)});function m(){r=this.value,t(2,r)}const h=()=>!a;function _(S){ee[S?"unshift":"push"](()=>{s=S,t(3,s)})}function g(S){Ce.call(this,n,S)}function y(S){Ce.call(this,n,S)}return n.$$.update=()=>{n.$$.dirty&6&&t(5,i=r!=""&&o==r)},[c,o,r,s,a,i,l,d,u,m,h,_,g,y]}class p7 extends ge{constructor(e){super(),_e(this,e,d7,c7,me,{show:8,hide:0})}get show(){return this.$$.ctx[8]}get hide(){return this.$$.ctx[0]}}function cg(n,e,t){const i=n.slice();return i[22]=e[t],i}function dg(n,e,t){const i=n.slice();return i[19]=e[t],i}function m7(n){let e=[],t=new Map,i,l,s=ue(n[3]);const o=a=>a[22].key;for(let a=0;aNo backups yet. ',p(e,"class","list-item list-item-placeholder svelte-1ulbkf5")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function mg(n,e){let t,i,l,s,o,r=e[22].key+"",a,f,u,c,d,m=j.formattedFileSize(e[22].size)+"",h,_,g,y,S,T,$,C,O,D,I,L,R,F,N,P,q,H,W,G;function U(){return e[10](e[22])}function Y(){return e[11](e[22])}function ie(){return e[12](e[22])}return{key:n,first:null,c(){t=b("div"),i=b("i"),l=M(),s=b("div"),o=b("span"),a=K(r),u=M(),c=b("span"),d=K("("),h=K(m),_=K(")"),g=M(),y=b("div"),S=b("button"),T=b("i"),C=M(),O=b("button"),D=b("i"),L=M(),R=b("button"),F=b("i"),P=M(),p(i,"class","ri-folder-zip-line"),p(o,"class","name backup-name svelte-1ulbkf5"),p(o,"title",f=e[22].key),p(c,"class","size txt-hint txt-nowrap"),p(s,"class","content"),p(T,"class","ri-download-line"),p(S,"type","button"),p(S,"class","btn btn-sm btn-circle btn-hint btn-transparent"),S.disabled=$=e[6][e[22].key]||e[5][e[22].key],p(S,"aria-label","Download"),x(S,"btn-loading",e[5][e[22].key]),p(D,"class","ri-restart-line"),p(O,"type","button"),p(O,"class","btn btn-sm btn-circle btn-hint btn-transparent"),O.disabled=I=e[6][e[22].key],p(O,"aria-label","Restore"),p(F,"class","ri-delete-bin-7-line"),p(R,"type","button"),p(R,"class","btn btn-sm btn-circle btn-hint btn-transparent"),R.disabled=N=e[6][e[22].key],p(R,"aria-label","Delete"),x(R,"btn-loading",e[6][e[22].key]),p(y,"class","actions nonintrusive"),p(t,"class","list-item svelte-1ulbkf5"),this.first=t},m(te,pe){w(te,t,pe),k(t,i),k(t,l),k(t,s),k(s,o),k(o,a),k(s,u),k(s,c),k(c,d),k(c,h),k(c,_),k(t,g),k(t,y),k(y,S),k(S,T),k(y,C),k(y,O),k(O,D),k(y,L),k(y,R),k(R,F),k(t,P),H=!0,W||(G=[Se(Pe.call(null,S,"Download")),J(S,"click",Be(U)),Se(Pe.call(null,O,"Restore")),J(O,"click",Be(Y)),Se(Pe.call(null,R,"Delete")),J(R,"click",Be(ie))],W=!0)},p(te,pe){e=te,(!H||pe&8)&&r!==(r=e[22].key+"")&&oe(a,r),(!H||pe&8&&f!==(f=e[22].key))&&p(o,"title",f),(!H||pe&8)&&m!==(m=j.formattedFileSize(e[22].size)+"")&&oe(h,m),(!H||pe&104&&$!==($=e[6][e[22].key]||e[5][e[22].key]))&&(S.disabled=$),(!H||pe&40)&&x(S,"btn-loading",e[5][e[22].key]),(!H||pe&72&&I!==(I=e[6][e[22].key]))&&(O.disabled=I),(!H||pe&72&&N!==(N=e[6][e[22].key]))&&(R.disabled=N),(!H||pe&72)&&x(R,"btn-loading",e[6][e[22].key])},i(te){H||(te&&Ke(()=>{H&&(q||(q=Fe(t,et,{duration:150},!0)),q.run(1))}),H=!0)},o(te){te&&(q||(q=Fe(t,et,{duration:150},!1)),q.run(0)),H=!1},d(te){te&&v(t),te&&q&&q.end(),W=!1,$e(G)}}}function hg(n){let e;return{c(){e=b("div"),e.innerHTML=' ',p(e,"class","list-item list-item-loader svelte-1ulbkf5")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function _7(n){let e,t,i;return{c(){e=b("span"),t=M(),i=b("span"),i.textContent="Backup/restore operation is in process",p(e,"class","loader loader-sm"),p(i,"class","txt")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},d(l){l&&(v(e),v(t),v(i))}}}function g7(n){let e,t,i;return{c(){e=b("i"),t=M(),i=b("span"),i.textContent="Initialize new backup",p(e,"class","ri-play-circle-line"),p(i,"class","txt")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s)},d(l){l&&(v(e),v(t),v(i))}}}function b7(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_;const g=[h7,m7],y=[];function S(I,L){return I[4]?0:1}i=S(n),l=y[i]=g[i](n);function T(I,L){return I[7]?g7:_7}let $=T(n),C=$(n),O={};u=new o7({props:O}),n[14](u),u.$on("submit",n[15]);let D={};return d=new p7({props:D}),n[16](d),{c(){e=b("div"),t=b("div"),l.c(),s=M(),o=b("div"),r=b("button"),C.c(),f=M(),B(u.$$.fragment),c=M(),B(d.$$.fragment),p(t,"class","list-content svelte-1ulbkf5"),p(r,"type","button"),p(r,"class","btn btn-block btn-transparent"),r.disabled=a=n[4]||!n[7],p(o,"class","list-item list-item-btn"),p(e,"class","list list-compact")},m(I,L){w(I,e,L),k(e,t),y[i].m(t,null),k(e,s),k(e,o),k(o,r),C.m(r,null),w(I,f,L),z(u,I,L),w(I,c,L),z(d,I,L),m=!0,h||(_=J(r,"click",n[13]),h=!0)},p(I,[L]){let R=i;i=S(I),i===R?y[i].p(I,L):(le(),A(y[R],1,1,()=>{y[R]=null}),se(),l=y[i],l?l.p(I,L):(l=y[i]=g[i](I),l.c()),E(l,1),l.m(t,null)),$!==($=T(I))&&(C.d(1),C=$(I),C&&(C.c(),C.m(r,null))),(!m||L&144&&a!==(a=I[4]||!I[7]))&&(r.disabled=a);const F={};u.$set(F);const N={};d.$set(N)},i(I){m||(E(l),E(u.$$.fragment,I),E(d.$$.fragment,I),m=!0)},o(I){A(l),A(u.$$.fragment,I),A(d.$$.fragment,I),m=!1},d(I){I&&(v(e),v(f),v(c)),y[i].d(),C.d(),n[14](null),V(u,I),n[16](null),V(d,I),h=!1,_()}}}function k7(n,e,t){let i,l,s=[],o=!1,r={},a={},f=!0;u(),h();async function u(){t(4,o=!0);try{t(3,s=await ae.backups.getFullList()),s.sort((O,D)=>O.modifiedD.modified?-1:0),t(4,o=!1)}catch(O){O.isAbort||(ae.error(O),t(4,o=!1))}}async function c(O){if(!r[O]){t(5,r[O]=!0,r);try{const D=await ae.getAdminFileToken(),I=ae.backups.getDownloadUrl(D,O);j.download(I)}catch(D){D.isAbort||ae.error(D)}delete r[O],t(5,r)}}function d(O){fn(`Do you really want to delete ${O}?`,()=>m(O))}async function m(O){if(!a[O]){t(6,a[O]=!0,a);try{await ae.backups.delete(O),j.removeByKey(s,"name",O),u(),Lt(`Successfully deleted ${O}.`)}catch(D){D.isAbort||ae.error(D)}delete a[O],t(6,a)}}async function h(){var O;try{const D=await ae.health.check({$autoCancel:!1}),I=f;t(7,f=((O=D==null?void 0:D.data)==null?void 0:O.canBackup)||!1),I!=f&&f&&u()}catch{}}Ht(()=>{let O=setInterval(()=>{h()},3e3);return()=>{clearInterval(O)}});const _=O=>c(O.key),g=O=>l.show(O.key),y=O=>d(O.key),S=()=>i==null?void 0:i.show();function T(O){ee[O?"unshift":"push"](()=>{i=O,t(1,i)})}const $=()=>{u()};function C(O){ee[O?"unshift":"push"](()=>{l=O,t(2,l)})}return[u,i,l,s,o,r,a,f,c,d,_,g,y,S,T,$,C]}class y7 extends ge{constructor(e){super(),_e(this,e,k7,b7,me,{loadBackups:0})}get loadBackups(){return this.$$.ctx[0]}}function v7(n){let e,t,i,l,s,o,r;return{c(){e=b("button"),t=b("i"),l=M(),s=b("input"),p(t,"class","ri-upload-cloud-line"),p(e,"type","button"),p(e,"class",i="btn btn-circle btn-transparent "+n[0]),p(e,"aria-label","Upload backup"),x(e,"btn-loading",n[2]),x(e,"btn-disabled",n[2]),p(s,"type","file"),p(s,"accept","application/zip"),p(s,"class","hidden")},m(a,f){w(a,e,f),k(e,t),w(a,l,f),w(a,s,f),n[5](s),o||(r=[Se(Pe.call(null,e,"Upload backup")),J(e,"click",n[4]),J(s,"change",n[3])],o=!0)},p(a,[f]){f&1&&i!==(i="btn btn-circle btn-transparent "+a[0])&&p(e,"class",i),f&5&&x(e,"btn-loading",a[2]),f&5&&x(e,"btn-disabled",a[2])},i:Q,o:Q,d(a){a&&(v(e),v(l),v(s)),n[5](null),o=!1,$e(r)}}}const _g="upload_backup";function w7(n,e,t){const i=lt();let{class:l=""}=e,s,o=!1;async function r(u){var d,m,h,_,g;if(o||!((m=(d=u==null?void 0:u.target)==null?void 0:d.files)!=null&&m.length))return;t(2,o=!0);const c=new FormData;c.set("file",u.target.files[0]);try{await ae.backups.upload(c,{requestKey:_g}),t(2,o=!1),i("success"),Lt("Successfully uploaded a new backup.")}catch(y){y.isAbort||(t(2,o=!1),(g=(_=(h=y.response)==null?void 0:h.data)==null?void 0:_.file)!=null&&g.message?ii(y.response.data.file.message):ae.error(y))}}ks(()=>{ae.cancelRequest(_g)});const a=()=>s==null?void 0:s.click();function f(u){ee[u?"unshift":"push"](()=>{s=u,t(1,s)})}return n.$$set=u=>{"class"in u&&t(0,l=u.class)},[l,s,o,r,a,f]}class S7 extends ge{constructor(e){super(),_e(this,e,w7,v7,me,{class:0})}}function $7(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-down-s-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function T7(n){let e;return{c(){e=b("i"),p(e,"class","ri-arrow-up-s-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function gg(n){var H,W,G;let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D;t=new ce({props:{class:"form-field form-field-toggle m-t-base m-b-0",$$slots:{default:[C7,({uniqueId:U})=>({31:U}),({uniqueId:U})=>[0,U?1:0]]},$$scope:{ctx:n}}});let I=n[2]&&bg(n);function L(U){n[24](U)}function R(U){n[25](U)}function F(U){n[26](U)}let N={toggleLabel:"Store backups in S3 storage",testFilesystem:"backups",configKey:"backups.s3",originalConfig:(H=n[0].backups)==null?void 0:H.s3};n[1].backups.s3!==void 0&&(N.config=n[1].backups.s3),n[7]!==void 0&&(N.isTesting=n[7]),n[8]!==void 0&&(N.testError=n[8]),r=new Xb({props:N}),ee.push(()=>be(r,"config",L)),ee.push(()=>be(r,"isTesting",R)),ee.push(()=>be(r,"testError",F));let P=((G=(W=n[1].backups)==null?void 0:W.s3)==null?void 0:G.enabled)&&!n[9]&&!n[5]&&kg(n),q=n[9]&&yg(n);return{c(){e=b("form"),B(t.$$.fragment),i=M(),I&&I.c(),l=M(),s=b("div"),o=M(),B(r.$$.fragment),c=M(),d=b("div"),m=b("div"),h=M(),P&&P.c(),_=M(),q&&q.c(),g=M(),y=b("button"),S=b("span"),S.textContent="Save changes",p(s,"class","clearfix m-b-base"),p(m,"class","flex-fill"),p(S,"class","txt"),p(y,"type","submit"),p(y,"class","btn btn-expanded"),y.disabled=T=!n[9]||n[5],x(y,"btn-loading",n[5]),p(d,"class","flex"),p(e,"class","block"),p(e,"autocomplete","off")},m(U,Y){w(U,e,Y),z(t,e,null),k(e,i),I&&I.m(e,null),k(e,l),k(e,s),k(e,o),z(r,e,null),k(e,c),k(e,d),k(d,m),k(d,h),P&&P.m(d,null),k(d,_),q&&q.m(d,null),k(d,g),k(d,y),k(y,S),C=!0,O||(D=[J(y,"click",n[28]),J(e,"submit",Be(n[11]))],O=!0)},p(U,Y){var pe,Ne,He;const ie={};Y[0]&4|Y[1]&3&&(ie.$$scope={dirty:Y,ctx:U}),t.$set(ie),U[2]?I?(I.p(U,Y),Y[0]&4&&E(I,1)):(I=bg(U),I.c(),E(I,1),I.m(e,l)):I&&(le(),A(I,1,1,()=>{I=null}),se());const te={};Y[0]&1&&(te.originalConfig=(pe=U[0].backups)==null?void 0:pe.s3),!a&&Y[0]&2&&(a=!0,te.config=U[1].backups.s3,ke(()=>a=!1)),!f&&Y[0]&128&&(f=!0,te.isTesting=U[7],ke(()=>f=!1)),!u&&Y[0]&256&&(u=!0,te.testError=U[8],ke(()=>u=!1)),r.$set(te),(He=(Ne=U[1].backups)==null?void 0:Ne.s3)!=null&&He.enabled&&!U[9]&&!U[5]?P?P.p(U,Y):(P=kg(U),P.c(),P.m(d,_)):P&&(P.d(1),P=null),U[9]?q?q.p(U,Y):(q=yg(U),q.c(),q.m(d,g)):q&&(q.d(1),q=null),(!C||Y[0]&544&&T!==(T=!U[9]||U[5]))&&(y.disabled=T),(!C||Y[0]&32)&&x(y,"btn-loading",U[5])},i(U){C||(E(t.$$.fragment,U),E(I),E(r.$$.fragment,U),U&&Ke(()=>{C&&($||($=Fe(e,et,{duration:150},!0)),$.run(1))}),C=!0)},o(U){A(t.$$.fragment,U),A(I),A(r.$$.fragment,U),U&&($||($=Fe(e,et,{duration:150},!1)),$.run(0)),C=!1},d(U){U&&v(e),V(t),I&&I.d(),V(r),P&&P.d(),q&&q.d(),U&&$&&$.end(),O=!1,$e(D)}}}function C7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("input"),i=M(),l=b("label"),s=K("Enable auto backups"),p(e,"type","checkbox"),p(e,"id",t=n[31]),e.required=!0,p(l,"for",o=n[31])},m(f,u){w(f,e,u),e.checked=n[2],w(f,i,u),w(f,l,u),k(l,s),r||(a=J(e,"change",n[17]),r=!0)},p(f,u){u[1]&1&&t!==(t=f[31])&&p(e,"id",t),u[0]&4&&(e.checked=f[2]),u[1]&1&&o!==(o=f[31])&&p(l,"for",o)},d(f){f&&(v(e),v(i),v(l)),r=!1,a()}}}function bg(n){let e,t,i,l,s,o,r,a,f;return l=new ce({props:{class:"form-field required",name:"backups.cron",$$slots:{default:[M7,({uniqueId:u})=>({31:u}),({uniqueId:u})=>[0,u?1:0]]},$$scope:{ctx:n}}}),r=new ce({props:{class:"form-field required",name:"backups.cronMaxKeep",$$slots:{default:[D7,({uniqueId:u})=>({31:u}),({uniqueId:u})=>[0,u?1:0]]},$$scope:{ctx:n}}}),{c(){e=b("div"),t=b("div"),i=b("div"),B(l.$$.fragment),s=M(),o=b("div"),B(r.$$.fragment),p(i,"class","col-lg-6"),p(o,"class","col-lg-6"),p(t,"class","grid p-t-base p-b-sm"),p(e,"class","block")},m(u,c){w(u,e,c),k(e,t),k(t,i),z(l,i,null),k(t,s),k(t,o),z(r,o,null),f=!0},p(u,c){const d={};c[0]&3|c[1]&3&&(d.$$scope={dirty:c,ctx:u}),l.$set(d);const m={};c[0]&2|c[1]&3&&(m.$$scope={dirty:c,ctx:u}),r.$set(m)},i(u){f||(E(l.$$.fragment,u),E(r.$$.fragment,u),u&&Ke(()=>{f&&(a||(a=Fe(e,et,{duration:150},!0)),a.run(1))}),f=!0)},o(u){A(l.$$.fragment,u),A(r.$$.fragment,u),u&&(a||(a=Fe(e,et,{duration:150},!1)),a.run(0)),f=!1},d(u){u&&v(e),V(l),V(r),u&&a&&a.end()}}}function O7(n){let e,t,i,l,s,o,r,a,f;return{c(){e=b("button"),e.innerHTML='Every day at 00:00h',t=M(),i=b("button"),i.innerHTML='Every sunday at 00:00h',l=M(),s=b("button"),s.innerHTML='Every Mon and Wed at 00:00h',o=M(),r=b("button"),r.innerHTML='Every first day of the month at 00:00h',p(e,"type","button"),p(e,"class","dropdown-item closable"),p(i,"type","button"),p(i,"class","dropdown-item closable"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(r,"type","button"),p(r,"class","dropdown-item closable")},m(u,c){w(u,e,c),w(u,t,c),w(u,i,c),w(u,l,c),w(u,s,c),w(u,o,c),w(u,r,c),a||(f=[J(e,"click",n[19]),J(i,"click",n[20]),J(s,"click",n[21]),J(r,"click",n[22])],a=!0)},p:Q,d(u){u&&(v(e),v(t),v(i),v(l),v(s),v(o),v(r)),a=!1,$e(f)}}}function M7(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I,L,R;return _=new On({props:{class:"dropdown dropdown-nowrap dropdown-right",$$slots:{default:[O7]},$$scope:{ctx:n}}}),{c(){var F,N;e=b("label"),t=K("Cron expression"),l=M(),s=b("input"),a=M(),f=b("div"),u=b("button"),c=b("span"),c.textContent="Presets",d=M(),m=b("i"),h=M(),B(_.$$.fragment),g=M(),y=b("div"),S=b("p"),T=K(`Supports numeric list, steps, ranges or - `),$=b("span"),$.textContent="macros",C=K(`. - `),O=b("br"),D=K(` - The timezone is in UTC.`),p(e,"for",i=n[31]),s.required=!0,p(s,"type","text"),p(s,"id",o=n[31]),p(s,"class","txt-lg txt-mono"),p(s,"placeholder","* * * * *"),s.autofocus=r=!((N=(F=n[0])==null?void 0:F.backups)!=null&&N.cron),p(c,"class","txt"),p(m,"class","ri-arrow-drop-down-fill"),p(u,"type","button"),p(u,"class","btn btn-sm btn-outline p-r-0"),p(f,"class","form-field-addon"),p($,"class","link-primary"),p(y,"class","help-block")},m(F,N){var P,q;w(F,e,N),k(e,t),w(F,l,N),w(F,s,N),re(s,n[1].backups.cron),w(F,a,N),w(F,f,N),k(f,u),k(u,c),k(u,d),k(u,m),k(u,h),z(_,u,null),w(F,g,N),w(F,y,N),k(y,S),k(S,T),k(S,$),k(S,C),k(S,O),k(S,D),I=!0,(q=(P=n[0])==null?void 0:P.backups)!=null&&q.cron||s.focus(),L||(R=[J(s,"input",n[18]),Se(Pe.call(null,$,`@yearly -@annually -@monthly -@weekly -@daily -@midnight -@hourly`))],L=!0)},p(F,N){var q,H;(!I||N[1]&1&&i!==(i=F[31]))&&p(e,"for",i),(!I||N[1]&1&&o!==(o=F[31]))&&p(s,"id",o),(!I||N[0]&1&&r!==(r=!((H=(q=F[0])==null?void 0:q.backups)!=null&&H.cron)))&&(s.autofocus=r),N[0]&2&&s.value!==F[1].backups.cron&&re(s,F[1].backups.cron);const P={};N[0]&2|N[1]&2&&(P.$$scope={dirty:N,ctx:F}),_.$set(P)},i(F){I||(E(_.$$.fragment,F),I=!0)},o(F){A(_.$$.fragment,F),I=!1},d(F){F&&(v(e),v(l),v(s),v(a),v(f),v(g),v(y)),V(_),L=!1,$e(R)}}}function D7(n){let e,t,i,l,s,o,r,a;return{c(){e=b("label"),t=K("Max @auto backups to keep"),l=M(),s=b("input"),p(e,"for",i=n[31]),p(s,"type","number"),p(s,"id",o=n[31]),p(s,"min","1")},m(f,u){w(f,e,u),k(e,t),w(f,l,u),w(f,s,u),re(s,n[1].backups.cronMaxKeep),r||(a=J(s,"input",n[23]),r=!0)},p(f,u){u[1]&1&&i!==(i=f[31])&&p(e,"for",i),u[1]&1&&o!==(o=f[31])&&p(s,"id",o),u[0]&2&&it(s.value)!==f[1].backups.cronMaxKeep&&re(s,f[1].backups.cronMaxKeep)},d(f){f&&(v(e),v(l),v(s)),r=!1,a()}}}function kg(n){let e;function t(s,o){return s[7]?A7:s[8]?I7:E7}let i=t(n),l=i(n);return{c(){l.c(),e=ye()},m(s,o){l.m(s,o),w(s,e,o)},p(s,o){i===(i=t(s))&&l?l.p(s,o):(l.d(1),l=i(s),l&&(l.c(),l.m(e.parentNode,e)))},d(s){s&&v(e),l.d(s)}}}function E7(n){let e;return{c(){e=b("div"),e.innerHTML=' S3 connected successfully',p(e,"class","label label-sm label-success entrance-right")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function I7(n){let e,t,i,l;return{c(){e=b("div"),e.innerHTML=' Failed to establish S3 connection',p(e,"class","label label-sm label-warning entrance-right")},m(s,o){var r;w(s,e,o),i||(l=Se(t=Pe.call(null,e,(r=n[8].data)==null?void 0:r.message)),i=!0)},p(s,o){var r;t&&Ct(t.update)&&o[0]&256&&t.update.call(null,(r=s[8].data)==null?void 0:r.message)},d(s){s&&v(e),i=!1,l()}}}function A7(n){let e;return{c(){e=b("span"),p(e,"class","loader loader-sm")},m(t,i){w(t,e,i)},p:Q,d(t){t&&v(e)}}}function yg(n){let e,t,i,l,s;return{c(){e=b("button"),t=b("span"),t.textContent="Reset",p(t,"class","txt"),p(e,"type","submit"),p(e,"class","btn btn-hint btn-transparent"),e.disabled=i=!n[9]||n[5]},m(o,r){w(o,e,r),k(e,t),l||(s=J(e,"click",n[27]),l=!0)},p(o,r){r[0]&544&&i!==(i=!o[9]||o[5])&&(e.disabled=i)},d(o){o&&v(e),l=!1,s()}}}function L7(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S,T,$,C,O,D,I,L,R,F;m=new Zo({props:{class:"btn-sm",tooltip:"Refresh"}}),m.$on("refresh",n[13]),_=new S7({props:{class:"btn-sm"}}),_.$on("success",n[13]);let N={};y=new y7({props:N}),n[15](y);function P(G,U){return G[6]?T7:$7}let q=P(n),H=q(n),W=n[6]&&!n[4]&&gg(n);return{c(){e=b("header"),t=b("nav"),i=b("div"),i.textContent="Settings",l=M(),s=b("div"),o=K(n[10]),r=M(),a=b("div"),f=b("div"),u=b("div"),c=b("span"),c.textContent="Backup and restore your PocketBase data",d=M(),B(m.$$.fragment),h=M(),B(_.$$.fragment),g=M(),B(y.$$.fragment),S=M(),T=b("hr"),$=M(),C=b("button"),O=b("span"),O.textContent="Backups options",D=M(),H.c(),I=M(),W&&W.c(),p(i,"class","breadcrumb-item"),p(s,"class","breadcrumb-item"),p(t,"class","breadcrumbs"),p(e,"class","page-header"),p(c,"class","txt-xl"),p(u,"class","flex m-b-sm flex-gap-10"),p(O,"class","txt"),p(C,"type","button"),p(C,"class","btn btn-secondary"),C.disabled=n[4],x(C,"btn-loading",n[4]),p(f,"class","panel"),p(f,"autocomplete","off"),p(a,"class","wrapper")},m(G,U){w(G,e,U),k(e,t),k(t,i),k(t,l),k(t,s),k(s,o),w(G,r,U),w(G,a,U),k(a,f),k(f,u),k(u,c),k(u,d),z(m,u,null),k(u,h),z(_,u,null),k(f,g),z(y,f,null),k(f,S),k(f,T),k(f,$),k(f,C),k(C,O),k(C,D),H.m(C,null),k(f,I),W&&W.m(f,null),L=!0,R||(F=[J(C,"click",n[16]),J(f,"submit",Be(n[11]))],R=!0)},p(G,U){(!L||U[0]&1024)&&oe(o,G[10]);const Y={};y.$set(Y),q!==(q=P(G))&&(H.d(1),H=q(G),H&&(H.c(),H.m(C,null))),(!L||U[0]&16)&&(C.disabled=G[4]),(!L||U[0]&16)&&x(C,"btn-loading",G[4]),G[6]&&!G[4]?W?(W.p(G,U),U[0]&80&&E(W,1)):(W=gg(G),W.c(),E(W,1),W.m(f,null)):W&&(le(),A(W,1,1,()=>{W=null}),se())},i(G){L||(E(m.$$.fragment,G),E(_.$$.fragment,G),E(y.$$.fragment,G),E(W),L=!0)},o(G){A(m.$$.fragment,G),A(_.$$.fragment,G),A(y.$$.fragment,G),A(W),L=!1},d(G){G&&(v(e),v(r),v(a)),V(m),V(_),n[15](null),V(y),H.d(),W&&W.d(),R=!1,$e(F)}}}function N7(n){let e,t,i,l;return e=new _i({}),i=new bn({props:{$$slots:{default:[L7]},$$scope:{ctx:n}}}),{c(){B(e.$$.fragment),t=M(),B(i.$$.fragment)},m(s,o){z(e,s,o),w(s,t,o),z(i,s,o),l=!0},p(s,o){const r={};o[0]&2047|o[1]&2&&(r.$$scope={dirty:o,ctx:s}),i.$set(r)},i(s){l||(E(e.$$.fragment,s),E(i.$$.fragment,s),l=!0)},o(s){A(e.$$.fragment,s),A(i.$$.fragment,s),l=!1},d(s){s&&v(t),V(e,s),V(i,s)}}}function P7(n,e,t){let i,l;Ue(n,It,U=>t(10,l=U)),xt(It,l="Backups",l);let s,o={},r={},a=!1,f=!1,u="",c=!1,d=!1,m=!1,h=null;_();async function _(){t(4,a=!0);try{const U=await ae.settings.getAll()||{};y(U)}catch(U){ae.error(U)}t(4,a=!1)}async function g(){if(!(f||!i)){t(5,f=!0);try{const U=await ae.settings.update(j.filterRedactedProps(r));await T(),y(U),Lt("Successfully saved application settings.")}catch(U){ae.error(U)}t(5,f=!1)}}function y(U={}){t(1,r={backups:(U==null?void 0:U.backups)||{}}),t(2,c=r.backups.cron!=""),t(0,o=JSON.parse(JSON.stringify(r)))}function S(){t(1,r=JSON.parse(JSON.stringify(o||{backups:{}}))),t(2,c=r.backups.cron!="")}async function T(){return s==null?void 0:s.loadBackups()}function $(U){ee[U?"unshift":"push"](()=>{s=U,t(3,s)})}const C=()=>t(6,d=!d);function O(){c=this.checked,t(2,c)}function D(){r.backups.cron=this.value,t(1,r),t(2,c)}const I=()=>{t(1,r.backups.cron="0 0 * * *",r)},L=()=>{t(1,r.backups.cron="0 0 * * 0",r)},R=()=>{t(1,r.backups.cron="0 0 * * 1,3",r)},F=()=>{t(1,r.backups.cron="0 0 1 * *",r)};function N(){r.backups.cronMaxKeep=it(this.value),t(1,r),t(2,c)}function P(U){n.$$.not_equal(r.backups.s3,U)&&(r.backups.s3=U,t(1,r),t(2,c))}function q(U){m=U,t(7,m)}function H(U){h=U,t(8,h)}const W=()=>S(),G=()=>g();return n.$$.update=()=>{var U;n.$$.dirty[0]&1&&t(14,u=JSON.stringify(o)),n.$$.dirty[0]&6&&!c&&(U=r==null?void 0:r.backups)!=null&&U.cron&&(li("backups.cron"),t(1,r.backups.cron="",r)),n.$$.dirty[0]&16386&&t(9,i=u!=JSON.stringify(r))},[o,r,c,s,a,f,d,m,h,i,l,g,S,T,u,$,C,O,D,I,L,R,F,N,P,q,H,W,G]}class F7 extends ge{constructor(e){super(),_e(this,e,P7,N7,me,{},null,[-1,-1])}}const Pt=[async n=>{const e=new URLSearchParams(window.location.search);return n.location!=="/"&&e.has("installer")?tl("/"):!0}],R7={"/login":At({component:FE,conditions:Pt.concat([n=>!ae.authStore.isValid]),userData:{showAppSidebar:!1}}),"/request-password-reset":At({asyncComponent:()=>tt(()=>import("./PageAdminRequestPasswordReset-ByYh3pEr.js"),[],import.meta.url),conditions:Pt.concat([n=>!ae.authStore.isValid]),userData:{showAppSidebar:!1}}),"/confirm-password-reset/:token":At({asyncComponent:()=>tt(()=>import("./PageAdminConfirmPasswordReset-C3mHsOYN.js"),[],import.meta.url),conditions:Pt.concat([n=>!ae.authStore.isValid]),userData:{showAppSidebar:!1}}),"/collections":At({component:iE,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/logs":At({component:n$,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings":At({component:WE,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/admins":At({component:EE,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/mail":At({component:AI,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/storage":At({component:xI,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/auth-providers":At({component:_A,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/tokens":At({component:TA,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/export-collections":At({component:LA,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/import-collections":At({component:xA,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/settings/backups":At({component:F7,conditions:Pt.concat([n=>ae.authStore.isValid]),userData:{showAppSidebar:!0}}),"/users/confirm-password-reset/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmPasswordReset-DiHRjH5i.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/auth/confirm-password-reset/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmPasswordReset-DiHRjH5i.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/users/confirm-verification/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmVerification-BEu14spu.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/auth/confirm-verification/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmVerification-BEu14spu.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/users/confirm-email-change/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmEmailChange-GhjOHgK4.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/auth/confirm-email-change/:token":At({asyncComponent:()=>tt(()=>import("./PageRecordConfirmEmailChange-GhjOHgK4.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/auth/oauth2-redirect-success":At({asyncComponent:()=>tt(()=>import("./PageOAuth2RedirectSuccess-DEJwxR01.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"/auth/oauth2-redirect-failure":At({asyncComponent:()=>tt(()=>import("./PageOAuth2RedirectFailure-C67m4ehZ.js"),[],import.meta.url),conditions:Pt,userData:{showAppSidebar:!1}}),"*":At({component:Mv,userData:{showAppSidebar:!1}})};function q7(n,{from:e,to:t},i={}){const l=getComputedStyle(n),s=l.transform==="none"?"":l.transform,[o,r]=l.transformOrigin.split(" ").map(parseFloat),a=e.left+e.width*o/t.width-(t.left+o),f=e.top+e.height*r/t.height-(t.top+r),{delay:u=0,duration:c=m=>Math.sqrt(m)*120,easing:d=Jo}=i;return{delay:u,duration:Ct(c)?c(Math.sqrt(a*a+f*f)):c,easing:d,css:(m,h)=>{const _=h*a,g=h*f,y=m+h*e.width/t.width,S=m+h*e.height/t.height;return`transform: ${s} translate(${_}px, ${g}px) scale(${y}, ${S});`}}}function vg(n,e,t){const i=n.slice();return i[2]=e[t],i}function j7(n){let e;return{c(){e=b("i"),p(e,"class","ri-alert-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function H7(n){let e;return{c(){e=b("i"),p(e,"class","ri-error-warning-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function z7(n){let e;return{c(){e=b("i"),p(e,"class","ri-checkbox-circle-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function V7(n){let e;return{c(){e=b("i"),p(e,"class","ri-information-line")},m(t,i){w(t,e,i)},d(t){t&&v(e)}}}function wg(n,e){let t,i,l,s,o=e[2].message+"",r,a,f,u,c,d,m,h=Q,_,g,y;function S(O,D){return O[2].type==="info"?V7:O[2].type==="success"?z7:O[2].type==="warning"?H7:j7}let T=S(e),$=T(e);function C(){return e[1](e[2])}return{key:n,first:null,c(){t=b("div"),i=b("div"),$.c(),l=M(),s=b("div"),r=K(o),a=M(),f=b("button"),f.innerHTML='',u=M(),p(i,"class","icon"),p(s,"class","content"),p(f,"type","button"),p(f,"class","close"),p(t,"class","alert txt-break"),x(t,"alert-info",e[2].type=="info"),x(t,"alert-success",e[2].type=="success"),x(t,"alert-danger",e[2].type=="error"),x(t,"alert-warning",e[2].type=="warning"),this.first=t},m(O,D){w(O,t,D),k(t,i),$.m(i,null),k(t,l),k(t,s),k(s,r),k(t,a),k(t,f),k(t,u),_=!0,g||(y=J(f,"click",Be(C)),g=!0)},p(O,D){e=O,T!==(T=S(e))&&($.d(1),$=T(e),$&&($.c(),$.m(i,null))),(!_||D&1)&&o!==(o=e[2].message+"")&&oe(r,o),(!_||D&1)&&x(t,"alert-info",e[2].type=="info"),(!_||D&1)&&x(t,"alert-success",e[2].type=="success"),(!_||D&1)&&x(t,"alert-danger",e[2].type=="error"),(!_||D&1)&&x(t,"alert-warning",e[2].type=="warning")},r(){m=t.getBoundingClientRect()},f(){S0(t),h(),Ag(t,m)},a(){h(),h=w0(t,m,q7,{duration:150})},i(O){_||(O&&Ke(()=>{_&&(d&&d.end(1),c=Pg(t,et,{duration:150}),c.start())}),_=!0)},o(O){c&&c.invalidate(),O&&(d=ca(t,rs,{duration:150})),_=!1},d(O){O&&v(t),$.d(),O&&d&&d.end(),g=!1,y()}}}function B7(n){let e,t=[],i=new Map,l,s=ue(n[0]);const o=r=>r[2].message;for(let r=0;rt(0,i=s)),[i,s=>W1(s)]}class W7 extends ge{constructor(e){super(),_e(this,e,U7,B7,me,{})}}function Y7(n){var l;let e,t=((l=n[1])==null?void 0:l.text)+"",i;return{c(){e=b("h4"),i=K(t),p(e,"class","block center txt-break"),p(e,"slot","header")},m(s,o){w(s,e,o),k(e,i)},p(s,o){var r;o&2&&t!==(t=((r=s[1])==null?void 0:r.text)+"")&&oe(i,t)},d(s){s&&v(e)}}}function K7(n){let e,t,i,l,s,o,r;return{c(){e=b("button"),t=b("span"),t.textContent="No",i=M(),l=b("button"),s=b("span"),s.textContent="Yes",p(t,"class","txt"),e.autofocus=!0,p(e,"type","button"),p(e,"class","btn btn-transparent btn-expanded-sm"),e.disabled=n[2],p(s,"class","txt"),p(l,"type","button"),p(l,"class","btn btn-danger btn-expanded"),l.disabled=n[2],x(l,"btn-loading",n[2])},m(a,f){w(a,e,f),k(e,t),w(a,i,f),w(a,l,f),k(l,s),e.focus(),o||(r=[J(e,"click",n[4]),J(l,"click",n[5])],o=!0)},p(a,f){f&4&&(e.disabled=a[2]),f&4&&(l.disabled=a[2]),f&4&&x(l,"btn-loading",a[2])},d(a){a&&(v(e),v(i),v(l)),o=!1,$e(r)}}}function J7(n){let e,t,i={class:"confirm-popup hide-content overlay-panel-sm",overlayClose:!n[2],escClose:!n[2],btnClose:!1,popup:!0,$$slots:{footer:[K7],header:[Y7]},$$scope:{ctx:n}};return e=new Zt({props:i}),n[6](e),e.$on("hide",n[7]),{c(){B(e.$$.fragment)},m(l,s){z(e,l,s),t=!0},p(l,[s]){const o={};s&4&&(o.overlayClose=!l[2]),s&4&&(o.escClose=!l[2]),s&271&&(o.$$scope={dirty:s,ctx:l}),e.$set(o)},i(l){t||(E(e.$$.fragment,l),t=!0)},o(l){A(e.$$.fragment,l),t=!1},d(l){n[6](null),V(e,l)}}}function Z7(n,e,t){let i;Ue(n,za,c=>t(1,i=c));let l,s=!1,o=!1;const r=()=>{t(3,o=!1),l==null||l.hide()},a=async()=>{i!=null&&i.yesCallback&&(t(2,s=!0),await Promise.resolve(i.yesCallback()),t(2,s=!1)),t(3,o=!0),l==null||l.hide()};function f(c){ee[c?"unshift":"push"](()=>{l=c,t(0,l)})}const u=async()=>{!o&&(i!=null&&i.noCallback)&&i.noCallback(),await Qt(),t(3,o=!1),zb()};return n.$$.update=()=>{n.$$.dirty&3&&i!=null&&i.text&&(t(3,o=!1),l==null||l.show())},[l,i,s,o,r,a,f,u]}class G7 extends ge{constructor(e){super(),_e(this,e,Z7,J7,me,{})}}function Sg(n){let e,t,i,l,s,o,r,a,f,u,c,d,m,h,_,g,y,S;return _=new On({props:{class:"dropdown dropdown-nowrap dropdown-upside dropdown-left",$$slots:{default:[X7]},$$scope:{ctx:n}}}),{c(){var T;e=b("aside"),t=b("a"),t.innerHTML='PocketBase logo',i=M(),l=b("nav"),s=b("a"),s.innerHTML='',o=M(),r=b("a"),r.innerHTML='',a=M(),f=b("a"),f.innerHTML='',u=M(),c=b("div"),d=b("img"),h=M(),B(_.$$.fragment),p(t,"href","/"),p(t,"class","logo logo-sm"),p(s,"href","/collections"),p(s,"class","menu-item"),p(s,"aria-label","Collections"),p(r,"href","/logs"),p(r,"class","menu-item"),p(r,"aria-label","Logs"),p(f,"href","/settings"),p(f,"class","menu-item"),p(f,"aria-label","Settings"),p(l,"class","main-menu"),en(d.src,m="./images/avatars/avatar"+(((T=n[0])==null?void 0:T.avatar)||0)+".svg")||p(d,"src",m),p(d,"alt","Avatar"),p(d,"aria-hidden","true"),p(c,"tabindex","0"),p(c,"role","button"),p(c,"aria-label","Logged admin menu"),p(c,"class","thumb thumb-circle link-hint closable"),p(e,"class","app-sidebar")},m(T,$){w(T,e,$),k(e,t),k(e,i),k(e,l),k(l,s),k(l,o),k(l,r),k(l,a),k(l,f),k(e,u),k(e,c),k(c,d),k(c,h),z(_,c,null),g=!0,y||(S=[Se(nn.call(null,t)),Se(nn.call(null,s)),Se(Ln.call(null,s,{path:"/collections/?.*",className:"current-route"})),Se(Pe.call(null,s,{text:"Collections",position:"right"})),Se(nn.call(null,r)),Se(Ln.call(null,r,{path:"/logs/?.*",className:"current-route"})),Se(Pe.call(null,r,{text:"Logs",position:"right"})),Se(nn.call(null,f)),Se(Ln.call(null,f,{path:"/settings/?.*",className:"current-route"})),Se(Pe.call(null,f,{text:"Settings",position:"right"}))],y=!0)},p(T,$){var O;(!g||$&1&&!en(d.src,m="./images/avatars/avatar"+(((O=T[0])==null?void 0:O.avatar)||0)+".svg"))&&p(d,"src",m);const C={};$&4096&&(C.$$scope={dirty:$,ctx:T}),_.$set(C)},i(T){g||(E(_.$$.fragment,T),g=!0)},o(T){A(_.$$.fragment,T),g=!1},d(T){T&&v(e),V(_),y=!1,$e(S)}}}function X7(n){let e,t,i,l,s,o,r;return{c(){e=b("a"),e.innerHTML=' Manage admins',t=M(),i=b("hr"),l=M(),s=b("button"),s.innerHTML=' Logout',p(e,"href","/settings/admins"),p(e,"class","dropdown-item closable"),p(e,"role","menuitem"),p(s,"type","button"),p(s,"class","dropdown-item closable"),p(s,"role","menuitem")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),w(a,l,f),w(a,s,f),o||(r=[Se(nn.call(null,e)),J(s,"click",n[7])],o=!0)},p:Q,d(a){a&&(v(e),v(t),v(i),v(l),v(s)),o=!1,$e(r)}}}function $g(n){let e,t,i;return t=new Wa({props:{conf:j.defaultEditorOptions()}}),t.$on("init",n[8]),{c(){e=b("div"),B(t.$$.fragment),p(e,"class","tinymce-preloader hidden")},m(l,s){w(l,e,s),z(t,e,null),i=!0},p:Q,i(l){i||(E(t.$$.fragment,l),i=!0)},o(l){A(t.$$.fragment,l),i=!1},d(l){l&&v(e),V(t)}}}function Q7(n){var g;let e,t,i,l,s,o,r,a,f,u,c,d,m;document.title=e=j.joinNonEmpty([n[4],n[3],"PocketBase"]," - ");let h=((g=n[0])==null?void 0:g.id)&&n[1]&&Sg(n);o=new R0({props:{routes:R7}}),o.$on("routeLoading",n[5]),o.$on("conditionsFailed",n[6]),a=new W7({}),u=new G7({});let _=n[1]&&!n[2]&&$g(n);return{c(){t=M(),i=b("div"),h&&h.c(),l=M(),s=b("div"),B(o.$$.fragment),r=M(),B(a.$$.fragment),f=M(),B(u.$$.fragment),c=M(),_&&_.c(),d=ye(),p(s,"class","app-body"),p(i,"class","app-layout")},m(y,S){w(y,t,S),w(y,i,S),h&&h.m(i,null),k(i,l),k(i,s),z(o,s,null),k(s,r),z(a,s,null),w(y,f,S),z(u,y,S),w(y,c,S),_&&_.m(y,S),w(y,d,S),m=!0},p(y,[S]){var T;(!m||S&24)&&e!==(e=j.joinNonEmpty([y[4],y[3],"PocketBase"]," - "))&&(document.title=e),(T=y[0])!=null&&T.id&&y[1]?h?(h.p(y,S),S&3&&E(h,1)):(h=Sg(y),h.c(),E(h,1),h.m(i,l)):h&&(le(),A(h,1,1,()=>{h=null}),se()),y[1]&&!y[2]?_?(_.p(y,S),S&6&&E(_,1)):(_=$g(y),_.c(),E(_,1),_.m(d.parentNode,d)):_&&(le(),A(_,1,1,()=>{_=null}),se())},i(y){m||(E(h),E(o.$$.fragment,y),E(a.$$.fragment,y),E(u.$$.fragment,y),E(_),m=!0)},o(y){A(h),A(o.$$.fragment,y),A(a.$$.fragment,y),A(u.$$.fragment,y),A(_),m=!1},d(y){y&&(v(t),v(i),v(f),v(c),v(d)),h&&h.d(),V(o),V(a),V(u,y),_&&_.d(y)}}}function x7(n,e,t){let i,l,s,o;Ue(n,Xi,_=>t(10,i=_)),Ue(n,Oo,_=>t(3,l=_)),Ue(n,$a,_=>t(0,s=_)),Ue(n,It,_=>t(4,o=_));let r,a=!1,f=!1;function u(_){var g,y,S,T;((g=_==null?void 0:_.detail)==null?void 0:g.location)!==r&&(t(1,a=!!((S=(y=_==null?void 0:_.detail)==null?void 0:y.userData)!=null&&S.showAppSidebar)),r=(T=_==null?void 0:_.detail)==null?void 0:T.location,xt(It,o="",o),Jt({}),zb())}function c(){tl("/")}async function d(){var _,g;if(s!=null&&s.id)try{const y=await ae.settings.getAll({$cancelKey:"initialAppSettings"});xt(Oo,l=((_=y==null?void 0:y.meta)==null?void 0:_.appName)||"",l),xt(Xi,i=!!((g=y==null?void 0:y.meta)!=null&&g.hideControls),i)}catch(y){y!=null&&y.isAbort||console.warn("Failed to load app settings.",y)}}function m(){ae.logout()}const h=()=>{t(2,f=!0)};return n.$$.update=()=>{n.$$.dirty&1&&s!=null&&s.id&&d()},[s,a,f,l,o,u,c,m,h]}class eL extends ge{constructor(e){super(),_e(this,e,x7,Q7,me,{})}}new eL({target:document.getElementById("app")});export{ae as A,Lt as B,j as C,tl as D,ye as E,Z1 as F,Ho as G,so as H,Ht as I,Ue as J,Rn as K,lt as L,ee as M,Rb as N,ue as O,at as P,Ii as Q,Et as R,ge as S,ot as T,b0 as U,A as a,M as b,B as c,V as d,b as e,p as f,w as g,k as h,_e as i,Se as j,le as k,nn as l,z as m,se as n,v as o,ce as p,x as q,J as r,me as s,E as t,Be as u,K as v,oe as w,Q as x,re as y,$e as z}; diff --git a/ui/dist/assets/index-BztyTJOx.js b/ui/dist/assets/index-BztyTJOx.js deleted file mode 100644 index c5d3b074..00000000 --- a/ui/dist/assets/index-BztyTJOx.js +++ /dev/null @@ -1,14 +0,0 @@ -class V{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,e,i){[t,e]=Ue(this,t,e);let n=[];return this.decompose(0,t,n,2),i.length&&i.decompose(0,i.length,n,3),this.decompose(e,this.length,n,1),Gt.from(n,this.length-(e-t)+i.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,e=this.length){[t,e]=Ue(this,t,e);let i=[];return this.decompose(t,e,i,0),Gt.from(i,e-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let e=this.scanIdentical(t,1),i=this.length-this.scanIdentical(t,-1),n=new gi(this),r=new gi(t);for(let o=e,l=e;;){if(n.next(o),r.next(o),o=0,n.lineBreak!=r.lineBreak||n.done!=r.done||n.value!=r.value)return!1;if(l+=n.value.length,n.done||l>=i)return!0}}iter(t=1){return new gi(this,t)}iterRange(t,e=this.length){return new Sl(this,t,e)}iterLines(t,e){let i;if(t==null)i=this.iter();else{e==null&&(e=this.lines+1);let n=this.line(t).from;i=this.iterRange(n,Math.max(n,e==this.lines+1?this.length:e<=1?0:this.line(e-1).to))}return new vl(i)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(t.length==0)throw new RangeError("A document must have at least one line");return t.length==1&&!t[0]?V.empty:t.length<=32?new _(t):Gt.from(_.split(t,[]))}}class _ extends V{constructor(t,e=mc(t)){super(),this.text=t,this.length=e}get lines(){return this.text.length}get children(){return null}lineInner(t,e,i,n){for(let r=0;;r++){let o=this.text[r],l=n+o.length;if((e?i:l)>=t)return new yc(n,l,i,o);n=l+1,i++}}decompose(t,e,i,n){let r=t<=0&&e>=this.length?this:new _(Vr(this.text,t,e),Math.min(e,this.length)-Math.max(0,t));if(n&1){let o=i.pop(),l=on(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new _(l,o.length+r.length));else{let a=l.length>>1;i.push(new _(l.slice(0,a)),new _(l.slice(a)))}}else i.push(r)}replace(t,e,i){if(!(i instanceof _))return super.replace(t,e,i);[t,e]=Ue(this,t,e);let n=on(this.text,on(i.text,Vr(this.text,0,t)),e),r=this.length+i.length-(e-t);return n.length<=32?new _(n,r):Gt.from(_.split(n,[]),r)}sliceString(t,e=this.length,i=` -`){[t,e]=Ue(this,t,e);let n="";for(let r=0,o=0;r<=e&&ot&&o&&(n+=i),tr&&(n+=l.slice(Math.max(0,t-r),e-r)),r=a+1}return n}flatten(t){for(let e of this.text)t.push(e)}scanIdentical(){return 0}static split(t,e){let i=[],n=-1;for(let r of t)i.push(r),n+=r.length+1,i.length==32&&(e.push(new _(i,n)),i=[],n=-1);return n>-1&&e.push(new _(i,n)),e}}class Gt extends V{constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;for(let i of t)this.lines+=i.lines}lineInner(t,e,i,n){for(let r=0;;r++){let o=this.children[r],l=n+o.length,a=i+o.lines-1;if((e?a:l)>=t)return o.lineInner(t,e,i,n);n=l+1,i=a+1}}decompose(t,e,i,n){for(let r=0,o=0;o<=e&&r=o){let c=n&((o<=t?1:0)|(a>=e?2:0));o>=t&&a<=e&&!c?i.push(l):l.decompose(t-o,e-o,i,c)}o=a+1}}replace(t,e,i){if([t,e]=Ue(this,t,e),i.lines=r&&e<=l){let a=o.replace(t-r,e-r,i),c=this.lines-o.lines+a.lines;if(a.lines>4&&a.lines>c>>6){let h=this.children.slice();return h[n]=a,new Gt(h,this.length-(e-t)+i.length)}return super.replace(r,l,a)}r=l+1}return super.replace(t,e,i)}sliceString(t,e=this.length,i=` -`){[t,e]=Ue(this,t,e);let n="";for(let r=0,o=0;rt&&r&&(n+=i),to&&(n+=l.sliceString(t-o,e-o,i)),o=a+1}return n}flatten(t){for(let e of this.children)e.flatten(t)}scanIdentical(t,e){if(!(t instanceof Gt))return 0;let i=0,[n,r,o,l]=e>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;n+=e,r+=e){if(n==o||r==l)return i;let a=this.children[n],c=t.children[r];if(a!=c)return i+a.scanIdentical(c,e);i+=a.length+1}}static from(t,e=t.reduce((i,n)=>i+n.length+1,-1)){let i=0;for(let d of t)i+=d.lines;if(i<32){let d=[];for(let p of t)p.flatten(d);return new _(d,e)}let n=Math.max(32,i>>5),r=n<<1,o=n>>1,l=[],a=0,c=-1,h=[];function f(d){let p;if(d.lines>r&&d instanceof Gt)for(let g of d.children)f(g);else d.lines>o&&(a>o||!a)?(u(),l.push(d)):d instanceof _&&a&&(p=h[h.length-1])instanceof _&&d.lines+p.lines<=32?(a+=d.lines,c+=d.length+1,h[h.length-1]=new _(p.text.concat(d.text),p.length+1+d.length)):(a+d.lines>n&&u(),a+=d.lines,c+=d.length+1,h.push(d))}function u(){a!=0&&(l.push(h.length==1?h[0]:Gt.from(h,c)),c=-1,a=h.length=0)}for(let d of t)f(d);return u(),l.length==1?l[0]:new Gt(l,e)}}V.empty=new _([""],0);function mc(s){let t=-1;for(let e of s)t+=e.length+1;return t}function on(s,t,e=0,i=1e9){for(let n=0,r=0,o=!0;r=e&&(a>i&&(l=l.slice(0,i-n)),n0?1:(t instanceof _?t.text.length:t.children.length)<<1]}nextInner(t,e){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,n=this.nodes[i],r=this.offsets[i],o=r>>1,l=n instanceof _?n.text.length:n.children.length;if(o==(e>0?l:0)){if(i==0)return this.done=!0,this.value="",this;e>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(e>0?0:1)){if(this.offsets[i]+=e,t==0)return this.lineBreak=!0,this.value=` -`,this;t--}else if(n instanceof _){let a=n.text[o+(e<0?-1:0)];if(this.offsets[i]+=e,a.length>Math.max(0,t))return this.value=t==0?a:e>0?a.slice(t):a.slice(0,a.length-t),this;t-=a.length}else{let a=n.children[o+(e<0?-1:0)];t>a.length?(t-=a.length,this.offsets[i]+=e):(e<0&&this.offsets[i]--,this.nodes.push(a),this.offsets.push(e>0?1:(a instanceof _?a.text.length:a.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class Sl{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new gi(t,e>i?-1:1),this.pos=e>i?t.length:0,this.from=Math.min(e,i),this.to=Math.max(e,i)}nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,e<0?this.pos-this.to:this.from-this.pos);let i=e<0?this.pos-this.from:this.to-this.pos;t>i&&(t=i),i-=t;let{value:n}=this.cursor.next(t);return this.pos+=(n.length+t)*e,this.value=n.length<=i?n:e<0?n.slice(n.length-i):n.slice(0,i),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class vl{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e&&this.afterBreak?(this.value="",this.afterBreak=!1):e?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=n,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(V.prototype[Symbol.iterator]=function(){return this.iter()},gi.prototype[Symbol.iterator]=Sl.prototype[Symbol.iterator]=vl.prototype[Symbol.iterator]=function(){return this});class yc{constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}get length(){return this.to-this.from}}function Ue(s,t,e){return t=Math.max(0,Math.min(s.length,t)),[t,Math.max(t,Math.min(s.length,e))]}let He="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(s=>s?parseInt(s,36):1);for(let s=1;ss)return He[t-1]<=s;return!1}function Wr(s){return s>=127462&&s<=127487}const Hr=8205;function ot(s,t,e=!0,i=!0){return(e?kl:xc)(s,t,i)}function kl(s,t,e){if(t==s.length)return t;t&&Cl(s.charCodeAt(t))&&Al(s.charCodeAt(t-1))&&t--;let i=nt(s,t);for(t+=Bt(i);t=0&&Wr(nt(s,o));)r++,o-=2;if(r%2==0)break;t+=2}else break}return t}function xc(s,t,e){for(;t>0;){let i=kl(s,t-2,e);if(i=56320&&s<57344}function Al(s){return s>=55296&&s<56320}function nt(s,t){let e=s.charCodeAt(t);if(!Al(e)||t+1==s.length)return e;let i=s.charCodeAt(t+1);return Cl(i)?(e-55296<<10)+(i-56320)+65536:e}function or(s){return s<=65535?String.fromCharCode(s):(s-=65536,String.fromCharCode((s>>10)+55296,(s&1023)+56320))}function Bt(s){return s<65536?1:2}const us=/\r\n?|\n/;var ht=function(s){return s[s.Simple=0]="Simple",s[s.TrackDel=1]="TrackDel",s[s.TrackBefore=2]="TrackBefore",s[s.TrackAfter=3]="TrackAfter",s}(ht||(ht={}));class Qt{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;et)return r+(t-n);r+=l}else{if(i!=ht.Simple&&c>=t&&(i==ht.TrackDel&&nt||i==ht.TrackBefore&&nt))return null;if(c>t||c==t&&e<0&&!l)return t==n||e<0?r:r+a;r+=a}n=c}if(t>n)throw new RangeError(`Position ${t} is out of range for changeset of length ${n}`);return r}touchesRange(t,e=t){for(let i=0,n=0;i=0&&n<=e&&l>=t)return ne?"cover":!0;n=l}return!1}toString(){let t="";for(let e=0;e=0?":"+n:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(e=>typeof e!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new Qt(t)}static create(t){return new Qt(t)}}class et extends Qt{constructor(t,e){super(t),this.inserted=e}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return ds(this,(e,i,n,r,o)=>t=t.replace(n,n+(i-e),o),!1),t}mapDesc(t,e=!1){return ps(this,t,e,!0)}invert(t){let e=this.sections.slice(),i=[];for(let n=0,r=0;n=0){e[n]=l,e[n+1]=o;let a=n>>1;for(;i.length0&&he(i,e,r.text),r.forward(h),l+=h}let c=t[o++];for(;l>1].toJSON()))}return t}static of(t,e,i){let n=[],r=[],o=0,l=null;function a(h=!1){if(!h&&!n.length)return;ou||f<0||u>e)throw new RangeError(`Invalid change range ${f} to ${u} (in doc of length ${e})`);let p=d?typeof d=="string"?V.of(d.split(i||us)):d:V.empty,g=p.length;if(f==u&&g==0)return;fo&&at(n,f-o,-1),at(n,u-f,g),he(r,n,p),o=u}}return c(t),a(!l),l}static empty(t){return new et(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let e=[],i=[];for(let n=0;nl&&typeof o!="string"))throw new RangeError("Invalid JSON representation of ChangeSet");if(r.length==1)e.push(r[0],0);else{for(;i.length=0&&e<=0&&e==s[n+1]?s[n]+=t:t==0&&s[n]==0?s[n+1]+=e:i?(s[n]+=t,s[n+1]+=e):s.push(t,e)}function he(s,t,e){if(e.length==0)return;let i=t.length-2>>1;if(i>1])),!(e||o==s.sections.length||s.sections[o+1]<0);)l=s.sections[o++],a=s.sections[o++];t(n,c,r,h,f),n=c,r=h}}}function ps(s,t,e,i=!1){let n=[],r=i?[]:null,o=new xi(s),l=new xi(t);for(let a=-1;;)if(o.ins==-1&&l.ins==-1){let c=Math.min(o.len,l.len);at(n,c,-1),o.forward(c),l.forward(c)}else if(l.ins>=0&&(o.ins<0||a==o.i||o.off==0&&(l.len=0&&a=0){let c=0,h=o.len;for(;h;)if(l.ins==-1){let f=Math.min(h,l.len);c+=f,h-=f,l.forward(f)}else if(l.ins==0&&l.lena||o.ins>=0&&o.len>a)&&(l||i.length>c),r.forward2(a),o.forward(a)}}}}class xi{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i>1;return e>=t.length?V.empty:t[e]}textBit(t){let{inserted:e}=this.set,i=this.i-2>>1;return i>=e.length&&!t?V.empty:e[i].slice(this.off,t==null?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){this.ins==-1?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class ve{constructor(t,e,i){this.from=t,this.to=e,this.flags=i}get anchor(){return this.flags&32?this.to:this.from}get head(){return this.flags&32?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return this.flags&8?-1:this.flags&16?1:0}get bidiLevel(){let t=this.flags&7;return t==7?null:t}get goalColumn(){let t=this.flags>>6;return t==16777215?void 0:t}map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.mapPos(this.from,1),n=t.mapPos(this.to,-1)),i==this.from&&n==this.to?this:new ve(i,n,this.flags)}extend(t,e=t){if(t<=this.anchor&&e>=this.anchor)return b.range(t,e);let i=Math.abs(t-this.anchor)>Math.abs(e-this.anchor)?t:e;return b.range(this.anchor,i)}eq(t,e=!1){return this.anchor==t.anchor&&this.head==t.head&&(!e||!this.empty||this.assoc==t.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||typeof t.anchor!="number"||typeof t.head!="number")throw new RangeError("Invalid JSON representation for SelectionRange");return b.range(t.anchor,t.head)}static create(t,e,i){return new ve(t,e,i)}}class b{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){return t.empty?this:b.create(this.ranges.map(i=>i.map(t,e)),this.mainIndex)}eq(t,e=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let i=0;it.toJSON()),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||typeof t.main!="number"||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new b(t.ranges.map(e=>ve.fromJSON(e)),t.main)}static single(t,e=t){return new b([b.range(t,e)],0)}static create(t,e=0){if(t.length==0)throw new RangeError("A selection needs at least one range");for(let i=0,n=0;nt?8:0)|r)}static normalized(t,e=0){let i=t[e];t.sort((n,r)=>n.from-r.from),e=t.indexOf(i);for(let n=1;nr.head?b.range(a,l):b.range(l,a))}}return new b(t,e)}}function Dl(s,t){for(let e of s.ranges)if(e.to>t)throw new RangeError("Selection points outside of document")}let lr=0;class T{constructor(t,e,i,n,r){this.combine=t,this.compareInput=e,this.compare=i,this.isStatic=n,this.id=lr++,this.default=t([]),this.extensions=typeof r=="function"?r(this):r}get reader(){return this}static define(t={}){return new T(t.combine||(e=>e),t.compareInput||((e,i)=>e===i),t.compare||(t.combine?(e,i)=>e===i:ar),!!t.static,t.enables)}of(t){return new ln([],this,0,t)}compute(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new ln(t,this,1,e)}computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new ln(t,this,2,e)}from(t,e){return e||(e=i=>i),this.compute([t],i=>e(i.field(t)))}}function ar(s,t){return s==t||s.length==t.length&&s.every((e,i)=>e===t[i])}class ln{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this.value=n,this.id=lr++}dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,r=this.id,o=t[r]>>1,l=this.type==2,a=!1,c=!1,h=[];for(let f of this.dependencies)f=="doc"?a=!0:f=="selection"?c=!0:((e=t[f.id])!==null&&e!==void 0?e:1)&1||h.push(t[f.id]);return{create(f){return f.values[o]=i(f),1},update(f,u){if(a&&u.docChanged||c&&(u.docChanged||u.selection)||gs(f,h)){let d=i(f);if(l?!zr(d,f.values[o],n):!n(d,f.values[o]))return f.values[o]=d,1}return 0},reconfigure:(f,u)=>{let d,p=u.config.address[r];if(p!=null){let g=gn(u,p);if(this.dependencies.every(m=>m instanceof T?u.facet(m)===f.facet(m):m instanceof yt?u.field(m,!1)==f.field(m,!1):!0)||(l?zr(d=i(f),g,n):n(d=i(f),g)))return f.values[o]=g,0}else d=i(f);return f.values[o]=d,1}}}}function zr(s,t,e){if(s.length!=t.length)return!1;for(let i=0;is[a.id]),n=e.map(a=>a.type),r=i.filter(a=>!(a&1)),o=s[t.id]>>1;function l(a){let c=[];for(let h=0;hi===n),t);return t.provide&&(e.provides=t.provide(e)),e}create(t){let e=t.facet(qr).find(i=>i.field==this);return((e==null?void 0:e.create)||this.createF)(t)}slot(t){let e=t[this.id]>>1;return{create:i=>(i.values[e]=this.create(i),1),update:(i,n)=>{let r=i.values[e],o=this.updateF(r,n);return this.compareF(r,o)?0:(i.values[e]=o,1)},reconfigure:(i,n)=>n.config.address[this.id]!=null?(i.values[e]=n.field(this),0):(i.values[e]=this.create(i),1)}}init(t){return[this,qr.of({field:this,create:t})]}get extension(){return this}}const Se={lowest:4,low:3,default:2,high:1,highest:0};function si(s){return t=>new Ol(t,s)}const ye={highest:si(Se.highest),high:si(Se.high),default:si(Se.default),low:si(Se.low),lowest:si(Se.lowest)};class Ol{constructor(t,e){this.inner=t,this.prec=e}}class In{of(t){return new ms(this,t)}reconfigure(t){return In.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class ms{constructor(t,e){this.compartment=t,this.inner=e}}class pn{constructor(t,e,i,n,r,o){for(this.base=t,this.compartments=e,this.dynamicSlots=i,this.address=n,this.staticValues=r,this.facets=o,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(t,e,i){let n=[],r=Object.create(null),o=new Map;for(let u of Sc(t,e,o))u instanceof yt?n.push(u):(r[u.facet.id]||(r[u.facet.id]=[])).push(u);let l=Object.create(null),a=[],c=[];for(let u of n)l[u.id]=c.length<<1,c.push(d=>u.slot(d));let h=i==null?void 0:i.config.facets;for(let u in r){let d=r[u],p=d[0].facet,g=h&&h[u]||[];if(d.every(m=>m.type==0))if(l[p.id]=a.length<<1|1,ar(g,d))a.push(i.facet(p));else{let m=p.combine(d.map(y=>y.value));a.push(i&&p.compare(m,i.facet(p))?i.facet(p):m)}else{for(let m of d)m.type==0?(l[m.id]=a.length<<1|1,a.push(m.value)):(l[m.id]=c.length<<1,c.push(y=>m.dynamicSlot(y)));l[p.id]=c.length<<1,c.push(m=>wc(m,p,d))}}let f=c.map(u=>u(l));return new pn(t,o,f,l,a,r)}}function Sc(s,t,e){let i=[[],[],[],[],[]],n=new Map;function r(o,l){let a=n.get(o);if(a!=null){if(a<=l)return;let c=i[a].indexOf(o);c>-1&&i[a].splice(c,1),o instanceof ms&&e.delete(o.compartment)}if(n.set(o,l),Array.isArray(o))for(let c of o)r(c,l);else if(o instanceof ms){if(e.has(o.compartment))throw new RangeError("Duplicate use of compartment in extensions");let c=t.get(o.compartment)||o.inner;e.set(o.compartment,c),r(c,l)}else if(o instanceof Ol)r(o.inner,o.prec);else if(o instanceof yt)i[l].push(o),o.provides&&r(o.provides,l);else if(o instanceof ln)i[l].push(o),o.facet.extensions&&r(o.facet.extensions,Se.default);else{let c=o.extension;if(!c)throw new Error(`Unrecognized extension value in extension set (${o}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(c,l)}}return r(s,Se.default),i.reduce((o,l)=>o.concat(l))}function mi(s,t){if(t&1)return 2;let e=t>>1,i=s.status[e];if(i==4)throw new Error("Cyclic dependency between fields and/or facets");if(i&2)return i;s.status[e]=4;let n=s.computeSlot(s,s.config.dynamicSlots[e]);return s.status[e]=2|n}function gn(s,t){return t&1?s.config.staticValues[t>>1]:s.values[t>>1]}const Tl=T.define(),ys=T.define({combine:s=>s.some(t=>t),static:!0}),Pl=T.define({combine:s=>s.length?s[0]:void 0,static:!0}),Bl=T.define(),Rl=T.define(),Ll=T.define(),El=T.define({combine:s=>s.length?s[0]:!1});class se{constructor(t,e){this.type=t,this.value=e}static define(){return new vc}}class vc{of(t){return new se(this,t)}}class kc{constructor(t){this.map=t}of(t){return new F(this,t)}}class F{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.type.map(this.value,t);return e===void 0?void 0:e==this.value?this:new F(this.type,e)}is(t){return this.type==t}static define(t={}){return new kc(t.map||(e=>e))}static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){let r=n.map(e);r&&i.push(r)}return i}}F.reconfigure=F.define();F.appendConfig=F.define();class Z{constructor(t,e,i,n,r,o){this.startState=t,this.changes=e,this.selection=i,this.effects=n,this.annotations=r,this.scrollIntoView=o,this._doc=null,this._state=null,i&&Dl(i,e.newLength),r.some(l=>l.type==Z.time)||(this.annotations=r.concat(Z.time.of(Date.now())))}static create(t,e,i,n,r,o){return new Z(t,e,i,n,r,o)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let e=this.annotation(Z.userEvent);return!!(e&&(e==t||e.length>t.length&&e.slice(0,t.length)==t&&e[t.length]=="."))}}Z.time=se.define();Z.userEvent=se.define();Z.addToHistory=se.define();Z.remote=se.define();function Cc(s,t){let e=[];for(let i=0,n=0;;){let r,o;if(i=s[i]))r=s[i++],o=s[i++];else if(n=0;n--){let r=i[n](s);r instanceof Z?s=r:Array.isArray(r)&&r.length==1&&r[0]instanceof Z?s=r[0]:s=Nl(t,ze(r),!1)}return s}function Mc(s){let t=s.startState,e=t.facet(Ll),i=s;for(let n=e.length-1;n>=0;n--){let r=e[n](s);r&&Object.keys(r).length&&(i=Il(i,bs(t,r,s.changes.newLength),!0))}return i==s?s:Z.create(t,s.changes,s.selection,i.effects,i.annotations,i.scrollIntoView)}const Dc=[];function ze(s){return s==null?Dc:Array.isArray(s)?s:[s]}var G=function(s){return s[s.Word=0]="Word",s[s.Space=1]="Space",s[s.Other=2]="Other",s}(G||(G={}));const Oc=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let xs;try{xs=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch{}function Tc(s){if(xs)return xs.test(s);for(let t=0;t"€"&&(e.toUpperCase()!=e.toLowerCase()||Oc.test(e)))return!0}return!1}function Pc(s){return t=>{if(!/\S/.test(t))return G.Space;if(Tc(t))return G.Word;for(let e=0;e-1)return G.Word;return G.Other}}class H{constructor(t,e,i,n,r,o){this.config=t,this.doc=e,this.selection=i,this.values=n,this.status=t.statusTemplate.slice(),this.computeSlot=r,o&&(o._state=this);for(let l=0;ln.set(c,a)),e=null),n.set(l.value.compartment,l.value.extension)):l.is(F.reconfigure)?(e=null,i=l.value):l.is(F.appendConfig)&&(e=null,i=ze(i).concat(l.value));let r;e?r=t.startState.values.slice():(e=pn.resolve(i,n,this),r=new H(e,this.doc,this.selection,e.dynamicSlots.map(()=>null),(a,c)=>c.reconfigure(a,this),null).values);let o=t.startState.facet(ys)?t.newSelection:t.newSelection.asSingle();new H(e,t.newDoc,o,r,(l,a)=>a.update(l,t),t)}replaceSelection(t){return typeof t=="string"&&(t=this.toText(t)),this.changeByRange(e=>({changes:{from:e.from,to:e.to,insert:t},range:b.cursor(e.from+t.length)}))}changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(i.changes),r=[i.range],o=ze(i.effects);for(let l=1;lo.spec.fromJSON(l,a)))}}return H.create({doc:t.doc,selection:b.fromJSON(t.selection),extensions:e.extensions?n.concat([e.extensions]):n})}static create(t={}){let e=pn.resolve(t.extensions||[],new Map),i=t.doc instanceof V?t.doc:V.of((t.doc||"").split(e.staticFacet(H.lineSeparator)||us)),n=t.selection?t.selection instanceof b?t.selection:b.single(t.selection.anchor,t.selection.head):b.single(0);return Dl(n,i.length),e.staticFacet(ys)||(n=n.asSingle()),new H(e,i,n,e.dynamicSlots.map(()=>null),(r,o)=>o.create(r),null)}get tabSize(){return this.facet(H.tabSize)}get lineBreak(){return this.facet(H.lineSeparator)||` -`}get readOnly(){return this.facet(El)}phrase(t,...e){for(let i of this.facet(H.phrases))if(Object.prototype.hasOwnProperty.call(i,t)){t=i[t];break}return e.length&&(t=t.replace(/\$(\$|\d*)/g,(i,n)=>{if(n=="$")return"$";let r=+(n||1);return!r||r>e.length?i:e[r-1]})),t}languageDataAt(t,e,i=-1){let n=[];for(let r of this.facet(Tl))for(let o of r(this,e,i))Object.prototype.hasOwnProperty.call(o,t)&&n.push(o[t]);return n}charCategorizer(t){return Pc(this.languageDataAt("wordChars",t).join(""))}wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),r=this.charCategorizer(t),o=t-i,l=t-i;for(;o>0;){let a=ot(e,o,!1);if(r(e.slice(a,o))!=G.Word)break;o=a}for(;ls.length?s[0]:4});H.lineSeparator=Pl;H.readOnly=El;H.phrases=T.define({compare(s,t){let e=Object.keys(s),i=Object.keys(t);return e.length==i.length&&e.every(n=>s[n]==t[n])}});H.languageData=Tl;H.changeFilter=Bl;H.transactionFilter=Rl;H.transactionExtender=Ll;In.reconfigure=F.define();function Le(s,t,e={}){let i={};for(let n of s)for(let r of Object.keys(n)){let o=n[r],l=i[r];if(l===void 0)i[r]=o;else if(!(l===o||o===void 0))if(Object.hasOwnProperty.call(e,r))i[r]=e[r](l,o);else throw new Error("Config merge conflict for field "+r)}for(let n in t)i[n]===void 0&&(i[n]=t[n]);return i}class Me{eq(t){return this==t}range(t,e=t){return ws.create(t,e,this)}}Me.prototype.startSide=Me.prototype.endSide=0;Me.prototype.point=!1;Me.prototype.mapMode=ht.TrackDel;let ws=class Fl{constructor(t,e,i){this.from=t,this.to=e,this.value=i}static create(t,e,i){return new Fl(t,e,i)}};function Ss(s,t){return s.from-t.from||s.value.startSide-t.value.startSide}class hr{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}get length(){return this.to[this.to.length-1]}findIndex(t,e,i,n=0){let r=i?this.to:this.from;for(let o=n,l=r.length;;){if(o==l)return o;let a=o+l>>1,c=r[a]-t||(i?this.value[a].endSide:this.value[a].startSide)-e;if(a==o)return c>=0?o:l;c>=0?l=a:o=a+1}}between(t,e,i,n){for(let r=this.findIndex(e,-1e9,!0),o=this.findIndex(i,1e9,!1,r);rd||u==d&&c.startSide>0&&c.endSide<=0)continue;(d-u||c.endSide-c.startSide)<0||(o<0&&(o=u),c.point&&(l=Math.max(l,d-u)),i.push(c),n.push(u-o),r.push(d-o))}return{mapped:i.length?new hr(n,r,i,l):null,pos:o}}}class K{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,this.maxPoint=n}static create(t,e,i,n){return new K(t,e,i,n)}get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chunkEnd(t),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let e of this.chunk)t+=e.value.length;return t}chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:r=this.length}=t,o=t.filter;if(e.length==0&&!o)return this;if(i&&(e=e.slice().sort(Ss)),this.isEmpty)return e.length?K.of(e):this;let l=new Vl(this,null,-1).goto(0),a=0,c=[],h=new De;for(;l.value||a=0){let f=e[a++];h.addInner(f.from,f.to,f.value)||c.push(f)}else l.rangeIndex==1&&l.chunkIndexthis.chunkEnd(l.chunkIndex)||rl.to||r=r&&t<=r+o.length&&o.between(r,t-r,e-r,i)===!1)return}this.nextLayer.between(t,e,i)}}iter(t=0){return wi.from([this]).goto(t)}get isEmpty(){return this.nextLayer==this}static iter(t,e=0){return wi.from(t).goto(e)}static compare(t,e,i,n,r=-1){let o=t.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),l=e.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),a=Kr(o,l,i),c=new ri(o,a,r),h=new ri(l,a,r);i.iterGaps((f,u,d)=>$r(c,f,h,u,d,n)),i.empty&&i.length==0&&$r(c,0,h,0,0,n)}static eq(t,e,i=0,n){n==null&&(n=999999999);let r=t.filter(h=>!h.isEmpty&&e.indexOf(h)<0),o=e.filter(h=>!h.isEmpty&&t.indexOf(h)<0);if(r.length!=o.length)return!1;if(!r.length)return!0;let l=Kr(r,o),a=new ri(r,l,0).goto(i),c=new ri(o,l,0).goto(i);for(;;){if(a.to!=c.to||!vs(a.active,c.active)||a.point&&(!c.point||!a.point.eq(c.point)))return!1;if(a.to>n)return!0;a.next(),c.next()}}static spans(t,e,i,n,r=-1){let o=new ri(t,null,r).goto(e),l=e,a=o.openStart;for(;;){let c=Math.min(o.to,i);if(o.point){let h=o.activeForPoint(o.to),f=o.pointFroml&&(n.span(l,c,o.active,a),a=o.openEnd(c));if(o.to>i)return a+(o.point&&o.to>i?1:0);l=o.to,o.next()}}static of(t,e=!1){let i=new De;for(let n of t instanceof ws?[t]:e?Bc(t):t)i.add(n.from,n.to,n.value);return i.finish()}static join(t){if(!t.length)return K.empty;let e=t[t.length-1];for(let i=t.length-2;i>=0;i--)for(let n=t[i];n!=K.empty;n=n.nextLayer)e=new K(n.chunkPos,n.chunk,e,Math.max(n.maxPoint,e.maxPoint));return e}}K.empty=new K([],[],null,-1);function Bc(s){if(s.length>1)for(let t=s[0],e=1;e0)return s.slice().sort(Ss);t=i}return s}K.empty.nextLayer=K.empty;class De{finishChunk(t){this.chunks.push(new hr(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,t&&(this.from=[],this.to=[],this.value=[])}constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new De)).add(t,e,i)}addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(n<=0&&(t-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return n<0?!1:(this.from.length==250&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=t),this.from.push(t-this.chunkStart),this.to.push(e-this.chunkStart),this.last=i,this.lastFrom=t,this.lastTo=e,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,e-t)),!0)}addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,e.maxPoint),this.chunks.push(e),this.chunkPos.push(t);let i=e.value.length-1;return this.last=e.value[i],this.lastFrom=e.from[i]+t,this.lastTo=e.to[i]+t,!0}finish(){return this.finishInner(K.empty)}finishInner(t){if(this.from.length&&this.finishChunk(!1),this.chunks.length==0)return t;let e=K.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(t):t,this.setMaxPoint);return this.from=null,e}}function Kr(s,t,e){let i=new Map;for(let r of s)for(let o=0;o=this.minPoint)break}}setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&n.push(new Vl(o,e,i,r));return n.length==1?n[0]:new wi(n)}get startSide(){return this.value?this.value.startSide:0}goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let i=this.heap.length>>1;i>=0;i--)Un(this.heap,i);return this.next(),this}forward(t,e){for(let i of this.heap)i.forward(t,e);for(let i=this.heap.length>>1;i>=0;i--)Un(this.heap,i);(this.to-t||this.value.endSide-e)<0&&this.next()}next(){if(this.heap.length==0)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let t=this.heap[0];this.from=t.from,this.to=t.to,this.value=t.value,this.rank=t.rank,t.value&&t.next(),Un(this.heap,0)}}}function Un(s,t){for(let e=s[t];;){let i=(t<<1)+1;if(i>=s.length)break;let n=s[i];if(i+1=0&&(n=s[i+1],i++),e.compare(n)<0)break;s[i]=e,s[t]=n,t=i}}class ri{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=wi.from(t,e,i)}goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=t,this.endSide=e,this.openStart=-1,this.next(),this}forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t||this.active[this.minActive].endSide-e)<0;)this.removeActive(this.minActive);this.cursor.forward(t,e)}removeActive(t){Wi(this.active,t),Wi(this.activeTo,t),Wi(this.activeRank,t),this.minActive=jr(this.active,this.activeTo)}addActive(t){let e=0,{value:i,to:n,rank:r}=this.cursor;for(;e0;)e++;Hi(this.active,e,i),Hi(this.activeTo,e,n),Hi(this.activeRank,e,r),t&&Hi(t,e,this.cursor.from),this.minActive=jr(this.active,this.activeTo)}next(){let t=this.to,e=this.point;this.point=null;let i=this.openStart<0?[]:null;for(;;){let n=this.minActive;if(n>-1&&(this.activeTo[n]-this.cursor.from||this.active[n].endSide-this.cursor.startSide)<0){if(this.activeTo[n]>t){this.to=this.activeTo[n],this.endSide=this.active[n].endSide;break}this.removeActive(n),i&&Wi(i,n)}else if(this.cursor.value)if(this.cursor.from>t){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}else{let r=this.cursor.value;if(!r.point)this.addActive(i),this.cursor.next();else if(e&&this.cursor.to==this.to&&this.cursor.from=0&&i[n]=0&&!(this.activeRank[i]t||this.activeTo[i]==t&&this.active[i].endSide>=this.point.endSide)&&e.push(this.active[i]);return e.reverse()}openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>t;i--)e++;return e}}function $r(s,t,e,i,n,r){s.goto(t),e.goto(i);let o=i+n,l=i,a=i-t;for(;;){let c=s.to+a-e.to||s.endSide-e.endSide,h=c<0?s.to+a:e.to,f=Math.min(h,o);if(s.point||e.point?s.point&&e.point&&(s.point==e.point||s.point.eq(e.point))&&vs(s.activeForPoint(s.to),e.activeForPoint(e.to))||r.comparePoint(l,f,s.point,e.point):f>l&&!vs(s.active,e.active)&&r.compareRange(l,f,s.active,e.active),h>o)break;l=h,c<=0&&s.next(),c>=0&&e.next()}}function vs(s,t){if(s.length!=t.length)return!1;for(let e=0;e=t;i--)s[i+1]=s[i];s[t]=e}function jr(s,t){let e=-1,i=1e9;for(let n=0;n=t)return n;if(n==s.length)break;r+=s.charCodeAt(n)==9?e-r%e:1,n=ot(s,n)}return i===!0?-1:s.length}const Cs="ͼ",Ur=typeof Symbol>"u"?"__"+Cs:Symbol.for(Cs),As=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),Gr=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{};class de{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(o){return/^@/.test(o)?[o]:o.split(/,\s*/)}function r(o,l,a,c){let h=[],f=/^@(\w+)\b/.exec(o[0]),u=f&&f[1]=="keyframes";if(f&&l==null)return a.push(o[0]+";");for(let d in l){let p=l[d];if(/&/.test(d))r(d.split(/,\s*/).map(g=>o.map(m=>g.replace(/&/,m))).reduce((g,m)=>g.concat(m)),p,a);else if(p&&typeof p=="object"){if(!f)throw new RangeError("The value of a property ("+d+") should be a primitive value.");r(n(d),p,h,u)}else p!=null&&h.push(d.replace(/_.*/,"").replace(/[A-Z]/g,g=>"-"+g.toLowerCase())+": "+p+";")}(h.length||u)&&a.push((i&&!f&&!c?o.map(i):o).join(", ")+" {"+h.join(" ")+"}")}for(let o in t)r(n(o),t[o],this.rules)}getRules(){return this.rules.join(` -`)}static newName(){let t=Gr[Ur]||1;return Gr[Ur]=t+1,Cs+t.toString(36)}static mount(t,e,i){let n=t[As],r=i&&i.nonce;n?r&&n.setNonce(r):n=new Rc(t,r),n.mount(Array.isArray(e)?e:[e],t)}}let Jr=new Map;class Rc{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&t.adoptedStyleSheets&&n.CSSStyleSheet){let r=Jr.get(i);if(r)return t[As]=r;this.sheet=new n.CSSStyleSheet,Jr.set(i,this)}else this.styleTag=i.createElement("style"),e&&this.styleTag.setAttribute("nonce",e);this.modules=[],t[As]=this}mount(t,e){let i=this.sheet,n=0,r=0;for(let o=0;o-1&&(this.modules.splice(a,1),r--,a=-1),a==-1){if(this.modules.splice(r++,0,l),i)for(let c=0;c",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},Lc=typeof navigator<"u"&&/Mac/.test(navigator.platform),Ec=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);for(var st=0;st<10;st++)pe[48+st]=pe[96+st]=String(st);for(var st=1;st<=24;st++)pe[st+111]="F"+st;for(var st=65;st<=90;st++)pe[st]=String.fromCharCode(st+32),Si[st]=String.fromCharCode(st);for(var Gn in pe)Si.hasOwnProperty(Gn)||(Si[Gn]=pe[Gn]);function Ic(s){var t=Lc&&s.metaKey&&s.shiftKey&&!s.ctrlKey&&!s.altKey||Ec&&s.shiftKey&&s.key&&s.key.length==1||s.key=="Unidentified",e=!t&&s.key||(s.shiftKey?Si:pe)[s.keyCode]||s.key||"Unidentified";return e=="Esc"&&(e="Escape"),e=="Del"&&(e="Delete"),e=="Left"&&(e="ArrowLeft"),e=="Up"&&(e="ArrowUp"),e=="Right"&&(e="ArrowRight"),e=="Down"&&(e="ArrowDown"),e}function vi(s){let t;return s.nodeType==11?t=s.getSelection?s:s.ownerDocument:t=s,t.getSelection()}function Ms(s,t){return t?s==t||s.contains(t.nodeType!=1?t.parentNode:t):!1}function Nc(s){let t=s.activeElement;for(;t&&t.shadowRoot;)t=t.shadowRoot.activeElement;return t}function an(s,t){if(!t.anchorNode)return!1;try{return Ms(s,t.anchorNode)}catch{return!1}}function Ge(s){return s.nodeType==3?Te(s,0,s.nodeValue.length).getClientRects():s.nodeType==1?s.getClientRects():[]}function yi(s,t,e,i){return e?Yr(s,t,e,i,-1)||Yr(s,t,e,i,1):!1}function Oe(s){for(var t=0;;t++)if(s=s.previousSibling,!s)return t}function mn(s){return s.nodeType==1&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(s.nodeName)}function Yr(s,t,e,i,n){for(;;){if(s==e&&t==i)return!0;if(t==(n<0?0:ie(s))){if(s.nodeName=="DIV")return!1;let r=s.parentNode;if(!r||r.nodeType!=1)return!1;t=Oe(s)+(n<0?0:1),s=r}else if(s.nodeType==1){if(s=s.childNodes[t+(n<0?-1:0)],s.nodeType==1&&s.contentEditable=="false")return!1;t=n<0?ie(s):0}else return!1}}function ie(s){return s.nodeType==3?s.nodeValue.length:s.childNodes.length}function Nn(s,t){let e=t?s.left:s.right;return{left:e,right:e,top:s.top,bottom:s.bottom}}function Fc(s){let t=s.visualViewport;return t?{left:0,right:t.width,top:0,bottom:t.height}:{left:0,right:s.innerWidth,top:0,bottom:s.innerHeight}}function Wl(s,t){let e=t.width/s.offsetWidth,i=t.height/s.offsetHeight;return(e>.995&&e<1.005||!isFinite(e)||Math.abs(t.width-s.offsetWidth)<1)&&(e=1),(i>.995&&i<1.005||!isFinite(i)||Math.abs(t.height-s.offsetHeight)<1)&&(i=1),{scaleX:e,scaleY:i}}function Vc(s,t,e,i,n,r,o,l){let a=s.ownerDocument,c=a.defaultView||window;for(let h=s,f=!1;h&&!f;)if(h.nodeType==1){let u,d=h==a.body,p=1,g=1;if(d)u=Fc(c);else{if(/^(fixed|sticky)$/.test(getComputedStyle(h).position)&&(f=!0),h.scrollHeight<=h.clientHeight&&h.scrollWidth<=h.clientWidth){h=h.assignedSlot||h.parentNode;continue}let x=h.getBoundingClientRect();({scaleX:p,scaleY:g}=Wl(h,x)),u={left:x.left,right:x.left+h.clientWidth*p,top:x.top,bottom:x.top+h.clientHeight*g}}let m=0,y=0;if(n=="nearest")t.top0&&t.bottom>u.bottom+y&&(y=t.bottom-u.bottom+y+o)):t.bottom>u.bottom&&(y=t.bottom-u.bottom+o,e<0&&t.top-y0&&t.right>u.right+m&&(m=t.right-u.right+m+r)):t.right>u.right&&(m=t.right-u.right+r,e<0&&t.lefte.clientHeight||e.scrollWidth>e.clientWidth)return e;e=e.assignedSlot||e.parentNode}else if(e.nodeType==11)e=e.host;else break;return null}class Hc{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anchorOffset,e?ie(e):0),i,Math.min(t.focusOffset,i?ie(i):0))}set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=n}}let Ne=null;function Hl(s){if(s.setActive)return s.setActive();if(Ne)return s.focus(Ne);let t=[];for(let e=s;e&&(t.push(e,e.scrollTop,e.scrollLeft),e!=e.ownerDocument);e=e.parentNode);if(s.focus(Ne==null?{get preventScroll(){return Ne={preventScroll:!0},!0}}:void 0),!Ne){Ne=!1;for(let e=0;eMath.max(1,s.scrollHeight-s.clientHeight-4)}function Kl(s,t){for(let e=s,i=t;;){if(e.nodeType==3&&i>0)return{node:e,offset:i};if(e.nodeType==1&&i>0){if(e.contentEditable=="false")return null;e=e.childNodes[i-1],i=ie(e)}else if(e.parentNode&&!mn(e))i=Oe(e),e=e.parentNode;else return null}}function $l(s,t){for(let e=s,i=t;;){if(e.nodeType==3&&ie)return f.domBoundsAround(t,e,c);if(u>=t&&n==-1&&(n=a,r=c),c>e&&f.dom.parentNode==this.dom){o=a,l=h;break}h=u,c=u+f.breakAfter}return{from:r,to:l<0?i+this.length:l,startDOM:(n?this.children[n-1].dom.nextSibling:null)||this.dom.firstChild,endDOM:o=0?this.children[o].dom:null}}markDirty(t=!1){this.flags|=2,this.markParentsDirty(t)}markParentsDirty(t){for(let e=this.parent;e;e=e.parent){if(t&&(e.flags|=2),e.flags&1)return;e.flags|=1,t=!1}}setParent(t){this.parent!=t&&(this.parent=t,this.flags&7&&this.markParentsDirty(!0))}setDOM(t){this.dom!=t&&(this.dom&&(this.dom.cmView=null),this.dom=t,t.cmView=this)}get rootView(){for(let t=this;;){let e=t.parent;if(!e)return t;t=e}}replaceChildren(t,e,i=cr){this.markDirty();for(let n=t;nthis.pos||t==this.pos&&(e>0||this.i==0||this.children[this.i-1].breakAfter))return this.off=t-this.pos,this;let i=this.children[--this.i];this.pos-=i.length+i.breakAfter}}}function Ul(s,t,e,i,n,r,o,l,a){let{children:c}=s,h=c.length?c[t]:null,f=r.length?r[r.length-1]:null,u=f?f.breakAfter:o;if(!(t==i&&h&&!o&&!u&&r.length<2&&h.merge(e,n,r.length?f:null,e==0,l,a))){if(i0&&(!o&&r.length&&h.merge(e,h.length,r[0],!1,l,0)?h.breakAfter=r.shift().breakAfter:(e2);var D={mac:to||/Mac/.test(wt.platform),windows:/Win/.test(wt.platform),linux:/Linux|X11/.test(wt.platform),ie:Fn,ie_version:Jl?Ds.documentMode||6:Ts?+Ts[1]:Os?+Os[1]:0,gecko:Qr,gecko_version:Qr?+(/Firefox\/(\d+)/.exec(wt.userAgent)||[0,0])[1]:0,chrome:!!Jn,chrome_version:Jn?+Jn[1]:0,ios:to,android:/Android\b/.test(wt.userAgent),webkit:Zr,safari:Yl,webkit_version:Zr?+(/\bAppleWebKit\/(\d+)/.exec(wt.userAgent)||[0,0])[1]:0,tabSize:Ds.documentElement.style.tabSize!=null?"tab-size":"-moz-tab-size"};const Kc=256;class Vt extends ${constructor(t){super(),this.text=t}get length(){return this.text.length}createDOM(t){this.setDOM(t||document.createTextNode(this.text))}sync(t,e){this.dom||this.createDOM(),this.dom.nodeValue!=this.text&&(e&&e.node==this.dom&&(e.written=!0),this.dom.nodeValue=this.text)}reuseDOM(t){t.nodeType==3&&this.createDOM(t)}merge(t,e,i){return this.flags&8||i&&(!(i instanceof Vt)||this.length-(e-t)+i.length>Kc||i.flags&8)?!1:(this.text=this.text.slice(0,t)+(i?i.text:"")+this.text.slice(e),this.markDirty(),!0)}split(t){let e=new Vt(this.text.slice(t));return this.text=this.text.slice(0,t),this.markDirty(),e.flags|=this.flags&8,e}localPosFromDOM(t,e){return t==this.dom?e:e?this.text.length:0}domAtPos(t){return new ct(this.dom,t)}domBoundsAround(t,e,i){return{from:i,to:i+this.length,startDOM:this.dom,endDOM:this.dom.nextSibling}}coordsAt(t,e){return $c(this.dom,t,e)}}class ne extends ${constructor(t,e=[],i=0){super(),this.mark=t,this.children=e,this.length=i;for(let n of e)n.setParent(this)}setAttrs(t){if(zl(t),this.mark.class&&(t.className=this.mark.class),this.mark.attrs)for(let e in this.mark.attrs)t.setAttribute(e,this.mark.attrs[e]);return t}canReuseDOM(t){return super.canReuseDOM(t)&&!((this.flags|t.flags)&8)}reuseDOM(t){t.nodeName==this.mark.tagName.toUpperCase()&&(this.setDOM(t),this.flags|=6)}sync(t,e){this.dom?this.flags&4&&this.setAttrs(this.dom):this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))),super.sync(t,e)}merge(t,e,i,n,r,o){return i&&(!(i instanceof ne&&i.mark.eq(this.mark))||t&&r<=0||et&&e.push(i=t&&(n=r),i=a,r++}let o=this.length-t;return this.length=t,n>-1&&(this.children.length=n,this.markDirty()),new ne(this.mark,e,o)}domAtPos(t){return Xl(this,t)}coordsAt(t,e){return Ql(this,t,e)}}function $c(s,t,e){let i=s.nodeValue.length;t>i&&(t=i);let n=t,r=t,o=0;t==0&&e<0||t==i&&e>=0?D.chrome||D.gecko||(t?(n--,o=1):r=0)?0:l.length-1];return D.safari&&!o&&a.width==0&&(a=Array.prototype.find.call(l,c=>c.width)||a),o?Nn(a,o<0):a||null}class ke extends ${static create(t,e,i){return new ke(t,e,i)}constructor(t,e,i){super(),this.widget=t,this.length=e,this.side=i,this.prevWidget=null}split(t){let e=ke.create(this.widget,this.length-t,this.side);return this.length-=t,e}sync(t){(!this.dom||!this.widget.updateDOM(this.dom,t))&&(this.dom&&this.prevWidget&&this.prevWidget.destroy(this.dom),this.prevWidget=null,this.setDOM(this.widget.toDOM(t)),this.widget.editable||(this.dom.contentEditable="false"))}getSide(){return this.side}merge(t,e,i,n,r,o){return i&&(!(i instanceof ke)||!this.widget.compare(i.widget)||t>0&&r<=0||e0)?ct.before(this.dom):ct.after(this.dom,t==this.length)}domBoundsAround(){return null}coordsAt(t,e){let i=this.widget.coordsAt(this.dom,t,e);if(i)return i;let n=this.dom.getClientRects(),r=null;if(!n.length)return null;let o=this.side?this.side<0:t>0;for(let l=o?n.length-1:0;r=n[l],!(t>0?l==0:l==n.length-1||r.top0?ct.before(this.dom):ct.after(this.dom)}localPosFromDOM(){return 0}domBoundsAround(){return null}coordsAt(t){return this.dom.getBoundingClientRect()}get overrideDOMText(){return V.empty}get isHidden(){return!0}}Vt.prototype.children=ke.prototype.children=Je.prototype.children=cr;function Xl(s,t){let e=s.dom,{children:i}=s,n=0;for(let r=0;nr&&t0;r--){let o=i[r-1];if(o.dom.parentNode==e)return o.domAtPos(o.length)}for(let r=n;r0&&t instanceof ne&&n.length&&(i=n[n.length-1])instanceof ne&&i.mark.eq(t.mark)?_l(i,t.children[0],e-1):(n.push(t),t.setParent(s)),s.length+=t.length}function Ql(s,t,e){let i=null,n=-1,r=null,o=-1;function l(c,h){for(let f=0,u=0;f=h&&(d.children.length?l(d,h-u):(!r||r.isHidden&&e>0)&&(p>h||u==p&&d.getSide()>0)?(r=d,o=h-u):(u-1?1:0)!=n.length-(e&&n.indexOf(e)>-1?1:0))return!1;for(let r of i)if(r!=e&&(n.indexOf(r)==-1||s[r]!==t[r]))return!1;return!0}function Bs(s,t,e){let i=!1;if(t)for(let n in t)e&&n in e||(i=!0,n=="style"?s.style.cssText="":s.removeAttribute(n));if(e)for(let n in e)t&&t[n]==e[n]||(i=!0,n=="style"?s.style.cssText=e[n]:s.setAttribute(n,e[n]));return i}function Uc(s){let t=Object.create(null);for(let e=0;e0&&this.children[i-1].length==0;)this.children[--i].destroy();return this.children.length=i,this.markDirty(),this.length=t,e}transferDOM(t){this.dom&&(this.markDirty(),t.setDOM(this.dom),t.prevAttrs=this.prevAttrs===void 0?this.attrs:this.prevAttrs,this.prevAttrs=void 0,this.dom=null)}setDeco(t){yn(this.attrs,t)||(this.dom&&(this.prevAttrs=this.attrs,this.markDirty()),this.attrs=t)}append(t,e){_l(this,t,e)}addLineDeco(t){let e=t.spec.attributes,i=t.spec.class;e&&(this.attrs=Ps(e,this.attrs||{})),i&&(this.attrs=Ps({class:i},this.attrs||{}))}domAtPos(t){return Xl(this,t)}reuseDOM(t){t.nodeName=="DIV"&&(this.setDOM(t),this.flags|=6)}sync(t,e){var i;this.dom?this.flags&4&&(zl(this.dom),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0):(this.setDOM(document.createElement("div")),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0),this.prevAttrs!==void 0&&(Bs(this.dom,this.prevAttrs,this.attrs),this.dom.classList.add("cm-line"),this.prevAttrs=void 0),super.sync(t,e);let n=this.dom.lastChild;for(;n&&$.get(n)instanceof ne;)n=n.lastChild;if(!n||!this.length||n.nodeName!="BR"&&((i=$.get(n))===null||i===void 0?void 0:i.isEditable)==!1&&(!D.ios||!this.children.some(r=>r instanceof Vt))){let r=document.createElement("BR");r.cmIgnore=!0,this.dom.appendChild(r)}}measureTextSize(){if(this.children.length==0||this.length>20)return null;let t=0,e;for(let i of this.children){if(!(i instanceof Vt)||/[^ -~]/.test(i.text))return null;let n=Ge(i.dom);if(n.length!=1)return null;t+=n[0].width,e=n[0].height}return t?{lineHeight:this.dom.getBoundingClientRect().height,charWidth:t/this.length,textHeight:e}:null}coordsAt(t,e){let i=Ql(this,t,e);if(!this.children.length&&i&&this.parent){let{heightOracle:n}=this.parent.view.viewState,r=i.bottom-i.top;if(Math.abs(r-n.lineHeight)<2&&n.textHeight=e){if(r instanceof Q)return r;if(o>e)break}n=o+r.breakAfter}return null}}class te extends ${constructor(t,e,i){super(),this.widget=t,this.length=e,this.deco=i,this.breakAfter=0,this.prevWidget=null}merge(t,e,i,n,r,o){return i&&(!(i instanceof te)||!this.widget.compare(i.widget)||t>0&&r<=0||e0}}class Ee{eq(t){return!1}updateDOM(t,e){return!1}compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}get estimatedHeight(){return-1}get lineBreaks(){return 0}ignoreEvent(t){return!0}coordsAt(t,e,i){return null}get isHidden(){return!1}get editable(){return!1}destroy(t){}}var Ot=function(s){return s[s.Text=0]="Text",s[s.WidgetBefore=1]="WidgetBefore",s[s.WidgetAfter=2]="WidgetAfter",s[s.WidgetRange=3]="WidgetRange",s}(Ot||(Ot={}));class P extends Me{constructor(t,e,i,n){super(),this.startSide=t,this.endSide=e,this.widget=i,this.spec=n}get heightRelevant(){return!1}static mark(t){return new Ri(t)}static widget(t){let e=Math.max(-1e4,Math.min(1e4,t.side||0)),i=!!t.block;return e+=i&&!t.inlineOrder?e>0?3e8:-4e8:e>0?1e8:-1e8,new ge(t,e,e,i,t.widget||null,!1)}static replace(t){let e=!!t.block,i,n;if(t.isBlockGap)i=-5e8,n=4e8;else{let{start:r,end:o}=Zl(t,e);i=(r?e?-3e8:-1:5e8)-1,n=(o?e?2e8:1:-6e8)+1}return new ge(t,i,n,e,t.widget||null,!0)}static line(t){return new Li(t)}static set(t,e=!1){return K.of(t,e)}hasHeight(){return this.widget?this.widget.estimatedHeight>-1:!1}}P.none=K.empty;class Ri extends P{constructor(t){let{start:e,end:i}=Zl(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.class=t.class||"",this.attrs=t.attributes||null}eq(t){var e,i;return this==t||t instanceof Ri&&this.tagName==t.tagName&&(this.class||((e=this.attrs)===null||e===void 0?void 0:e.class))==(t.class||((i=t.attrs)===null||i===void 0?void 0:i.class))&&yn(this.attrs,t.attrs,"class")}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}Ri.prototype.point=!1;class Li extends P{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof Li&&this.spec.class==t.spec.class&&yn(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}Li.prototype.mapMode=ht.TrackBefore;Li.prototype.point=!0;class ge extends P{constructor(t,e,i,n,r,o){super(e,i,r,t),this.block=n,this.isReplace=o,this.mapMode=n?e<=0?ht.TrackBefore:ht.TrackAfter:ht.TrackDel}get type(){return this.startSide!=this.endSide?Ot.WidgetRange:this.startSide<=0?Ot.WidgetBefore:Ot.WidgetAfter}get heightRelevant(){return this.block||!!this.widget&&(this.widget.estimatedHeight>=5||this.widget.lineBreaks>0)}eq(t){return t instanceof ge&&Gc(this.widget,t.widget)&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}ge.prototype.point=!0;function Zl(s,t=!1){let{inclusiveStart:e,inclusiveEnd:i}=s;return e==null&&(e=s.inclusive),i==null&&(i=s.inclusive),{start:e??t,end:i??t}}function Gc(s,t){return s==t||!!(s&&t&&s.compare(t))}function Rs(s,t,e,i=0){let n=e.length-1;n>=0&&e[n]+i>=s?e[n]=Math.max(e[n],t):e.push(s,t)}class bi{constructor(t,e,i,n){this.doc=t,this.pos=e,this.end=i,this.disallowBlockEffectsFor=n,this.content=[],this.curLine=null,this.breakAtStart=0,this.pendingBuffer=0,this.bufferMarks=[],this.atCursorPos=!0,this.openStart=-1,this.openEnd=-1,this.text="",this.textOff=0,this.cursor=t.iter(),this.skip=e}posCovered(){if(this.content.length==0)return!this.breakAtStart&&this.doc.lineAt(this.pos).from!=this.pos;let t=this.content[this.content.length-1];return!(t.breakAfter||t instanceof te&&t.deco.endSide<0)}getLine(){return this.curLine||(this.content.push(this.curLine=new Q),this.atCursorPos=!0),this.curLine}flushBuffer(t=this.bufferMarks){this.pendingBuffer&&(this.curLine.append(zi(new Je(-1),t),t.length),this.pendingBuffer=0)}addBlockWidget(t){this.flushBuffer(),this.curLine=null,this.content.push(t)}finish(t){this.pendingBuffer&&t<=this.bufferMarks.length?this.flushBuffer():this.pendingBuffer=0,!this.posCovered()&&!(t&&this.content.length&&this.content[this.content.length-1]instanceof te)&&this.getLine()}buildText(t,e,i){for(;t>0;){if(this.textOff==this.text.length){let{value:r,lineBreak:o,done:l}=this.cursor.next(this.skip);if(this.skip=0,l)throw new Error("Ran out of text content when drawing inline views");if(o){this.posCovered()||this.getLine(),this.content.length?this.content[this.content.length-1].breakAfter=1:this.breakAtStart=1,this.flushBuffer(),this.curLine=null,this.atCursorPos=!0,t--;continue}else this.text=r,this.textOff=0}let n=Math.min(this.text.length-this.textOff,t,512);this.flushBuffer(e.slice(e.length-i)),this.getLine().append(zi(new Vt(this.text.slice(this.textOff,this.textOff+n)),e),i),this.atCursorPos=!0,this.textOff+=n,t-=n,i=0}}span(t,e,i,n){this.buildText(e-t,i,n),this.pos=e,this.openStart<0&&(this.openStart=n)}point(t,e,i,n,r,o){if(this.disallowBlockEffectsFor[o]&&i instanceof ge){if(i.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.doc.lineAt(this.pos).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}let l=e-t;if(i instanceof ge)if(i.block)i.startSide>0&&!this.posCovered()&&this.getLine(),this.addBlockWidget(new te(i.widget||Ye.block,l,i));else{let a=ke.create(i.widget||Ye.inline,l,l?0:i.startSide),c=this.atCursorPos&&!a.isEditable&&r<=n.length&&(t0),h=!a.isEditable&&(tn.length||i.startSide<=0),f=this.getLine();this.pendingBuffer==2&&!c&&!a.isEditable&&(this.pendingBuffer=0),this.flushBuffer(n),c&&(f.append(zi(new Je(1),n),r),r=n.length+Math.max(0,r-n.length)),f.append(zi(a,n),r),this.atCursorPos=h,this.pendingBuffer=h?tn.length?1:2:0,this.pendingBuffer&&(this.bufferMarks=n.slice())}else this.doc.lineAt(this.pos).from==this.pos&&this.getLine().addLineDeco(i);l&&(this.textOff+l<=this.text.length?this.textOff+=l:(this.skip+=l-(this.text.length-this.textOff),this.text="",this.textOff=0),this.pos=e),this.openStart<0&&(this.openStart=r)}static build(t,e,i,n,r){let o=new bi(t,e,i,r);return o.openEnd=K.spans(n,e,i,o),o.openStart<0&&(o.openStart=o.openEnd),o.finish(o.openEnd),o}}function zi(s,t){for(let e of t)s=new ne(e,[s],s.length);return s}class Ye extends Ee{constructor(t){super(),this.tag=t}eq(t){return t.tag==this.tag}toDOM(){return document.createElement(this.tag)}updateDOM(t){return t.nodeName.toLowerCase()==this.tag}get isHidden(){return!0}}Ye.inline=new Ye("span");Ye.block=new Ye("div");var X=function(s){return s[s.LTR=0]="LTR",s[s.RTL=1]="RTL",s}(X||(X={}));const Pe=X.LTR,fr=X.RTL;function ta(s){let t=[];for(let e=0;e=e){if(l.level==i)return o;(r<0||(n!=0?n<0?l.frome:t[r].level>l.level))&&(r=o)}}if(r<0)throw new RangeError("Index out of range");return r}}function ia(s,t){if(s.length!=t.length)return!1;for(let e=0;e=0;g-=3)if(qt[g+1]==-d){let m=qt[g+2],y=m&2?n:m&4?m&1?r:n:0;y&&(q[f]=q[qt[g]]=y),l=g;break}}else{if(qt.length==189)break;qt[l++]=f,qt[l++]=u,qt[l++]=a}else if((p=q[f])==2||p==1){let g=p==n;a=g?0:1;for(let m=l-3;m>=0;m-=3){let y=qt[m+2];if(y&2)break;if(g)qt[m+2]|=2;else{if(y&4)break;qt[m+2]|=4}}}}}function Zc(s,t,e,i){for(let n=0,r=i;n<=e.length;n++){let o=n?e[n-1].to:s,l=na;)p==m&&(p=e[--g].from,m=g?e[g-1].to:s),q[--p]=d;a=h}else r=c,a++}}}function Es(s,t,e,i,n,r,o){let l=i%2?2:1;if(i%2==n%2)for(let a=t,c=0;aa&&o.push(new ce(a,g.from,d));let m=g.direction==Pe!=!(d%2);Is(s,m?i+1:i,n,g.inner,g.from,g.to,o),a=g.to}p=g.to}else{if(p==e||(h?q[p]!=l:q[p]==l))break;p++}u?Es(s,a,p,i+1,n,u,o):at;){let h=!0,f=!1;if(!c||a>r[c-1].to){let g=q[a-1];g!=l&&(h=!1,f=g==16)}let u=!h&&l==1?[]:null,d=h?i:i+1,p=a;t:for(;;)if(c&&p==r[c-1].to){if(f)break t;let g=r[--c];if(!h)for(let m=g.from,y=c;;){if(m==t)break t;if(y&&r[y-1].to==m)m=r[--y].from;else{if(q[m-1]==l)break t;break}}if(u)u.push(g);else{g.toq.length;)q[q.length]=256;let i=[],n=t==Pe?0:1;return Is(s,n,n,e,0,s.length,i),i}function na(s){return[new ce(0,s,0)]}let sa="";function ef(s,t,e,i,n){var r;let o=i.head-s.from,l=ce.find(t,o,(r=i.bidiLevel)!==null&&r!==void 0?r:-1,i.assoc),a=t[l],c=a.side(n,e);if(o==c){let u=l+=n?1:-1;if(u<0||u>=t.length)return null;a=t[l=u],o=a.side(!n,e),c=a.side(n,e)}let h=ot(s.text,o,a.forward(n,e));(ha.to)&&(h=c),sa=s.text.slice(Math.min(o,h),Math.max(o,h));let f=l==(n?t.length-1:0)?null:t[l+(n?1:-1)];return f&&h==c&&f.level+(n?0:1)s.some(t=>t)}),ua=T.define({combine:s=>s.some(t=>t)}),da=T.define();class Ke{constructor(t,e="nearest",i="nearest",n=5,r=5,o=!1){this.range=t,this.y=e,this.x=i,this.yMargin=n,this.xMargin=r,this.isSnapshot=o}map(t){return t.empty?this:new Ke(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}clip(t){return this.range.to<=t.doc.length?this:new Ke(b.cursor(t.doc.length),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}}const qi=F.define({map:(s,t)=>s.map(t)}),pa=F.define();function Dt(s,t,e){let i=s.facet(aa);i.length?i[0](t):window.onerror?window.onerror(String(t),e,void 0,void 0,t):e?console.error(e+":",t):console.error(t)}const le=T.define({combine:s=>s.length?s[0]:!0});let sf=0;const ci=T.define();class ut{constructor(t,e,i,n,r){this.id=t,this.create=e,this.domEventHandlers=i,this.domEventObservers=n,this.extension=r(this)}static define(t,e){const{eventHandlers:i,eventObservers:n,provide:r,decorations:o}=e||{};return new ut(sf++,t,i,n,l=>{let a=[ci.of(l)];return o&&a.push(ki.of(c=>{let h=c.plugin(l);return h?o(h):P.none})),r&&a.push(r(l)),a})}static fromClass(t,e){return ut.define(i=>new t(i),e)}}class Yn{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}update(t){if(this.value){if(this.mustUpdate){let e=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(e)}catch(i){if(Dt(e.state,i,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch{}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.create(t)}catch(e){Dt(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(!((e=this.value)===null||e===void 0)&&e.destroy)try{this.value.destroy()}catch(i){Dt(t.state,i,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const ga=T.define(),ur=T.define(),ki=T.define(),ma=T.define(),dr=T.define(),ya=T.define();function io(s,t){let e=s.state.facet(ya);if(!e.length)return e;let i=e.map(r=>r instanceof Function?r(s):r),n=[];return K.spans(i,t.from,t.to,{point(){},span(r,o,l,a){let c=r-t.from,h=o-t.from,f=n;for(let u=l.length-1;u>=0;u--,a--){let d=l[u].spec.bidiIsolate,p;if(d==null&&(d=nf(t.text,c,h)),a>0&&f.length&&(p=f[f.length-1]).to==c&&p.direction==d)p.to=h,f=p.inner;else{let g={from:c,to:h,direction:d,inner:[]};f.push(g),f=g.inner}}}}),n}const ba=T.define();function xa(s){let t=0,e=0,i=0,n=0;for(let r of s.state.facet(ba)){let o=r(s);o&&(o.left!=null&&(t=Math.max(t,o.left)),o.right!=null&&(e=Math.max(e,o.right)),o.top!=null&&(i=Math.max(i,o.top)),o.bottom!=null&&(n=Math.max(n,o.bottom)))}return{left:t,right:e,top:i,bottom:n}}const fi=T.define();class It{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}join(t){return new It(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fromA>i.toA)){if(n.toAh)break;r+=2}if(!a)return i;new It(a.fromA,a.toA,a.fromB,a.toB).addToSet(i),o=a.toA,l=a.toB}}}class bn{constructor(t,e,i){this.view=t,this.state=e,this.transactions=i,this.flags=0,this.startState=t.state,this.changes=et.empty(this.startState.doc.length);for(let r of i)this.changes=this.changes.compose(r.changes);let n=[];this.changes.iterChangedRanges((r,o,l,a)=>n.push(new It(r,o,l,a))),this.changedRanges=n}static create(t,e,i){return new bn(t,e,i)}get viewportChanged(){return(this.flags&4)>0}get heightChanged(){return(this.flags&2)>0}get geometryChanged(){return this.docChanged||(this.flags&10)>0}get focusChanged(){return(this.flags&1)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(t=>t.selection)}get empty(){return this.flags==0&&this.transactions.length==0}}class no extends ${get length(){return this.view.state.doc.length}constructor(t){super(),this.view=t,this.decorations=[],this.dynamicDecorationMap=[!1],this.domChanged=null,this.hasComposition=null,this.markedForComposition=new Set,this.editContextFormatting=P.none,this.lastCompositionAfterCursor=!1,this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.setDOM(t.contentDOM),this.children=[new Q],this.children[0].setParent(this),this.updateDeco(),this.updateInner([new It(0,0,0,t.state.doc.length)],0,null)}update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.every(({fromA:c,toA:h})=>hthis.minWidthTo)?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.updateEditContextFormatting(t);let n=-1;this.view.inputState.composing>=0&&!this.view.observer.editContext&&(!((e=this.domChanged)===null||e===void 0)&&e.newSel?n=this.domChanged.newSel.head:!ff(t.changes,this.hasComposition)&&!t.selectionSet&&(n=t.state.selection.main.head));let r=n>-1?of(this.view,t.changes,n):null;if(this.domChanged=null,this.hasComposition){this.markedForComposition.clear();let{from:c,to:h}=this.hasComposition;i=new It(c,h,t.changes.mapPos(c,-1),t.changes.mapPos(h,1)).addToSet(i.slice())}this.hasComposition=r?{from:r.range.fromB,to:r.range.toB}:null,(D.ie||D.chrome)&&!r&&t&&t.state.doc.lines!=t.startState.doc.lines&&(this.forceSelection=!0);let o=this.decorations,l=this.updateDeco(),a=hf(o,l,t.changes);return i=It.extendWithRanges(i,a),!(this.flags&7)&&i.length==0?!1:(this.updateInner(i,t.startState.doc.length,r),t.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(t,e,i){this.view.viewState.mustMeasureContent=!0,this.updateChildren(t,e,i);let{observer:n}=this.view;n.ignore(()=>{this.dom.style.height=this.view.viewState.contentHeight/this.view.scaleY+"px",this.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let o=D.chrome||D.ios?{node:n.selectionRange.focusNode,written:!1}:void 0;this.sync(this.view,o),this.flags&=-8,o&&(o.written||n.selectionRange.focusNode!=o.node)&&(this.forceSelection=!0),this.dom.style.height=""}),this.markedForComposition.forEach(o=>o.flags&=-9);let r=[];if(this.view.viewport.from||this.view.viewport.to=0?n[o]:null;if(!l)break;let{fromA:a,toA:c,fromB:h,toB:f}=l,u,d,p,g;if(i&&i.range.fromBh){let S=bi.build(this.view.state.doc,h,i.range.fromB,this.decorations,this.dynamicDecorationMap),w=bi.build(this.view.state.doc,i.range.toB,f,this.decorations,this.dynamicDecorationMap);d=S.breakAtStart,p=S.openStart,g=w.openEnd;let A=this.compositionView(i);w.breakAtStart?A.breakAfter=1:w.content.length&&A.merge(A.length,A.length,w.content[0],!1,w.openStart,0)&&(A.breakAfter=w.content[0].breakAfter,w.content.shift()),S.content.length&&A.merge(0,0,S.content[S.content.length-1],!0,0,S.openEnd)&&S.content.pop(),u=S.content.concat(A).concat(w.content)}else({content:u,breakAtStart:d,openStart:p,openEnd:g}=bi.build(this.view.state.doc,h,f,this.decorations,this.dynamicDecorationMap));let{i:m,off:y}=r.findPos(c,1),{i:x,off:v}=r.findPos(a,-1);Ul(this,x,v,m,y,u,d,p,g)}i&&this.fixCompositionDOM(i)}updateEditContextFormatting(t){this.editContextFormatting=this.editContextFormatting.map(t.changes);for(let e of t.transactions)for(let i of e.effects)i.is(pa)&&(this.editContextFormatting=i.value)}compositionView(t){let e=new Vt(t.text.nodeValue);e.flags|=8;for(let{deco:n}of t.marks)e=new ne(n,[e],e.length);let i=new Q;return i.append(e,0),i}fixCompositionDOM(t){let e=(r,o)=>{o.flags|=8|(o.children.some(a=>a.flags&7)?1:0),this.markedForComposition.add(o);let l=$.get(r);l&&l!=o&&(l.dom=null),o.setDOM(r)},i=this.childPos(t.range.fromB,1),n=this.children[i.i];e(t.line,n);for(let r=t.marks.length-1;r>=-1;r--)i=n.childPos(i.off,1),n=n.children[i.i],e(r>=0?t.marks[r].node:t.text,n)}updateSelection(t=!1,e=!1){(t||!this.view.observer.selectionRange.focusNode)&&this.view.observer.readSelectionRange();let i=this.view.root.activeElement,n=i==this.dom,r=!n&&an(this.dom,this.view.observer.selectionRange)&&!(i&&this.dom.contains(i));if(!(n||e||r))return;let o=this.forceSelection;this.forceSelection=!1;let l=this.view.state.selection.main,a=this.moveToLine(this.domAtPos(l.anchor)),c=l.empty?a:this.moveToLine(this.domAtPos(l.head));if(D.gecko&&l.empty&&!this.hasComposition&&rf(a)){let f=document.createTextNode("");this.view.observer.ignore(()=>a.node.insertBefore(f,a.node.childNodes[a.offset]||null)),a=c=new ct(f,0),o=!0}let h=this.view.observer.selectionRange;(o||!h.focusNode||(!yi(a.node,a.offset,h.anchorNode,h.anchorOffset)||!yi(c.node,c.offset,h.focusNode,h.focusOffset))&&!this.suppressWidgetCursorChange(h,l))&&(this.view.observer.ignore(()=>{D.android&&D.chrome&&this.dom.contains(h.focusNode)&&cf(h.focusNode,this.dom)&&(this.dom.blur(),this.dom.focus({preventScroll:!0}));let f=vi(this.view.root);if(f)if(l.empty){if(D.gecko){let u=lf(a.node,a.offset);if(u&&u!=3){let d=(u==1?Kl:$l)(a.node,a.offset);d&&(a=new ct(d.node,d.offset))}}f.collapse(a.node,a.offset),l.bidiLevel!=null&&f.caretBidiLevel!==void 0&&(f.caretBidiLevel=l.bidiLevel)}else if(f.extend){f.collapse(a.node,a.offset);try{f.extend(c.node,c.offset)}catch{}}else{let u=document.createRange();l.anchor>l.head&&([a,c]=[c,a]),u.setEnd(c.node,c.offset),u.setStart(a.node,a.offset),f.removeAllRanges(),f.addRange(u)}r&&this.view.root.activeElement==this.dom&&(this.dom.blur(),i&&i.focus())}),this.view.observer.setSelectionRange(a,c)),this.impreciseAnchor=a.precise?null:new ct(h.anchorNode,h.anchorOffset),this.impreciseHead=c.precise?null:new ct(h.focusNode,h.focusOffset)}suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&yi(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset)&&this.posFromDOM(t.focusNode,t.focusOffset)==e.head}enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=t.state.selection.main,i=vi(t.root),{anchorNode:n,anchorOffset:r}=t.observer.selectionRange;if(!i||!e.empty||!e.assoc||!i.modify)return;let o=Q.find(this,e.head);if(!o)return;let l=o.posAtStart;if(e.head==l||e.head==l+o.length)return;let a=this.coordsAt(e.head,-1),c=this.coordsAt(e.head,1);if(!a||!c||a.bottom>c.top)return;let h=this.domAtPos(e.head+e.assoc);i.collapse(h.node,h.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let f=t.observer.selectionRange;t.docView.posFromDOM(f.anchorNode,f.anchorOffset)!=e.from&&i.collapse(n,r)}moveToLine(t){let e=this.dom,i;if(t.node!=e)return t;for(let n=t.offset;!i&&n=0;n--){let r=$.get(e.childNodes[n]);r instanceof Q&&(i=r.domAtPos(r.length))}return i?new ct(i.node,i.offset,!0):t}nearest(t){for(let e=t;e;){let i=$.get(e);if(i&&i.rootView==this)return i;e=e.parentNode}return null}posFromDOM(t,e){let i=this.nearest(t);if(!i)throw new RangeError("Trying to find position for a DOM position outside of the document");return i.localPosFromDOM(t,e)+i.posAtStart}domAtPos(t){let{i:e,off:i}=this.childCursor().findPos(t,-1);for(;e=0;o--){let l=this.children[o],a=r-l.breakAfter,c=a-l.length;if(at||l.covers(1))&&(!i||l instanceof Q&&!(i instanceof Q&&e>=0)))i=l,n=c;else if(i&&c==t&&a==t&&l instanceof te&&Math.abs(e)<2){if(l.deco.startSide<0)break;o&&(i=null)}r=c}return i?i.coordsAt(t-n,e):null}coordsForChar(t){let{i:e,off:i}=this.childPos(t,1),n=this.children[e];if(!(n instanceof Q))return null;for(;n.children.length;){let{i:l,off:a}=n.childPos(i,1);for(;;l++){if(l==n.children.length)return null;if((n=n.children[l]).length)break}i=a}if(!(n instanceof Vt))return null;let r=ot(n.text,i);if(r==i)return null;let o=Te(n.dom,i,r).getClientRects();for(let l=0;lMath.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,l=-1,a=this.view.textDirection==X.LTR;for(let c=0,h=0;hn)break;if(c>=i){let d=f.dom.getBoundingClientRect();if(e.push(d.height),o){let p=f.dom.lastChild,g=p?Ge(p):[];if(g.length){let m=g[g.length-1],y=a?m.right-d.left:d.right-m.left;y>l&&(l=y,this.minWidth=r,this.minWidthFrom=c,this.minWidthTo=u)}}}c=u+f.breakAfter}return e}textDirectionAt(t){let{i:e}=this.childPos(t,1);return getComputedStyle(this.children[e].dom).direction=="rtl"?X.RTL:X.LTR}measureTextSize(){for(let r of this.children)if(r instanceof Q){let o=r.measureTextSize();if(o)return o}let t=document.createElement("div"),e,i,n;return t.className="cm-line",t.style.width="99999px",t.style.position="absolute",t.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.dom.appendChild(t);let r=Ge(t.firstChild)[0];e=t.getBoundingClientRect().height,i=r?r.width/27:7,n=r?r.height:e,t.remove()}),{lineHeight:e,charWidth:i,textHeight:n}}childCursor(t=this.length){let e=this.children.length;return e&&(t-=this.children[--e].length),new jl(this.children,t,e)}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;n++){let r=n==e.viewports.length?null:e.viewports[n],o=r?r.from-1:this.length;if(o>i){let l=(e.lineBlockAt(o).bottom-e.lineBlockAt(i).top)/this.view.scaleY;t.push(P.replace({widget:new so(l),block:!0,inclusive:!0,isBlockGap:!0}).range(i,o))}if(!r)break;i=r.to+1}return P.set(t)}updateDeco(){let t=1,e=this.view.state.facet(ki).map(r=>(this.dynamicDecorationMap[t++]=typeof r=="function")?r(this.view):r),i=!1,n=this.view.state.facet(ma).map((r,o)=>{let l=typeof r=="function";return l&&(i=!0),l?r(this.view):r});for(n.length&&(this.dynamicDecorationMap[t++]=i,e.push(K.join(n))),this.decorations=[this.editContextFormatting,...e,this.computeBlockGapDeco(),this.view.viewState.lineGapDeco];te.anchor?-1:1),n;if(!i)return;!e.empty&&(n=this.coordsAt(e.anchor,e.anchor>e.head?-1:1))&&(i={left:Math.min(i.left,n.left),top:Math.min(i.top,n.top),right:Math.max(i.right,n.right),bottom:Math.max(i.bottom,n.bottom)});let r=xa(this.view),o={left:i.left-r.left,top:i.top-r.top,right:i.right+r.right,bottom:i.bottom+r.bottom},{offsetWidth:l,offsetHeight:a}=this.view.scrollDOM;Vc(this.view.scrollDOM,o,e.head{it.from&&(e=!0)}),e}function uf(s,t,e=1){let i=s.charCategorizer(t),n=s.doc.lineAt(t),r=t-n.from;if(n.length==0)return b.cursor(t);r==0?e=1:r==n.length&&(e=-1);let o=r,l=r;e<0?o=ot(n.text,r,!1):l=ot(n.text,r);let a=i(n.text.slice(o,l));for(;o>0;){let c=ot(n.text,o,!1);if(i(n.text.slice(c,o))!=a)break;o=c}for(;ls?t.left-s:Math.max(0,s-t.right)}function pf(s,t){return t.top>s?t.top-s:Math.max(0,s-t.bottom)}function Xn(s,t){return s.topt.top+1}function ro(s,t){return ts.bottom?{top:s.top,left:s.left,right:s.right,bottom:t}:s}function Fs(s,t,e){let i,n,r,o,l=!1,a,c,h,f;for(let p=s.firstChild;p;p=p.nextSibling){let g=Ge(p);for(let m=0;mv||o==v&&r>x){i=p,n=y,r=x,o=v;let S=v?e0?m0)}x==0?e>y.bottom&&(!h||h.bottomy.top)&&(c=p,f=y):h&&Xn(h,y)?h=oo(h,y.bottom):f&&Xn(f,y)&&(f=ro(f,y.top))}}if(h&&h.bottom>=e?(i=a,n=h):f&&f.top<=e&&(i=c,n=f),!i)return{node:s,offset:0};let u=Math.max(n.left,Math.min(n.right,t));if(i.nodeType==3)return lo(i,u,e);if(l&&i.contentEditable!="false")return Fs(i,u,e);let d=Array.prototype.indexOf.call(s.childNodes,i)+(t>=(n.left+n.right)/2?1:0);return{node:s,offset:d}}function lo(s,t,e){let i=s.nodeValue.length,n=-1,r=1e9,o=0;for(let l=0;le?h.top-e:e-h.bottom)-1;if(h.left-1<=t&&h.right+1>=t&&f=(h.left+h.right)/2,d=u;if((D.chrome||D.gecko)&&Te(s,l).getBoundingClientRect().left==h.right&&(d=!u),f<=0)return{node:s,offset:l+(d?1:0)};n=l+(d?1:0),r=f}}}return{node:s,offset:n>-1?n:o>0?s.nodeValue.length:0}}function Sa(s,t,e,i=-1){var n,r;let o=s.contentDOM.getBoundingClientRect(),l=o.top+s.viewState.paddingTop,a,{docHeight:c}=s.viewState,{x:h,y:f}=t,u=f-l;if(u<0)return 0;if(u>c)return s.state.doc.length;for(let S=s.viewState.heightOracle.textHeight/2,w=!1;a=s.elementAtHeight(u),a.type!=Ot.Text;)for(;u=i>0?a.bottom+S:a.top-S,!(u>=0&&u<=c);){if(w)return e?null:0;w=!0,i=-i}f=l+u;let d=a.from;if(ds.viewport.to)return s.viewport.to==s.state.doc.length?s.state.doc.length:e?null:ao(s,o,a,h,f);let p=s.dom.ownerDocument,g=s.root.elementFromPoint?s.root:p,m=g.elementFromPoint(h,f);m&&!s.contentDOM.contains(m)&&(m=null),m||(h=Math.max(o.left+1,Math.min(o.right-1,h)),m=g.elementFromPoint(h,f),m&&!s.contentDOM.contains(m)&&(m=null));let y,x=-1;if(m&&((n=s.docView.nearest(m))===null||n===void 0?void 0:n.isEditable)!=!1){if(p.caretPositionFromPoint){let S=p.caretPositionFromPoint(h,f);S&&({offsetNode:y,offset:x}=S)}else if(p.caretRangeFromPoint){let S=p.caretRangeFromPoint(h,f);S&&({startContainer:y,startOffset:x}=S,(!s.contentDOM.contains(y)||D.safari&&gf(y,x,h)||D.chrome&&mf(y,x,h))&&(y=void 0))}}if(!y||!s.docView.dom.contains(y)){let S=Q.find(s.docView,d);if(!S)return u>a.top+a.height/2?a.to:a.from;({node:y,offset:x}=Fs(S.dom,h,f))}let v=s.docView.nearest(y);if(!v)return null;if(v.isWidget&&((r=v.dom)===null||r===void 0?void 0:r.nodeType)==1){let S=v.dom.getBoundingClientRect();return t.ys.defaultLineHeight*1.5){let l=s.viewState.heightOracle.textHeight,a=Math.floor((n-e.top-(s.defaultLineHeight-l)*.5)/l);r+=a*s.viewState.heightOracle.lineLength}let o=s.state.sliceDoc(e.from,e.to);return e.from+ks(o,r,s.state.tabSize)}function gf(s,t,e){let i;if(s.nodeType!=3||t!=(i=s.nodeValue.length))return!1;for(let n=s.nextSibling;n;n=n.nextSibling)if(n.nodeType!=1||n.nodeName!="BR")return!1;return Te(s,i-1,i).getBoundingClientRect().left>e}function mf(s,t,e){if(t!=0)return!1;for(let n=s;;){let r=n.parentNode;if(!r||r.nodeType!=1||r.firstChild!=n)return!1;if(r.classList.contains("cm-line"))break;n=r}let i=s.nodeType==1?s.getBoundingClientRect():Te(s,0,Math.max(s.nodeValue.length,1)).getBoundingClientRect();return e-i.left>5}function Vs(s,t){let e=s.lineBlockAt(t);if(Array.isArray(e.type)){for(let i of e.type)if(i.to>t||i.to==t&&(i.to==e.to||i.type==Ot.Text))return i}return e}function yf(s,t,e,i){let n=Vs(s,t.head),r=!i||n.type!=Ot.Text||!(s.lineWrapping||n.widgetLineBreaks)?null:s.coordsAtPos(t.assoc<0&&t.head>n.from?t.head-1:t.head);if(r){let o=s.dom.getBoundingClientRect(),l=s.textDirectionAt(n.from),a=s.posAtCoords({x:e==(l==X.LTR)?o.right-1:o.left+1,y:(r.top+r.bottom)/2});if(a!=null)return b.cursor(a,e?-1:1)}return b.cursor(e?n.to:n.from,e?-1:1)}function ho(s,t,e,i){let n=s.state.doc.lineAt(t.head),r=s.bidiSpans(n),o=s.textDirectionAt(n.from);for(let l=t,a=null;;){let c=ef(n,r,o,l,e),h=sa;if(!c){if(n.number==(e?s.state.doc.lines:1))return l;h=` -`,n=s.state.doc.line(n.number+(e?1:-1)),r=s.bidiSpans(n),c=s.visualLineSide(n,!e)}if(a){if(!a(h))return l}else{if(!i)return c;a=i(h)}l=c}}function bf(s,t,e){let i=s.state.charCategorizer(t),n=i(e);return r=>{let o=i(r);return n==G.Space&&(n=o),n==o}}function xf(s,t,e,i){let n=t.head,r=e?1:-1;if(n==(e?s.state.doc.length:0))return b.cursor(n,t.assoc);let o=t.goalColumn,l,a=s.contentDOM.getBoundingClientRect(),c=s.coordsAtPos(n,t.assoc||-1),h=s.documentTop;if(c)o==null&&(o=c.left-a.left),l=r<0?c.top:c.bottom;else{let d=s.viewState.lineBlockAt(n);o==null&&(o=Math.min(a.right-a.left,s.defaultCharacterWidth*(n-d.from))),l=(r<0?d.top:d.bottom)+h}let f=a.left+o,u=i??s.viewState.heightOracle.textHeight>>1;for(let d=0;;d+=10){let p=l+(u+d)*r,g=Sa(s,{x:f,y:p},!1,r);if(pa.bottom||(r<0?gn)){let m=s.docView.coordsForChar(g),y=!m||p{if(t>r&&tn(s)),e.from,t.head>e.from?-1:1);return i==e.from?e:b.cursor(i,inull),D.gecko&&Nf(t.contentDOM.ownerDocument)}handleEvent(t){!Of(this.view,t)||this.ignoreDuringComposition(t)||t.type=="keydown"&&this.keydown(t)||this.runHandlers(t.type,t)}runHandlers(t,e){let i=this.handlers[t];if(i){for(let n of i.observers)n(this.view,e);for(let n of i.handlers){if(e.defaultPrevented)break;if(n(this.view,e)){e.preventDefault();break}}}}ensureHandlers(t){let e=Sf(t),i=this.handlers,n=this.view.contentDOM;for(let r in e)if(r!="scroll"){let o=!e[r].handlers.length,l=i[r];l&&o!=!l.handlers.length&&(n.removeEventListener(r,this.handleEvent),l=null),l||n.addEventListener(r,this.handleEvent,{passive:o})}for(let r in i)r!="scroll"&&!e[r]&&n.removeEventListener(r,this.handleEvent);this.handlers=e}keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),t.keyCode==9&&this.tabFocusMode>-1&&(!this.tabFocusMode||Date.now()<=this.tabFocusMode))return!0;if(this.tabFocusMode>0&&t.keyCode!=27&&ka.indexOf(t.keyCode)<0&&(this.tabFocusMode=-1),D.android&&D.chrome&&!t.synthetic&&(t.keyCode==13||t.keyCode==8))return this.view.observer.delayAndroidKey(t.key,t.keyCode),!0;let e;return D.ios&&!t.synthetic&&!t.altKey&&!t.metaKey&&((e=va.find(i=>i.keyCode==t.keyCode))&&!t.ctrlKey||vf.indexOf(t.key)>-1&&t.ctrlKey&&!t.shiftKey)?(this.pendingIOSKey=e||t,setTimeout(()=>this.flushIOSKey(),250),!0):(t.keyCode!=229&&this.view.observer.forceFlush(),!1)}flushIOSKey(t){let e=this.pendingIOSKey;return!e||e.key=="Enter"&&t&&t.from0?!0:D.safari&&!D.ios&&this.compositionPendingKey&&Date.now()-this.compositionEndedAt<100?(this.compositionPendingKey=!1,!0):!1:!1}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.view.observer.update(t),this.mouseSelection&&this.mouseSelection.update(t),this.draggedContent&&t.docChanged&&(this.draggedContent=this.draggedContent.map(t.changes)),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}function co(s,t){return(e,i)=>{try{return t.call(s,i,e)}catch(n){Dt(e.state,n)}}}function Sf(s){let t=Object.create(null);function e(i){return t[i]||(t[i]={observers:[],handlers:[]})}for(let i of s){let n=i.spec;if(n&&n.domEventHandlers)for(let r in n.domEventHandlers){let o=n.domEventHandlers[r];o&&e(r).handlers.push(co(i.value,o))}if(n&&n.domEventObservers)for(let r in n.domEventObservers){let o=n.domEventObservers[r];o&&e(r).observers.push(co(i.value,o))}}for(let i in Wt)e(i).handlers.push(Wt[i]);for(let i in Nt)e(i).observers.push(Nt[i]);return t}const va=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Enter",keyCode:13,inputType:"insertLineBreak"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],vf="dthko",ka=[16,17,18,20,91,92,224,225],Ki=6;function $i(s){return Math.max(0,s)*.7+8}function kf(s,t){return Math.max(Math.abs(s.clientX-t.clientX),Math.abs(s.clientY-t.clientY))}class Cf{constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i,this.mustSelect=n,this.scrollSpeed={x:0,y:0},this.scrolling=-1,this.lastEvent=e,this.scrollParent=Wc(t.contentDOM),this.atoms=t.state.facet(dr).map(o=>o(t));let r=t.contentDOM.ownerDocument;r.addEventListener("mousemove",this.move=this.move.bind(this)),r.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(H.allowMultipleSelections)&&Af(t,e),this.dragging=Df(t,e)&&Da(e)==1?null:!1}start(t){this.dragging===!1&&this.select(t)}move(t){var e;if(t.buttons==0)return this.destroy();if(this.dragging||this.dragging==null&&kf(this.startEvent,t)<10)return;this.select(this.lastEvent=t);let i=0,n=0,r=((e=this.scrollParent)===null||e===void 0?void 0:e.getBoundingClientRect())||{left:0,top:0,right:this.view.win.innerWidth,bottom:this.view.win.innerHeight},o=xa(this.view);t.clientX-o.left<=r.left+Ki?i=-$i(r.left-t.clientX):t.clientX+o.right>=r.right-Ki&&(i=$i(t.clientX-r.right)),t.clientY-o.top<=r.top+Ki?n=-$i(r.top-t.clientY):t.clientY+o.bottom>=r.bottom-Ki&&(n=$i(t.clientY-r.bottom)),this.setScrollSpeed(i,n)}up(t){this.dragging==null&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=this.view.inputState.draggedContent=null}setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&(this.scrolling=setInterval(()=>this.scroll(),50)):this.scrolling>-1&&(clearInterval(this.scrolling),this.scrolling=-1)}scroll(){this.scrollParent?(this.scrollParent.scrollLeft+=this.scrollSpeed.x,this.scrollParent.scrollTop+=this.scrollSpeed.y):this.view.win.scrollBy(this.scrollSpeed.x,this.scrollSpeed.y),this.dragging===!1&&this.select(this.lastEvent)}skipAtoms(t){let e=null;for(let i=0;ie.isUserEvent("input.type"))?this.destroy():this.style.update(t)&&setTimeout(()=>this.select(this.lastEvent),20)}}function Af(s,t){let e=s.state.facet(ra);return e.length?e[0](t):D.mac?t.metaKey:t.ctrlKey}function Mf(s,t){let e=s.state.facet(oa);return e.length?e[0](t):D.mac?!t.altKey:!t.ctrlKey}function Df(s,t){let{main:e}=s.state.selection;if(e.empty)return!1;let i=vi(s.root);if(!i||i.rangeCount==0)return!0;let n=i.getRangeAt(0).getClientRects();for(let r=0;r=t.clientX&&o.top<=t.clientY&&o.bottom>=t.clientY)return!0}return!1}function Of(s,t){if(!t.bubbles)return!0;if(t.defaultPrevented)return!1;for(let e=t.target,i;e!=s.contentDOM;e=e.parentNode)if(!e||e.nodeType==11||(i=$.get(e))&&i.ignoreEvent(t))return!1;return!0}const Wt=Object.create(null),Nt=Object.create(null),Ca=D.ie&&D.ie_version<15||D.ios&&D.webkit_version<604;function Tf(s){let t=s.dom.parentNode;if(!t)return;let e=t.appendChild(document.createElement("textarea"));e.style.cssText="position: fixed; left: -10000px; top: 10px",e.focus(),setTimeout(()=>{s.focus(),e.remove(),Aa(s,e.value)},50)}function Aa(s,t){let{state:e}=s,i,n=1,r=e.toText(t),o=r.lines==e.selection.ranges.length;if(Ws!=null&&e.selection.ranges.every(a=>a.empty)&&Ws==r.toString()){let a=-1;i=e.changeByRange(c=>{let h=e.doc.lineAt(c.from);if(h.from==a)return{range:c};a=h.from;let f=e.toText((o?r.line(n++).text:t)+e.lineBreak);return{changes:{from:h.from,insert:f},range:b.cursor(c.from+f.length)}})}else o?i=e.changeByRange(a=>{let c=r.line(n++);return{changes:{from:a.from,to:a.to,insert:c.text},range:b.cursor(a.from+c.length)}}):i=e.replaceSelection(r);s.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}Nt.scroll=s=>{s.inputState.lastScrollTop=s.scrollDOM.scrollTop,s.inputState.lastScrollLeft=s.scrollDOM.scrollLeft};Wt.keydown=(s,t)=>(s.inputState.setSelectionOrigin("select"),t.keyCode==27&&s.inputState.tabFocusMode!=0&&(s.inputState.tabFocusMode=Date.now()+2e3),!1);Nt.touchstart=(s,t)=>{s.inputState.lastTouchTime=Date.now(),s.inputState.setSelectionOrigin("select.pointer")};Nt.touchmove=s=>{s.inputState.setSelectionOrigin("select.pointer")};Wt.mousedown=(s,t)=>{if(s.observer.flush(),s.inputState.lastTouchTime>Date.now()-2e3)return!1;let e=null;for(let i of s.state.facet(la))if(e=i(s,t),e)break;if(!e&&t.button==0&&(e=Rf(s,t)),e){let i=!s.hasFocus;s.inputState.startMouseSelection(new Cf(s,t,e,i)),i&&s.observer.ignore(()=>{Hl(s.contentDOM);let r=s.root.activeElement;r&&!r.contains(s.contentDOM)&&r.blur()});let n=s.inputState.mouseSelection;if(n)return n.start(t),n.dragging===!1}return!1};function fo(s,t,e,i){if(i==1)return b.cursor(t,e);if(i==2)return uf(s.state,t,e);{let n=Q.find(s.docView,t),r=s.state.doc.lineAt(n?n.posAtEnd:t),o=n?n.posAtStart:r.from,l=n?n.posAtEnd:r.to;return ls>=t.top&&s<=t.bottom,uo=(s,t,e)=>Ma(t,e)&&s>=e.left&&s<=e.right;function Pf(s,t,e,i){let n=Q.find(s.docView,t);if(!n)return 1;let r=t-n.posAtStart;if(r==0)return 1;if(r==n.length)return-1;let o=n.coordsAt(r,-1);if(o&&uo(e,i,o))return-1;let l=n.coordsAt(r,1);return l&&uo(e,i,l)?1:o&&Ma(i,o)?-1:1}function po(s,t){let e=s.posAtCoords({x:t.clientX,y:t.clientY},!1);return{pos:e,bias:Pf(s,e,t.clientX,t.clientY)}}const Bf=D.ie&&D.ie_version<=11;let go=null,mo=0,yo=0;function Da(s){if(!Bf)return s.detail;let t=go,e=yo;return go=s,yo=Date.now(),mo=!t||e>Date.now()-400&&Math.abs(t.clientX-s.clientX)<2&&Math.abs(t.clientY-s.clientY)<2?(mo+1)%3:1}function Rf(s,t){let e=po(s,t),i=Da(t),n=s.state.selection;return{update(r){r.docChanged&&(e.pos=r.changes.mapPos(e.pos),n=n.map(r.changes))},get(r,o,l){let a=po(s,r),c,h=fo(s,a.pos,a.bias,i);if(e.pos!=a.pos&&!o){let f=fo(s,e.pos,e.bias,i),u=Math.min(f.from,h.from),d=Math.max(f.to,h.to);h=u1&&(c=Lf(n,a.pos))?c:l?n.addRange(h):b.create([h])}}}function Lf(s,t){for(let e=0;e=t)return b.create(s.ranges.slice(0,e).concat(s.ranges.slice(e+1)),s.mainIndex==e?0:s.mainIndex-(s.mainIndex>e?1:0))}return null}Wt.dragstart=(s,t)=>{let{selection:{main:e}}=s.state;if(t.target.draggable){let n=s.docView.nearest(t.target);if(n&&n.isWidget){let r=n.posAtStart,o=r+n.length;(r>=e.to||o<=e.from)&&(e=b.range(r,o))}}let{inputState:i}=s;return i.mouseSelection&&(i.mouseSelection.dragging=!0),i.draggedContent=e,t.dataTransfer&&(t.dataTransfer.setData("Text",s.state.sliceDoc(e.from,e.to)),t.dataTransfer.effectAllowed="copyMove"),!1};Wt.dragend=s=>(s.inputState.draggedContent=null,!1);function bo(s,t,e,i){if(!e)return;let n=s.posAtCoords({x:t.clientX,y:t.clientY},!1),{draggedContent:r}=s.inputState,o=i&&r&&Mf(s,t)?{from:r.from,to:r.to}:null,l={from:n,insert:e},a=s.state.changes(o?[o,l]:l);s.focus(),s.dispatch({changes:a,selection:{anchor:a.mapPos(n,-1),head:a.mapPos(n,1)},userEvent:o?"move.drop":"input.drop"}),s.inputState.draggedContent=null}Wt.drop=(s,t)=>{if(!t.dataTransfer)return!1;if(s.state.readOnly)return!0;let e=t.dataTransfer.files;if(e&&e.length){let i=Array(e.length),n=0,r=()=>{++n==e.length&&bo(s,t,i.filter(o=>o!=null).join(s.state.lineBreak),!1)};for(let o=0;o{/[\x00-\x08\x0e-\x1f]{2}/.test(l.result)||(i[o]=l.result),r()},l.readAsText(e[o])}return!0}else{let i=t.dataTransfer.getData("Text");if(i)return bo(s,t,i,!0),!0}return!1};Wt.paste=(s,t)=>{if(s.state.readOnly)return!0;s.observer.flush();let e=Ca?null:t.clipboardData;return e?(Aa(s,e.getData("text/plain")||e.getData("text/uri-list")),!0):(Tf(s),!1)};function Ef(s,t){let e=s.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.value=t,i.focus(),i.selectionEnd=t.length,i.selectionStart=0,setTimeout(()=>{i.remove(),s.focus()},50)}function If(s){let t=[],e=[],i=!1;for(let n of s.selection.ranges)n.empty||(t.push(s.sliceDoc(n.from,n.to)),e.push(n));if(!t.length){let n=-1;for(let{from:r}of s.selection.ranges){let o=s.doc.lineAt(r);o.number>n&&(t.push(o.text),e.push({from:o.from,to:Math.min(s.doc.length,o.to+1)})),n=o.number}i=!0}return{text:t.join(s.lineBreak),ranges:e,linewise:i}}let Ws=null;Wt.copy=Wt.cut=(s,t)=>{let{text:e,ranges:i,linewise:n}=If(s.state);if(!e&&!n)return!1;Ws=n?e:null,t.type=="cut"&&!s.state.readOnly&&s.dispatch({changes:i,scrollIntoView:!0,userEvent:"delete.cut"});let r=Ca?null:t.clipboardData;return r?(r.clearData(),r.setData("text/plain",e),!0):(Ef(s,e),!1)};const Oa=se.define();function Ta(s,t){let e=[];for(let i of s.facet(ca)){let n=i(s,t);n&&e.push(n)}return e?s.update({effects:e,annotations:Oa.of(!0)}):null}function Pa(s){setTimeout(()=>{let t=s.hasFocus;if(t!=s.inputState.notifiedFocused){let e=Ta(s.state,t);e?s.dispatch(e):s.update([])}},10)}Nt.focus=s=>{s.inputState.lastFocusTime=Date.now(),!s.scrollDOM.scrollTop&&(s.inputState.lastScrollTop||s.inputState.lastScrollLeft)&&(s.scrollDOM.scrollTop=s.inputState.lastScrollTop,s.scrollDOM.scrollLeft=s.inputState.lastScrollLeft),Pa(s)};Nt.blur=s=>{s.observer.clearSelectionRange(),Pa(s)};Nt.compositionstart=Nt.compositionupdate=s=>{s.observer.editContext||(s.inputState.compositionFirstChange==null&&(s.inputState.compositionFirstChange=!0),s.inputState.composing<0&&(s.inputState.composing=0))};Nt.compositionend=s=>{s.observer.editContext||(s.inputState.composing=-1,s.inputState.compositionEndedAt=Date.now(),s.inputState.compositionPendingKey=!0,s.inputState.compositionPendingChange=s.observer.pendingRecords().length>0,s.inputState.compositionFirstChange=null,D.chrome&&D.android?s.observer.flushSoon():s.inputState.compositionPendingChange?Promise.resolve().then(()=>s.observer.flush()):setTimeout(()=>{s.inputState.composing<0&&s.docView.hasComposition&&s.update([])},50))};Nt.contextmenu=s=>{s.inputState.lastContextMenu=Date.now()};Wt.beforeinput=(s,t)=>{var e;let i;if(D.chrome&&D.android&&(i=va.find(n=>n.inputType==t.inputType))&&(s.observer.delayAndroidKey(i.key,i.keyCode),i.key=="Backspace"||i.key=="Delete")){let n=((e=window.visualViewport)===null||e===void 0?void 0:e.height)||0;setTimeout(()=>{var r;(((r=window.visualViewport)===null||r===void 0?void 0:r.height)||0)>n+10&&s.hasFocus&&(s.contentDOM.blur(),s.focus())},100)}return D.ios&&t.inputType=="deleteContentForward"&&s.observer.flushSoon(),D.safari&&t.inputType=="insertText"&&s.inputState.composing>=0&&setTimeout(()=>Nt.compositionend(s,t),20),!1};const xo=new Set;function Nf(s){xo.has(s)||(xo.add(s),s.addEventListener("copy",()=>{}),s.addEventListener("cut",()=>{}))}const wo=["pre-wrap","normal","pre-line","break-spaces"];class Ff{constructor(t){this.lineWrapping=t,this.doc=V.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.textHeight=14,this.lineLength=30,this.heightChanged=!1}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.max(0,Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength))),this.lineHeight*i}heightForLine(t){return this.lineWrapping?(1+Math.max(0,Math.ceil((t-this.lineLength)/(this.lineLength-5))))*this.lineHeight:this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return wo.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,a=Math.round(e)!=Math.round(this.lineHeight)||this.lineWrapping!=l;if(this.lineWrapping=l,this.lineHeight=e,this.charWidth=i,this.textHeight=n,this.lineLength=r,a){this.heightSamples={};for(let c=0;c0}set outdated(t){this.flags=(t?2:0)|this.flags&-3}setHeight(t,e){this.height!=e&&(Math.abs(this.height-e)>cn&&(t.heightChanged=!0),this.height=e)}replace(t,e,i){return pt.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,n){let r=this,o=i.doc;for(let l=n.length-1;l>=0;l--){let{fromA:a,toA:c,fromB:h,toB:f}=n[l],u=r.lineAt(a,U.ByPosNoHeight,i.setDoc(e),0,0),d=u.to>=c?u:r.lineAt(c,U.ByPosNoHeight,i,0,0);for(f+=d.to-c,c=d.to;l>0&&u.from<=n[l-1].toA;)a=n[l-1].fromA,h=n[l-1].fromB,l--,ar*2){let l=t[e-1];l.break?t.splice(--e,1,l.left,null,l.right):t.splice(--e,1,l.left,l.right),i+=1+l.break,n-=l.size}else if(r>n*2){let l=t[i];l.break?t.splice(i,1,l.left,null,l.right):t.splice(i,1,l.left,l.right),i+=2+l.break,r-=l.size}else break;else if(n=r&&o(this.blockAt(0,i,n,r))}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setHeight(t,n.heights[n.index++]),this.outdated=!1,this}toString(){return`block(${this.length})`}}class At extends Ba{constructor(t,e){super(t,e,null),this.collapsed=0,this.widgetHeight=0,this.breaks=0}blockAt(t,e,i,n){return new Jt(n,this.length,i,this.height,this.breaks)}replace(t,e,i){let n=i[0];return i.length==1&&(n instanceof At||n instanceof it&&n.flags&4)&&Math.abs(this.length-n.length)<10?(n instanceof it?n=new At(n.length,this.height):n.height=this.height,this.outdated||(n.outdated=!1),n):pt.of(i)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setHeight(t,n.heights[n.index++]):(i||this.outdated)&&this.setHeight(t,Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))+this.breaks*t.lineHeight),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class it extends pt{constructor(t){super(t,0)}heightMetrics(t,e){let i=t.doc.lineAt(e).number,n=t.doc.lineAt(e+this.length).number,r=n-i+1,o,l=0;if(t.lineWrapping){let a=Math.min(this.height,t.lineHeight*r);o=a/r,this.length>r+1&&(l=(this.height-a)/(this.length-r-1))}else o=this.height/r;return{firstLine:i,lastLine:n,perLine:o,perChar:l}}blockAt(t,e,i,n){let{firstLine:r,lastLine:o,perLine:l,perChar:a}=this.heightMetrics(e,n);if(e.lineWrapping){let c=n+(t0){let r=i[i.length-1];r instanceof it?i[i.length-1]=new it(r.length+n):i.push(null,new it(n-1))}if(t>0){let r=i[0];r instanceof it?i[0]=new it(t+r.length):i.unshift(new it(t-1),null)}return pt.of(i)}decomposeLeft(t,e){e.push(new it(t-1),null)}decomposeRight(t,e){e.push(null,new it(this.length-t-1))}updateHeight(t,e=0,i=!1,n){let r=e+this.length;if(n&&n.from<=e+this.length&&n.more){let o=[],l=Math.max(e,n.from),a=-1;for(n.from>e&&o.push(new it(n.from-e-1).updateHeight(t,e));l<=r&&n.more;){let h=t.doc.lineAt(l).length;o.length&&o.push(null);let f=n.heights[n.index++];a==-1?a=f:Math.abs(f-a)>=cn&&(a=-2);let u=new At(h,f);u.outdated=!1,o.push(u),l+=h+1}l<=r&&o.push(null,new it(r-l).updateHeight(t,l));let c=pt.of(o);return(a<0||Math.abs(c.height-this.height)>=cn||Math.abs(a-this.heightMetrics(t,e).perLine)>=cn)&&(t.heightChanged=!0),c}else(i||this.outdated)&&(this.setHeight(t,t.heightForGap(e,e+this.length)),this.outdated=!1);return this}toString(){return`gap(${this.length})`}}class Wf extends pt{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return this.flags&1}blockAt(t,e,i,n){let r=i+this.left.height;return tl))return c;let h=e==U.ByPosNoHeight?U.ByPosNoHeight:U.ByPos;return a?c.join(this.right.lineAt(l,h,i,o,l)):this.left.lineAt(l,h,i,n,r).join(c)}forEachLine(t,e,i,n,r,o){let l=n+this.left.height,a=r+this.left.length+this.break;if(this.break)t=a&&this.right.forEachLine(t,e,i,l,a,o);else{let c=this.lineAt(a,U.ByPos,i,n,r);t=t&&c.from<=e&&o(c),e>c.to&&this.right.forEachLine(c.to+1,e,i,l,a,o)}}replace(t,e,i){let n=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-n,e-n,i));let r=[];t>0&&this.decomposeLeft(t,r);let o=r.length;for(let l of i)r.push(l);if(t>0&&So(r,o-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)return this.right.decomposeRight(t-n,e);t2*e.size||e.size>2*t.size?pt.of(this.break?[t,null,e]:[t,e]):(this.left=t,this.right=e,this.height=t.height+e.height,this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,n){let{left:r,right:o}=this,l=e+r.length+this.break,a=null;return n&&n.from<=e+r.length&&n.more?a=r=r.updateHeight(t,e,i,n):r.updateHeight(t,e,i),n&&n.from<=l+o.length&&n.more?a=o=o.updateHeight(t,l,i,n):o.updateHeight(t,l,i),a?this.balanced(r,o):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function So(s,t){let e,i;s[t]==null&&(e=s[t-1])instanceof it&&(i=s[t+1])instanceof it&&s.splice(t-1,3,new it(e.length+1+i.length))}const Hf=5;class pr{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let i=Math.min(e,this.lineEnd),n=this.nodes[this.nodes.length-1];n instanceof At?n.length+=i-this.pos:(i>this.pos||!this.isCovered)&&this.nodes.push(new At(i-this.pos,-1)),this.writtenTo=i,e>i&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=Hf)&&this.addLineDeco(n,r,o)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new At(this.pos-t,-1)),this.writtenTo=this.pos}blankContent(t,e){let i=new it(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof At)return t;let e=new At(0,-1);return this.nodes.push(e),e}addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCovered&&this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,e&&e.endSide>0&&(this.covering=t)}addLineDeco(t,e,i){let n=this.ensureLine();n.length+=i,n.collapsed+=i,n.widgetHeight=Math.max(n.widgetHeight,t),n.breaks+=e,this.writtenTo=this.pos=this.pos+i}finish(t){let e=this.nodes.length==0?null:this.nodes[this.nodes.length-1];this.lineStart>-1&&!(e instanceof At)&&!this.isCovered?this.nodes.push(new At(0,-1)):(this.writtenToh.clientHeight||h.scrollWidth>h.clientWidth)&&f.overflow!="visible"){let u=h.getBoundingClientRect();r=Math.max(r,u.left),o=Math.min(o,u.right),l=Math.max(l,u.top),a=c==s.parentNode?u.bottom:Math.min(a,u.bottom)}c=f.position=="absolute"||f.position=="fixed"?h.offsetParent:h.parentNode}else if(c.nodeType==11)c=c.host;else break;return{left:r-e.left,right:Math.max(r,o)-e.left,top:l-(e.top+t),bottom:Math.max(l,a)-(e.top+t)}}function $f(s,t){let e=s.getBoundingClientRect();return{left:0,right:e.right-e.left,top:t,bottom:e.bottom-(e.top+t)}}class Qn{constructor(t,e,i){this.from=t,this.to=e,this.size=i}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;itypeof i!="function"&&i.class=="cm-lineWrapping");this.heightOracle=new Ff(e),this.stateDeco=t.facet(ki).filter(i=>typeof i!="function"),this.heightMap=pt.empty().applyChanges(this.stateDeco,V.empty,this.heightOracle.setDoc(t.doc),[new It(0,0,0,t.doc.length)]);for(let i=0;i<2&&(this.viewport=this.getViewport(0,null),!!this.updateForViewport());i++);this.updateViewportLines(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=P.set(this.lineGaps.map(i=>i.draw(this,!1))),this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let n=i?e.head:e.anchor;if(!t.some(({from:r,to:o})=>n>=r&&n<=o)){let{from:r,to:o}=this.lineBlockAt(n);t.push(new ji(r,o))}}return this.viewports=t.sort((i,n)=>i.from-n.from),this.updateScaler()}updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.height<=7e6?ko:new gr(this.heightOracle,this.heightMap,this.viewports),t.eq(this.scaler)?0:2}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.heightOracle.setDoc(this.state.doc),0,0,t=>{this.viewportLines.push(ui(t,this.scaler))})}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=this.state.facet(ki).filter(h=>typeof h!="function");let n=t.changedRanges,r=It.extendWithRanges(n,zf(i,this.stateDeco,t?t.changes:et.empty(this.state.doc.length))),o=this.heightMap.height,l=this.scrolledToBottom?null:this.scrollAnchorAt(this.scrollTop);this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),r),this.heightMap.height!=o&&(t.flags|=2),l?(this.scrollAnchorPos=t.changes.mapPos(l.from,-1),this.scrollAnchorHeight=l.top):(this.scrollAnchorPos=-1,this.scrollAnchorHeight=this.heightMap.height);let a=r.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.heada.to)||!this.viewportIsAppropriate(a))&&(a=this.getViewport(0,e));let c=a.from!=this.viewport.from||a.to!=this.viewport.to;this.viewport=a,t.flags|=this.updateForViewport(),(c||!t.changes.empty||t.flags&2)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&t.selectionSet&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(ua)&&(this.mustEnforceCursorAssoc=!0)}measure(t){let e=t.contentDOM,i=window.getComputedStyle(e),n=this.heightOracle,r=i.whiteSpace;this.defaultTextDirection=i.direction=="rtl"?X.RTL:X.LTR;let o=this.heightOracle.mustRefreshForWrapping(r),l=e.getBoundingClientRect(),a=o||this.mustMeasureContent||this.contentDOMHeight!=l.height;this.contentDOMHeight=l.height,this.mustMeasureContent=!1;let c=0,h=0;if(l.width&&l.height){let{scaleX:S,scaleY:w}=Wl(e,l);(S>.005&&Math.abs(this.scaleX-S)>.005||w>.005&&Math.abs(this.scaleY-w)>.005)&&(this.scaleX=S,this.scaleY=w,c|=8,o=a=!0)}let f=(parseInt(i.paddingTop)||0)*this.scaleY,u=(parseInt(i.paddingBottom)||0)*this.scaleY;(this.paddingTop!=f||this.paddingBottom!=u)&&(this.paddingTop=f,this.paddingBottom=u,c|=10),this.editorWidth!=t.scrollDOM.clientWidth&&(n.lineWrapping&&(a=!0),this.editorWidth=t.scrollDOM.clientWidth,c|=8);let d=t.scrollDOM.scrollTop*this.scaleY;this.scrollTop!=d&&(this.scrollAnchorHeight=-1,this.scrollTop=d),this.scrolledToBottom=ql(t.scrollDOM);let p=(this.printing?$f:Kf)(e,this.paddingTop),g=p.top-this.pixelViewport.top,m=p.bottom-this.pixelViewport.bottom;this.pixelViewport=p;let y=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(y!=this.inView&&(this.inView=y,y&&(a=!0)),!this.inView&&!this.scrollTarget)return 0;let x=l.width;if((this.contentDOMWidth!=x||this.editorHeight!=t.scrollDOM.clientHeight)&&(this.contentDOMWidth=l.width,this.editorHeight=t.scrollDOM.clientHeight,c|=8),a){let S=t.docView.measureVisibleLineHeights(this.viewport);if(n.mustRefreshForHeights(S)&&(o=!0),o||n.lineWrapping&&Math.abs(x-this.contentDOMWidth)>n.charWidth){let{lineHeight:w,charWidth:A,textHeight:C}=t.docView.measureTextSize();o=w>0&&n.refresh(r,w,A,C,x/A,S),o&&(t.docView.minWidth=0,c|=8)}g>0&&m>0?h=Math.max(g,m):g<0&&m<0&&(h=Math.min(g,m)),n.heightChanged=!1;for(let w of this.viewports){let A=w.from==this.viewport.from?S:t.docView.measureVisibleLineHeights(w);this.heightMap=(o?pt.empty().applyChanges(this.stateDeco,V.empty,this.heightOracle,[new It(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(n,0,o,new Vf(w.from,A))}n.heightChanged&&(c|=2)}let v=!this.viewportIsAppropriate(this.viewport,h)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return v&&(c&2&&(c|=this.updateScaler()),this.viewport=this.getViewport(h,this.scrollTarget),c|=this.updateForViewport()),(c&2||v)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(o?[]:this.lineGaps,t)),c|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),c}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.heightMap,r=this.heightOracle,{visibleTop:o,visibleBottom:l}=this,a=new ji(n.lineAt(o-i*1e3,U.ByHeight,r,0,0).from,n.lineAt(l+(1-i)*1e3,U.ByHeight,r,0,0).to);if(e){let{head:c}=e.range;if(ca.to){let h=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),f=n.lineAt(c,U.ByPos,r,0,0),u;e.y=="center"?u=(f.top+f.bottom)/2-h/2:e.y=="start"||e.y=="nearest"&&c=l+Math.max(10,Math.min(i,250)))&&n>o-2*1e3&&r>1,o=n<<1;if(this.defaultTextDirection!=X.LTR&&!i)return[];let l=[],a=(h,f,u,d)=>{if(f-hh&&yy.from>=u.from&&y.to<=u.to&&Math.abs(y.from-h)y.fromx));if(!m){if(fy.from<=f&&y.to>=f)){let y=e.moveToLineBoundary(b.cursor(f),!1,!0).head;y>h&&(f=y)}m=new Qn(h,f,this.gapSize(u,h,f,d))}l.push(m)},c=h=>{if(h.lengthh.from&&a(h.from,d,h,f),pe.draw(this,this.heightOracle.lineWrapping))))}computeVisibleRanges(){let t=this.stateDeco;this.lineGaps.length&&(t=t.concat(this.lineGapDeco));let e=[];K.spans(t,this.viewport.from,this.viewport.to,{span(n,r){e.push({from:n,to:r})},point(){}},20);let i=e.length!=this.visibleRanges.length||this.visibleRanges.some((n,r)=>n.from!=e[r].from||n.to!=e[r].to);return this.visibleRanges=e,i?4:0}lineBlockAt(t){return t>=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find(e=>e.from<=t&&e.to>=t)||ui(this.heightMap.lineAt(t,U.ByPos,this.heightOracle,0,0),this.scaler)}lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.viewportLines[this.viewportLines.length-1].bottom&&this.viewportLines.find(e=>e.top<=t&&e.bottom>=t)||ui(this.heightMap.lineAt(this.scaler.fromDOM(t),U.ByHeight,this.heightOracle,0,0),this.scaler)}scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=this.viewport.from||this.viewportLines[0].top-t>200?e:this.viewportLines[0]}elementAtHeight(t){return ui(this.heightMap.blockAt(this.scaler.fromDOM(t),this.heightOracle,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class ji{constructor(t,e){this.from=t,this.to=e}}function Uf(s,t,e){let i=[],n=s,r=0;return K.spans(e,s,t,{span(){},point(o,l){o>n&&(i.push({from:n,to:o}),r+=o-n),n=l}},20),n=1)return t[t.length-1].to;let i=Math.floor(s*e);for(let n=0;;n++){let{from:r,to:o}=t[n],l=o-r;if(i<=l)return r+i;i-=l}}function Gi(s,t){let e=0;for(let{from:i,to:n}of s.ranges){if(t<=n){e+=t-i;break}e+=n-i}return e/s.total}function Gf(s,t){for(let e of s)if(t(e))return e}const ko={toDOM(s){return s},fromDOM(s){return s},scale:1,eq(s){return s==this}};class gr{constructor(t,e,i){let n=0,r=0,o=0;this.viewports=i.map(({from:l,to:a})=>{let c=e.lineAt(l,U.ByPos,t,0,0).top,h=e.lineAt(a,U.ByPos,t,0,0).bottom;return n+=h-c,{from:l,to:a,top:c,bottom:h,domTop:0,domBottom:0}}),this.scale=(7e6-n)/(e.height-n);for(let l of this.viewports)l.domTop=o+(l.top-r)*this.scale,o=l.domBottom=l.domTop+(l.bottom-l.top),r=l.bottom}toDOM(t){for(let e=0,i=0,n=0;;e++){let r=ee.from==t.viewports[i].from&&e.to==t.viewports[i].to):!1}}function ui(s,t){if(t.scale==1)return s;let e=t.toDOM(s.top),i=t.toDOM(s.bottom);return new Jt(s.from,s.length,e,i-e,Array.isArray(s._content)?s._content.map(n=>ui(n,t)):s._content)}const Ji=T.define({combine:s=>s.join(" ")}),Hs=T.define({combine:s=>s.indexOf(!0)>-1}),zs=de.newName(),Ra=de.newName(),La=de.newName(),Ea={"&light":"."+Ra,"&dark":"."+La};function qs(s,t,e){return new de(t,{finish(i){return/&/.test(i)?i.replace(/&\w*/,n=>{if(n=="&")return s;if(!e||!e[n])throw new RangeError(`Unsupported selector: ${n}`);return e[n]}):s+" "+i}})}const Jf=qs("."+zs,{"&":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0},".cm-content":{margin:0,flexGrow:2,flexShrink:0,display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",minHeight:"100%",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 6px"},".cm-layer":{position:"absolute",left:0,top:0,contain:"size style","& > *":{position:"absolute"}},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{pointerEvents:"none"},"&.cm-focused > .cm-scroller > .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#444"},".cm-dropCursor":{position:"absolute"},"&.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor":{display:"block"},".cm-iso":{unicodeBidi:"isolate"},".cm-announced":{position:"fixed",top:"-10000px"},"@media print":{".cm-announced":{display:"none"}},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",insetInlineStart:0,zIndex:200},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",borderRight:"1px solid #ddd"},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top"},".cm-highlightSpace:before":{content:"attr(data-display)",position:"absolute",pointerEvents:"none",color:"#888"},".cm-highlightTab":{backgroundImage:`url('data:image/svg+xml,')`,backgroundSize:"auto 100%",backgroundPosition:"right 90%",backgroundRepeat:"no-repeat"},".cm-trailingSpace":{backgroundColor:"#ff332255"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},Ea),di="￿";class Yf{constructor(t,e){this.points=t,this.text="",this.lineSeparator=e.facet(H.lineSeparator)}append(t){this.text+=t}lineBreak(){this.text+=di}readRange(t,e){if(!t)return this;let i=t.parentNode;for(let n=t;;){this.findPointBefore(i,n);let r=this.text.length;this.readNode(n);let o=n.nextSibling;if(o==e)break;let l=$.get(n),a=$.get(o);(l&&a?l.breakAfter:(l?l.breakAfter:mn(n))||mn(o)&&(n.nodeName!="BR"||n.cmIgnore)&&this.text.length>r)&&this.lineBreak(),n=o}return this.findPointBefore(i,e),this}readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&(i.pos=this.text.length+Math.min(i.offset,e.length));for(let i=0,n=this.lineSeparator?null:/\r\n?|\n/g;;){let r=-1,o=1,l;if(this.lineSeparator?(r=e.indexOf(this.lineSeparator,i),o=this.lineSeparator.length):(l=n.exec(e))&&(r=l.index,o=l[0].length),this.append(e.slice(i,r<0?e.length:r)),r<0)break;if(this.lineBreak(),o>1)for(let a of this.points)a.node==t&&a.pos>this.text.length&&(a.pos-=o-1);i=r+o}}readNode(t){if(t.cmIgnore)return;let e=$.get(t),i=e&&e.overrideDOMText;if(i!=null){this.findPointInside(t,i.length);for(let n=i.iter();!n.next().done;)n.lineBreak?this.lineBreak():this.append(n.value)}else t.nodeType==3?this.readTextNode(t):t.nodeName=="BR"?t.nextSibling&&this.lineBreak():t.nodeType==1&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(t.nodeType==3?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+(Xf(t,i.node,i.offset)?e:0))}}function Xf(s,t,e){for(;;){if(!t||e-1;let{impreciseHead:r,impreciseAnchor:o}=t.docView;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=t.docView.domBoundsAround(e,i,0))){let l=r||o?[]:tu(t),a=new Yf(l,t.state);a.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=a.text,this.newSel=eu(l,this.bounds.from)}else{let l=t.observer.selectionRange,a=r&&r.node==l.focusNode&&r.offset==l.focusOffset||!Ms(t.contentDOM,l.focusNode)?t.state.selection.main.head:t.docView.posFromDOM(l.focusNode,l.focusOffset),c=o&&o.node==l.anchorNode&&o.offset==l.anchorOffset||!Ms(t.contentDOM,l.anchorNode)?t.state.selection.main.anchor:t.docView.posFromDOM(l.anchorNode,l.anchorOffset),h=t.viewport;if((D.ios||D.chrome)&&t.state.selection.main.empty&&a!=c&&(h.from>0||h.toDate.now()-100?s.inputState.lastKeyCode:-1;if(t.bounds){let{from:o,to:l}=t.bounds,a=n.from,c=null;(r===8||D.android&&t.text.length=n.from&&e.to<=n.to&&(e.from!=n.from||e.to!=n.to)&&n.to-n.from-(e.to-e.from)<=4?e={from:n.from,to:n.to,insert:s.state.doc.slice(n.from,e.from).append(e.insert).append(s.state.doc.slice(e.to,n.to))}:(D.mac||D.android)&&e&&e.from==e.to&&e.from==n.head-1&&/^\. ?$/.test(e.insert.toString())&&s.contentDOM.getAttribute("autocorrect")=="off"?(i&&e.insert.length==2&&(i=b.single(i.main.anchor-1,i.main.head-1)),e={from:n.from,to:n.to,insert:V.of([" "])}):D.chrome&&e&&e.from==e.to&&e.from==n.head&&e.insert.toString()==` - `&&s.lineWrapping&&(i&&(i=b.single(i.main.anchor-1,i.main.head-1)),e={from:n.from,to:n.to,insert:V.of([" "])}),e)return Na(s,e,i,r);if(i&&!i.main.eq(n)){let o=!1,l="select";return s.inputState.lastSelectionTime>Date.now()-50&&(s.inputState.lastSelectionOrigin=="select"&&(o=!0),l=s.inputState.lastSelectionOrigin),s.dispatch({selection:i,scrollIntoView:o,userEvent:l}),!0}else return!1}function Na(s,t,e,i=-1){if(D.ios&&s.inputState.flushIOSKey(t))return!0;let n=s.state.selection.main;if(D.android&&(t.to==n.to&&(t.from==n.from||t.from==n.from-1&&s.state.sliceDoc(t.from,n.from)==" ")&&t.insert.length==1&&t.insert.lines==2&&qe(s.contentDOM,"Enter",13)||(t.from==n.from-1&&t.to==n.to&&t.insert.length==0||i==8&&t.insert.lengthn.head)&&qe(s.contentDOM,"Backspace",8)||t.from==n.from&&t.to==n.to+1&&t.insert.length==0&&qe(s.contentDOM,"Delete",46)))return!0;let r=t.insert.toString();s.inputState.composing>=0&&s.inputState.composing++;let o,l=()=>o||(o=Qf(s,t,e));return s.state.facet(ha).some(a=>a(s,t.from,t.to,r,l))||s.dispatch(l()),!0}function Qf(s,t,e){let i,n=s.state,r=n.selection.main;if(t.from>=r.from&&t.to<=r.to&&t.to-t.from>=(r.to-r.from)/3&&(!e||e.main.empty&&e.main.from==t.from+t.insert.length)&&s.inputState.composing<0){let l=r.fromt.to?n.sliceDoc(t.to,r.to):"";i=n.replaceSelection(s.state.toText(l+t.insert.sliceString(0,void 0,s.state.lineBreak)+a))}else{let l=n.changes(t),a=e&&e.main.to<=l.newLength?e.main:void 0;if(n.selection.ranges.length>1&&s.inputState.composing>=0&&t.to<=r.to&&t.to>=r.to-10){let c=s.state.sliceDoc(t.from,t.to),h,f=e&&wa(s,e.main.head);if(f){let p=t.insert.length-(t.to-t.from);h={from:f.from,to:f.to-p}}else h=s.state.doc.lineAt(r.head);let u=r.to-t.to,d=r.to-r.from;i=n.changeByRange(p=>{if(p.from==r.from&&p.to==r.to)return{changes:l,range:a||p.map(l)};let g=p.to-u,m=g-c.length;if(p.to-p.from!=d||s.state.sliceDoc(m,g)!=c||p.to>=h.from&&p.from<=h.to)return{range:p};let y=n.changes({from:m,to:g,insert:t.insert}),x=p.to-r.to;return{changes:y,range:a?b.range(Math.max(0,a.anchor+x),Math.max(0,a.head+x)):p.map(y)}})}else i={changes:l,selection:a&&n.selection.replaceRange(a)}}let o="input.type";return(s.composing||s.inputState.compositionPendingChange&&s.inputState.compositionEndedAt>Date.now()-50)&&(s.inputState.compositionPendingChange=!1,o+=".compose",s.inputState.compositionFirstChange&&(o+=".start",s.inputState.compositionFirstChange=!1)),n.update(i,{userEvent:o,scrollIntoView:!0})}function Zf(s,t,e,i){let n=Math.min(s.length,t.length),r=0;for(;r0&&l>0&&s.charCodeAt(o-1)==t.charCodeAt(l-1);)o--,l--;if(i=="end"){let a=Math.max(0,r-Math.min(o,l));e-=o+a-r}if(o=o?r-e:0;r-=a,l=r+(l-o),o=r}else if(l=l?r-e:0;r-=a,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}function tu(s){let t=[];if(s.root.activeElement!=s.contentDOM)return t;let{anchorNode:e,anchorOffset:i,focusNode:n,focusOffset:r}=s.observer.selectionRange;return e&&(t.push(new Co(e,i)),(n!=e||r!=i)&&t.push(new Co(n,r))),t}function eu(s,t){if(s.length==0)return null;let e=s[0].pos,i=s.length==2?s[1].pos:e;return e>-1&&i>-1?b.single(e+t,i+t):null}const iu={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},Zn=D.ie&&D.ie_version<=11;class nu{constructor(t){this.view=t,this.active=!1,this.editContext=null,this.selectionRange=new Hc,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resizeScroll=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.printQuery=null,this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver(e=>{for(let i of e)this.queue.push(i);(D.ie&&D.ie_version<=11||D.ios&&t.composing)&&e.some(i=>i.type=="childList"&&i.removedNodes.length||i.type=="characterData"&&i.oldValue.length>i.target.nodeValue.length)?this.flushSoon():this.flush()}),window.EditContext&&t.constructor.EDIT_CONTEXT!==!1&&!(D.chrome&&D.chrome_version<126)&&(this.editContext=new ru(t),t.state.facet(le)&&(t.contentDOM.editContext=this.editContext.editContext)),Zn&&(this.onCharData=e=>{this.queue.push({target:e.target,type:"characterData",oldValue:e.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),window.matchMedia&&(this.printQuery=window.matchMedia("print")),typeof ResizeObserver=="function"&&(this.resizeScroll=new ResizeObserver(()=>{var e;((e=this.view.docView)===null||e===void 0?void 0:e.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),e.length>0&&e[e.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{threshold:[0,.001]}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(e=>{e.length>0&&e[e.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.view.requestMeasure(this.editContext.measureReq),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(t){t.type=="change"&&!t.matches||(this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500))}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some((e,i)=>e!=t[i]))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,n=this.selectionRange;if(i.state.facet(le)?i.root.activeElement!=this.dom:!an(i.dom,n))return;let r=n.anchorNode&&i.docView.nearest(n.anchorNode);if(r&&r.ignoreEvent(t)){e||(this.selectionChanged=!1);return}(D.ie&&D.ie_version<=11||D.android&&D.chrome)&&!i.state.selection.main.empty&&n.focusNode&&yi(n.focusNode,n.focusOffset,n.anchorNode,n.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=vi(t.root);if(!e)return!1;let i=D.safari&&t.root.nodeType==11&&Nc(this.dom.ownerDocument)==this.dom&&su(this.view,e)||e;if(!i||this.selectionRange.eq(i))return!1;let n=an(this.dom,i);return n&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let r=this.delayedAndroidKey;r&&(this.clearDelayedAndroidKey(),this.view.inputState.lastKeyCode=r.keyCode,this.view.inputState.lastKeyTime=Date.now(),!this.flush()&&r.force&&qe(this.dom,r.key,r.keyCode))};this.flushingAndroidKey=this.view.win.requestAnimationFrame(n)}(!this.delayedAndroidKey||t=="Enter")&&(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()}))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}pendingRecords(){for(let t of this.observer.takeRecords())this.queue.push(t);return this.queue}processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[]);let e=-1,i=-1,n=!1;for(let r of t){let o=this.readMutation(r);o&&(o.typeOver&&(n=!0),e==-1?{from:e,to:i}=o:(e=Math.min(o.from,e),i=Math.max(o.to,i)))}return{from:e,to:i,typeOver:n}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this.selectionChanged&&an(this.dom,this.selectionRange);if(t<0&&!n)return null;t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1;let r=new _f(this.view,t,e,i);return this.view.docView.domChanged={newSel:r.newSel?r.newSel.main:null},r}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return this.view.requestMeasure(),!1;let i=this.view.state,n=Ia(this.view,e);return this.view.state==i&&(e.domChanged||e.newSel&&!e.newSel.main.eq(this.view.state.selection.main))&&this.view.update([]),n}readMutation(t){let e=this.view.docView.nearest(t.target);if(!e||e.ignoreMutation(t))return null;if(e.markDirty(t.type=="attributes"),t.type=="attributes"&&(e.flags|=4),t.type=="childList"){let i=Ao(e,t.previousSibling||t.target.previousSibling,-1),n=Ao(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:n?e.posBefore(n):e.posAtEnd,typeOver:!1}}else return t.type=="characterData"?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),this.printQuery?this.printQuery.addEventListener("change",this.onPrint):t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),this.printQuery?this.printQuery.removeEventListener("change",this.onPrint):t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}update(t){this.editContext&&(this.editContext.update(t),t.startState.facet(le)!=t.state.facet(le)&&(t.view.contentDOM.editContext=t.state.facet(le)?this.editContext.editContext:null))}destroy(){var t,e,i;this.stop(),(t=this.intersection)===null||t===void 0||t.disconnect(),(e=this.gapIntersection)===null||e===void 0||e.disconnect(),(i=this.resizeScroll)===null||i===void 0||i.disconnect();for(let n of this.scrollTargets)n.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey)}}function Ao(s,t,e){for(;t;){let i=$.get(t);if(i&&i.parent==s)return i;let n=t.parentNode;t=n!=s.dom?n:e>0?t.nextSibling:t.previousSibling}return null}function Mo(s,t){let e=t.startContainer,i=t.startOffset,n=t.endContainer,r=t.endOffset,o=s.docView.domAtPos(s.state.selection.main.anchor);return yi(o.node,o.offset,n,r)&&([e,i,n,r]=[n,r,e,i]),{anchorNode:e,anchorOffset:i,focusNode:n,focusOffset:r}}function su(s,t){if(t.getComposedRanges){let n=t.getComposedRanges(s.root)[0];if(n)return Mo(s,n)}let e=null;function i(n){n.preventDefault(),n.stopImmediatePropagation(),e=n.getTargetRanges()[0]}return s.contentDOM.addEventListener("beforeinput",i,!0),s.dom.ownerDocument.execCommand("indent"),s.contentDOM.removeEventListener("beforeinput",i,!0),e?Mo(s,e):null}class ru{constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,this.resetRange(t.state);let e=this.editContext=new window.EditContext({text:t.state.doc.sliceString(this.from,this.to),selectionStart:this.toContextPos(Math.max(this.from,Math.min(this.to,t.state.selection.main.anchor))),selectionEnd:this.toContextPos(t.state.selection.main.head)});e.addEventListener("textupdate",i=>{let{anchor:n}=t.state.selection.main,r={from:this.toEditorPos(i.updateRangeStart),to:this.toEditorPos(i.updateRangeEnd),insert:V.of(i.text.split(` -`))};r.from==this.from&&nthis.to&&(r.to=n),!(r.from==r.to&&!r.insert.length)&&(this.pendingContextChange=r,Na(t,r,b.single(this.toEditorPos(i.selectionStart),this.toEditorPos(i.selectionEnd))),this.pendingContextChange&&this.revertPending(t.state))}),e.addEventListener("characterboundsupdate",i=>{let n=[],r=null;for(let o=this.toEditorPos(i.rangeStart),l=this.toEditorPos(i.rangeEnd);o{let n=[];for(let r of i.getTextFormats()){let o=r.underlineStyle,l=r.underlineThickness;if(o!="None"&&l!="None"){let a=`text-decoration: underline ${o=="Dashed"?"dashed ":o=="Squiggle"?"wavy ":""}${l=="Thin"?1:2}px`;n.push(P.mark({attributes:{style:a}}).range(this.toEditorPos(r.rangeStart),this.toEditorPos(r.rangeEnd)))}}t.dispatch({effects:pa.of(P.set(n))})}),e.addEventListener("compositionstart",()=>{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)}),e.addEventListener("compositionend",()=>{t.inputState.composing=-1,t.inputState.compositionFirstChange=null}),this.measureReq={read:i=>{this.editContext.updateControlBounds(i.contentDOM.getBoundingClientRect());let n=vi(i.root);n&&n.rangeCount&&this.editContext.updateSelectionBounds(n.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.changes.iterChanges((r,o,l,a,c)=>{if(i)return;let h=c.length-(o-r);if(n&&o>=n.to)if(n.from==r&&n.to==o&&n.insert.eq(c)){n=this.pendingContextChange=null,e+=h,this.to+=h;return}else n=null,this.revertPending(t.state);if(r+=e,o+=e,o<=this.from)this.from+=h,this.to+=h;else if(rthis.to||this.to-this.from+c.length>3e4){i=!0;return}this.editContext.updateText(this.toContextPos(r),this.toContextPos(o),c.toString()),this.to+=h}e+=h}),n&&!i&&this.revertPending(t.state),!i}update(t){!this.applyEdits(t)||!this.rangeIsValid(t.state)?(this.pendingContextChange=null,this.resetRange(t.state),this.editContext.updateText(0,this.editContext.text.length,t.state.doc.sliceString(this.from,this.to)),this.setSelection(t.state)):(t.docChanged||t.selectionSet)&&this.setSelection(t.state),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.to+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),n=this.toContextPos(e.head);(this.editContext.selectionStart!=i||this.editContext.selectionEnd!=n)&&this.editContext.updateSelection(i,n)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to1e4*3)}toEditorPos(t){return t+this.from}toContextPos(t){return t-this.from}}class O{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return this.inputState.composing>0}get compositionStarted(){return this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.className="cm-announced",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),t.parent&&t.parent.appendChild(this.dom);let{dispatch:e}=t;this.dispatchTransactions=t.dispatchTransactions||e&&(i=>i.forEach(n=>e(n,this)))||(i=>this.update(i)),this.dispatch=this.dispatch.bind(this),this._root=t.root||zc(t.parent)||document,this.viewState=new vo(t.state||H.create(t)),t.scrollTo&&t.scrollTo.is(qi)&&(this.viewState.scrollTarget=t.scrollTo.value.clip(this.viewState.state)),this.plugins=this.state.facet(ci).map(i=>new Yn(i));for(let i of this.plugins)i.update(this);this.observer=new nu(this),this.inputState=new wf(this),this.inputState.ensureHandlers(this.plugins),this.docView=new no(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure()}dispatch(...t){let e=t.length==1&&t[0]instanceof Z?t:t.length==1&&Array.isArray(t[0])?t[0]:[this.state.update(...t)];this.dispatchTransactions(e,this)}update(t){if(this.updateState!=0)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e=!1,i=!1,n,r=this.state;for(let u of t){if(u.startState!=r)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");r=u.state}if(this.destroyed){this.viewState.state=r;return}let o=this.hasFocus,l=0,a=null;t.some(u=>u.annotation(Oa))?(this.inputState.notifiedFocused=o,l=1):o!=this.inputState.notifiedFocused&&(this.inputState.notifiedFocused=o,a=Ta(r,o),a||(l=1));let c=this.observer.delayedAndroidKey,h=null;if(c?(this.observer.clearDelayedAndroidKey(),h=this.observer.readChange(),(h&&!this.state.doc.eq(r.doc)||!this.state.selection.eq(r.selection))&&(h=null)):this.observer.clear(),r.facet(H.phrases)!=this.state.facet(H.phrases))return this.setState(r);n=bn.create(this,r,t),n.flags|=l;let f=this.viewState.scrollTarget;try{this.updateState=2;for(let u of t){if(f&&(f=f.map(u.changes)),u.scrollIntoView){let{main:d}=u.state.selection;f=new Ke(d.empty?d:b.cursor(d.head,d.head>d.anchor?-1:1))}for(let d of u.effects)d.is(qi)&&(f=d.value.clip(this.state))}this.viewState.update(n,f),this.bidiCache=xn.update(this.bidiCache,n.changes),n.empty||(this.updatePlugins(n),this.inputState.update(n)),e=this.docView.update(n),this.state.facet(fi)!=this.styleModules&&this.mountStyles(),i=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(e,t.some(u=>u.isUserEvent("select.pointer")))}finally{this.updateState=0}if(n.startState.facet(Ji)!=n.state.facet(Ji)&&(this.viewState.mustMeasureContent=!0),(e||i||f||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),e&&this.docViewUpdate(),!n.empty)for(let u of this.state.facet(Ns))try{u(n)}catch(d){Dt(this.state,d,"update listener")}(a||h)&&Promise.resolve().then(()=>{a&&this.state==a.startState&&this.dispatch(a),h&&!Ia(this,h)&&c.force&&qe(this.contentDOM,c.key,c.keyCode)})}setState(t){if(this.updateState!=0)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed){this.viewState.state=t;return}this.updateState=2;let e=this.hasFocus;try{for(let i of this.plugins)i.destroy(this);this.viewState=new vo(t),this.plugins=t.facet(ci).map(i=>new Yn(i)),this.pluginMap.clear();for(let i of this.plugins)i.update(this);this.docView.destroy(),this.docView=new no(this),this.inputState.ensureHandlers(this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(ci),i=t.state.facet(ci);if(e!=i){let n=[];for(let r of i){let o=e.indexOf(r);if(o<0)n.push(new Yn(r));else{let l=this.plugins[o];l.mustUpdate=t,n.push(l)}}for(let r of this.plugins)r.mustUpdate!=t&&r.destroy(this);this.plugins=n,this.pluginMap.clear()}else for(let n of this.plugins)n.mustUpdate=t;for(let n=0;n-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.observer.delayedAndroidKey){this.measureScheduled=-1,this.requestMeasure();return}this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,i=this.scrollDOM,n=i.scrollTop*this.scaleY,{scrollAnchorPos:r,scrollAnchorHeight:o}=this.viewState;Math.abs(n-this.viewState.scrollTop)>1&&(o=-1),this.viewState.scrollAnchorHeight=-1;try{for(let l=0;;l++){if(o<0)if(ql(i))r=-1,o=this.viewState.heightMap.height;else{let d=this.viewState.scrollAnchorAt(n);r=d.from,o=d.top}this.updateState=1;let a=this.viewState.measure(this);if(!a&&!this.measureRequests.length&&this.viewState.scrollTarget==null)break;if(l>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let c=[];a&4||([this.measureRequests,c]=[c,this.measureRequests]);let h=c.map(d=>{try{return d.read(this)}catch(p){return Dt(this.state,p),Do}}),f=bn.create(this,this.state,[]),u=!1;f.flags|=a,e?e.flags|=a:e=f,this.updateState=2,f.empty||(this.updatePlugins(f),this.inputState.update(f),this.updateAttrs(),u=this.docView.update(f),u&&this.docViewUpdate());for(let d=0;d1||p<-1){n=n+p,i.scrollTop=n/this.scaleY,o=-1;continue}}break}}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let l of this.state.facet(Ns))l(e)}get themeClasses(){return zs+" "+(this.state.facet(Hs)?La:Ra)+" "+this.state.facet(Ji)}updateAttrs(){let t=Oo(this,ga,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",translate:"no",contenteditable:this.state.facet(le)?"true":"false",class:"cm-content",style:`${D.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),Oo(this,ur,e);let i=this.observer.ignore(()=>{let n=Bs(this.contentDOM,this.contentAttrs,e),r=Bs(this.dom,this.editorAttrs,t);return n||r});return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let n of i.effects)if(n.is(O.announce)){e&&(this.announceDOM.textContent=""),e=!1;let r=this.announceDOM.appendChild(document.createElement("div"));r.textContent=n.value}}mountStyles(){this.styleModules=this.state.facet(fi);let t=this.state.facet(O.cspNonce);de.mount(this.root,this.styleModules.concat(Jf).reverse(),t?{nonce:t}:void 0)}readMeasured(){if(this.updateState==2)throw new Error("Reading the editor layout isn't allowed during an update");this.updateState==0&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame(()=>this.measure())),t){if(this.measureRequests.indexOf(t)>-1)return;if(t.key!=null){for(let e=0;ei.spec==t)||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}get scaleX(){return this.viewState.scaleX}get scaleY(){return this.viewState.scaleY}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return _n(this,t,ho(this,t,e,i))}moveByGroup(t,e){return _n(this,t,ho(this,t,e,i=>bf(this,t.head,i)))}visualLineSide(t,e){let i=this.bidiSpans(t),n=this.textDirectionAt(t.from),r=i[e?i.length-1:0];return b.cursor(r.side(e,n)+t.from,r.forward(!e,n)?1:-1)}moveToLineBoundary(t,e,i=!0){return yf(this,t,e,i)}moveVertically(t,e,i){return _n(this,t,xf(this,t,e,i))}domAtPos(t){return this.docView.domAtPos(t)}posAtDOM(t,e=0){return this.docView.posFromDOM(t,e)}posAtCoords(t,e=!0){return this.readMeasured(),Sa(this,t,e)}coordsAtPos(t,e=1){this.readMeasured();let i=this.docView.coordsAt(t,e);if(!i||i.left==i.right)return i;let n=this.state.doc.lineAt(t),r=this.bidiSpans(n),o=r[ce.find(r,t-n.from,-1,e)];return Nn(i,o.dir==X.LTR==e>0)}coordsForChar(t){return this.readMeasured(),this.docView.coordsForChar(t)}get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}get textDirection(){return this.viewState.defaultTextDirection}textDirectionAt(t){return!this.state.facet(fa)||tthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>ou)return na(t.length);let e=this.textDirectionAt(t.from),i;for(let r of this.bidiCache)if(r.from==t.from&&r.dir==e&&(r.fresh||ia(r.isolates,i=io(this,t))))return r.order;i||(i=io(this,t));let n=tf(t.text,e,i);return this.bidiCache.push(new xn(t.from,t.to,e,i,!0,n)),n}get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||D.safari&&((t=this.inputState)===null||t===void 0?void 0:t.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{Hl(this.contentDOM),this.docView.updateSelection()})}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((t.nodeType==9?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.blur();for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.docView.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){return qi.of(new Ke(typeof t=="number"?b.cursor(t):t,e.y,e.x,e.yMargin,e.xMargin))}scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.viewState.scrollAnchorAt(t);return qi.of(new Ke(b.cursor(i.from),"start","start",i.top-t,e,!0))}setTabFocusMode(t){t==null?this.inputState.tabFocusMode=this.inputState.tabFocusMode<0?0:-1:typeof t=="boolean"?this.inputState.tabFocusMode=t?0:-1:this.inputState.tabFocusMode!=0&&(this.inputState.tabFocusMode=Date.now()+t)}static domEventHandlers(t){return ut.define(()=>({}),{eventHandlers:t})}static domEventObservers(t){return ut.define(()=>({}),{eventObservers:t})}static theme(t,e){let i=de.newName(),n=[Ji.of(i),fi.of(qs(`.${i}`,t))];return e&&e.dark&&n.push(Hs.of(!0)),n}static baseTheme(t){return ye.lowest(fi.of(qs("."+zs,t,Ea)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&$.get(i)||$.get(t);return((e=n==null?void 0:n.rootView)===null||e===void 0?void 0:e.view)||null}}O.styleModule=fi;O.inputHandler=ha;O.scrollHandler=da;O.focusChangeEffect=ca;O.perLineTextDirection=fa;O.exceptionSink=aa;O.updateListener=Ns;O.editable=le;O.mouseSelectionStyle=la;O.dragMovesSelection=oa;O.clickAddsSelectionRange=ra;O.decorations=ki;O.outerDecorations=ma;O.atomicRanges=dr;O.bidiIsolatedRanges=ya;O.scrollMargins=ba;O.darkTheme=Hs;O.cspNonce=T.define({combine:s=>s.length?s[0]:""});O.contentAttributes=ur;O.editorAttributes=ga;O.lineWrapping=O.contentAttributes.of({class:"cm-lineWrapping"});O.announce=F.define();const ou=4096,Do={};class xn{constructor(t,e,i,n,r,o){this.from=t,this.to=e,this.dir=i,this.isolates=n,this.fresh=r,this.order=o}static update(t,e){if(e.empty&&!t.some(r=>r.fresh))return t;let i=[],n=t.length?t[t.length-1].dir:X.LTR;for(let r=Math.max(0,t.length-10);r=0;n--){let r=i[n],o=typeof r=="function"?r(s):r;o&&Ps(o,e)}return e}const lu=D.mac?"mac":D.windows?"win":D.linux?"linux":"key";function au(s,t){const e=s.split(/-(?!$)/);let i=e[e.length-1];i=="Space"&&(i=" ");let n,r,o,l;for(let a=0;ai.concat(n),[]))),e}function cu(s,t,e){return Va(Fa(s.state),t,s,e)}let ae=null;const fu=4e3;function uu(s,t=lu){let e=Object.create(null),i=Object.create(null),n=(o,l)=>{let a=i[o];if(a==null)i[o]=l;else if(a!=l)throw new Error("Key binding "+o+" is used both as a regular binding and as a multi-stroke prefix")},r=(o,l,a,c,h)=>{var f,u;let d=e[o]||(e[o]=Object.create(null)),p=l.split(/ (?!$)/).map(y=>au(y,t));for(let y=1;y{let S=ae={view:v,prefix:x,scope:o};return setTimeout(()=>{ae==S&&(ae=null)},fu),!0}]})}let g=p.join(" ");n(g,!1);let m=d[g]||(d[g]={preventDefault:!1,stopPropagation:!1,run:((u=(f=d._any)===null||f===void 0?void 0:f.run)===null||u===void 0?void 0:u.slice())||[]});a&&m.run.push(a),c&&(m.preventDefault=!0),h&&(m.stopPropagation=!0)};for(let o of s){let l=o.scope?o.scope.split(" "):["editor"];if(o.any)for(let c of l){let h=e[c]||(e[c]=Object.create(null));h._any||(h._any={preventDefault:!1,stopPropagation:!1,run:[]});let{any:f}=o;for(let u in h)h[u].run.push(d=>f(d,Ks))}let a=o[t]||o.key;if(a)for(let c of l)r(c,a,o.run,o.preventDefault,o.stopPropagation),o.shift&&r(c,"Shift-"+a,o.shift,o.preventDefault,o.stopPropagation)}return e}let Ks=null;function Va(s,t,e,i){Ks=t;let n=Ic(t),r=nt(n,0),o=Bt(r)==n.length&&n!=" ",l="",a=!1,c=!1,h=!1;ae&&ae.view==e&&ae.scope==i&&(l=ae.prefix+" ",ka.indexOf(t.keyCode)<0&&(c=!0,ae=null));let f=new Set,u=m=>{if(m){for(let y of m.run)if(!f.has(y)&&(f.add(y),y(e)))return m.stopPropagation&&(h=!0),!0;m.preventDefault&&(m.stopPropagation&&(h=!0),c=!0)}return!1},d=s[i],p,g;return d&&(u(d[l+Yi(n,t,!o)])?a=!0:o&&(t.altKey||t.metaKey||t.ctrlKey)&&!(D.windows&&t.ctrlKey&&t.altKey)&&(p=pe[t.keyCode])&&p!=n?(u(d[l+Yi(p,t,!0)])||t.shiftKey&&(g=Si[t.keyCode])!=n&&g!=p&&u(d[l+Yi(g,t,!1)]))&&(a=!0):o&&t.shiftKey&&u(d[l+Yi(n,t,!0)])&&(a=!0),!a&&u(d._any)&&(a=!0)),c&&(a=!0),a&&h&&t.stopPropagation(),Ks=null,a}class Ei{constructor(t,e,i,n,r){this.className=t,this.left=e,this.top=i,this.width=n,this.height=r}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}update(t,e){return e.className!=this.className?!1:(this.adjust(t),!0)}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",this.width!=null&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}static forRange(t,e,i){if(i.empty){let n=t.coordsAtPos(i.head,i.assoc||1);if(!n)return[];let r=Wa(t);return[new Ei(e,n.left-r.left,n.top-r.top,null,n.bottom-n.top)]}else return du(t,e,i)}}function Wa(s){let t=s.scrollDOM.getBoundingClientRect();return{left:(s.textDirection==X.LTR?t.left:t.right-s.scrollDOM.clientWidth*s.scaleX)-s.scrollDOM.scrollLeft*s.scaleX,top:t.top-s.scrollDOM.scrollTop*s.scaleY}}function Po(s,t,e,i){let n=s.coordsAtPos(t,e*2);if(!n)return i;let r=s.dom.getBoundingClientRect(),o=(n.top+n.bottom)/2,l=s.posAtCoords({x:r.left+1,y:o}),a=s.posAtCoords({x:r.right-1,y:o});return l==null||a==null?i:{from:Math.max(i.from,Math.min(l,a)),to:Math.min(i.to,Math.max(l,a))}}function du(s,t,e){if(e.to<=s.viewport.from||e.from>=s.viewport.to)return[];let i=Math.max(e.from,s.viewport.from),n=Math.min(e.to,s.viewport.to),r=s.textDirection==X.LTR,o=s.contentDOM,l=o.getBoundingClientRect(),a=Wa(s),c=o.querySelector(".cm-line"),h=c&&window.getComputedStyle(c),f=l.left+(h?parseInt(h.paddingLeft)+Math.min(0,parseInt(h.textIndent)):0),u=l.right-(h?parseInt(h.paddingRight):0),d=Vs(s,i),p=Vs(s,n),g=d.type==Ot.Text?d:null,m=p.type==Ot.Text?p:null;if(g&&(s.lineWrapping||d.widgetLineBreaks)&&(g=Po(s,i,1,g)),m&&(s.lineWrapping||p.widgetLineBreaks)&&(m=Po(s,n,-1,m)),g&&m&&g.from==m.from&&g.to==m.to)return x(v(e.from,e.to,g));{let w=g?v(e.from,null,g):S(d,!1),A=m?v(null,e.to,m):S(p,!0),C=[];return(g||d).to<(m||p).from-(g&&m?1:0)||d.widgetLineBreaks>1&&w.bottom+s.defaultLineHeight/2R&&W.from=bt)break;tt>J&&E(Math.max(Tt,J),w==null&&Tt<=R,Math.min(tt,bt),A==null&&tt>=z,kt.dir)}if(J=xt.to+1,J>=bt)break}return N.length==0&&E(R,w==null,z,A==null,s.textDirection),{top:B,bottom:I,horizontal:N}}function S(w,A){let C=l.top+(A?w.top:w.bottom);return{top:C,bottom:C,horizontal:[]}}}function pu(s,t){return s.constructor==t.constructor&&s.eq(t)}class gu{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,this.scaleY=1,this.measureReq={read:this.measure.bind(this),write:this.draw.bind(this)},this.dom=t.scrollDOM.appendChild(document.createElement("div")),this.dom.classList.add("cm-layer"),e.above&&this.dom.classList.add("cm-layer-above"),e.class&&this.dom.classList.add(e.class),this.scale(),this.dom.setAttribute("aria-hidden","true"),this.setOrder(t.state),t.requestMeasure(this.measureReq),e.mount&&e.mount(this.dom,t)}update(t){t.startState.facet(fn)!=t.state.facet(fn)&&this.setOrder(t.state),(this.layer.update(t,this.dom)||t.geometryChanged)&&(this.scale(),t.view.requestMeasure(this.measureReq))}docViewUpdate(t){this.layer.updateOnDocViewUpdate!==!1&&t.requestMeasure(this.measureReq)}setOrder(t){let e=0,i=t.facet(fn);for(;e!pu(e,this.drawn[i]))){let e=this.dom.firstChild,i=0;for(let n of t)n.update&&e&&n.constructor&&this.drawn[i].constructor&&n.update(e,this.drawn[i])?(e=e.nextSibling,i++):this.dom.insertBefore(n.draw(),e);for(;e;){let n=e.nextSibling;e.remove(),e=n}this.drawn=t}}destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),this.dom.remove()}}const fn=T.define();function Ha(s){return[ut.define(t=>new gu(t,s)),fn.of(s)]}const za=!D.ios,Ci=T.define({combine(s){return Le(s,{cursorBlinkRate:1200,drawRangeCursor:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})}});function pm(s={}){return[Ci.of(s),mu,yu,bu,ua.of(!0)]}function qa(s){return s.startState.facet(Ci)!=s.state.facet(Ci)}const mu=Ha({above:!0,markers(s){let{state:t}=s,e=t.facet(Ci),i=[];for(let n of t.selection.ranges){let r=n==t.selection.main;if(n.empty?!r||za:e.drawRangeCursor){let o=r?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary",l=n.empty?n:b.cursor(n.head,n.head>n.anchor?-1:1);for(let a of Ei.forRange(s,o,l))i.push(a)}}return i},update(s,t){s.transactions.some(i=>i.selection)&&(t.style.animationName=t.style.animationName=="cm-blink"?"cm-blink2":"cm-blink");let e=qa(s);return e&&Bo(s.state,t),s.docChanged||s.selectionSet||e},mount(s,t){Bo(t.state,s)},class:"cm-cursorLayer"});function Bo(s,t){t.style.animationDuration=s.facet(Ci).cursorBlinkRate+"ms"}const yu=Ha({above:!1,markers(s){return s.state.selection.ranges.map(t=>t.empty?[]:Ei.forRange(s,"cm-selectionBackground",t)).reduce((t,e)=>t.concat(e))},update(s,t){return s.docChanged||s.selectionSet||s.viewportChanged||qa(s)},class:"cm-selectionLayer"}),$s={".cm-line":{"& ::selection, &::selection":{backgroundColor:"transparent !important"}},".cm-content":{"& :focus":{caretColor:"initial !important","&::selection, & ::selection":{backgroundColor:"Highlight !important"}}}};za&&($s[".cm-line"].caretColor=$s[".cm-content"].caretColor="transparent !important");const bu=ye.highest(O.theme($s)),Ka=F.define({map(s,t){return s==null?null:t.mapPos(s)}}),pi=yt.define({create(){return null},update(s,t){return s!=null&&(s=t.changes.mapPos(s)),t.effects.reduce((e,i)=>i.is(Ka)?i.value:e,s)}}),xu=ut.fromClass(class{constructor(s){this.view=s,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(s){var t;let e=s.state.field(pi);e==null?this.cursor!=null&&((t=this.cursor)===null||t===void 0||t.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(s.startState.field(pi)!=e||s.docChanged||s.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let{view:s}=this,t=s.state.field(pi),e=t!=null&&s.coordsAtPos(t);if(!e)return null;let i=s.scrollDOM.getBoundingClientRect();return{left:e.left-i.left+s.scrollDOM.scrollLeft*s.scaleX,top:e.top-i.top+s.scrollDOM.scrollTop*s.scaleY,height:e.bottom-e.top}}drawCursor(s){if(this.cursor){let{scaleX:t,scaleY:e}=this.view;s?(this.cursor.style.left=s.left/t+"px",this.cursor.style.top=s.top/e+"px",this.cursor.style.height=s.height/e+"px"):this.cursor.style.left="-100000px"}}destroy(){this.cursor&&this.cursor.remove()}setDropPos(s){this.view.state.field(pi)!=s&&this.view.dispatch({effects:Ka.of(s)})}},{eventObservers:{dragover(s){this.setDropPos(this.view.posAtCoords({x:s.clientX,y:s.clientY}))},dragleave(s){(s.target==this.view.contentDOM||!this.view.contentDOM.contains(s.relatedTarget))&&this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function gm(){return[pi,xu]}function Ro(s,t,e,i,n){t.lastIndex=0;for(let r=s.iterRange(e,i),o=e,l;!r.next().done;o+=r.value.length)if(!r.lineBreak)for(;l=t.exec(r.value);)n(o+l.index,l)}function wu(s,t){let e=s.visibleRanges;if(e.length==1&&e[0].from==s.viewport.from&&e[0].to==s.viewport.to)return e;let i=[];for(let{from:n,to:r}of e)n=Math.max(s.state.doc.lineAt(n).from,n-t),r=Math.min(s.state.doc.lineAt(r).to,r+t),i.length&&i[i.length-1].to>=n?i[i.length-1].to=r:i.push({from:n,to:r});return i}class Su{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:r,maxLength:o=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,n)this.addMatch=(l,a,c,h)=>n(h,c,c+l[0].length,l,a);else if(typeof i=="function")this.addMatch=(l,a,c,h)=>{let f=i(l,a,c);f&&h(c,c+l[0].length,f)};else if(i)this.addMatch=(l,a,c,h)=>h(c,c+l[0].length,i);else throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.boundary=r,this.maxLength=o}createDeco(t){let e=new De,i=e.add.bind(e);for(let{from:n,to:r}of wu(t,this.maxLength))Ro(t.state.doc,this.regexp,n,r,(o,l)=>this.addMatch(l,t,o,i));return e.finish()}updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChanges((r,o,l,a)=>{a>t.view.viewport.from&&l1e3?this.createDeco(t.view):n>-1?this.updateRange(t.view,e.map(t.changes),i,n):e}updateRange(t,e,i,n){for(let r of t.visibleRanges){let o=Math.max(r.from,i),l=Math.min(r.to,n);if(l>o){let a=t.state.doc.lineAt(o),c=a.toa.from;o--)if(this.boundary.test(a.text[o-1-a.from])){h=o;break}for(;lu.push(y.range(g,m));if(a==c)for(this.regexp.lastIndex=h-a.from;(d=this.regexp.exec(a.text))&&d.indexthis.addMatch(m,t,g,p));e=e.update({filterFrom:h,filterTo:f,filter:(g,m)=>gf,add:u})}}return e}}const js=/x/.unicode!=null?"gu":"g",vu=new RegExp(`[\0-\b ---Ÿ­؜​‎‏\u2028\u2029‭‮⁦⁧⁩\uFEFF-]`,js),ku={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let ts=null;function Cu(){var s;if(ts==null&&typeof document<"u"&&document.body){let t=document.body.style;ts=((s=t.tabSize)!==null&&s!==void 0?s:t.MozTabSize)!=null}return ts||!1}const un=T.define({combine(s){let t=Le(s,{render:null,specialChars:vu,addSpecialChars:null});return(t.replaceTabs=!Cu())&&(t.specialChars=new RegExp(" |"+t.specialChars.source,js)),t.addSpecialChars&&(t.specialChars=new RegExp(t.specialChars.source+"|"+t.addSpecialChars.source,js)),t}});function mm(s={}){return[un.of(s),Au()]}let Lo=null;function Au(){return Lo||(Lo=ut.fromClass(class{constructor(s){this.view=s,this.decorations=P.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(s.state.facet(un)),this.decorations=this.decorator.createDeco(s)}makeDecorator(s){return new Su({regexp:s.specialChars,decoration:(t,e,i)=>{let{doc:n}=e.state,r=nt(t[0],0);if(r==9){let o=n.lineAt(i),l=e.state.tabSize,a=ti(o.text,l,i-o.from);return P.replace({widget:new Tu((l-a%l)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[r]||(this.decorationCache[r]=P.replace({widget:new Ou(s,r)}))},boundary:s.replaceTabs?void 0:/[^]/})}update(s){let t=s.state.facet(un);s.startState.facet(un)!=t?(this.decorator=this.makeDecorator(t),this.decorations=this.decorator.createDeco(s.view)):this.decorations=this.decorator.updateDeco(s,this.decorations)}},{decorations:s=>s.decorations}))}const Mu="•";function Du(s){return s>=32?Mu:s==10?"␤":String.fromCharCode(9216+s)}class Ou extends Ee{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=Du(this.code),i=t.state.phrase("Control character")+" "+(ku[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let r=document.createElement("span");return r.textContent=e,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}}class Tu extends Ee{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent=" ",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}class Pu extends Ee{constructor(t){super(),this.content=t}toDOM(){let t=document.createElement("span");return t.className="cm-placeholder",t.style.pointerEvents="none",t.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?t.setAttribute("aria-label","placeholder "+this.content):t.setAttribute("aria-hidden","true"),t}coordsAt(t){let e=t.firstChild?Ge(t.firstChild):[];if(!e.length)return null;let i=window.getComputedStyle(t.parentNode),n=Nn(e[0],i.direction!="rtl"),r=parseInt(i.lineHeight);return n.bottom-n.top>r*1.5?{left:n.left,right:n.right,top:n.top,bottom:n.top+r}:n}ignoreEvent(){return!1}}function ym(s){return ut.fromClass(class{constructor(t){this.view=t,this.placeholder=s?P.set([P.widget({widget:new Pu(s),side:1}).range(0)]):P.none}get decorations(){return this.view.state.doc.length?P.none:this.placeholder}},{decorations:t=>t.decorations})}const Us=2e3;function Bu(s,t,e){let i=Math.min(t.line,e.line),n=Math.max(t.line,e.line),r=[];if(t.off>Us||e.off>Us||t.col<0||e.col<0){let o=Math.min(t.off,e.off),l=Math.max(t.off,e.off);for(let a=i;a<=n;a++){let c=s.doc.line(a);c.length<=l&&r.push(b.range(c.from+o,c.to+l))}}else{let o=Math.min(t.col,e.col),l=Math.max(t.col,e.col);for(let a=i;a<=n;a++){let c=s.doc.line(a),h=ks(c.text,o,s.tabSize,!0);if(h<0)r.push(b.cursor(c.to));else{let f=ks(c.text,l,s.tabSize);r.push(b.range(c.from+h,c.from+f))}}}return r}function Ru(s,t){let e=s.coordsAtPos(s.viewport.from);return e?Math.round(Math.abs((e.left-t)/s.defaultCharacterWidth)):-1}function Eo(s,t){let e=s.posAtCoords({x:t.clientX,y:t.clientY},!1),i=s.state.doc.lineAt(e),n=e-i.from,r=n>Us?-1:n==i.length?Ru(s,t.clientX):ti(i.text,s.state.tabSize,e-i.from);return{line:i.number,col:r,off:n}}function Lu(s,t){let e=Eo(s,t),i=s.state.selection;return e?{update(n){if(n.docChanged){let r=n.changes.mapPos(n.startState.doc.line(e.line).from),o=n.state.doc.lineAt(r);e={line:o.number,col:e.col,off:Math.min(e.off,o.length)},i=i.map(n.changes)}},get(n,r,o){let l=Eo(s,n);if(!l)return i;let a=Bu(s.state,e,l);return a.length?o?b.create(a.concat(i.ranges)):b.create(a):i}}:null}function bm(s){let t=e=>e.altKey&&e.button==0;return O.mouseSelectionStyle.of((e,i)=>t(i)?Lu(e,i):null)}const oi="-10000px";class Eu{constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this.removeTooltipView=n,this.input=t.state.facet(e),this.tooltips=this.input.filter(o=>o);let r=null;this.tooltipViews=this.tooltips.map(o=>r=i(o,r))}update(t,e){var i;let n=t.state.facet(this.facet),r=n.filter(a=>a);if(n===this.input){for(let a of this.tooltipViews)a.update&&a.update(t);return!1}let o=[],l=e?[]:null;for(let a=0;ae[c]=a),e.length=l.length),this.input=n,this.tooltips=r,this.tooltipViews=o,!0}}function Iu(s){let{win:t}=s;return{top:0,left:0,bottom:t.innerHeight,right:t.innerWidth}}const es=T.define({combine:s=>{var t,e,i;return{position:D.ios?"absolute":((t=s.find(n=>n.position))===null||t===void 0?void 0:t.position)||"fixed",parent:((e=s.find(n=>n.parent))===null||e===void 0?void 0:e.parent)||null,tooltipSpace:((i=s.find(n=>n.tooltipSpace))===null||i===void 0?void 0:i.tooltipSpace)||Iu}}}),Io=new WeakMap,$a=ut.fromClass(class{constructor(s){this.view=s,this.above=[],this.inView=!0,this.madeAbsolute=!1,this.lastTransaction=0,this.measureTimeout=-1;let t=s.state.facet(es);this.position=t.position,this.parent=t.parent,this.classes=s.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.resizeObserver=typeof ResizeObserver=="function"?new ResizeObserver(()=>this.measureSoon()):null,this.manager=new Eu(s,ja,(e,i)=>this.createTooltip(e,i),e=>{this.resizeObserver&&this.resizeObserver.unobserve(e.dom),e.dom.remove()}),this.above=this.manager.tooltips.map(e=>!!e.above),this.intersectionObserver=typeof IntersectionObserver=="function"?new IntersectionObserver(e=>{Date.now()>this.lastTransaction-50&&e.length>0&&e[e.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),s.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let s of this.manager.tooltipViews)this.intersectionObserver.observe(s.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(s){s.transactions.length&&(this.lastTransaction=Date.now());let t=this.manager.update(s,this.above);t&&this.observeIntersection();let e=t||s.geometryChanged,i=s.state.facet(es);if(i.position!=this.position&&!this.madeAbsolute){this.position=i.position;for(let n of this.manager.tooltipViews)n.dom.style.position=this.position;e=!0}if(i.parent!=this.parent){this.parent&&this.container.remove(),this.parent=i.parent,this.createContainer();for(let n of this.manager.tooltipViews)this.container.appendChild(n.dom);e=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);e&&this.maybeMeasure()}createTooltip(s,t){let e=s.create(this.view),i=t?t.dom:null;if(e.dom.classList.add("cm-tooltip"),s.arrow&&!e.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let n=document.createElement("div");n.className="cm-tooltip-arrow",e.dom.appendChild(n)}return e.dom.style.position=this.position,e.dom.style.top=oi,e.dom.style.left="0px",this.container.insertBefore(e.dom,i),e.mount&&e.mount(this.view),this.resizeObserver&&this.resizeObserver.observe(e.dom),e}destroy(){var s,t,e;this.view.win.removeEventListener("resize",this.measureSoon);for(let i of this.manager.tooltipViews)i.dom.remove(),(s=i.destroy)===null||s===void 0||s.call(i);this.parent&&this.container.remove(),(t=this.resizeObserver)===null||t===void 0||t.disconnect(),(e=this.intersectionObserver)===null||e===void 0||e.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let s=this.view.dom.getBoundingClientRect(),t=1,e=1,i=!1;if(this.position=="fixed"&&this.manager.tooltipViews.length){let{dom:n}=this.manager.tooltipViews[0];if(D.gecko)i=n.offsetParent!=this.container.ownerDocument.body;else if(n.style.top==oi&&n.style.left=="0px"){let r=n.getBoundingClientRect();i=Math.abs(r.top+1e4)>1||Math.abs(r.left)>1}}if(i||this.position=="absolute")if(this.parent){let n=this.parent.getBoundingClientRect();n.width&&n.height&&(t=n.width/this.parent.offsetWidth,e=n.height/this.parent.offsetHeight)}else({scaleX:t,scaleY:e}=this.view.viewState);return{editor:s,parent:this.parent?this.container.getBoundingClientRect():s,pos:this.manager.tooltips.map((n,r)=>{let o=this.manager.tooltipViews[r];return o.getCoords?o.getCoords(n.pos):this.view.coordsAtPos(n.pos)}),size:this.manager.tooltipViews.map(({dom:n})=>n.getBoundingClientRect()),space:this.view.state.facet(es).tooltipSpace(this.view),scaleX:t,scaleY:e,makeAbsolute:i}}writeMeasure(s){var t;if(s.makeAbsolute){this.madeAbsolute=!0,this.position="absolute";for(let l of this.manager.tooltipViews)l.dom.style.position="absolute"}let{editor:e,space:i,scaleX:n,scaleY:r}=s,o=[];for(let l=0;l=Math.min(e.bottom,i.bottom)||f.rightMath.min(e.right,i.right)+.1){h.style.top=oi;continue}let d=a.arrow?c.dom.querySelector(".cm-tooltip-arrow"):null,p=d?7:0,g=u.right-u.left,m=(t=Io.get(c))!==null&&t!==void 0?t:u.bottom-u.top,y=c.offset||Fu,x=this.view.textDirection==X.LTR,v=u.width>i.right-i.left?x?i.left:i.right-u.width:x?Math.min(f.left-(d?14:0)+y.x,i.right-g):Math.max(i.left,f.left-g+(d?14:0)-y.x),S=this.above[l];!a.strictSide&&(S?f.top-(u.bottom-u.top)-y.yi.bottom)&&S==i.bottom-f.bottom>f.top-i.top&&(S=this.above[l]=!S);let w=(S?f.top-i.top:i.bottom-f.bottom)-p;if(wv&&B.topA&&(A=S?B.top-m-2-p:B.bottom+p+2);if(this.position=="absolute"?(h.style.top=(A-s.parent.top)/r+"px",h.style.left=(v-s.parent.left)/n+"px"):(h.style.top=A/r+"px",h.style.left=v/n+"px"),d){let B=f.left+(x?y.x:-y.x)-(v+14-7);d.style.left=B/n+"px"}c.overlap!==!0&&o.push({left:v,top:A,right:C,bottom:A+m}),h.classList.toggle("cm-tooltip-above",S),h.classList.toggle("cm-tooltip-below",!S),c.positioned&&c.positioned(s.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let s of this.manager.tooltipViews)s.dom.style.top=oi}},{eventObservers:{scroll(){this.maybeMeasure()}}}),Nu=O.baseTheme({".cm-tooltip":{zIndex:100,boxSizing:"border-box"},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:`${7*2}px`,position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),Fu={x:0,y:0},ja=T.define({enables:[$a,Nu]});function Ua(s,t){let e=s.plugin($a);if(!e)return null;let i=e.manager.tooltips.indexOf(t);return i<0?null:e.manager.tooltipViews[i]}const No=T.define({combine(s){let t,e;for(let i of s)t=t||i.topContainer,e=e||i.bottomContainer;return{topContainer:t,bottomContainer:e}}});function wn(s,t){let e=s.plugin(Ga),i=e?e.specs.indexOf(t):-1;return i>-1?e.panels[i]:null}const Ga=ut.fromClass(class{constructor(s){this.input=s.state.facet(Sn),this.specs=this.input.filter(e=>e),this.panels=this.specs.map(e=>e(s));let t=s.state.facet(No);this.top=new Xi(s,!0,t.topContainer),this.bottom=new Xi(s,!1,t.bottomContainer),this.top.sync(this.panels.filter(e=>e.top)),this.bottom.sync(this.panels.filter(e=>!e.top));for(let e of this.panels)e.dom.classList.add("cm-panel"),e.mount&&e.mount()}update(s){let t=s.state.facet(No);this.top.container!=t.topContainer&&(this.top.sync([]),this.top=new Xi(s.view,!0,t.topContainer)),this.bottom.container!=t.bottomContainer&&(this.bottom.sync([]),this.bottom=new Xi(s.view,!1,t.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let e=s.state.facet(Sn);if(e!=this.input){let i=e.filter(a=>a),n=[],r=[],o=[],l=[];for(let a of i){let c=this.specs.indexOf(a),h;c<0?(h=a(s.view),l.push(h)):(h=this.panels[c],h.update&&h.update(s)),n.push(h),(h.top?r:o).push(h)}this.specs=i,this.panels=n,this.top.sync(r),this.bottom.sync(o);for(let a of l)a.dom.classList.add("cm-panel"),a.mount&&a.mount()}else for(let i of this.panels)i.update&&i.update(s)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:s=>O.scrollMargins.of(t=>{let e=t.plugin(s);return e&&{top:e.top.scrollMargin(),bottom:e.bottom.scrollMargin()}})});class Xi{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(this.panels.length==0){this.dom&&(this.dom.remove(),this.dom=void 0);return}if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let e=this.container||this.view.dom;e.insertBefore(this.dom,this.top?e.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=Fo(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=Fo(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(!(!this.container||this.classes==this.view.themeClasses)){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function Fo(s){let t=s.nextSibling;return s.remove(),t}const Sn=T.define({enables:Ga});class Be extends Me{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}Be.prototype.elementClass="";Be.prototype.toDOM=void 0;Be.prototype.mapMode=ht.TrackBefore;Be.prototype.startSide=Be.prototype.endSide=-1;Be.prototype.point=!0;const Vu=T.define(),Wu=new class extends Be{constructor(){super(...arguments),this.elementClass="cm-activeLineGutter"}},Hu=Vu.compute(["selection"],s=>{let t=[],e=-1;for(let i of s.selection.ranges){let n=s.doc.lineAt(i.head).from;n>e&&(e=n,t.push(Wu.range(n)))}return K.of(t)});function xm(){return Hu}const zu=1024;let qu=0;class Rt{constructor(t,e){this.from=t,this.to=e}}class L{constructor(t={}){this.id=qu++,this.perNode=!!t.perNode,this.deserialize=t.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")})}add(t){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return typeof t!="function"&&(t=gt.match(t)),e=>{let i=t(e);return i===void 0?null:[this,i]}}}L.closedBy=new L({deserialize:s=>s.split(" ")});L.openedBy=new L({deserialize:s=>s.split(" ")});L.group=new L({deserialize:s=>s.split(" ")});L.isolate=new L({deserialize:s=>{if(s&&s!="rtl"&&s!="ltr"&&s!="auto")throw new RangeError("Invalid value for isolate: "+s);return s||"auto"}});L.contextHash=new L({perNode:!0});L.lookAhead=new L({perNode:!0});L.mounted=new L({perNode:!0});class Ai{constructor(t,e,i){this.tree=t,this.overlay=e,this.parser=i}static get(t){return t&&t.props&&t.props[L.mounted.id]}}const Ku=Object.create(null);class gt{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}static define(t){let e=t.props&&t.props.length?Object.create(null):Ku,i=(t.top?1:0)|(t.skipped?2:0)|(t.error?4:0)|(t.name==null?8:0),n=new gt(t.name||"",e,t.id,i);if(t.props){for(let r of t.props)if(Array.isArray(r)||(r=r(n)),r){if(r[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");e[r[0].id]=r[1]}}return n}prop(t){return this.props[t.id]}get isTop(){return(this.flags&1)>0}get isSkipped(){return(this.flags&2)>0}get isError(){return(this.flags&4)>0}get isAnonymous(){return(this.flags&8)>0}is(t){if(typeof t=="string"){if(this.name==t)return!0;let e=this.prop(L.group);return e?e.indexOf(t)>-1:!1}return this.id==t}static match(t){let e=Object.create(null);for(let i in t)for(let n of i.split(" "))e[n]=t[i];return i=>{for(let n=i.prop(L.group),r=-1;r<(n?n.length:0);r++){let o=e[r<0?i.name:n[r]];if(o)return o}}}}gt.none=new gt("",Object.create(null),0,8);class yr{constructor(t){this.types=t;for(let e=0;e0;for(let a=this.cursor(o|Y.IncludeAnonymous);;){let c=!1;if(a.from<=r&&a.to>=n&&(!l&&a.type.isAnonymous||e(a)!==!1)){if(a.firstChild())continue;c=!0}for(;c&&i&&(l||!a.type.isAnonymous)&&i(a),!a.nextSibling();){if(!a.parent())return;c=!0}}}prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type.prop(t)}get propValues(){let t=[];if(this.props)for(let e in this.props)t.push([+e,this.props[e]]);return t}balance(t={}){return this.children.length<=8?this:wr(gt.none,this.children,this.positions,0,this.children.length,0,this.length,(e,i,n)=>new j(this.type,e,i,n,this.propValues),t.makeTree||((e,i,n)=>new j(gt.none,e,i,n)))}static build(t){return Gu(t)}}j.empty=new j(gt.none,[],[],0);class br{constructor(t,e){this.buffer=t,this.index=e}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new br(this.buffer,this.index)}}class me{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get type(){return gt.none}toString(){let t=[];for(let e=0;e0));a=o[a+3]);return l}slice(t,e,i){let n=this.buffer,r=new Uint16Array(e-t),o=0;for(let l=t,a=0;l=t&&et;case 1:return e<=t&&i>t;case 2:return i>t;case 4:return!0}}function Mi(s,t,e,i){for(var n;s.from==s.to||(e<1?s.from>=t:s.from>t)||(e>-1?s.to<=t:s.to0?l.length:-1;t!=c;t+=e){let h=l[t],f=a[t]+o.from;if(Ja(n,i,f,f+h.length)){if(h instanceof me){if(r&Y.ExcludeBuffers)continue;let u=h.findChild(0,h.buffer.length,e,i-f,n);if(u>-1)return new Yt(new $u(o,h,t,f),null,u)}else if(r&Y.IncludeAnonymous||!h.type.isAnonymous||xr(h)){let u;if(!(r&Y.IgnoreMounts)&&(u=Ai.get(h))&&!u.overlay)return new ft(u.tree,f,t,o);let d=new ft(h,f,t,o);return r&Y.IncludeAnonymous||!d.type.isAnonymous?d:d.nextChild(e<0?h.children.length-1:0,e,i,n)}}}if(r&Y.IncludeAnonymous||!o.type.isAnonymous||(o.index>=0?t=o.index+e:t=e<0?-1:o._parent._tree.children.length,o=o._parent,!o))return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(t){return this.nextChild(0,1,t,2)}childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t,-2)}enter(t,e,i=0){let n;if(!(i&Y.IgnoreOverlays)&&(n=Ai.get(this._tree))&&n.overlay){let r=t-this.from;for(let{from:o,to:l}of n.overlay)if((e>0?o<=r:o=r:l>r))return new ft(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,t,e,i)}nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;)t=t._parent;return t}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}get tree(){return this._tree}toTree(){return this._tree}toString(){return this._tree.toString()}}function Wo(s,t,e,i){let n=s.cursor(),r=[];if(!n.firstChild())return r;if(e!=null){for(let o=!1;!o;)if(o=n.type.is(e),!n.nextSibling())return r}for(;;){if(i!=null&&n.type.is(i))return r;if(n.type.is(t)&&r.push(n.node),!n.nextSibling())return i==null?r:[]}}function Gs(s,t,e=t.length-1){for(let i=s.parent;e>=0;i=i.parent){if(!i)return!1;if(!i.type.isAnonymous){if(t[e]&&t[e]!=i.name)return!1;e--}}return!0}class $u{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.start=n}}class Yt extends Ya{get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}constructor(t,e,i){super(),this.context=t,this._parent=e,this.index=i,this.type=t.buffer.set.types[t.buffer.buffer[i]]}child(t,e,i){let{buffer:n}=this.context,r=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.context.start,i);return r<0?null:new Yt(this.context,this,r)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(t){return this.child(1,t,2)}childBefore(t){return this.child(-1,t,-2)}enter(t,e,i=0){if(i&Y.ExcludeBuffers)return null;let{buffer:n}=this.context,r=n.findChild(this.index+4,n.buffer[this.index+3],e>0?1:-1,t-this.context.start,e);return r<0?null:new Yt(this.context,this,r)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(t){return this._parent?null:this.context.parent.nextChild(this.context.index+t,t,0,4)}get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];return e<(this._parent?t.buffer[this._parent.index+3]:t.buffer.length)?new Yt(this.context,this._parent,e):this.externalSibling(1)}get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._parent.index+4:0;return this.index==e?this.externalSibling(-1):new Yt(this.context,this._parent,t.findChild(e,this.index,-1,0,4))}get tree(){return null}toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,r=i.buffer[this.index+3];if(r>n){let o=i.buffer[this.index+1];t.push(i.slice(n,r,o)),e.push(0)}return new j(this.type,t,e,this.to-this.from)}toString(){return this.context.buffer.childString(this.index)}}function Xa(s){if(!s.length)return null;let t=0,e=s[0];for(let r=1;re.from||o.to=t){let l=new ft(o.tree,o.overlay[0].from+r.from,-1,r);(n||(n=[i])).push(Mi(l,t,e,!1))}}return n?Xa(n):i}class vn{get name(){return this.type.name}constructor(t,e=0){if(this.mode=e,this.buffer=null,this.stack=[],this.index=0,this.bufferNode=null,t instanceof ft)this.yieldNode(t);else{this._tree=t.context.parent,this.buffer=t.context;for(let i=t._parent;i;i=i._parent)this.stack.unshift(i.index);this.bufferNode=t,this.yieldBuf(t.index)}}yieldNode(t){return t?(this._tree=t,this.type=t.type,this.from=t.from,this.to=t.to,!0):!1}yieldBuf(t,e){this.index=t;let{start:i,buffer:n}=this.buffer;return this.type=e||n.set.types[n.buffer[t]],this.from=i+n.buffer[t+1],this.to=i+n.buffer[t+2],!0}yield(t){return t?t instanceof ft?(this.buffer=null,this.yieldNode(t)):(this.buffer=t.context,this.yieldBuf(t.index,t.type)):!1}toString(){return this.buffer?this.buffer.buffer.childString(this.index):this._tree.toString()}enterChild(t,e,i){if(!this.buffer)return this.yield(this._tree.nextChild(t<0?this._tree._tree.children.length-1:0,t,e,i,this.mode));let{buffer:n}=this.buffer,r=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.buffer.start,i);return r<0?!1:(this.stack.push(this.index),this.yieldBuf(r))}firstChild(){return this.enterChild(1,0,4)}lastChild(){return this.enterChild(-1,0,4)}childAfter(t){return this.enterChild(1,t,2)}childBefore(t){return this.enterChild(-1,t,-2)}enter(t,e,i=this.mode){return this.buffer?i&Y.ExcludeBuffers?!1:this.enterChild(1,t,e):this.yield(this._tree.enter(t,e,i))}parent(){if(!this.buffer)return this.yieldNode(this.mode&Y.IncludeAnonymous?this._tree._parent:this._tree.parent);if(this.stack.length)return this.yieldBuf(this.stack.pop());let t=this.mode&Y.IncludeAnonymous?this.buffer.parent:this.buffer.parent.nextSignificantParent();return this.buffer=null,this.yieldNode(t)}sibling(t){if(!this.buffer)return this._tree._parent?this.yield(this._tree.index<0?null:this._tree._parent.nextChild(this._tree.index+t,t,0,4,this.mode)):!1;let{buffer:e}=this.buffer,i=this.stack.length-1;if(t<0){let n=i<0?0:this.stack[i]+4;if(this.index!=n)return this.yieldBuf(e.findChild(n,this.index,-1,0,4))}else{let n=e.buffer[this.index+3];if(n<(i<0?e.buffer.length:e.buffer[this.stack[i]+3]))return this.yieldBuf(n)}return i<0?this.yield(this.buffer.parent.nextChild(this.buffer.index+t,t,0,4,this.mode)):!1}nextSibling(){return this.sibling(1)}prevSibling(){return this.sibling(-1)}atLastNode(t){let e,i,{buffer:n}=this;if(n){if(t>0){if(this.index-1)for(let r=e+t,o=t<0?-1:i._tree.children.length;r!=o;r+=t){let l=i._tree.children[r];if(this.mode&Y.IncludeAnonymous||l instanceof me||!l.type.isAnonymous||xr(l))return!1}return!0}move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.sibling(t))return!0;if(this.atLastNode(t)||!this.parent())return!1}}next(t=!0){return this.move(1,t)}prev(t=!0){return this.move(-1,t)}moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)||(e>-1?this.to<=t:this.to=0;){for(let o=t;o;o=o._parent)if(o.index==n){if(n==this.index)return o;e=o,i=r+1;break t}n=this.stack[--r]}for(let n=i;n=0;r--){if(r<0)return Gs(this.node,t,n);let o=i[e.buffer[this.stack[r]]];if(!o.isAnonymous){if(t[n]&&t[n]!=o.name)return!1;n--}}return!0}}function xr(s){return s.children.some(t=>t instanceof me||!t.type.isAnonymous||xr(t))}function Gu(s){var t;let{buffer:e,nodeSet:i,maxBufferLength:n=zu,reused:r=[],minRepeatType:o=i.types.length}=s,l=Array.isArray(e)?new br(e,e.length):e,a=i.types,c=0,h=0;function f(w,A,C,B,I,N){let{id:E,start:R,end:z,size:W}=l,J=h;for(;W<0;)if(l.next(),W==-1){let tt=r[E];C.push(tt),B.push(R-w);return}else if(W==-3){c=E;return}else if(W==-4){h=E;return}else throw new RangeError(`Unrecognized record size: ${W}`);let bt=a[E],xt,kt,Tt=R-w;if(z-R<=n&&(kt=m(l.pos-A,I))){let tt=new Uint16Array(kt.size-kt.skip),Pt=l.pos-kt.size,zt=tt.length;for(;l.pos>Pt;)zt=y(kt.start,tt,zt);xt=new me(tt,z-kt.start,i),Tt=kt.start-w}else{let tt=l.pos-W;l.next();let Pt=[],zt=[],xe=E>=o?E:-1,Ie=0,Vi=z;for(;l.pos>tt;)xe>=0&&l.id==xe&&l.size>=0?(l.end<=Vi-n&&(p(Pt,zt,R,Ie,l.end,Vi,xe,J),Ie=Pt.length,Vi=l.end),l.next()):N>2500?u(R,tt,Pt,zt):f(R,tt,Pt,zt,xe,N+1);if(xe>=0&&Ie>0&&Ie-1&&Ie>0){let Fr=d(bt);xt=wr(bt,Pt,zt,0,Pt.length,0,z-R,Fr,Fr)}else xt=g(bt,Pt,zt,z-R,J-z)}C.push(xt),B.push(Tt)}function u(w,A,C,B){let I=[],N=0,E=-1;for(;l.pos>A;){let{id:R,start:z,end:W,size:J}=l;if(J>4)l.next();else{if(E>-1&&z=0;W-=3)R[J++]=I[W],R[J++]=I[W+1]-z,R[J++]=I[W+2]-z,R[J++]=J;C.push(new me(R,I[2]-z,i)),B.push(z-w)}}function d(w){return(A,C,B)=>{let I=0,N=A.length-1,E,R;if(N>=0&&(E=A[N])instanceof j){if(!N&&E.type==w&&E.length==B)return E;(R=E.prop(L.lookAhead))&&(I=C[N]+E.length+R)}return g(w,A,C,B,I)}}function p(w,A,C,B,I,N,E,R){let z=[],W=[];for(;w.length>B;)z.push(w.pop()),W.push(A.pop()+C-I);w.push(g(i.types[E],z,W,N-I,R-N)),A.push(I-C)}function g(w,A,C,B,I=0,N){if(c){let E=[L.contextHash,c];N=N?[E].concat(N):[E]}if(I>25){let E=[L.lookAhead,I];N=N?[E].concat(N):[E]}return new j(w,A,C,B,N)}function m(w,A){let C=l.fork(),B=0,I=0,N=0,E=C.end-n,R={size:0,start:0,skip:0};t:for(let z=C.pos-w;C.pos>z;){let W=C.size;if(C.id==A&&W>=0){R.size=B,R.start=I,R.skip=N,N+=4,B+=4,C.next();continue}let J=C.pos-W;if(W<0||J=o?4:0,xt=C.start;for(C.next();C.pos>J;){if(C.size<0)if(C.size==-3)bt+=4;else break t;else C.id>=o&&(bt+=4);C.next()}I=xt,B+=W,N+=bt}return(A<0||B==w)&&(R.size=B,R.start=I,R.skip=N),R.size>4?R:void 0}function y(w,A,C){let{id:B,start:I,end:N,size:E}=l;if(l.next(),E>=0&&B4){let z=l.pos-(E-4);for(;l.pos>z;)C=y(w,A,C)}A[--C]=R,A[--C]=N-w,A[--C]=I-w,A[--C]=B}else E==-3?c=B:E==-4&&(h=B);return C}let x=[],v=[];for(;l.pos>0;)f(s.start||0,s.bufferStart||0,x,v,-1,0);let S=(t=s.length)!==null&&t!==void 0?t:x.length?v[0]+x[0].length:0;return new j(a[s.topID],x.reverse(),v.reverse(),S)}const Ho=new WeakMap;function dn(s,t){if(!s.isAnonymous||t instanceof me||t.type!=s)return 1;let e=Ho.get(t);if(e==null){e=1;for(let i of t.children){if(i.type!=s||!(i instanceof j)){e=1;break}e+=dn(s,i)}Ho.set(t,e)}return e}function wr(s,t,e,i,n,r,o,l,a){let c=0;for(let p=i;p=h)break;A+=C}if(v==S+1){if(A>h){let C=p[S];d(C.children,C.positions,0,C.children.length,g[S]+x);continue}f.push(p[S])}else{let C=g[v-1]+p[v-1].length-w;f.push(wr(s,p,g,S,v,w,C,null,a))}u.push(w+x-r)}}return d(t,e,i,n,0),(l||a)(f,u,o)}class wm{constructor(){this.map=new WeakMap}setBuffer(t,e,i){let n=this.map.get(t);n||this.map.set(t,n=new Map),n.set(e,i)}getBuffer(t,e){let i=this.map.get(t);return i&&i.get(e)}set(t,e){t instanceof Yt?this.setBuffer(t.context.buffer,t.index,e):t instanceof ft&&this.map.set(t.tree,e)}get(t){return t instanceof Yt?this.getBuffer(t.context.buffer,t.index):t instanceof ft?this.map.get(t.tree):void 0}cursorSet(t,e){t.buffer?this.setBuffer(t.buffer.buffer,t.index,e):this.map.set(t.tree,e)}cursorGet(t){return t.buffer?this.getBuffer(t.buffer.buffer,t.index):this.map.get(t.tree)}}class ee{constructor(t,e,i,n,r=!1,o=!1){this.from=t,this.to=e,this.tree=i,this.offset=n,this.open=(r?1:0)|(o?2:0)}get openStart(){return(this.open&1)>0}get openEnd(){return(this.open&2)>0}static addTree(t,e=[],i=!1){let n=[new ee(0,t.length,t,0,!1,i)];for(let r of e)r.to>t.length&&n.push(r);return n}static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],r=1,o=t.length?t[0]:null;for(let l=0,a=0,c=0;;l++){let h=l=i)for(;o&&o.from=u.from||f<=u.to||c){let d=Math.max(u.from,a)-c,p=Math.min(u.to,f)-c;u=d>=p?null:new ee(d,p,u.tree,u.offset+c,l>0,!!h)}if(u&&n.push(u),o.to>f)break;o=rnew Rt(n.from,n.to)):[new Rt(0,0)]:[new Rt(0,t.length)],this.createParse(t,e||[],i)}parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let r=n.advance();if(r)return r}}}class Ju{constructor(t){this.string=t}get length(){return this.string.length}chunk(t){return this.string.slice(t)}get lineChunks(){return!1}read(t,e){return this.string.slice(t,e)}}function Sm(s){return(t,e,i,n)=>new Xu(t,s,e,i,n)}class zo{constructor(t,e,i,n,r){this.parser=t,this.parse=e,this.overlay=i,this.target=n,this.from=r}}function qo(s){if(!s.length||s.some(t=>t.from>=t.to))throw new RangeError("Invalid inner parse ranges given: "+JSON.stringify(s))}class Yu{constructor(t,e,i,n,r,o,l){this.parser=t,this.predicate=e,this.mounts=i,this.index=n,this.start=r,this.target=o,this.prev=l,this.depth=0,this.ranges=[]}}const Js=new L({perNode:!0});class Xu{constructor(t,e,i,n,r){this.nest=e,this.input=i,this.fragments=n,this.ranges=r,this.inner=[],this.innerDone=0,this.baseTree=null,this.stoppedAt=null,this.baseParse=t}advance(){if(this.baseParse){let i=this.baseParse.advance();if(!i)return null;if(this.baseParse=null,this.baseTree=i,this.startInner(),this.stoppedAt!=null)for(let n of this.inner)n.parse.stopAt(this.stoppedAt)}if(this.innerDone==this.inner.length){let i=this.baseTree;return this.stoppedAt!=null&&(i=new j(i.type,i.children,i.positions,i.length,i.propValues.concat([[Js,this.stoppedAt]]))),i}let t=this.inner[this.innerDone],e=t.parse.advance();if(e){this.innerDone++;let i=Object.assign(Object.create(null),t.target.props);i[L.mounted.id]=new Ai(e,t.overlay,t.parser),t.target.props=i}return null}get parsedPos(){if(this.baseParse)return 0;let t=this.input.length;for(let e=this.innerDone;e=this.stoppedAt)l=!1;else if(t.hasNode(n)){if(e){let c=e.mounts.find(h=>h.frag.from<=n.from&&h.frag.to>=n.to&&h.mount.overlay);if(c)for(let h of c.mount.overlay){let f=h.from+c.pos,u=h.to+c.pos;f>=n.from&&u<=n.to&&!e.ranges.some(d=>d.fromf)&&e.ranges.push({from:f,to:u})}}l=!1}else if(i&&(o=_u(i.ranges,n.from,n.to)))l=o!=2;else if(!n.type.isAnonymous&&(r=this.nest(n,this.input))&&(n.fromnew Rt(f.from-n.from,f.to-n.from)):null,n.tree,h.length?h[0].from:n.from)),r.overlay?h.length&&(i={ranges:h,depth:0,prev:i}):l=!1}}else e&&(a=e.predicate(n))&&(a===!0&&(a=new Rt(n.from,n.to)),a.fromnew Rt(h.from-e.start,h.to-e.start)),e.target,c[0].from))),e=e.prev}i&&!--i.depth&&(i=i.prev)}}}}function _u(s,t,e){for(let i of s){if(i.from>=e)break;if(i.to>t)return i.from<=t&&i.to>=e?2:1}return 0}function Ko(s,t,e,i,n,r){if(t=t&&e.enter(i,1,Y.IgnoreOverlays|Y.ExcludeBuffers)||e.next(!1)||(this.done=!0)}hasNode(t){if(this.moveTo(t.from),!this.done&&this.cursor.from+this.offset==t.from&&this.cursor.tree)for(let e=this.cursor.tree;;){if(e==t.tree)return!0;if(e.children.length&&e.positions[0]==0&&e.children[0]instanceof j)e=e.children[0];else break}return!1}}class Zu{constructor(t){var e;if(this.fragments=t,this.curTo=0,this.fragI=0,t.length){let i=this.curFrag=t[0];this.curTo=(e=i.tree.prop(Js))!==null&&e!==void 0?e:i.to,this.inner=new $o(i.tree,-i.offset)}else this.curFrag=this.inner=null}hasNode(t){for(;this.curFrag&&t.from>=this.curTo;)this.nextFrag();return this.curFrag&&this.curFrag.from<=t.from&&this.curTo>=t.to&&this.inner.hasNode(t)}nextFrag(){var t;if(this.fragI++,this.fragI==this.fragments.length)this.curFrag=this.inner=null;else{let e=this.curFrag=this.fragments[this.fragI];this.curTo=(t=e.tree.prop(Js))!==null&&t!==void 0?t:e.to,this.inner=new $o(e.tree,-e.offset)}}findMounts(t,e){var i;let n=[];if(this.inner){this.inner.cursor.moveTo(t,1);for(let r=this.inner.cursor.node;r;r=r.parent){let o=(i=r.tree)===null||i===void 0?void 0:i.prop(L.mounted);if(o&&o.parser==e)for(let l=this.fragI;l=r.to)break;a.tree==this.curFrag.tree&&n.push({frag:a,pos:r.from-a.offset,mount:o})}}}return n}}function jo(s,t){let e=null,i=t;for(let n=1,r=0;n=l)break;a.to<=o||(e||(i=e=t.slice()),a.froml&&e.splice(r+1,0,new Rt(l,a.to))):a.to>l?e[r--]=new Rt(l,a.to):e.splice(r--,1))}}return i}function td(s,t,e,i){let n=0,r=0,o=!1,l=!1,a=-1e9,c=[];for(;;){let h=n==s.length?1e9:o?s[n].to:s[n].from,f=r==t.length?1e9:l?t[r].to:t[r].from;if(o!=l){let u=Math.max(a,e),d=Math.min(h,f,i);unew Rt(u.from+i,u.to+i)),f=td(t,h,a,c);for(let u=0,d=a;;u++){let p=u==f.length,g=p?c:f[u].from;if(g>d&&e.push(new ee(d,g,n.tree,-o,r.from>=d||r.openStart,r.to<=g||r.openEnd)),p)break;d=f[u].to}}else e.push(new ee(a,c,n.tree,-o,r.from>=o||r.openStart,r.to<=l||r.openEnd))}return e}let ed=0;class Ut{constructor(t,e,i){this.set=t,this.base=e,this.modified=i,this.id=ed++}static define(t){if(t!=null&&t.base)throw new Error("Can not derive from a modified tag");let e=new Ut([],null,[]);if(e.set.push(e),t)for(let i of t.set)e.set.push(i);return e}static defineModifier(){let t=new kn;return e=>e.modified.indexOf(t)>-1?e:kn.get(e.base||e,e.modified.concat(t).sort((i,n)=>i.id-n.id))}}let id=0;class kn{constructor(){this.instances=[],this.id=id++}static get(t,e){if(!e.length)return t;let i=e[0].instances.find(l=>l.base==t&&nd(e,l.modified));if(i)return i;let n=[],r=new Ut(n,t,e);for(let l of e)l.instances.push(r);let o=sd(e);for(let l of t.set)if(!l.modified.length)for(let a of o)n.push(kn.get(l,a));return r}}function nd(s,t){return s.length==t.length&&s.every((e,i)=>e==t[i])}function sd(s){let t=[[]];for(let e=0;ei.length-e.length)}function rd(s){let t=Object.create(null);for(let e in s){let i=s[e];Array.isArray(i)||(i=[i]);for(let n of e.split(" "))if(n){let r=[],o=2,l=n;for(let f=0;;){if(l=="..."&&f>0&&f+3==n.length){o=1;break}let u=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(l);if(!u)throw new RangeError("Invalid path: "+n);if(r.push(u[0]=="*"?"":u[0][0]=='"'?JSON.parse(u[0]):u[0]),f+=u[0].length,f==n.length)break;let d=n[f++];if(f==n.length&&d=="!"){o=0;break}if(d!="/")throw new RangeError("Invalid path: "+n);l=n.slice(f)}let a=r.length-1,c=r[a];if(!c)throw new RangeError("Invalid path: "+n);let h=new Cn(i,o,a>0?r.slice(0,a):null);t[c]=h.sort(t[c])}}return Qa.add(t)}const Qa=new L;class Cn{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}get opaque(){return this.mode==0}get inherit(){return this.mode==1}sort(t){return!t||t.depth{let o=n;for(let l of r)for(let a of l.set){let c=e[a.id];if(c){o=o?o+" "+c:c;break}}return o},scope:i}}function od(s,t){let e=null;for(let i of s){let n=i.style(t);n&&(e=e?e+" "+n:n)}return e}function ld(s,t,e,i=0,n=s.length){let r=new ad(i,Array.isArray(t)?t:[t],e);r.highlightRange(s.cursor(),i,n,"",r.highlighters),r.flush(n)}class ad{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.class=""}startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=e)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,e,i,n,r){let{type:o,from:l,to:a}=t;if(l>=i||a<=e)return;o.isTop&&(r=this.highlighters.filter(d=>!d.scope||d.scope(o)));let c=n,h=hd(t)||Cn.empty,f=od(r,h.tags);if(f&&(c&&(c+=" "),c+=f,h.mode==1&&(n+=(n?" ":"")+f)),this.startSpan(Math.max(e,l),c),h.opaque)return;let u=t.tree&&t.tree.prop(L.mounted);if(u&&u.overlay){let d=t.node.enter(u.overlay[0].from+l,1),p=this.highlighters.filter(m=>!m.scope||m.scope(u.tree.type)),g=t.firstChild();for(let m=0,y=l;;m++){let x=m=v||!t.nextSibling())););if(!x||v>i)break;y=x.to+l,y>e&&(this.highlightRange(d.cursor(),Math.max(e,x.from+l),Math.min(i,y),"",p),this.startSpan(Math.min(i,y),c))}g&&t.parent()}else if(t.firstChild()){u&&(n="");do if(!(t.to<=e)){if(t.from>=i)break;this.highlightRange(t,e,i,n,r),this.startSpan(Math.min(i,t.to),c)}while(t.nextSibling());t.parent()}}}function hd(s){let t=s.type.prop(Qa);for(;t&&t.context&&!s.matchContext(t.context);)t=t.next;return t||null}const k=Ut.define,Qi=k(),re=k(),Go=k(re),Jo=k(re),oe=k(),Zi=k(oe),is=k(oe),jt=k(),we=k(jt),Kt=k(),$t=k(),Ys=k(),li=k(Ys),tn=k(),M={comment:Qi,lineComment:k(Qi),blockComment:k(Qi),docComment:k(Qi),name:re,variableName:k(re),typeName:Go,tagName:k(Go),propertyName:Jo,attributeName:k(Jo),className:k(re),labelName:k(re),namespace:k(re),macroName:k(re),literal:oe,string:Zi,docString:k(Zi),character:k(Zi),attributeValue:k(Zi),number:is,integer:k(is),float:k(is),bool:k(oe),regexp:k(oe),escape:k(oe),color:k(oe),url:k(oe),keyword:Kt,self:k(Kt),null:k(Kt),atom:k(Kt),unit:k(Kt),modifier:k(Kt),operatorKeyword:k(Kt),controlKeyword:k(Kt),definitionKeyword:k(Kt),moduleKeyword:k(Kt),operator:$t,derefOperator:k($t),arithmeticOperator:k($t),logicOperator:k($t),bitwiseOperator:k($t),compareOperator:k($t),updateOperator:k($t),definitionOperator:k($t),typeOperator:k($t),controlOperator:k($t),punctuation:Ys,separator:k(Ys),bracket:li,angleBracket:k(li),squareBracket:k(li),paren:k(li),brace:k(li),content:jt,heading:we,heading1:k(we),heading2:k(we),heading3:k(we),heading4:k(we),heading5:k(we),heading6:k(we),contentSeparator:k(jt),list:k(jt),quote:k(jt),emphasis:k(jt),strong:k(jt),link:k(jt),monospace:k(jt),strikethrough:k(jt),inserted:k(),deleted:k(),changed:k(),invalid:k(),meta:tn,documentMeta:k(tn),annotation:k(tn),processingInstruction:k(tn),definition:Ut.defineModifier(),constant:Ut.defineModifier(),function:Ut.defineModifier(),standard:Ut.defineModifier(),local:Ut.defineModifier(),special:Ut.defineModifier()};Za([{tag:M.link,class:"tok-link"},{tag:M.heading,class:"tok-heading"},{tag:M.emphasis,class:"tok-emphasis"},{tag:M.strong,class:"tok-strong"},{tag:M.keyword,class:"tok-keyword"},{tag:M.atom,class:"tok-atom"},{tag:M.bool,class:"tok-bool"},{tag:M.url,class:"tok-url"},{tag:M.labelName,class:"tok-labelName"},{tag:M.inserted,class:"tok-inserted"},{tag:M.deleted,class:"tok-deleted"},{tag:M.literal,class:"tok-literal"},{tag:M.string,class:"tok-string"},{tag:M.number,class:"tok-number"},{tag:[M.regexp,M.escape,M.special(M.string)],class:"tok-string2"},{tag:M.variableName,class:"tok-variableName"},{tag:M.local(M.variableName),class:"tok-variableName tok-local"},{tag:M.definition(M.variableName),class:"tok-variableName tok-definition"},{tag:M.special(M.variableName),class:"tok-variableName2"},{tag:M.definition(M.propertyName),class:"tok-propertyName tok-definition"},{tag:M.typeName,class:"tok-typeName"},{tag:M.namespace,class:"tok-namespace"},{tag:M.className,class:"tok-className"},{tag:M.macroName,class:"tok-macroName"},{tag:M.propertyName,class:"tok-propertyName"},{tag:M.operator,class:"tok-operator"},{tag:M.comment,class:"tok-comment"},{tag:M.meta,class:"tok-meta"},{tag:M.invalid,class:"tok-invalid"},{tag:M.punctuation,class:"tok-punctuation"}]);var ns;const Ce=new L;function th(s){return T.define({combine:s?t=>t.concat(s):void 0})}const cd=new L;class Lt{constructor(t,e,i=[],n=""){this.data=t,this.name=n,H.prototype.hasOwnProperty("tree")||Object.defineProperty(H.prototype,"tree",{get(){return mt(this)}}),this.parser=e,this.extension=[Qe.of(this),H.languageData.of((r,o,l)=>{let a=Yo(r,o,l),c=a.type.prop(Ce);if(!c)return[];let h=r.facet(c),f=a.type.prop(cd);if(f){let u=a.resolve(o-a.from,l);for(let d of f)if(d.test(u,r)){let p=r.facet(d.facet);return d.type=="replace"?p:p.concat(h)}}return h})].concat(i)}isActiveAt(t,e,i=-1){return Yo(t,e,i).type.prop(Ce)==this.data}findRegions(t){let e=t.facet(Qe);if((e==null?void 0:e.data)==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let i=[],n=(r,o)=>{if(r.prop(Ce)==this.data){i.push({from:o,to:o+r.length});return}let l=r.prop(L.mounted);if(l){if(l.tree.prop(Ce)==this.data){if(l.overlay)for(let a of l.overlay)i.push({from:a.from+o,to:a.to+o});else i.push({from:o,to:o+r.length});return}else if(l.overlay){let a=i.length;if(n(l.tree,l.overlay[0].from+o),i.length>a)return}}for(let a=0;ai.isTop?e:void 0)]}),t.name)}configure(t,e){return new Xs(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function mt(s){let t=s.field(Lt.state,!1);return t?t.tree:j.empty}class fd{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=t.iter()}get length(){return this.doc.length}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let i=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-i,e-i)}}let ai=null;class Xe{constructor(t,e,i=[],n,r,o,l,a){this.parser=t,this.state=e,this.fragments=i,this.tree=n,this.treeLen=r,this.viewport=o,this.skipped=l,this.scheduleOn=a,this.parse=null,this.tempSkipped=[]}static create(t,e,i){return new Xe(t,e,[],j.empty,0,i,[],null)}startParse(){return this.parser.startParse(new fd(this.state.doc),this.fragments)}work(t,e){return e!=null&&e>=this.state.doc.length&&(e=void 0),this.tree!=j.empty&&this.isDone(e??this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var i;if(typeof t=="number"){let n=Date.now()+t;t=()=>Date.now()>n}for(this.parse||(this.parse=this.startParse()),e!=null&&(this.parse.stoppedAt==null||this.parse.stoppedAt>e)&&e=this.treeLen&&((this.parse.stoppedAt==null||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext(()=>{for(;!(e=this.parse.advance()););}),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(ee.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=ai;ai=this;try{return t()}finally{ai=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=Xo(t,e.from,e.to);return t}changes(t,e){let{fragments:i,tree:n,treeLen:r,viewport:o,skipped:l}=this;if(this.takeTree(),!t.empty){let a=[];if(t.iterChangedRanges((c,h,f,u)=>a.push({fromA:c,toA:h,fromB:f,toB:u})),i=ee.applyChanges(i,a),n=j.empty,r=0,o={from:t.mapPos(o.from,-1),to:t.mapPos(o.to,1)},this.skipped.length){l=[];for(let c of this.skipped){let h=t.mapPos(c.from,1),f=t.mapPos(c.to,-1);ht.from&&(this.fragments=Xo(this.fragments,n,r),this.skipped.splice(i--,1))}return this.skipped.length>=e?!1:(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends _a{createParse(e,i,n){let r=n[0].from,o=n[n.length-1].to;return{parsedPos:r,advance(){let a=ai;if(a){for(let c of n)a.tempSkipped.push(c);t&&(a.scheduleOn=a.scheduleOn?Promise.all([a.scheduleOn,t]):t)}return this.parsedPos=o,new j(gt.none,[],[],o-r)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&e[0].from==0&&e[0].to>=t}static get(){return ai}}function Xo(s,t,e){return ee.applyChanges(s,[{fromA:t,toA:e,fromB:t,toB:e}])}class _e{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),i=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,i)||e.takeTree(),new _e(e)}static init(t){let e=Math.min(3e3,t.doc.length),i=Xe.create(t.facet(Qe).parser,t,{from:0,to:e});return i.work(20,e)||i.takeTree(),new _e(i)}}Lt.state=yt.define({create:_e.init,update(s,t){for(let e of t.effects)if(e.is(Lt.setState))return e.value;return t.startState.facet(Qe)!=t.state.facet(Qe)?_e.init(t.state):s.apply(t)}});let eh=s=>{let t=setTimeout(()=>s(),500);return()=>clearTimeout(t)};typeof requestIdleCallback<"u"&&(eh=s=>{let t=-1,e=setTimeout(()=>{t=requestIdleCallback(s,{timeout:400})},100);return()=>t<0?clearTimeout(e):cancelIdleCallback(t)});const ss=typeof navigator<"u"&&(!((ns=navigator.scheduling)===null||ns===void 0)&&ns.isInputPending)?()=>navigator.scheduling.isInputPending():null,ud=ut.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(Lt.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),(t.docChanged||t.selectionSet)&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(Lt.state);(e.tree!=e.context.tree||!e.context.isDone(t.doc.length))&&(this.working=eh(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndn+1e3,a=r.context.work(()=>ss&&ss()||Date.now()>o,n+(l?0:1e5));this.chunkBudget-=Date.now()-e,(a||this.chunkBudget<=0)&&(r.context.takeTree(),this.view.dispatch({effects:Lt.setState.of(new _e(r.context))})),this.chunkBudget>0&&!(a&&!l)&&this.scheduleWork(),this.checkAsyncSchedule(r.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then(()=>this.scheduleWork()).catch(e=>Dt(this.view.state,e)).then(()=>this.workScheduled--),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Qe=T.define({combine(s){return s.length?s[0]:null},enables:s=>[Lt.state,ud,O.contentAttributes.compute([s],t=>{let e=t.facet(s);return e&&e.name?{"data-language":e.name}:{}})]});class km{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}const ih=T.define(),Vn=T.define({combine:s=>{if(!s.length)return" ";let t=s[0];if(!t||/\S/.test(t)||Array.from(t).some(e=>e!=t[0]))throw new Error("Invalid indent unit: "+JSON.stringify(s[0]));return t}});function Re(s){let t=s.facet(Vn);return t.charCodeAt(0)==9?s.tabSize*t.length:t.length}function An(s,t){let e="",i=s.tabSize,n=s.facet(Vn)[0];if(n==" "){for(;t>=i;)e+=" ",t-=i;n=" "}for(let r=0;r=t?pd(s,e,t):null}class Wn{constructor(t,e={}){this.state=t,this.options=e,this.unit=Re(t)}lineAt(t,e=1){let i=this.state.doc.lineAt(t),{simulateBreak:n,simulateDoubleBreak:r}=this.options;return n!=null&&n>=i.from&&n<=i.to?r&&n==t?{text:"",from:t}:(e<0?n-1&&(r+=o-this.countColumn(i,i.search(/\S|$/))),r}countColumn(t,e=t.length){return ti(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:i,from:n}=this.lineAt(t,e),r=this.options.overrideIndentation;if(r){let o=r(n);if(o>-1)return o}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const dd=new L;function pd(s,t,e){let i=t.resolveStack(e),n=i.node.enterUnfinishedNodesBefore(e);if(n!=i.node){let r=[];for(let o=n;o!=i.node;o=o.parent)r.push(o);for(let o=r.length-1;o>=0;o--)i={node:r[o],next:i}}return sh(i,s,e)}function sh(s,t,e){for(let i=s;i;i=i.next){let n=md(i.node);if(n)return n(Sr.create(t,e,i))}return 0}function gd(s){return s.pos==s.options.simulateBreak&&s.options.simulateDoubleBreak}function md(s){let t=s.type.prop(dd);if(t)return t;let e=s.firstChild,i;if(e&&(i=e.type.prop(L.closedBy))){let n=s.lastChild,r=n&&i.indexOf(n.name)>-1;return o=>rh(o,!0,1,void 0,r&&!gd(o)?n.from:void 0)}return s.parent==null?yd:null}function yd(){return 0}class Sr extends Wn{constructor(t,e,i){super(t.state,t.options),this.base=t,this.pos=e,this.context=i}get node(){return this.context.node}static create(t,e,i){return new Sr(t,e,i)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){return this.baseIndentFor(this.node)}baseIndentFor(t){let e=this.state.doc.lineAt(t.from);for(;;){let i=t.resolve(e.from);for(;i.parent&&i.parent.from==i.from;)i=i.parent;if(bd(i,t))break;e=this.state.doc.lineAt(i.from)}return this.lineIndent(e.from)}continue(){return sh(this.context.next,this.base,this.pos)}}function bd(s,t){for(let e=t;e;e=e.parent)if(s==e)return!0;return!1}function xd(s){let t=s.node,e=t.childAfter(t.from),i=t.lastChild;if(!e)return null;let n=s.options.simulateBreak,r=s.state.doc.lineAt(e.from),o=n==null||n<=r.from?r.to:Math.min(r.to,n);for(let l=e.to;;){let a=t.childAfter(l);if(!a||a==i)return null;if(!a.type.isSkipped)return a.fromrh(i,t,e,s)}function rh(s,t,e,i,n){let r=s.textAfter,o=r.match(/^\s*/)[0].length,l=i&&r.slice(o,o+i.length)==i||n==s.pos+o,a=t?xd(s):null;return a?l?s.column(a.from):s.column(a.to):s.baseIndent+(l?0:s.unit*e)}const Am=s=>s.baseIndent;function Mm({except:s,units:t=1}={}){return e=>{let i=s&&s.test(e.textAfter);return e.baseIndent+(i?0:t*e.unit)}}const Dm=new L;function Om(s){let t=s.firstChild,e=s.lastChild;return t&&t.tol.prop(Ce)==o.data:o?l=>l==o:void 0,this.style=Za(t.map(l=>({tag:l.tag,class:l.class||n(Object.assign({},l,{tag:null}))})),{all:r}).style,this.module=i?new de(i):null,this.themeType=e.themeType}static define(t,e){return new Hn(t,e||{})}}const _s=T.define(),oh=T.define({combine(s){return s.length?[s[0]]:null}});function rs(s){let t=s.facet(_s);return t.length?t:s.facet(oh)}function Tm(s,t){let e=[Sd],i;return s instanceof Hn&&(s.module&&e.push(O.styleModule.of(s.module)),i=s.themeType),t!=null&&t.fallback?e.push(oh.of(s)):i?e.push(_s.computeN([O.darkTheme],n=>n.facet(O.darkTheme)==(i=="dark")?[s]:[])):e.push(_s.of(s)),e}class wd{constructor(t){this.markCache=Object.create(null),this.tree=mt(t.state),this.decorations=this.buildDeco(t,rs(t.state)),this.decoratedTo=t.viewport.to}update(t){let e=mt(t.state),i=rs(t.state),n=i!=rs(t.startState),{viewport:r}=t.view,o=t.changes.mapPos(this.decoratedTo,1);e.length=r.to?(this.decorations=this.decorations.map(t.changes),this.decoratedTo=o):(e!=this.tree||t.viewportChanged||n)&&(this.tree=e,this.decorations=this.buildDeco(t.view,i),this.decoratedTo=r.to)}buildDeco(t,e){if(!e||!this.tree.length)return P.none;let i=new De;for(let{from:n,to:r}of t.visibleRanges)ld(this.tree,e,(o,l,a)=>{i.add(o,l,this.markCache[a]||(this.markCache[a]=P.mark({class:a})))},n,r);return i.finish()}}const Sd=ye.high(ut.fromClass(wd,{decorations:s=>s.decorations})),Pm=Hn.define([{tag:M.meta,color:"#404740"},{tag:M.link,textDecoration:"underline"},{tag:M.heading,textDecoration:"underline",fontWeight:"bold"},{tag:M.emphasis,fontStyle:"italic"},{tag:M.strong,fontWeight:"bold"},{tag:M.strikethrough,textDecoration:"line-through"},{tag:M.keyword,color:"#708"},{tag:[M.atom,M.bool,M.url,M.contentSeparator,M.labelName],color:"#219"},{tag:[M.literal,M.inserted],color:"#164"},{tag:[M.string,M.deleted],color:"#a11"},{tag:[M.regexp,M.escape,M.special(M.string)],color:"#e40"},{tag:M.definition(M.variableName),color:"#00f"},{tag:M.local(M.variableName),color:"#30a"},{tag:[M.typeName,M.namespace],color:"#085"},{tag:M.className,color:"#167"},{tag:[M.special(M.variableName),M.macroName],color:"#256"},{tag:M.definition(M.propertyName),color:"#00c"},{tag:M.comment,color:"#940"},{tag:M.invalid,color:"#f00"}]),vd=O.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),lh=1e4,ah="()[]{}",hh=T.define({combine(s){return Le(s,{afterCursor:!0,brackets:ah,maxScanDistance:lh,renderMatch:Ad})}}),kd=P.mark({class:"cm-matchingBracket"}),Cd=P.mark({class:"cm-nonmatchingBracket"});function Ad(s){let t=[],e=s.matched?kd:Cd;return t.push(e.range(s.start.from,s.start.to)),s.end&&t.push(e.range(s.end.from,s.end.to)),t}const Md=yt.define({create(){return P.none},update(s,t){if(!t.docChanged&&!t.selection)return s;let e=[],i=t.state.facet(hh);for(let n of t.state.selection.ranges){if(!n.empty)continue;let r=Xt(t.state,n.head,-1,i)||n.head>0&&Xt(t.state,n.head-1,1,i)||i.afterCursor&&(Xt(t.state,n.head,1,i)||n.headO.decorations.from(s)}),Dd=[Md,vd];function Bm(s={}){return[hh.of(s),Dd]}const Od=new L;function Qs(s,t,e){let i=s.prop(t<0?L.openedBy:L.closedBy);if(i)return i;if(s.name.length==1){let n=e.indexOf(s.name);if(n>-1&&n%2==(t<0?1:0))return[e[n+t]]}return null}function Zs(s){let t=s.type.prop(Od);return t?t(s.node):s}function Xt(s,t,e,i={}){let n=i.maxScanDistance||lh,r=i.brackets||ah,o=mt(s),l=o.resolveInner(t,e);for(let a=l;a;a=a.parent){let c=Qs(a.type,e,r);if(c&&a.from0?t>=h.from&&th.from&&t<=h.to))return Td(s,t,e,a,h,c,r)}}return Pd(s,t,e,o,l.type,n,r)}function Td(s,t,e,i,n,r,o){let l=i.parent,a={from:n.from,to:n.to},c=0,h=l==null?void 0:l.cursor();if(h&&(e<0?h.childBefore(i.from):h.childAfter(i.to)))do if(e<0?h.to<=i.from:h.from>=i.to){if(c==0&&r.indexOf(h.type.name)>-1&&h.from0)return null;let c={from:e<0?t-1:t,to:e>0?t+1:t},h=s.doc.iterRange(t,e>0?s.doc.length:0),f=0;for(let u=0;!h.next().done&&u<=r;){let d=h.value;e<0&&(u+=d.length);let p=t+u*e;for(let g=e>0?0:d.length-1,m=e>0?d.length:-1;g!=m;g+=e){let y=o.indexOf(d[g]);if(!(y<0||i.resolveInner(p+g,1).type!=n))if(y%2==0==e>0)f++;else{if(f==1)return{start:c,end:{from:p+g,to:p+g+1},matched:y>>1==a>>1};f--}}e>0&&(u+=d.length)}return h.done?{start:c,matched:!1}:null}function _o(s,t,e,i=0,n=0){t==null&&(t=s.search(/[^\s\u00a0]/),t==-1&&(t=s.length));let r=n;for(let o=i;o=this.string.length}sol(){return this.pos==0}peek(){return this.string.charAt(this.pos)||void 0}next(){if(this.pose}eatSpace(){let t=this.pos;for(;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>t}skipToEnd(){this.pos=this.string.length}skipTo(t){let e=this.string.indexOf(t,this.pos);if(e>-1)return this.pos=e,!0}backUp(t){this.pos-=t}column(){return this.lastColumnPosi?o.toLowerCase():o,r=this.string.substr(this.pos,t.length);return n(r)==n(t)?(e!==!1&&(this.pos+=t.length),!0):null}else{let n=this.string.slice(this.pos).match(t);return n&&n.index>0?null:(n&&e!==!1&&(this.pos+=n[0].length),n)}}current(){return this.string.slice(this.start,this.pos)}}function Bd(s){return{name:s.name||"",token:s.token,blankLine:s.blankLine||(()=>{}),startState:s.startState||(()=>!0),copyState:s.copyState||Rd,indent:s.indent||(()=>null),languageData:s.languageData||{},tokenTable:s.tokenTable||kr}}function Rd(s){if(typeof s!="object")return s;let t={};for(let e in s){let i=s[e];t[e]=i instanceof Array?i.slice():i}return t}const Qo=new WeakMap;class fh extends Lt{constructor(t){let e=th(t.languageData),i=Bd(t),n,r=new class extends _a{createParse(o,l,a){return new Ed(n,o,l,a)}};super(e,r,[ih.of((o,l)=>this.getIndent(o,l))],t.name),this.topNode=Fd(e),n=this,this.streamParser=i,this.stateAfter=new L({perNode:!0}),this.tokenTable=t.tokenTable?new gh(i.tokenTable):Nd}static define(t){return new fh(t)}getIndent(t,e){let i=mt(t.state),n=i.resolve(e);for(;n&&n.type!=this.topNode;)n=n.parent;if(!n)return null;let r,{overrideIndentation:o}=t.options;o&&(r=Qo.get(t.state),r!=null&&r1e4)return null;for(;a=i&&e+t.length<=n&&t.prop(s.stateAfter);if(r)return{state:s.streamParser.copyState(r),pos:e+t.length};for(let o=t.children.length-1;o>=0;o--){let l=t.children[o],a=e+t.positions[o],c=l instanceof j&&a=t.length)return t;!n&&t.type==s.topNode&&(n=!0);for(let r=t.children.length-1;r>=0;r--){let o=t.positions[r],l=t.children[r],a;if(oe&&vr(s,n.tree,0-n.offset,e,o),a;if(l&&(a=uh(s,n.tree,e+n.offset,l.pos+n.offset,!1)))return{state:l.state,tree:a}}return{state:s.streamParser.startState(i?Re(i):4),tree:j.empty}}class Ed{constructor(t,e,i,n){this.lang=t,this.input=e,this.fragments=i,this.ranges=n,this.stoppedAt=null,this.chunks=[],this.chunkPos=[],this.chunk=[],this.chunkReused=void 0,this.rangeIndex=0,this.to=n[n.length-1].to;let r=Xe.get(),o=n[0].from,{state:l,tree:a}=Ld(t,i,o,r==null?void 0:r.state);this.state=l,this.parsedPos=this.chunkStart=o+a.length;for(let c=0;c=e?this.finish():t&&this.parsedPos>=t.viewport.to?(t.skipUntilInView(this.parsedPos,e),this.finish()):null}stopAt(t){this.stoppedAt=t}lineAfter(t){let e=this.input.chunk(t);if(this.input.lineChunks)e==` -`&&(e="");else{let i=e.indexOf(` -`);i>-1&&(e=e.slice(0,i))}return t+e.length<=this.to?e:e.slice(0,this.to-t)}nextLine(){let t=this.parsedPos,e=this.lineAfter(t),i=t+e.length;for(let n=this.rangeIndex;;){let r=this.ranges[n].to;if(r>=i||(e=e.slice(0,r-(i-e.length)),n++,n==this.ranges.length))break;let o=this.ranges[n].from,l=this.lineAfter(o);e+=l,i=o+l.length}return{line:e,end:i}}skipGapsTo(t,e,i){for(;;){let n=this.ranges[this.rangeIndex].to,r=t+e;if(i>0?n>r:n>=r)break;let o=this.ranges[++this.rangeIndex].from;e+=o-n}return e}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to1){r=this.skipGapsTo(e,r,1),e+=r;let o=this.chunk.length;r=this.skipGapsTo(i,r,-1),i+=r,n+=this.chunk.length-o}return this.chunk.push(t,e,i,n),r}parseLine(t){let{line:e,end:i}=this.nextLine(),n=0,{streamParser:r}=this.lang,o=new ch(e,t?t.state.tabSize:4,t?Re(t.state):2);if(o.eol())r.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=dh(r.token,o,this.state);if(l&&(n=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,n)),o.start>1e4)break}this.parsedPos=i,this.moveRangeIndex(),this.parsedPost.start)return n}throw new Error("Stream parser failed to advance stream.")}const kr=Object.create(null),Di=[gt.none],Id=new yr(Di),Zo=[],tl=Object.create(null),ph=Object.create(null);for(let[s,t]of[["variable","variableName"],["variable-2","variableName.special"],["string-2","string.special"],["def","variableName.definition"],["tag","tagName"],["attribute","attributeName"],["type","typeName"],["builtin","variableName.standard"],["qualifier","modifier"],["error","invalid"],["header","heading"],["property","propertyName"]])ph[s]=mh(kr,t);class gh{constructor(t){this.extra=t,this.table=Object.assign(Object.create(null),ph)}resolve(t){return t?this.table[t]||(this.table[t]=mh(this.extra,t)):0}}const Nd=new gh(kr);function os(s,t){Zo.indexOf(s)>-1||(Zo.push(s),console.warn(t))}function mh(s,t){let e=[];for(let l of t.split(" ")){let a=[];for(let c of l.split(".")){let h=s[c]||M[c];h?typeof h=="function"?a.length?a=a.map(h):os(c,`Modifier ${c} used at start of tag`):a.length?os(c,`Tag ${c} used as modifier`):a=Array.isArray(h)?h:[h]:os(c,`Unknown highlighting tag ${c}`)}for(let c of a)e.push(c)}if(!e.length)return 0;let i=t.replace(/ /g,"_"),n=i+" "+e.map(l=>l.id),r=tl[n];if(r)return r.id;let o=tl[n]=gt.define({id:Di.length,name:i,props:[rd({[i]:e})]});return Di.push(o),o.id}function Fd(s){let t=gt.define({id:Di.length,name:"Document",props:[Ce.add(()=>s)],top:!0});return Di.push(t),t}X.RTL,X.LTR;const Vd=s=>{let{state:t}=s,e=t.doc.lineAt(t.selection.main.from),i=Ar(s.state,e.from);return i.line?Wd(s):i.block?zd(s):!1};function Cr(s,t){return({state:e,dispatch:i})=>{if(e.readOnly)return!1;let n=s(t,e);return n?(i(e.update(n)),!0):!1}}const Wd=Cr($d,0),Hd=Cr(yh,0),zd=Cr((s,t)=>yh(s,t,Kd(t)),0);function Ar(s,t){let e=s.languageDataAt("commentTokens",t);return e.length?e[0]:{}}const hi=50;function qd(s,{open:t,close:e},i,n){let r=s.sliceDoc(i-hi,i),o=s.sliceDoc(n,n+hi),l=/\s*$/.exec(r)[0].length,a=/^\s*/.exec(o)[0].length,c=r.length-l;if(r.slice(c-t.length,c)==t&&o.slice(a,a+e.length)==e)return{open:{pos:i-l,margin:l&&1},close:{pos:n+a,margin:a&&1}};let h,f;n-i<=2*hi?h=f=s.sliceDoc(i,n):(h=s.sliceDoc(i,i+hi),f=s.sliceDoc(n-hi,n));let u=/^\s*/.exec(h)[0].length,d=/\s*$/.exec(f)[0].length,p=f.length-d-e.length;return h.slice(u,u+t.length)==t&&f.slice(p,p+e.length)==e?{open:{pos:i+u+t.length,margin:/\s/.test(h.charAt(u+t.length))?1:0},close:{pos:n-d-e.length,margin:/\s/.test(f.charAt(p-1))?1:0}}:null}function Kd(s){let t=[];for(let e of s.selection.ranges){let i=s.doc.lineAt(e.from),n=e.to<=i.to?i:s.doc.lineAt(e.to),r=t.length-1;r>=0&&t[r].to>i.from?t[r].to=n.to:t.push({from:i.from+/^\s*/.exec(i.text)[0].length,to:n.to})}return t}function yh(s,t,e=t.selection.ranges){let i=e.map(r=>Ar(t,r.from).block);if(!i.every(r=>r))return null;let n=e.map((r,o)=>qd(t,i[o],r.from,r.to));if(s!=2&&!n.every(r=>r))return{changes:t.changes(e.map((r,o)=>n[o]?[]:[{from:r.from,insert:i[o].open+" "},{from:r.to,insert:" "+i[o].close}]))};if(s!=1&&n.some(r=>r)){let r=[];for(let o=0,l;on&&(r==o||o>f.from)){n=f.from;let u=/^\s*/.exec(f.text)[0].length,d=u==f.length,p=f.text.slice(u,u+c.length)==c?u:-1;ur.comment<0&&(!r.empty||r.single))){let r=[];for(let{line:l,token:a,indent:c,empty:h,single:f}of i)(f||!h)&&r.push({from:l.from+c,insert:a+" "});let o=t.changes(r);return{changes:o,selection:t.selection.map(o,1)}}else if(s!=1&&i.some(r=>r.comment>=0)){let r=[];for(let{line:o,comment:l,token:a}of i)if(l>=0){let c=o.from+l,h=c+a.length;o.text[h-o.from]==" "&&h++,r.push({from:c,to:h})}return{changes:r}}return null}const tr=se.define(),jd=se.define(),Ud=T.define(),bh=T.define({combine(s){return Le(s,{minDepth:100,newGroupDelay:500,joinToEvent:(t,e)=>e},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,e)=>(i,n)=>t(i,n)||e(i,n)})}}),xh=yt.define({create(){return _t.empty},update(s,t){let e=t.state.facet(bh),i=t.annotation(tr);if(i){let a=vt.fromTransaction(t,i.selection),c=i.side,h=c==0?s.undone:s.done;return a?h=Mn(h,h.length,e.minDepth,a):h=vh(h,t.startState.selection),new _t(c==0?i.rest:h,c==0?h:i.rest)}let n=t.annotation(jd);if((n=="full"||n=="before")&&(s=s.isolate()),t.annotation(Z.addToHistory)===!1)return t.changes.empty?s:s.addMapping(t.changes.desc);let r=vt.fromTransaction(t),o=t.annotation(Z.time),l=t.annotation(Z.userEvent);return r?s=s.addChanges(r,o,l,e,t):t.selection&&(s=s.addSelection(t.startState.selection,o,l,e.newGroupDelay)),(n=="full"||n=="after")&&(s=s.isolate()),s},toJSON(s){return{done:s.done.map(t=>t.toJSON()),undone:s.undone.map(t=>t.toJSON())}},fromJSON(s){return new _t(s.done.map(vt.fromJSON),s.undone.map(vt.fromJSON))}});function Rm(s={}){return[xh,bh.of(s),O.domEventHandlers({beforeinput(t,e){let i=t.inputType=="historyUndo"?wh:t.inputType=="historyRedo"?er:null;return i?(t.preventDefault(),i(e)):!1}})]}function zn(s,t){return function({state:e,dispatch:i}){if(!t&&e.readOnly)return!1;let n=e.field(xh,!1);if(!n)return!1;let r=n.pop(s,e,t);return r?(i(r),!0):!1}}const wh=zn(0,!1),er=zn(1,!1),Gd=zn(0,!0),Jd=zn(1,!0);class vt{constructor(t,e,i,n,r){this.changes=t,this.effects=e,this.mapped=i,this.startSelection=n,this.selectionsAfter=r}setSelAfter(t){return new vt(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,e,i;return{changes:(t=this.changes)===null||t===void 0?void 0:t.toJSON(),mapped:(e=this.mapped)===null||e===void 0?void 0:e.toJSON(),startSelection:(i=this.startSelection)===null||i===void 0?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map(n=>n.toJSON())}}static fromJSON(t){return new vt(t.changes&&et.fromJSON(t.changes),[],t.mapped&&Qt.fromJSON(t.mapped),t.startSelection&&b.fromJSON(t.startSelection),t.selectionsAfter.map(b.fromJSON))}static fromTransaction(t,e){let i=Et;for(let n of t.startState.facet(Ud)){let r=n(t);r.length&&(i=i.concat(r))}return!i.length&&t.changes.empty?null:new vt(t.changes.invert(t.startState.doc),i,void 0,e||t.startState.selection,Et)}static selection(t){return new vt(void 0,Et,void 0,void 0,t)}}function Mn(s,t,e,i){let n=t+1>e+20?t-e-1:0,r=s.slice(n,t);return r.push(i),r}function Yd(s,t){let e=[],i=!1;return s.iterChangedRanges((n,r)=>e.push(n,r)),t.iterChangedRanges((n,r,o,l)=>{for(let a=0;a=c&&o<=h&&(i=!0)}}),i}function Xd(s,t){return s.ranges.length==t.ranges.length&&s.ranges.filter((e,i)=>e.empty!=t.ranges[i].empty).length===0}function Sh(s,t){return s.length?t.length?s.concat(t):s:t}const Et=[],_d=200;function vh(s,t){if(s.length){let e=s[s.length-1],i=e.selectionsAfter.slice(Math.max(0,e.selectionsAfter.length-_d));return i.length&&i[i.length-1].eq(t)?s:(i.push(t),Mn(s,s.length-1,1e9,e.setSelAfter(i)))}else return[vt.selection([t])]}function Qd(s){let t=s[s.length-1],e=s.slice();return e[s.length-1]=t.setSelAfter(t.selectionsAfter.slice(0,t.selectionsAfter.length-1)),e}function ls(s,t){if(!s.length)return s;let e=s.length,i=Et;for(;e;){let n=Zd(s[e-1],t,i);if(n.changes&&!n.changes.empty||n.effects.length){let r=s.slice(0,e);return r[e-1]=n,r}else t=n.mapped,e--,i=n.selectionsAfter}return i.length?[vt.selection(i)]:Et}function Zd(s,t,e){let i=Sh(s.selectionsAfter.length?s.selectionsAfter.map(l=>l.map(t)):Et,e);if(!s.changes)return vt.selection(i);let n=s.changes.map(t),r=t.mapDesc(s.changes,!0),o=s.mapped?s.mapped.composeDesc(r):r;return new vt(n,F.mapEffects(s.effects,t),o,s.startSelection.map(r),i)}const tp=/^(input\.type|delete)($|\.)/;class _t{constructor(t,e,i=0,n=void 0){this.done=t,this.undone=e,this.prevTime=i,this.prevUserEvent=n}isolate(){return this.prevTime?new _t(this.done,this.undone):this}addChanges(t,e,i,n,r){let o=this.done,l=o[o.length-1];return l&&l.changes&&!l.changes.empty&&t.changes&&(!i||tp.test(i))&&(!l.selectionsAfter.length&&e-this.prevTime0&&e-this.prevTimee.empty?s.moveByChar(e,t):qn(e,t))}function dt(s){return s.textDirectionAt(s.state.selection.main.head)==X.LTR}const Ch=s=>kh(s,!dt(s)),Ah=s=>kh(s,dt(s));function Mh(s,t){return Ht(s,e=>e.empty?s.moveByGroup(e,t):qn(e,t))}const ep=s=>Mh(s,!dt(s)),ip=s=>Mh(s,dt(s));function np(s,t,e){if(t.type.prop(e))return!0;let i=t.to-t.from;return i&&(i>2||/[^\s,.;:]/.test(s.sliceDoc(t.from,t.to)))||t.firstChild}function Kn(s,t,e){let i=mt(s).resolveInner(t.head),n=e?L.closedBy:L.openedBy;for(let a=t.head;;){let c=e?i.childAfter(a):i.childBefore(a);if(!c)break;np(s,c,n)?i=c:a=e?c.to:c.from}let r=i.type.prop(n),o,l;return r&&(o=e?Xt(s,i.from,1):Xt(s,i.to,-1))&&o.matched?l=e?o.end.to:o.end.from:l=e?i.to:i.from,b.cursor(l,e?-1:1)}const sp=s=>Ht(s,t=>Kn(s.state,t,!dt(s))),rp=s=>Ht(s,t=>Kn(s.state,t,dt(s)));function Dh(s,t){return Ht(s,e=>{if(!e.empty)return qn(e,t);let i=s.moveVertically(e,t);return i.head!=e.head?i:s.moveToLineBoundary(e,t)})}const Oh=s=>Dh(s,!1),Th=s=>Dh(s,!0);function Ph(s){let t=s.scrollDOM.clientHeighto.empty?s.moveVertically(o,t,e.height):qn(o,t));if(n.eq(i.selection))return!1;let r;if(e.selfScroll){let o=s.coordsAtPos(i.selection.main.head),l=s.scrollDOM.getBoundingClientRect(),a=l.top+e.marginTop,c=l.bottom-e.marginBottom;o&&o.top>a&&o.bottomBh(s,!1),ir=s=>Bh(s,!0);function be(s,t,e){let i=s.lineBlockAt(t.head),n=s.moveToLineBoundary(t,e);if(n.head==t.head&&n.head!=(e?i.to:i.from)&&(n=s.moveToLineBoundary(t,e,!1)),!e&&n.head==i.from&&i.length){let r=/^\s*/.exec(s.state.sliceDoc(i.from,Math.min(i.from+100,i.to)))[0].length;r&&t.head!=i.from+r&&(n=b.cursor(i.from+r))}return n}const op=s=>Ht(s,t=>be(s,t,!0)),lp=s=>Ht(s,t=>be(s,t,!1)),ap=s=>Ht(s,t=>be(s,t,!dt(s))),hp=s=>Ht(s,t=>be(s,t,dt(s))),cp=s=>Ht(s,t=>b.cursor(s.lineBlockAt(t.head).from,1)),fp=s=>Ht(s,t=>b.cursor(s.lineBlockAt(t.head).to,-1));function up(s,t,e){let i=!1,n=ei(s.selection,r=>{let o=Xt(s,r.head,-1)||Xt(s,r.head,1)||r.head>0&&Xt(s,r.head-1,1)||r.headup(s,t);function Ft(s,t){let e=ei(s.state.selection,i=>{let n=t(i);return b.range(i.anchor,n.head,n.goalColumn,n.bidiLevel||void 0)});return e.eq(s.state.selection)?!1:(s.dispatch(Zt(s.state,e)),!0)}function Rh(s,t){return Ft(s,e=>s.moveByChar(e,t))}const Lh=s=>Rh(s,!dt(s)),Eh=s=>Rh(s,dt(s));function Ih(s,t){return Ft(s,e=>s.moveByGroup(e,t))}const pp=s=>Ih(s,!dt(s)),gp=s=>Ih(s,dt(s)),mp=s=>Ft(s,t=>Kn(s.state,t,!dt(s))),yp=s=>Ft(s,t=>Kn(s.state,t,dt(s)));function Nh(s,t){return Ft(s,e=>s.moveVertically(e,t))}const Fh=s=>Nh(s,!1),Vh=s=>Nh(s,!0);function Wh(s,t){return Ft(s,e=>s.moveVertically(e,t,Ph(s).height))}const il=s=>Wh(s,!1),nl=s=>Wh(s,!0),bp=s=>Ft(s,t=>be(s,t,!0)),xp=s=>Ft(s,t=>be(s,t,!1)),wp=s=>Ft(s,t=>be(s,t,!dt(s))),Sp=s=>Ft(s,t=>be(s,t,dt(s))),vp=s=>Ft(s,t=>b.cursor(s.lineBlockAt(t.head).from)),kp=s=>Ft(s,t=>b.cursor(s.lineBlockAt(t.head).to)),sl=({state:s,dispatch:t})=>(t(Zt(s,{anchor:0})),!0),rl=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.doc.length})),!0),ol=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.selection.main.anchor,head:0})),!0),ll=({state:s,dispatch:t})=>(t(Zt(s,{anchor:s.selection.main.anchor,head:s.doc.length})),!0),Cp=({state:s,dispatch:t})=>(t(s.update({selection:{anchor:0,head:s.doc.length},userEvent:"select"})),!0),Ap=({state:s,dispatch:t})=>{let e=$n(s).map(({from:i,to:n})=>b.range(i,Math.min(n+1,s.doc.length)));return t(s.update({selection:b.create(e),userEvent:"select"})),!0},Mp=({state:s,dispatch:t})=>{let e=ei(s.selection,i=>{var n;let r=mt(s).resolveStack(i.from,1);for(let o=r;o;o=o.next){let{node:l}=o;if((l.from=i.to||l.to>i.to&&l.from<=i.from)&&(!((n=l.parent)===null||n===void 0)&&n.parent))return b.range(l.to,l.from)}return i});return t(Zt(s,e)),!0},Dp=({state:s,dispatch:t})=>{let e=s.selection,i=null;return e.ranges.length>1?i=b.create([e.main]):e.main.empty||(i=b.create([b.cursor(e.main.head)])),i?(t(Zt(s,i)),!0):!1};function Ii(s,t){if(s.state.readOnly)return!1;let e="delete.selection",{state:i}=s,n=i.changeByRange(r=>{let{from:o,to:l}=r;if(o==l){let a=t(r);ao&&(e="delete.forward",a=en(s,a,!0)),o=Math.min(o,a),l=Math.max(l,a)}else o=en(s,o,!1),l=en(s,l,!0);return o==l?{range:r}:{changes:{from:o,to:l},range:b.cursor(o,on(s)))i.between(t,t,(n,r)=>{nt&&(t=e?r:n)});return t}const Hh=(s,t,e)=>Ii(s,i=>{let n=i.from,{state:r}=s,o=r.doc.lineAt(n),l,a;if(e&&!t&&n>o.from&&nHh(s,!1,!0),zh=s=>Hh(s,!0,!1),qh=(s,t)=>Ii(s,e=>{let i=e.head,{state:n}=s,r=n.doc.lineAt(i),o=n.charCategorizer(i);for(let l=null;;){if(i==(t?r.to:r.from)){i==e.head&&r.number!=(t?n.doc.lines:1)&&(i+=t?1:-1);break}let a=ot(r.text,i-r.from,t)+r.from,c=r.text.slice(Math.min(i,a)-r.from,Math.max(i,a)-r.from),h=o(c);if(l!=null&&h!=l)break;(c!=" "||i!=e.head)&&(l=h),i=a}return i}),Kh=s=>qh(s,!1),Op=s=>qh(s,!0),Tp=s=>Ii(s,t=>{let e=s.lineBlockAt(t.head).to;return t.headIi(s,t=>{let e=s.moveToLineBoundary(t,!1).head;return t.head>e?e:Math.max(0,t.head-1)}),Bp=s=>Ii(s,t=>{let e=s.moveToLineBoundary(t,!0).head;return t.head{if(s.readOnly)return!1;let e=s.changeByRange(i=>({changes:{from:i.from,to:i.to,insert:V.of(["",""])},range:b.cursor(i.from)}));return t(s.update(e,{scrollIntoView:!0,userEvent:"input"})),!0},Lp=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let e=s.changeByRange(i=>{if(!i.empty||i.from==0||i.from==s.doc.length)return{range:i};let n=i.from,r=s.doc.lineAt(n),o=n==r.from?n-1:ot(r.text,n-r.from,!1)+r.from,l=n==r.to?n+1:ot(r.text,n-r.from,!0)+r.from;return{changes:{from:o,to:l,insert:s.doc.slice(n,l).append(s.doc.slice(o,n))},range:b.cursor(l)}});return e.changes.empty?!1:(t(s.update(e,{scrollIntoView:!0,userEvent:"move.character"})),!0)};function $n(s){let t=[],e=-1;for(let i of s.selection.ranges){let n=s.doc.lineAt(i.from),r=s.doc.lineAt(i.to);if(!i.empty&&i.to==r.from&&(r=s.doc.lineAt(i.to-1)),e>=n.number){let o=t[t.length-1];o.to=r.to,o.ranges.push(i)}else t.push({from:n.from,to:r.to,ranges:[i]});e=r.number+1}return t}function $h(s,t,e){if(s.readOnly)return!1;let i=[],n=[];for(let r of $n(s)){if(e?r.to==s.doc.length:r.from==0)continue;let o=s.doc.lineAt(e?r.to+1:r.from-1),l=o.length+1;if(e){i.push({from:r.to,to:o.to},{from:r.from,insert:o.text+s.lineBreak});for(let a of r.ranges)n.push(b.range(Math.min(s.doc.length,a.anchor+l),Math.min(s.doc.length,a.head+l)))}else{i.push({from:o.from,to:r.from},{from:r.to,insert:s.lineBreak+o.text});for(let a of r.ranges)n.push(b.range(a.anchor-l,a.head-l))}}return i.length?(t(s.update({changes:i,scrollIntoView:!0,selection:b.create(n,s.selection.mainIndex),userEvent:"move.line"})),!0):!1}const Ep=({state:s,dispatch:t})=>$h(s,t,!1),Ip=({state:s,dispatch:t})=>$h(s,t,!0);function jh(s,t,e){if(s.readOnly)return!1;let i=[];for(let n of $n(s))e?i.push({from:n.from,insert:s.doc.slice(n.from,n.to)+s.lineBreak}):i.push({from:n.to,insert:s.lineBreak+s.doc.slice(n.from,n.to)});return t(s.update({changes:i,scrollIntoView:!0,userEvent:"input.copyline"})),!0}const Np=({state:s,dispatch:t})=>jh(s,t,!1),Fp=({state:s,dispatch:t})=>jh(s,t,!0),Vp=s=>{if(s.state.readOnly)return!1;let{state:t}=s,e=t.changes($n(t).map(({from:n,to:r})=>(n>0?n--:r{let r;if(s.lineWrapping){let o=s.lineBlockAt(n.head),l=s.coordsAtPos(n.head,n.assoc||1);l&&(r=o.bottom+s.documentTop-l.bottom+s.defaultLineHeight/2)}return s.moveVertically(n,!0,r)}).map(e);return s.dispatch({changes:e,selection:i,scrollIntoView:!0,userEvent:"delete.line"}),!0};function Wp(s,t){if(/\(\)|\[\]|\{\}/.test(s.sliceDoc(t-1,t+1)))return{from:t,to:t};let e=mt(s).resolveInner(t),i=e.childBefore(t),n=e.childAfter(t),r;return i&&n&&i.to<=t&&n.from>=t&&(r=i.type.prop(L.closedBy))&&r.indexOf(n.name)>-1&&s.doc.lineAt(i.to).from==s.doc.lineAt(n.from).from&&!/\S/.test(s.sliceDoc(i.to,n.from))?{from:i.to,to:n.from}:null}const Hp=Uh(!1),zp=Uh(!0);function Uh(s){return({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=t.changeByRange(n=>{let{from:r,to:o}=n,l=t.doc.lineAt(r),a=!s&&r==o&&Wp(t,r);s&&(r=o=(o<=l.to?l:t.doc.lineAt(o)).to);let c=new Wn(t,{simulateBreak:r,simulateDoubleBreak:!!a}),h=nh(c,r);for(h==null&&(h=ti(/^\s*/.exec(t.doc.lineAt(r).text)[0],t.tabSize));ol.from&&r{let n=[];for(let o=i.from;o<=i.to;){let l=s.doc.lineAt(o);l.number>e&&(i.empty||i.to>l.from)&&(t(l,n,i),e=l.number),o=l.to+1}let r=s.changes(n);return{changes:n,range:b.range(r.mapPos(i.anchor,1),r.mapPos(i.head,1))}})}const qp=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let e=Object.create(null),i=new Wn(s,{overrideIndentation:r=>{let o=e[r];return o??-1}}),n=Mr(s,(r,o,l)=>{let a=nh(i,r.from);if(a==null)return;/\S/.test(r.text)||(a=0);let c=/^\s*/.exec(r.text)[0],h=An(s,a);(c!=h||l.froms.readOnly?!1:(t(s.update(Mr(s,(e,i)=>{i.push({from:e.from,insert:s.facet(Vn)})}),{userEvent:"input.indent"})),!0),Jh=({state:s,dispatch:t})=>s.readOnly?!1:(t(s.update(Mr(s,(e,i)=>{let n=/^\s*/.exec(e.text)[0];if(!n)return;let r=ti(n,s.tabSize),o=0,l=An(s,Math.max(0,r-Re(s)));for(;o(s.setTabFocusMode(),!0),$p=[{key:"Ctrl-b",run:Ch,shift:Lh,preventDefault:!0},{key:"Ctrl-f",run:Ah,shift:Eh},{key:"Ctrl-p",run:Oh,shift:Fh},{key:"Ctrl-n",run:Th,shift:Vh},{key:"Ctrl-a",run:cp,shift:vp},{key:"Ctrl-e",run:fp,shift:kp},{key:"Ctrl-d",run:zh},{key:"Ctrl-h",run:nr},{key:"Ctrl-k",run:Tp},{key:"Ctrl-Alt-h",run:Kh},{key:"Ctrl-o",run:Rp},{key:"Ctrl-t",run:Lp},{key:"Ctrl-v",run:ir}],jp=[{key:"ArrowLeft",run:Ch,shift:Lh,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:ep,shift:pp,preventDefault:!0},{mac:"Cmd-ArrowLeft",run:ap,shift:wp,preventDefault:!0},{key:"ArrowRight",run:Ah,shift:Eh,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:ip,shift:gp,preventDefault:!0},{mac:"Cmd-ArrowRight",run:hp,shift:Sp,preventDefault:!0},{key:"ArrowUp",run:Oh,shift:Fh,preventDefault:!0},{mac:"Cmd-ArrowUp",run:sl,shift:ol},{mac:"Ctrl-ArrowUp",run:el,shift:il},{key:"ArrowDown",run:Th,shift:Vh,preventDefault:!0},{mac:"Cmd-ArrowDown",run:rl,shift:ll},{mac:"Ctrl-ArrowDown",run:ir,shift:nl},{key:"PageUp",run:el,shift:il},{key:"PageDown",run:ir,shift:nl},{key:"Home",run:lp,shift:xp,preventDefault:!0},{key:"Mod-Home",run:sl,shift:ol},{key:"End",run:op,shift:bp,preventDefault:!0},{key:"Mod-End",run:rl,shift:ll},{key:"Enter",run:Hp},{key:"Mod-a",run:Cp},{key:"Backspace",run:nr,shift:nr},{key:"Delete",run:zh},{key:"Mod-Backspace",mac:"Alt-Backspace",run:Kh},{key:"Mod-Delete",mac:"Alt-Delete",run:Op},{mac:"Mod-Backspace",run:Pp},{mac:"Mod-Delete",run:Bp}].concat($p.map(s=>({mac:s.key,run:s.run,shift:s.shift}))),Em=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:sp,shift:mp},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:rp,shift:yp},{key:"Alt-ArrowUp",run:Ep},{key:"Shift-Alt-ArrowUp",run:Np},{key:"Alt-ArrowDown",run:Ip},{key:"Shift-Alt-ArrowDown",run:Fp},{key:"Escape",run:Dp},{key:"Mod-Enter",run:zp},{key:"Alt-l",mac:"Ctrl-l",run:Ap},{key:"Mod-i",run:Mp,preventDefault:!0},{key:"Mod-[",run:Jh},{key:"Mod-]",run:Gh},{key:"Mod-Alt-\\",run:qp},{key:"Shift-Mod-k",run:Vp},{key:"Shift-Mod-\\",run:dp},{key:"Mod-/",run:Vd},{key:"Alt-A",run:Hd},{key:"Ctrl-m",mac:"Shift-Alt-m",run:Kp}].concat(jp),Im={key:"Tab",run:Gh,shift:Jh};function lt(){var s=arguments[0];typeof s=="string"&&(s=document.createElement(s));var t=1,e=arguments[1];if(e&&typeof e=="object"&&e.nodeType==null&&!Array.isArray(e)){for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var n=e[i];typeof n=="string"?s.setAttribute(i,n):n!=null&&(s[i]=n)}t++}for(;ts.normalize("NFKD"):s=>s;class Ze{constructor(t,e,i=0,n=t.length,r,o){this.test=o,this.value={from:0,to:0},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=t.iterRange(i,n),this.bufferStart=i,this.normalize=r?l=>r(al(l)):al,this.query=this.normalize(e)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return nt(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let t=this.peek();if(t<0)return this.done=!0,this;let e=or(t),i=this.bufferStart+this.bufferPos;this.bufferPos+=Bt(t);let n=this.normalize(e);for(let r=0,o=i;;r++){let l=n.charCodeAt(r),a=this.match(l,o,this.bufferPos+this.bufferStart);if(r==n.length-1){if(a)return this.value=a,this;break}o==i&&rthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let t=this.matchPos-this.curLineStart;;){this.re.lastIndex=t;let e=this.matchPos<=this.to&&this.re.exec(this.curLine);if(e){let i=this.curLineStart+e.index,n=i+e[0].length;if(this.matchPos=Dn(this.text,n+(i==n?1:0)),i==this.curLineStart+this.curLine.length&&this.nextLine(),(ithis.value.to)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,match:e},this;t=this.matchPos-this.curLineStart}else if(this.curLineStart+this.curLine.length=i||n.to<=e){let l=new $e(e,t.sliceString(e,i));return as.set(t,l),l}if(n.from==e&&n.to==i)return n;let{text:r,from:o}=n;return o>e&&(r=t.sliceString(e,o)+r,o=e),n.to=this.to?this.to:this.text.lineAt(t).to}next(){for(;;){let t=this.re.lastIndex=this.matchPos-this.flat.from,e=this.re.exec(this.flat.text);if(e&&!e[0]&&e.index==t&&(this.re.lastIndex=t+1,e=this.re.exec(this.flat.text)),e){let i=this.flat.from+e.index,n=i+e[0].length;if((this.flat.to>=this.to||e.index+e[0].length<=this.flat.text.length-10)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,match:e},this.matchPos=Dn(this.text,n+(i==n?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=$e.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+this.flat.text.length*2))}}}typeof Symbol<"u"&&(_h.prototype[Symbol.iterator]=Qh.prototype[Symbol.iterator]=function(){return this});function Up(s){try{return new RegExp(s,Dr),!0}catch{return!1}}function Dn(s,t){if(t>=s.length)return t;let e=s.lineAt(t),i;for(;t=56320&&i<57344;)t++;return t}function sr(s){let t=String(s.state.doc.lineAt(s.state.selection.main.head).number),e=lt("input",{class:"cm-textfield",name:"line",value:t}),i=lt("form",{class:"cm-gotoLine",onkeydown:r=>{r.keyCode==27?(r.preventDefault(),s.dispatch({effects:On.of(!1)}),s.focus()):r.keyCode==13&&(r.preventDefault(),n())},onsubmit:r=>{r.preventDefault(),n()}},lt("label",s.state.phrase("Go to line"),": ",e)," ",lt("button",{class:"cm-button",type:"submit"},s.state.phrase("go")));function n(){let r=/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(e.value);if(!r)return;let{state:o}=s,l=o.doc.lineAt(o.selection.main.head),[,a,c,h,f]=r,u=h?+h.slice(1):0,d=c?+c:l.number;if(c&&f){let m=d/100;a&&(m=m*(a=="-"?-1:1)+l.number/o.doc.lines),d=Math.round(o.doc.lines*m)}else c&&a&&(d=d*(a=="-"?-1:1)+l.number);let p=o.doc.line(Math.max(1,Math.min(o.doc.lines,d))),g=b.cursor(p.from+Math.max(0,Math.min(u,p.length)));s.dispatch({effects:[On.of(!1),O.scrollIntoView(g.from,{y:"center"})],selection:g}),s.focus()}return{dom:i}}const On=F.define(),hl=yt.define({create(){return!0},update(s,t){for(let e of t.effects)e.is(On)&&(s=e.value);return s},provide:s=>Sn.from(s,t=>t?sr:null)}),Gp=s=>{let t=wn(s,sr);if(!t){let e=[On.of(!0)];s.state.field(hl,!1)==null&&e.push(F.appendConfig.of([hl,Jp])),s.dispatch({effects:e}),t=wn(s,sr)}return t&&t.dom.querySelector("input").select(),!0},Jp=O.baseTheme({".cm-panel.cm-gotoLine":{padding:"2px 6px 4px","& label":{fontSize:"80%"}}}),Yp={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},Xp=T.define({combine(s){return Le(s,Yp,{highlightWordAroundCursor:(t,e)=>t||e,minSelectionLength:Math.min,maxMatches:Math.min})}});function Nm(s){return[eg,tg]}const _p=P.mark({class:"cm-selectionMatch"}),Qp=P.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function cl(s,t,e,i){return(e==0||s(t.sliceDoc(e-1,e))!=G.Word)&&(i==t.doc.length||s(t.sliceDoc(i,i+1))!=G.Word)}function Zp(s,t,e,i){return s(t.sliceDoc(e,e+1))==G.Word&&s(t.sliceDoc(i-1,i))==G.Word}const tg=ut.fromClass(class{constructor(s){this.decorations=this.getDeco(s)}update(s){(s.selectionSet||s.docChanged||s.viewportChanged)&&(this.decorations=this.getDeco(s.view))}getDeco(s){let t=s.state.facet(Xp),{state:e}=s,i=e.selection;if(i.ranges.length>1)return P.none;let n=i.main,r,o=null;if(n.empty){if(!t.highlightWordAroundCursor)return P.none;let a=e.wordAt(n.head);if(!a)return P.none;o=e.charCategorizer(n.head),r=e.sliceDoc(a.from,a.to)}else{let a=n.to-n.from;if(a200)return P.none;if(t.wholeWords){if(r=e.sliceDoc(n.from,n.to),o=e.charCategorizer(n.head),!(cl(o,e,n.from,n.to)&&Zp(o,e,n.from,n.to)))return P.none}else if(r=e.sliceDoc(n.from,n.to),!r)return P.none}let l=[];for(let a of s.visibleRanges){let c=new Ze(e.doc,r,a.from,a.to);for(;!c.next().done;){let{from:h,to:f}=c.value;if((!o||cl(o,e,h,f))&&(n.empty&&h<=n.from&&f>=n.to?l.push(Qp.range(h,f)):(h>=n.to||f<=n.from)&&l.push(_p.range(h,f)),l.length>t.maxMatches))return P.none}}return P.set(l)}},{decorations:s=>s.decorations}),eg=O.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}}),ig=({state:s,dispatch:t})=>{let{selection:e}=s,i=b.create(e.ranges.map(n=>s.wordAt(n.head)||b.cursor(n.head)),e.mainIndex);return i.eq(e)?!1:(t(s.update({selection:i})),!0)};function ng(s,t){let{main:e,ranges:i}=s.selection,n=s.wordAt(e.head),r=n&&n.from==e.from&&n.to==e.to;for(let o=!1,l=new Ze(s.doc,t,i[i.length-1].to);;)if(l.next(),l.done){if(o)return null;l=new Ze(s.doc,t,0,Math.max(0,i[i.length-1].from-1)),o=!0}else{if(o&&i.some(a=>a.from==l.value.from))continue;if(r){let a=s.wordAt(l.value.from);if(!a||a.from!=l.value.from||a.to!=l.value.to)continue}return l.value}}const sg=({state:s,dispatch:t})=>{let{ranges:e}=s.selection;if(e.some(r=>r.from===r.to))return ig({state:s,dispatch:t});let i=s.sliceDoc(e[0].from,e[0].to);if(s.selection.ranges.some(r=>s.sliceDoc(r.from,r.to)!=i))return!1;let n=ng(s,i);return n?(t(s.update({selection:s.selection.addRange(b.range(n.from,n.to),!1),effects:O.scrollIntoView(n.to)})),!0):!1},ii=T.define({combine(s){return Le(s,{top:!1,caseSensitive:!1,literal:!1,regexp:!1,wholeWord:!1,createPanel:t=>new gg(t),scrollToMatch:t=>O.scrollIntoView(t)})}});class Zh{constructor(t){this.search=t.search,this.caseSensitive=!!t.caseSensitive,this.literal=!!t.literal,this.regexp=!!t.regexp,this.replace=t.replace||"",this.valid=!!this.search&&(!this.regexp||Up(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!t.wholeWord}unquote(t){return this.literal?t:t.replace(/\\([nrt\\])/g,(e,i)=>i=="n"?` -`:i=="r"?"\r":i=="t"?" ":"\\")}eq(t){return this.search==t.search&&this.replace==t.replace&&this.caseSensitive==t.caseSensitive&&this.regexp==t.regexp&&this.wholeWord==t.wholeWord}create(){return this.regexp?new ag(this):new og(this)}getCursor(t,e=0,i){let n=t.doc?t:H.create({doc:t});return i==null&&(i=n.doc.length),this.regexp?Ve(this,n,e,i):Fe(this,n,e,i)}}class tc{constructor(t){this.spec=t}}function Fe(s,t,e,i){return new Ze(t.doc,s.unquoted,e,i,s.caseSensitive?void 0:n=>n.toLowerCase(),s.wholeWord?rg(t.doc,t.charCategorizer(t.selection.main.head)):void 0)}function rg(s,t){return(e,i,n,r)=>((r>e||r+n.length=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let r=Fe(this.spec,t,Math.max(0,e-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,t.doc.length));for(;!r.next().done;)n(r.value.from,r.value.to)}}function Ve(s,t,e,i){return new _h(t.doc,s.search,{ignoreCase:!s.caseSensitive,test:s.wholeWord?lg(t.charCategorizer(t.selection.main.head)):void 0},e,i)}function Tn(s,t){return s.slice(ot(s,t,!1),t)}function Pn(s,t){return s.slice(t,ot(s,t))}function lg(s){return(t,e,i)=>!i[0].length||(s(Tn(i.input,i.index))!=G.Word||s(Pn(i.input,i.index))!=G.Word)&&(s(Pn(i.input,i.index+i[0].length))!=G.Word||s(Tn(i.input,i.index+i[0].length))!=G.Word)}class ag extends tc{nextMatch(t,e,i){let n=Ve(this.spec,t,i,t.doc.length).next();return n.done&&(n=Ve(this.spec,t,0,e).next()),n.done?null:n.value}prevMatchInRange(t,e,i){for(let n=1;;n++){let r=Math.max(e,i-n*1e4),o=Ve(this.spec,t,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==e||l.from>r+10))return l;if(r==e)return null}}prevMatch(t,e,i){return this.prevMatchInRange(t,0,e)||this.prevMatchInRange(t,i,t.doc.length)}getReplacement(t){return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g,(e,i)=>i=="$"?"$":i=="&"?t.match[0]:i!="0"&&+i=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let r=Ve(this.spec,t,Math.max(0,e-250),Math.min(i+250,t.doc.length));for(;!r.next().done;)n(r.value.from,r.value.to)}}const Oi=F.define(),Or=F.define(),fe=yt.define({create(s){return new hs(rr(s).create(),null)},update(s,t){for(let e of t.effects)e.is(Oi)?s=new hs(e.value.create(),s.panel):e.is(Or)&&(s=new hs(s.query,e.value?Tr:null));return s},provide:s=>Sn.from(s,t=>t.panel)});class hs{constructor(t,e){this.query=t,this.panel=e}}const hg=P.mark({class:"cm-searchMatch"}),cg=P.mark({class:"cm-searchMatch cm-searchMatch-selected"}),fg=ut.fromClass(class{constructor(s){this.view=s,this.decorations=this.highlight(s.state.field(fe))}update(s){let t=s.state.field(fe);(t!=s.startState.field(fe)||s.docChanged||s.selectionSet||s.viewportChanged)&&(this.decorations=this.highlight(t))}highlight({query:s,panel:t}){if(!t||!s.spec.valid)return P.none;let{view:e}=this,i=new De;for(let n=0,r=e.visibleRanges,o=r.length;nr[n+1].from-2*250;)a=r[++n].to;s.highlight(e.state,l,a,(c,h)=>{let f=e.state.selection.ranges.some(u=>u.from==c&&u.to==h);i.add(c,h,f?cg:hg)})}return i.finish()}},{decorations:s=>s.decorations});function Ni(s){return t=>{let e=t.state.field(fe,!1);return e&&e.query.spec.valid?s(t,e):nc(t)}}const Bn=Ni((s,{query:t})=>{let{to:e}=s.state.selection.main,i=t.nextMatch(s.state,e,e);if(!i)return!1;let n=b.single(i.from,i.to),r=s.state.facet(ii);return s.dispatch({selection:n,effects:[Pr(s,i),r.scrollToMatch(n.main,s)],userEvent:"select.search"}),ic(s),!0}),Rn=Ni((s,{query:t})=>{let{state:e}=s,{from:i}=e.selection.main,n=t.prevMatch(e,i,i);if(!n)return!1;let r=b.single(n.from,n.to),o=s.state.facet(ii);return s.dispatch({selection:r,effects:[Pr(s,n),o.scrollToMatch(r.main,s)],userEvent:"select.search"}),ic(s),!0}),ug=Ni((s,{query:t})=>{let e=t.matchAll(s.state,1e3);return!e||!e.length?!1:(s.dispatch({selection:b.create(e.map(i=>b.range(i.from,i.to))),userEvent:"select.search.matches"}),!0)}),dg=({state:s,dispatch:t})=>{let e=s.selection;if(e.ranges.length>1||e.main.empty)return!1;let{from:i,to:n}=e.main,r=[],o=0;for(let l=new Ze(s.doc,s.sliceDoc(i,n));!l.next().done;){if(r.length>1e3)return!1;l.value.from==i&&(o=r.length),r.push(b.range(l.value.from,l.value.to))}return t(s.update({selection:b.create(r,o),userEvent:"select.search.matches"})),!0},fl=Ni((s,{query:t})=>{let{state:e}=s,{from:i,to:n}=e.selection.main;if(e.readOnly)return!1;let r=t.nextMatch(e,i,i);if(!r)return!1;let o=[],l,a,c=[];if(r.from==i&&r.to==n&&(a=e.toText(t.getReplacement(r)),o.push({from:r.from,to:r.to,insert:a}),r=t.nextMatch(e,r.from,r.to),c.push(O.announce.of(e.phrase("replaced match on line $",e.doc.lineAt(i).number)+"."))),r){let h=o.length==0||o[0].from>=r.to?0:r.to-r.from-a.length;l=b.single(r.from-h,r.to-h),c.push(Pr(s,r)),c.push(e.facet(ii).scrollToMatch(l.main,s))}return s.dispatch({changes:o,selection:l,effects:c,userEvent:"input.replace"}),!0}),pg=Ni((s,{query:t})=>{if(s.state.readOnly)return!1;let e=t.matchAll(s.state,1e9).map(n=>{let{from:r,to:o}=n;return{from:r,to:o,insert:t.getReplacement(n)}});if(!e.length)return!1;let i=s.state.phrase("replaced $ matches",e.length)+".";return s.dispatch({changes:e,effects:O.announce.of(i),userEvent:"input.replace.all"}),!0});function Tr(s){return s.state.facet(ii).createPanel(s)}function rr(s,t){var e,i,n,r,o;let l=s.selection.main,a=l.empty||l.to>l.from+100?"":s.sliceDoc(l.from,l.to);if(t&&!a)return t;let c=s.facet(ii);return new Zh({search:((e=t==null?void 0:t.literal)!==null&&e!==void 0?e:c.literal)?a:a.replace(/\n/g,"\\n"),caseSensitive:(i=t==null?void 0:t.caseSensitive)!==null&&i!==void 0?i:c.caseSensitive,literal:(n=t==null?void 0:t.literal)!==null&&n!==void 0?n:c.literal,regexp:(r=t==null?void 0:t.regexp)!==null&&r!==void 0?r:c.regexp,wholeWord:(o=t==null?void 0:t.wholeWord)!==null&&o!==void 0?o:c.wholeWord})}function ec(s){let t=wn(s,Tr);return t&&t.dom.querySelector("[main-field]")}function ic(s){let t=ec(s);t&&t==s.root.activeElement&&t.select()}const nc=s=>{let t=s.state.field(fe,!1);if(t&&t.panel){let e=ec(s);if(e&&e!=s.root.activeElement){let i=rr(s.state,t.query.spec);i.valid&&s.dispatch({effects:Oi.of(i)}),e.focus(),e.select()}}else s.dispatch({effects:[Or.of(!0),t?Oi.of(rr(s.state,t.query.spec)):F.appendConfig.of(yg)]});return!0},sc=s=>{let t=s.state.field(fe,!1);if(!t||!t.panel)return!1;let e=wn(s,Tr);return e&&e.dom.contains(s.root.activeElement)&&s.focus(),s.dispatch({effects:Or.of(!1)}),!0},Fm=[{key:"Mod-f",run:nc,scope:"editor search-panel"},{key:"F3",run:Bn,shift:Rn,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:Bn,shift:Rn,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:sc,scope:"editor search-panel"},{key:"Mod-Shift-l",run:dg},{key:"Mod-Alt-g",run:Gp},{key:"Mod-d",run:sg,preventDefault:!0}];class gg{constructor(t){this.view=t;let e=this.query=t.state.field(fe).query.spec;this.commit=this.commit.bind(this),this.searchField=lt("input",{value:e.search,placeholder:Ct(t,"Find"),"aria-label":Ct(t,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=lt("input",{value:e.replace,placeholder:Ct(t,"Replace"),"aria-label":Ct(t,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=lt("input",{type:"checkbox",name:"case",form:"",checked:e.caseSensitive,onchange:this.commit}),this.reField=lt("input",{type:"checkbox",name:"re",form:"",checked:e.regexp,onchange:this.commit}),this.wordField=lt("input",{type:"checkbox",name:"word",form:"",checked:e.wholeWord,onchange:this.commit});function i(n,r,o){return lt("button",{class:"cm-button",name:n,onclick:r,type:"button"},o)}this.dom=lt("div",{onkeydown:n=>this.keydown(n),class:"cm-search"},[this.searchField,i("next",()=>Bn(t),[Ct(t,"next")]),i("prev",()=>Rn(t),[Ct(t,"previous")]),i("select",()=>ug(t),[Ct(t,"all")]),lt("label",null,[this.caseField,Ct(t,"match case")]),lt("label",null,[this.reField,Ct(t,"regexp")]),lt("label",null,[this.wordField,Ct(t,"by word")]),...t.state.readOnly?[]:[lt("br"),this.replaceField,i("replace",()=>fl(t),[Ct(t,"replace")]),i("replaceAll",()=>pg(t),[Ct(t,"replace all")])],lt("button",{name:"close",onclick:()=>sc(t),"aria-label":Ct(t,"close"),type:"button"},["×"])])}commit(){let t=new Zh({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});t.eq(this.query)||(this.query=t,this.view.dispatch({effects:Oi.of(t)}))}keydown(t){cu(this.view,t,"search-panel")?t.preventDefault():t.keyCode==13&&t.target==this.searchField?(t.preventDefault(),(t.shiftKey?Rn:Bn)(this.view)):t.keyCode==13&&t.target==this.replaceField&&(t.preventDefault(),fl(this.view))}update(t){for(let e of t.transactions)for(let i of e.effects)i.is(Oi)&&!i.value.eq(this.query)&&this.setQuery(i.value)}setQuery(t){this.query=t,this.searchField.value=t.search,this.replaceField.value=t.replace,this.caseField.checked=t.caseSensitive,this.reField.checked=t.regexp,this.wordField.checked=t.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(ii).top}}function Ct(s,t){return s.state.phrase(t)}const nn=30,sn=/[\s\.,:;?!]/;function Pr(s,{from:t,to:e}){let i=s.state.doc.lineAt(t),n=s.state.doc.lineAt(e).to,r=Math.max(i.from,t-nn),o=Math.min(n,e+nn),l=s.state.sliceDoc(r,o);if(r!=i.from){for(let a=0;al.length-nn;a--)if(!sn.test(l[a-1])&&sn.test(l[a])){l=l.slice(0,a);break}}return O.announce.of(`${s.state.phrase("current match")}. ${l} ${s.state.phrase("on line")} ${i.number}.`)}const mg=O.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),yg=[fe,ye.low(fg),mg];class rc{constructor(t,e,i,n){this.state=t,this.pos=e,this.explicit=i,this.view=n,this.abortListeners=[]}tokenBefore(t){let e=mt(this.state).resolveInner(this.pos,-1);for(;e&&t.indexOf(e.name)<0;)e=e.parent;return e?{from:e.from,to:this.pos,text:this.state.sliceDoc(e.from,this.pos),type:e.type}:null}matchBefore(t){let e=this.state.doc.lineAt(this.pos),i=Math.max(e.from,this.pos-250),n=e.text.slice(i-e.from,this.pos-e.from),r=n.search(oc(t,!1));return r<0?null:{from:i+r,to:this.pos,text:n.slice(r)}}get aborted(){return this.abortListeners==null}addEventListener(t,e){t=="abort"&&this.abortListeners&&this.abortListeners.push(e)}}function ul(s){let t=Object.keys(s).join(""),e=/\w/.test(t);return e&&(t=t.replace(/\w/g,"")),`[${e?"\\w":""}${t.replace(/[^\w\s]/g,"\\$&")}]`}function bg(s){let t=Object.create(null),e=Object.create(null);for(let{label:n}of s){t[n[0]]=!0;for(let r=1;rtypeof n=="string"?{label:n}:n),[e,i]=t.every(n=>/^\w+$/.test(n.label))?[/\w*$/,/\w+$/]:bg(t);return n=>{let r=n.matchBefore(i);return r||n.explicit?{from:r?r.from:n.pos,options:t,validFor:e}:null}}function Vm(s,t){return e=>{for(let i=mt(e.state).resolveInner(e.pos,-1);i;i=i.parent){if(s.indexOf(i.name)>-1)return null;if(i.type.isTop)break}return t(e)}}class dl{constructor(t,e,i,n){this.completion=t,this.source=e,this.match=i,this.score=n}}function ue(s){return s.selection.main.from}function oc(s,t){var e;let{source:i}=s,n=t&&i[0]!="^",r=i[i.length-1]!="$";return!n&&!r?s:new RegExp(`${n?"^":""}(?:${i})${r?"$":""}`,(e=s.flags)!==null&&e!==void 0?e:s.ignoreCase?"i":"")}const Br=se.define();function wg(s,t,e,i){let{main:n}=s.selection,r=e-n.from,o=i-n.from;return Object.assign(Object.assign({},s.changeByRange(l=>l!=n&&e!=i&&s.sliceDoc(l.from+r,l.from+o)!=s.sliceDoc(e,i)?{range:l}:{changes:{from:l.from+r,to:i==n.from?l.to:l.from+o,insert:t},range:b.cursor(l.from+r+t.length)})),{scrollIntoView:!0,userEvent:"input.complete"})}const pl=new WeakMap;function Sg(s){if(!Array.isArray(s))return s;let t=pl.get(s);return t||pl.set(s,t=xg(s)),t}const Ln=F.define(),Ti=F.define();class vg{constructor(t){this.pattern=t,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let e=0;e=48&&w<=57||w>=97&&w<=122?2:w>=65&&w<=90?1:0:(A=or(w))!=A.toLowerCase()?1:A!=A.toUpperCase()?2:0;(!x||C==1&&m||S==0&&C!=0)&&(e[f]==w||i[f]==w&&(u=!0)?o[f++]=x:o.length&&(y=!1)),S=C,x+=Bt(w)}return f==a&&o[0]==0&&y?this.result(-100+(u?-200:0),o,t):d==a&&p==0?this.ret(-200-t.length+(g==t.length?0:-100),[0,g]):l>-1?this.ret(-700-t.length,[l,l+this.pattern.length]):d==a?this.ret(-900-t.length,[p,g]):f==a?this.result(-100+(u?-200:0)+-700+(y?0:-1100),o,t):e.length==2?null:this.result((n[0]?-700:0)+-200+-1100,n,t)}result(t,e,i){let n=[],r=0;for(let o of e){let l=o+(this.astral?Bt(nt(i,o)):1);r&&n[r-1]==o?n[r-1]=l:(n[r++]=o,n[r++]=l)}return this.ret(t-i.length,n)}}class kg{constructor(t){this.pattern=t,this.matched=[],this.score=0,this.folded=t.toLowerCase()}match(t){if(t.length!1,activateOnTypingDelay:100,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,tooltipClass:()=>"",optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],positionInfo:Cg,filterStrict:!1,compareCompletions:(t,e)=>t.label.localeCompare(e.label),interactionDelay:75,updateSyncTime:100},{defaultKeymap:(t,e)=>t&&e,closeOnBlur:(t,e)=>t&&e,icons:(t,e)=>t&&e,tooltipClass:(t,e)=>i=>gl(t(i),e(i)),optionClass:(t,e)=>i=>gl(t(i),e(i)),addToOptions:(t,e)=>t.concat(e),filterStrict:(t,e)=>t||e})}});function gl(s,t){return s?t?s+" "+t:s:t}function Cg(s,t,e,i,n,r){let o=s.textDirection==X.RTL,l=o,a=!1,c="top",h,f,u=t.left-n.left,d=n.right-t.right,p=i.right-i.left,g=i.bottom-i.top;if(l&&u=g||x>t.top?h=e.bottom-t.top:(c="bottom",h=t.bottom-e.top)}let m=(t.bottom-t.top)/r.offsetHeight,y=(t.right-t.left)/r.offsetWidth;return{style:`${c}: ${h/m}px; max-width: ${f/y}px`,class:"cm-completionInfo-"+(a?o?"left-narrow":"right-narrow":l?"left":"right")}}function Ag(s){let t=s.addToOptions.slice();return s.icons&&t.push({render(e){let i=document.createElement("div");return i.classList.add("cm-completionIcon"),e.type&&i.classList.add(...e.type.split(/\s+/g).map(n=>"cm-completionIcon-"+n)),i.setAttribute("aria-hidden","true"),i},position:20}),t.push({render(e,i,n,r){let o=document.createElement("span");o.className="cm-completionLabel";let l=e.displayLabel||e.label,a=0;for(let c=0;ca&&o.appendChild(document.createTextNode(l.slice(a,h)));let u=o.appendChild(document.createElement("span"));u.appendChild(document.createTextNode(l.slice(h,f))),u.className="cm-completionMatchedText",a=f}return ae.position-i.position).map(e=>e.render)}function cs(s,t,e){if(s<=e)return{from:0,to:s};if(t<0&&(t=0),t<=s>>1){let n=Math.floor(t/e);return{from:n*e,to:(n+1)*e}}let i=Math.floor((s-t)/e);return{from:s-(i+1)*e,to:s-i*e}}class Mg{constructor(t,e,i){this.view=t,this.stateField=e,this.applyCompletion=i,this.info=null,this.infoDestroy=null,this.placeInfoReq={read:()=>this.measureInfo(),write:a=>this.placeInfo(a),key:this},this.space=null,this.currentClass="";let n=t.state.field(e),{options:r,selected:o}=n.open,l=t.state.facet(rt);this.optionContent=Ag(l),this.optionClass=l.optionClass,this.tooltipClass=l.tooltipClass,this.range=cs(r.length,o,l.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.updateTooltipClass(t.state),this.dom.addEventListener("mousedown",a=>{let{options:c}=t.state.field(e).open;for(let h=a.target,f;h&&h!=this.dom;h=h.parentNode)if(h.nodeName=="LI"&&(f=/-(\d+)$/.exec(h.id))&&+f[1]{let c=t.state.field(this.stateField,!1);c&&c.tooltip&&t.state.facet(rt).closeOnBlur&&a.relatedTarget!=t.contentDOM&&t.dispatch({effects:Ti.of(null)})}),this.showOptions(r,n.id)}mount(){this.updateSel()}showOptions(t,e){this.list&&this.list.remove(),this.list=this.dom.appendChild(this.createListBox(t,e,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfoReq)})}update(t){var e;let i=t.state.field(this.stateField),n=t.startState.field(this.stateField);if(this.updateTooltipClass(t.state),i!=n){let{options:r,selected:o,disabled:l}=i.open;(!n.open||n.open.options!=r)&&(this.range=cs(r.length,o,t.state.facet(rt).maxRenderedOptions),this.showOptions(r,i.id)),this.updateSel(),l!=((e=n.open)===null||e===void 0?void 0:e.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!l)}}updateTooltipClass(t){let e=this.tooltipClass(t);if(e!=this.currentClass){for(let i of this.currentClass.split(" "))i&&this.dom.classList.remove(i);for(let i of e.split(" "))i&&this.dom.classList.add(i);this.currentClass=e}}positioned(t){this.space=t,this.info&&this.view.requestMeasure(this.placeInfoReq)}updateSel(){let t=this.view.state.field(this.stateField),e=t.open;if((e.selected>-1&&e.selected=this.range.to)&&(this.range=cs(e.options.length,e.selected,this.view.state.facet(rt).maxRenderedOptions),this.showOptions(e.options,t.id)),this.updateSelectedOption(e.selected)){this.destroyInfo();let{completion:i}=e.options[e.selected],{info:n}=i;if(!n)return;let r=typeof n=="string"?document.createTextNode(n):n(i);if(!r)return;"then"in r?r.then(o=>{o&&this.view.state.field(this.stateField,!1)==t&&this.addInfoPane(o,i)}).catch(o=>Dt(this.view.state,o,"completion info")):this.addInfoPane(r,i)}}addInfoPane(t,e){this.destroyInfo();let i=this.info=document.createElement("div");if(i.className="cm-tooltip cm-completionInfo",t.nodeType!=null)i.appendChild(t),this.infoDestroy=null;else{let{dom:n,destroy:r}=t;i.appendChild(n),this.infoDestroy=r||null}this.dom.appendChild(i),this.view.requestMeasure(this.placeInfoReq)}updateSelectedOption(t){let e=null;for(let i=this.list.firstChild,n=this.range.from;i;i=i.nextSibling,n++)i.nodeName!="LI"||!i.id?n--:n==t?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),e=i):i.hasAttribute("aria-selected")&&i.removeAttribute("aria-selected");return e&&Og(this.list,e),e}measureInfo(){let t=this.dom.querySelector("[aria-selected]");if(!t||!this.info)return null;let e=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),n=t.getBoundingClientRect(),r=this.space;if(!r){let o=this.dom.ownerDocument.defaultView||window;r={left:0,top:0,right:o.innerWidth,bottom:o.innerHeight}}return n.top>Math.min(r.bottom,e.bottom)-10||n.bottomi.from||i.from==0))if(r=u,typeof c!="string"&&c.header)n.appendChild(c.header(c));else{let d=n.appendChild(document.createElement("completion-section"));d.textContent=u}}const h=n.appendChild(document.createElement("li"));h.id=e+"-"+o,h.setAttribute("role","option");let f=this.optionClass(l);f&&(h.className=f);for(let u of this.optionContent){let d=u(l,this.view.state,this.view,a);d&&h.appendChild(d)}}return i.from&&n.classList.add("cm-completionListIncompleteTop"),i.tonew Mg(e,s,t)}function Og(s,t){let e=s.getBoundingClientRect(),i=t.getBoundingClientRect(),n=e.height/s.offsetHeight;i.tope.bottom&&(s.scrollTop+=(i.bottom-e.bottom)/n)}function ml(s){return(s.boost||0)*100+(s.apply?10:0)+(s.info?5:0)+(s.type?1:0)}function Tg(s,t){let e=[],i=null,n=c=>{e.push(c);let{section:h}=c.completion;if(h){i||(i=[]);let f=typeof h=="string"?h:h.name;i.some(u=>u.name==f)||i.push(typeof h=="string"?{name:f}:h)}},r=t.facet(rt);for(let c of s)if(c.hasResult()){let h=c.result.getMatch;if(c.result.filter===!1)for(let f of c.result.options)n(new dl(f,c.source,h?h(f):[],1e9-e.length));else{let f=t.sliceDoc(c.from,c.to),u,d=r.filterStrict?new kg(f):new vg(f);for(let p of c.result.options)if(u=d.match(p.label)){let g=p.displayLabel?h?h(p,u.matched):[]:u.matched;n(new dl(p,c.source,g,u.score+(p.boost||0)))}}}if(i){let c=Object.create(null),h=0,f=(u,d)=>{var p,g;return((p=u.rank)!==null&&p!==void 0?p:1e9)-((g=d.rank)!==null&&g!==void 0?g:1e9)||(u.namef.score-h.score||a(h.completion,f.completion))){let h=c.completion;!l||l.label!=h.label||l.detail!=h.detail||l.type!=null&&h.type!=null&&l.type!=h.type||l.apply!=h.apply||l.boost!=h.boost?o.push(c):ml(c.completion)>ml(l)&&(o[o.length-1]=c),l=c.completion}return o}class We{constructor(t,e,i,n,r,o){this.options=t,this.attrs=e,this.tooltip=i,this.timestamp=n,this.selected=r,this.disabled=o}setSelected(t,e){return t==this.selected||t>=this.options.length?this:new We(this.options,yl(e,t),this.tooltip,this.timestamp,t,this.disabled)}static build(t,e,i,n,r){let o=Tg(t,e);if(!o.length)return n&&t.some(a=>a.state==1)?new We(n.options,n.attrs,n.tooltip,n.timestamp,n.selected,!0):null;let l=e.facet(rt).selectOnOpen?0:-1;if(n&&n.selected!=l&&n.selected!=-1){let a=n.options[n.selected].completion;for(let c=0;cc.hasResult()?Math.min(a,c.from):a,1e8),create:Ig,above:r.aboveCursor},n?n.timestamp:Date.now(),l,!1)}map(t){return new We(this.options,this.attrs,Object.assign(Object.assign({},this.tooltip),{pos:t.mapPos(this.tooltip.pos)}),this.timestamp,this.selected,this.disabled)}}class En{constructor(t,e,i){this.active=t,this.id=e,this.open=i}static start(){return new En(Lg,"cm-ac-"+Math.floor(Math.random()*2e6).toString(36),null)}update(t){let{state:e}=t,i=e.facet(rt),r=(i.override||e.languageDataAt("autocomplete",ue(e)).map(Sg)).map(l=>(this.active.find(c=>c.source==l)||new Mt(l,this.active.some(c=>c.state!=0)?1:0)).update(t,i));r.length==this.active.length&&r.every((l,a)=>l==this.active[a])&&(r=this.active);let o=this.open;o&&t.docChanged&&(o=o.map(t.changes)),t.selection||r.some(l=>l.hasResult()&&t.changes.touchesRange(l.from,l.to))||!Pg(r,this.active)?o=We.build(r,e,this.id,o,i):o&&o.disabled&&!r.some(l=>l.state==1)&&(o=null),!o&&r.every(l=>l.state!=1)&&r.some(l=>l.hasResult())&&(r=r.map(l=>l.hasResult()?new Mt(l.source,0):l));for(let l of t.effects)l.is(hc)&&(o=o&&o.setSelected(l.value,this.id));return r==this.active&&o==this.open?this:new En(r,this.id,o)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:this.active.length?Bg:Rg}}function Pg(s,t){if(s==t)return!0;for(let e=0,i=0;;){for(;e-1&&(e["aria-activedescendant"]=s+"-"+t),e}const Lg=[];function lc(s,t){if(s.isUserEvent("input.complete")){let i=s.annotation(Br);if(i&&t.activateOnCompletion(i))return 12}let e=s.isUserEvent("input.type");return e&&t.activateOnTyping?5:e?1:s.isUserEvent("delete.backward")?2:s.selection?8:s.docChanged?16:0}class Mt{constructor(t,e,i=-1){this.source=t,this.state=e,this.explicitPos=i}hasResult(){return!1}update(t,e){let i=lc(t,e),n=this;(i&8||i&16&&this.touches(t))&&(n=new Mt(n.source,0)),i&4&&n.state==0&&(n=new Mt(this.source,1)),n=n.updateFor(t,i);for(let r of t.effects)if(r.is(Ln))n=new Mt(n.source,1,r.value?ue(t.state):-1);else if(r.is(Ti))n=new Mt(n.source,0);else if(r.is(ac))for(let o of r.value)o.source==n.source&&(n=o);return n}updateFor(t,e){return this.map(t.changes)}map(t){return t.empty||this.explicitPos<0?this:new Mt(this.source,this.state,t.mapPos(this.explicitPos))}touches(t){return t.changes.touchesRange(ue(t.state))}}class je extends Mt{constructor(t,e,i,n,r){super(t,2,e),this.result=i,this.from=n,this.to=r}hasResult(){return!0}updateFor(t,e){var i;if(!(e&3))return this.map(t.changes);let n=this.result;n.map&&!t.changes.empty&&(n=n.map(n,t.changes));let r=t.changes.mapPos(this.from),o=t.changes.mapPos(this.to,1),l=ue(t.state);if((this.explicitPos<0?l<=r:lo||!n||e&2&&ue(t.startState)==this.from)return new Mt(this.source,e&4?1:0);let a=this.explicitPos<0?-1:t.changes.mapPos(this.explicitPos);return Eg(n.validFor,t.state,r,o)?new je(this.source,a,n,r,o):n.update&&(n=n.update(n,r,o,new rc(t.state,l,a>=0)))?new je(this.source,a,n,n.from,(i=n.to)!==null&&i!==void 0?i:ue(t.state)):new Mt(this.source,1,a)}map(t){return t.empty?this:(this.result.map?this.result.map(this.result,t):this.result)?new je(this.source,this.explicitPos<0?-1:t.mapPos(this.explicitPos),this.result,t.mapPos(this.from),t.mapPos(this.to,1)):new Mt(this.source,0)}touches(t){return t.changes.touchesRange(this.from,this.to)}}function Eg(s,t,e,i){if(!s)return!1;let n=t.sliceDoc(e,i);return typeof s=="function"?s(n,e,i,t):oc(s,!0).test(n)}const ac=F.define({map(s,t){return s.map(e=>e.map(t))}}),hc=F.define(),St=yt.define({create(){return En.start()},update(s,t){return s.update(t)},provide:s=>[ja.from(s,t=>t.tooltip),O.contentAttributes.from(s,t=>t.attrs)]});function Rr(s,t){const e=t.completion.apply||t.completion.label;let i=s.state.field(St).active.find(n=>n.source==t.source);return i instanceof je?(typeof e=="string"?s.dispatch(Object.assign(Object.assign({},wg(s.state,e,i.from,i.to)),{annotations:Br.of(t.completion)})):e(s,t.completion,i.from,i.to),!0):!1}const Ig=Dg(St,Rr);function rn(s,t="option"){return e=>{let i=e.state.field(St,!1);if(!i||!i.open||i.open.disabled||Date.now()-i.open.timestamp-1?i.open.selected+n*(s?1:-1):s?0:o-1;return l<0?l=t=="page"?0:o-1:l>=o&&(l=t=="page"?o-1:0),e.dispatch({effects:hc.of(l)}),!0}}const Ng=s=>{let t=s.state.field(St,!1);return s.state.readOnly||!t||!t.open||t.open.selected<0||t.open.disabled||Date.now()-t.open.timestamps.state.field(St,!1)?(s.dispatch({effects:Ln.of(!0)}),!0):!1,Vg=s=>{let t=s.state.field(St,!1);return!t||!t.active.some(e=>e.state!=0)?!1:(s.dispatch({effects:Ti.of(null)}),!0)};class Wg{constructor(t,e){this.active=t,this.context=e,this.time=Date.now(),this.updates=[],this.done=void 0}}const Hg=50,zg=1e3,qg=ut.fromClass(class{constructor(s){this.view=s,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.pendingStart=!1,this.composing=0;for(let t of s.state.field(St).active)t.state==1&&this.startQuery(t)}update(s){let t=s.state.field(St),e=s.state.facet(rt);if(!s.selectionSet&&!s.docChanged&&s.startState.field(St)==t)return;let i=s.transactions.some(r=>{let o=lc(r,e);return o&8||(r.selection||r.docChanged)&&!(o&3)});for(let r=0;rHg&&Date.now()-o.time>zg){for(let l of o.context.abortListeners)try{l()}catch(a){Dt(this.view.state,a)}o.context.abortListeners=null,this.running.splice(r--,1)}else o.updates.push(...s.transactions)}this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),s.transactions.some(r=>r.effects.some(o=>o.is(Ln)))&&(this.pendingStart=!0);let n=this.pendingStart?50:e.activateOnTypingDelay;if(this.debounceUpdate=t.active.some(r=>r.state==1&&!this.running.some(o=>o.active.source==r.source))?setTimeout(()=>this.startUpdate(),n):-1,this.composing!=0)for(let r of s.transactions)r.isUserEvent("input.type")?this.composing=2:this.composing==2&&r.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1,this.pendingStart=!1;let{state:s}=this.view,t=s.field(St);for(let e of t.active)e.state==1&&!this.running.some(i=>i.active.source==e.source)&&this.startQuery(e)}startQuery(s){let{state:t}=this.view,e=ue(t),i=new rc(t,e,s.explicitPos==e,this.view),n=new Wg(s,i);this.running.push(n),Promise.resolve(s.source(i)).then(r=>{n.context.aborted||(n.done=r||null,this.scheduleAccept())},r=>{this.view.dispatch({effects:Ti.of(null)}),Dt(this.view.state,r)})}scheduleAccept(){this.running.every(s=>s.done!==void 0)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(rt).updateSyncTime))}accept(){var s;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let t=[],e=this.view.state.facet(rt);for(let i=0;io.source==n.active.source);if(r&&r.state==1)if(n.done==null){let o=new Mt(n.active.source,0);for(let l of n.updates)o=o.update(l,e);o.state!=1&&t.push(o)}else this.startQuery(r)}t.length&&this.view.dispatch({effects:ac.of(t)})}},{eventHandlers:{blur(s){let t=this.view.state.field(St,!1);if(t&&t.tooltip&&this.view.state.facet(rt).closeOnBlur){let e=t.open&&Ua(this.view,t.open.tooltip);(!e||!e.dom.contains(s.relatedTarget))&&setTimeout(()=>this.view.dispatch({effects:Ti.of(null)}),10)}},compositionstart(){this.composing=1},compositionend(){this.composing==3&&setTimeout(()=>this.view.dispatch({effects:Ln.of(!1)}),20),this.composing=0}}}),Kg=typeof navigator=="object"&&/Win/.test(navigator.platform),$g=ye.highest(O.domEventHandlers({keydown(s,t){let e=t.state.field(St,!1);if(!e||!e.open||e.open.disabled||e.open.selected<0||s.key.length>1||s.ctrlKey&&!(Kg&&s.altKey)||s.metaKey)return!1;let i=e.open.options[e.open.selected],n=e.active.find(o=>o.source==i.source),r=i.completion.commitCharacters||n.result.commitCharacters;return r&&r.indexOf(s.key)>-1&&Rr(t,i),!1}})),cc=O.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",height:"100%",listStyle:"none",margin:0,padding:0,"& > li, & > completion-section":{padding:"1px 3px",lineHeight:1.2},"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer"},"& > completion-section":{display:"list-item",borderBottom:"1px solid silver",paddingLeft:"0.5em",opacity:.7}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"···"',opacity:.5,display:"block",textAlign:"center"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6",boxSizing:"content-box"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'ƒ'"}},".cm-completionIcon-class":{"&:after":{content:"'○'"}},".cm-completionIcon-interface":{"&:after":{content:"'◌'"}},".cm-completionIcon-variable":{"&:after":{content:"'𝑥'"}},".cm-completionIcon-constant":{"&:after":{content:"'𝐶'"}},".cm-completionIcon-type":{"&:after":{content:"'𝑡'"}},".cm-completionIcon-enum":{"&:after":{content:"'∪'"}},".cm-completionIcon-property":{"&:after":{content:"'□'"}},".cm-completionIcon-keyword":{"&:after":{content:"'🔑︎'"}},".cm-completionIcon-namespace":{"&:after":{content:"'▢'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}});class jg{constructor(t,e,i,n){this.field=t,this.line=e,this.from=i,this.to=n}}class Lr{constructor(t,e,i){this.field=t,this.from=e,this.to=i}map(t){let e=t.mapPos(this.from,-1,ht.TrackDel),i=t.mapPos(this.to,1,ht.TrackDel);return e==null||i==null?null:new Lr(this.field,e,i)}}class Er{constructor(t,e){this.lines=t,this.fieldPositions=e}instantiate(t,e){let i=[],n=[e],r=t.doc.lineAt(e),o=/^\s*/.exec(r.text)[0];for(let a of this.lines){if(i.length){let c=o,h=/^\t*/.exec(a)[0].length;for(let f=0;fnew Lr(a.field,n[a.line]+a.from,n[a.line]+a.to));return{text:i,ranges:l}}static parse(t){let e=[],i=[],n=[],r;for(let o of t.split(/\r\n?|\n/)){for(;r=/[#$]\{(?:(\d+)(?::([^}]*))?|((?:\\[{}]|[^}])*))\}/.exec(o);){let l=r[1]?+r[1]:null,a=r[2]||r[3]||"",c=-1,h=a.replace(/\\[{}]/g,f=>f[1]);for(let f=0;f=c&&u.field++}n.push(new jg(c,i.length,r.index,r.index+h.length)),o=o.slice(0,r.index)+a+o.slice(r.index+r[0].length)}o=o.replace(/\\([{}])/g,(l,a,c)=>{for(let h of n)h.line==i.length&&h.from>c&&(h.from--,h.to--);return a}),i.push(o)}return new Er(i,n)}}let Ug=P.widget({widget:new class extends Ee{toDOM(){let s=document.createElement("span");return s.className="cm-snippetFieldPosition",s}ignoreEvent(){return!1}}}),Gg=P.mark({class:"cm-snippetField"});class ni{constructor(t,e){this.ranges=t,this.active=e,this.deco=P.set(t.map(i=>(i.from==i.to?Ug:Gg).range(i.from,i.to)))}map(t){let e=[];for(let i of this.ranges){let n=i.map(t);if(!n)return null;e.push(n)}return new ni(e,this.active)}selectionInsideField(t){return t.ranges.every(e=>this.ranges.some(i=>i.field==this.active&&i.from<=e.from&&i.to>=e.to))}}const Fi=F.define({map(s,t){return s&&s.map(t)}}),Jg=F.define(),Pi=yt.define({create(){return null},update(s,t){for(let e of t.effects){if(e.is(Fi))return e.value;if(e.is(Jg)&&s)return new ni(s.ranges,e.value)}return s&&t.docChanged&&(s=s.map(t.changes)),s&&t.selection&&!s.selectionInsideField(t.selection)&&(s=null),s},provide:s=>O.decorations.from(s,t=>t?t.deco:P.none)});function Ir(s,t){return b.create(s.filter(e=>e.field==t).map(e=>b.range(e.from,e.to)))}function Yg(s){let t=Er.parse(s);return(e,i,n,r)=>{let{text:o,ranges:l}=t.instantiate(e.state,n),a={changes:{from:n,to:r,insert:V.of(o)},scrollIntoView:!0,annotations:i?[Br.of(i),Z.userEvent.of("input.complete")]:void 0};if(l.length&&(a.selection=Ir(l,0)),l.some(c=>c.field>0)){let c=new ni(l,0),h=a.effects=[Fi.of(c)];e.state.field(Pi,!1)===void 0&&h.push(F.appendConfig.of([Pi,tm,em,cc]))}e.dispatch(e.state.update(a))}}function fc(s){return({state:t,dispatch:e})=>{let i=t.field(Pi,!1);if(!i||s<0&&i.active==0)return!1;let n=i.active+s,r=s>0&&!i.ranges.some(o=>o.field==n+s);return e(t.update({selection:Ir(i.ranges,n),effects:Fi.of(r?null:new ni(i.ranges,n)),scrollIntoView:!0})),!0}}const Xg=({state:s,dispatch:t})=>s.field(Pi,!1)?(t(s.update({effects:Fi.of(null)})),!0):!1,_g=fc(1),Qg=fc(-1),Zg=[{key:"Tab",run:_g,shift:Qg},{key:"Escape",run:Xg}],bl=T.define({combine(s){return s.length?s[0]:Zg}}),tm=ye.highest(mr.compute([bl],s=>s.facet(bl)));function Wm(s,t){return Object.assign(Object.assign({},t),{apply:Yg(s)})}const em=O.domEventHandlers({mousedown(s,t){let e=t.state.field(Pi,!1),i;if(!e||(i=t.posAtCoords({x:s.clientX,y:s.clientY}))==null)return!1;let n=e.ranges.find(r=>r.from<=i&&r.to>=i);return!n||n.field==e.active?!1:(t.dispatch({selection:Ir(e.ranges,n.field),effects:Fi.of(e.ranges.some(r=>r.field>n.field)?new ni(e.ranges,n.field):null),scrollIntoView:!0}),!0)}}),Bi={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},Ae=F.define({map(s,t){let e=t.mapPos(s,-1,ht.TrackAfter);return e??void 0}}),Nr=new class extends Me{};Nr.startSide=1;Nr.endSide=-1;const uc=yt.define({create(){return K.empty},update(s,t){if(s=s.map(t.changes),t.selection){let e=t.state.doc.lineAt(t.selection.main.head);s=s.update({filter:i=>i>=e.from&&i<=e.to})}for(let e of t.effects)e.is(Ae)&&(s=s.update({add:[Nr.range(e.value,e.value+1)]}));return s}});function Hm(){return[nm,uc]}const fs="()[]{}<>";function dc(s){for(let t=0;t{if((im?s.composing:s.compositionStarted)||s.state.readOnly)return!1;let n=s.state.selection.main;if(i.length>2||i.length==2&&Bt(nt(i,0))==1||t!=n.from||e!=n.to)return!1;let r=rm(s.state,i);return r?(s.dispatch(r),!0):!1}),sm=({state:s,dispatch:t})=>{if(s.readOnly)return!1;let i=pc(s,s.selection.main.head).brackets||Bi.brackets,n=null,r=s.changeByRange(o=>{if(o.empty){let l=om(s.doc,o.head);for(let a of i)if(a==l&&jn(s.doc,o.head)==dc(nt(a,0)))return{changes:{from:o.head-a.length,to:o.head+a.length},range:b.cursor(o.head-a.length)}}return{range:n=o}});return n||t(s.update(r,{scrollIntoView:!0,userEvent:"delete.backward"})),!n},zm=[{key:"Backspace",run:sm}];function rm(s,t){let e=pc(s,s.selection.main.head),i=e.brackets||Bi.brackets;for(let n of i){let r=dc(nt(n,0));if(t==n)return r==n?hm(s,n,i.indexOf(n+n+n)>-1,e):lm(s,n,r,e.before||Bi.before);if(t==r&&gc(s,s.selection.main.from))return am(s,n,r)}return null}function gc(s,t){let e=!1;return s.field(uc).between(0,s.doc.length,i=>{i==t&&(e=!0)}),e}function jn(s,t){let e=s.sliceString(t,t+2);return e.slice(0,Bt(nt(e,0)))}function om(s,t){let e=s.sliceString(t-2,t);return Bt(nt(e,0))==e.length?e:e.slice(1)}function lm(s,t,e,i){let n=null,r=s.changeByRange(o=>{if(!o.empty)return{changes:[{insert:t,from:o.from},{insert:e,from:o.to}],effects:Ae.of(o.to+t.length),range:b.range(o.anchor+t.length,o.head+t.length)};let l=jn(s.doc,o.head);return!l||/\s/.test(l)||i.indexOf(l)>-1?{changes:{insert:t+e,from:o.head},effects:Ae.of(o.head+t.length),range:b.cursor(o.head+t.length)}:{range:n=o}});return n?null:s.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function am(s,t,e){let i=null,n=s.changeByRange(r=>r.empty&&jn(s.doc,r.head)==e?{changes:{from:r.head,to:r.head+e.length,insert:e},range:b.cursor(r.head+e.length)}:i={range:r});return i?null:s.update(n,{scrollIntoView:!0,userEvent:"input.type"})}function hm(s,t,e,i){let n=i.stringPrefixes||Bi.stringPrefixes,r=null,o=s.changeByRange(l=>{if(!l.empty)return{changes:[{insert:t,from:l.from},{insert:t,from:l.to}],effects:Ae.of(l.to+t.length),range:b.range(l.anchor+t.length,l.head+t.length)};let a=l.head,c=jn(s.doc,a),h;if(c==t){if(xl(s,a))return{changes:{insert:t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)};if(gc(s,a)){let u=e&&s.sliceDoc(a,a+t.length*3)==t+t+t?t+t+t:t;return{changes:{from:a,to:a+u.length,insert:u},range:b.cursor(a+u.length)}}}else{if(e&&s.sliceDoc(a-2*t.length,a)==t+t&&(h=wl(s,a-2*t.length,n))>-1&&xl(s,h))return{changes:{insert:t+t+t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)};if(s.charCategorizer(a)(c)!=G.Word&&wl(s,a,n)>-1&&!cm(s,a,t,n))return{changes:{insert:t+t,from:a},effects:Ae.of(a+t.length),range:b.cursor(a+t.length)}}return{range:r=l}});return r?null:s.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function xl(s,t){let e=mt(s).resolveInner(t+1);return e.parent&&e.from==t}function cm(s,t,e,i){let n=mt(s).resolveInner(t,-1),r=i.reduce((o,l)=>Math.max(o,l.length),0);for(let o=0;o<5;o++){let l=s.sliceDoc(n.from,Math.min(n.to,n.from+e.length+r)),a=l.indexOf(e);if(!a||a>-1&&i.indexOf(l.slice(0,a))>-1){let h=n.firstChild;for(;h&&h.from==n.from&&h.to-h.from>e.length+a;){if(s.sliceDoc(h.to-e.length,h.to)==e)return!1;h=h.firstChild}return!0}let c=n.to==t&&n.parent;if(!c)break;n=c}return!1}function wl(s,t,e){let i=s.charCategorizer(t);if(i(s.sliceDoc(t-1,t))!=G.Word)return t;for(let n of e){let r=t-n.length;if(s.sliceDoc(r,t)==n&&i(s.sliceDoc(r-1,r))!=G.Word)return r}return-1}function qm(s={}){return[$g,St,rt.of(s),qg,um,cc]}const fm=[{key:"Ctrl-Space",run:Fg},{key:"Escape",run:Vg},{key:"ArrowDown",run:rn(!0)},{key:"ArrowUp",run:rn(!1)},{key:"PageDown",run:rn(!0,"page")},{key:"PageUp",run:rn(!1,"page")},{key:"Enter",run:Ng}],um=ye.highest(mr.computeN([rt],s=>s.facet(rt).defaultKeymap?[fm]:[]));export{Sm as A,dd as B,In as C,zu as D,O as E,Mm as F,Dm as G,Om as H,Y as I,km as J,wm as K,Xs as L,Vm as M,yr as N,xg as O,_a as P,b as Q,Wm as R,fh as S,j as T,Am as U,Cm as V,cd as W,th as X,Od as Y,fm as a,H as b,zm as c,Em as d,xm as e,mm as f,Rm as g,Lm as h,pm as i,gm as j,Tm as k,Bm as l,Hm as m,Nm as n,mr as o,qm as p,ym as q,bm as r,Fm as s,Im as t,Pm as u,mt as v,gt as w,L as x,rd as y,M as z}; diff --git a/ui/dist/assets/index-Cwj4Cw5-.css b/ui/dist/assets/index-Cwj4Cw5-.css new file mode 100644 index 00000000..e861def9 --- /dev/null +++ b/ui/dist/assets/index-Cwj4Cw5-.css @@ -0,0 +1 @@ +@charset "UTF-8";@font-face{font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?v=4) format("woff2");font-display:swap}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:400;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:400;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:600;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:600;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:700;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:700;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2) format("woff2")}@font-face{font-display:swap;font-family:Ubuntu Mono;font-style:normal;font-weight:400;src:url(../fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2) format("woff2")}@font-face{font-display:swap;font-family:Ubuntu Mono;font-style:normal;font-weight:700;src:url(../fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2) format("woff2")}:root{--baseFontFamily: "Source Sans Pro", sans-serif, emoji;--monospaceFontFamily: "Ubuntu Mono", monospace, emoji;--iconFontFamily: "remixicon";--txtPrimaryColor: #1a1a24;--txtHintColor: #617079;--txtDisabledColor: #a0a6ac;--primaryColor: #1a1a24;--bodyColor: #f8f9fa;--baseColor: #ffffff;--baseAlt1Color: #e3e8ed;--baseAlt2Color: #d7dde3;--baseAlt3Color: #c9d0da;--baseAlt4Color: #a5b0c0;--infoColor: #5499e8;--infoAltColor: #cee2f8;--successColor: #32ad84;--successAltColor: #c4eedc;--dangerColor: #e34562;--dangerAltColor: #f7cad2;--warningColor: #ff944d;--warningAltColor: #ffd4b8;--overlayColor: rgba(53, 71, 104, .28);--tooltipColor: rgba(0, 0, 0, .85);--shadowColor: rgba(0, 0, 0, .06);--baseFontSize: 14.5px;--xsFontSize: 12px;--smFontSize: 13px;--lgFontSize: 15px;--xlFontSize: 16px;--baseLineHeight: 22px;--smLineHeight: 16px;--lgLineHeight: 24px;--inputHeight: 34px;--btnHeight: 40px;--xsBtnHeight: 22px;--smBtnHeight: 30px;--lgBtnHeight: 54px;--baseSpacing: 30px;--xsSpacing: 15px;--smSpacing: 20px;--lgSpacing: 50px;--xlSpacing: 60px;--wrapperWidth: 850px;--smWrapperWidth: 420px;--lgWrapperWidth: 1200px;--appSidebarWidth: 75px;--pageSidebarWidth: 235px;--baseAnimationSpeed: .15s;--activeAnimationSpeed: 70ms;--entranceAnimationSpeed: .25s;--baseRadius: 4px;--lgRadius: 12px;--btnRadius: 4px;accent-color:var(--primaryColor)}html,body,div,span,applet,object,iframe,h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}i{font-family:remixicon!important;font-style:normal;font-weight:400;font-size:1.1238rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}i:before{vertical-align:top;margin-top:1px;display:inline-block}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-arrow-left-down-fill:before{content:""}.ri-arrow-left-down-line:before{content:""}.ri-arrow-left-fill:before{content:""}.ri-arrow-left-line:before{content:""}.ri-arrow-left-right-fill:before{content:""}.ri-arrow-left-right-line:before{content:""}.ri-arrow-left-s-fill:before{content:""}.ri-arrow-left-s-line:before{content:""}.ri-arrow-left-up-fill:before{content:""}.ri-arrow-left-up-line:before{content:""}.ri-arrow-right-circle-fill:before{content:""}.ri-arrow-right-circle-line:before{content:""}.ri-arrow-right-down-fill:before{content:""}.ri-arrow-right-down-line:before{content:""}.ri-arrow-right-fill:before{content:""}.ri-arrow-right-line:before{content:""}.ri-arrow-right-s-fill:before{content:""}.ri-arrow-right-s-line:before{content:""}.ri-arrow-right-up-fill:before{content:""}.ri-arrow-right-up-line:before{content:""}.ri-arrow-up-circle-fill:before{content:""}.ri-arrow-up-circle-line:before{content:""}.ri-arrow-up-down-fill:before{content:""}.ri-arrow-up-down-line:before{content:""}.ri-arrow-up-fill:before{content:""}.ri-arrow-up-line:before{content:""}.ri-arrow-up-s-fill:before{content:""}.ri-arrow-up-s-line:before{content:""}.ri-artboard-2-fill:before{content:""}.ri-artboard-2-line:before{content:""}.ri-artboard-fill:before{content:""}.ri-artboard-line:before{content:""}.ri-article-fill:before{content:""}.ri-article-line:before{content:""}.ri-aspect-ratio-fill:before{content:""}.ri-aspect-ratio-line:before{content:""}.ri-asterisk:before{content:""}.ri-at-fill:before{content:""}.ri-at-line:before{content:""}.ri-attachment-2:before{content:""}.ri-attachment-fill:before{content:""}.ri-attachment-line:before{content:""}.ri-auction-fill:before{content:""}.ri-auction-line:before{content:""}.ri-award-fill:before{content:""}.ri-award-line:before{content:""}.ri-baidu-fill:before{content:""}.ri-baidu-line:before{content:""}.ri-ball-pen-fill:before{content:""}.ri-ball-pen-line:before{content:""}.ri-bank-card-2-fill:before{content:""}.ri-bank-card-2-line:before{content:""}.ri-bank-card-fill:before{content:""}.ri-bank-card-line:before{content:""}.ri-bank-fill:before{content:""}.ri-bank-line:before{content:""}.ri-bar-chart-2-fill:before{content:""}.ri-bar-chart-2-line:before{content:""}.ri-bar-chart-box-fill:before{content:""}.ri-bar-chart-box-line:before{content:""}.ri-bar-chart-fill:before{content:""}.ri-bar-chart-grouped-fill:before{content:""}.ri-bar-chart-grouped-line:before{content:""}.ri-bar-chart-horizontal-fill:before{content:""}.ri-bar-chart-horizontal-line:before{content:""}.ri-bar-chart-line:before{content:""}.ri-barcode-box-fill:before{content:""}.ri-barcode-box-line:before{content:""}.ri-barcode-fill:before{content:""}.ri-barcode-line:before{content:""}.ri-barricade-fill:before{content:""}.ri-barricade-line:before{content:""}.ri-base-station-fill:before{content:""}.ri-base-station-line:before{content:""}.ri-basketball-fill:before{content:""}.ri-basketball-line:before{content:""}.ri-battery-2-charge-fill:before{content:""}.ri-battery-2-charge-line:before{content:""}.ri-battery-2-fill:before{content:""}.ri-battery-2-line:before{content:""}.ri-battery-charge-fill:before{content:""}.ri-battery-charge-line:before{content:""}.ri-battery-fill:before{content:""}.ri-battery-line:before{content:""}.ri-battery-low-fill:before{content:""}.ri-battery-low-line:before{content:""}.ri-battery-saver-fill:before{content:""}.ri-battery-saver-line:before{content:""}.ri-battery-share-fill:before{content:""}.ri-battery-share-line:before{content:""}.ri-bear-smile-fill:before{content:""}.ri-bear-smile-line:before{content:""}.ri-behance-fill:before{content:""}.ri-behance-line:before{content:""}.ri-bell-fill:before{content:""}.ri-bell-line:before{content:""}.ri-bike-fill:before{content:""}.ri-bike-line:before{content:""}.ri-bilibili-fill:before{content:""}.ri-bilibili-line:before{content:""}.ri-bill-fill:before{content:""}.ri-bill-line:before{content:""}.ri-billiards-fill:before{content:""}.ri-billiards-line:before{content:""}.ri-bit-coin-fill:before{content:""}.ri-bit-coin-line:before{content:""}.ri-blaze-fill:before{content:""}.ri-blaze-line:before{content:""}.ri-bluetooth-connect-fill:before{content:""}.ri-bluetooth-connect-line:before{content:""}.ri-bluetooth-fill:before{content:""}.ri-bluetooth-line:before{content:""}.ri-blur-off-fill:before{content:""}.ri-blur-off-line:before{content:""}.ri-body-scan-fill:before{content:""}.ri-body-scan-line:before{content:""}.ri-bold:before{content:""}.ri-book-2-fill:before{content:""}.ri-book-2-line:before{content:""}.ri-book-3-fill:before{content:""}.ri-book-3-line:before{content:""}.ri-book-fill:before{content:""}.ri-book-line:before{content:""}.ri-book-marked-fill:before{content:""}.ri-book-marked-line:before{content:""}.ri-book-open-fill:before{content:""}.ri-book-open-line:before{content:""}.ri-book-read-fill:before{content:""}.ri-book-read-line:before{content:""}.ri-booklet-fill:before{content:""}.ri-booklet-line:before{content:""}.ri-bookmark-2-fill:before{content:""}.ri-bookmark-2-line:before{content:""}.ri-bookmark-3-fill:before{content:""}.ri-bookmark-3-line:before{content:""}.ri-bookmark-fill:before{content:""}.ri-bookmark-line:before{content:""}.ri-boxing-fill:before{content:""}.ri-boxing-line:before{content:""}.ri-braces-fill:before{content:""}.ri-braces-line:before{content:""}.ri-brackets-fill:before{content:""}.ri-brackets-line:before{content:""}.ri-briefcase-2-fill:before{content:""}.ri-briefcase-2-line:before{content:""}.ri-briefcase-3-fill:before{content:""}.ri-briefcase-3-line:before{content:""}.ri-briefcase-4-fill:before{content:""}.ri-briefcase-4-line:before{content:""}.ri-briefcase-5-fill:before{content:""}.ri-briefcase-5-line:before{content:""}.ri-briefcase-fill:before{content:""}.ri-briefcase-line:before{content:""}.ri-bring-forward:before{content:""}.ri-bring-to-front:before{content:""}.ri-broadcast-fill:before{content:""}.ri-broadcast-line:before{content:""}.ri-brush-2-fill:before{content:""}.ri-brush-2-line:before{content:""}.ri-brush-3-fill:before{content:""}.ri-brush-3-line:before{content:""}.ri-brush-4-fill:before{content:""}.ri-brush-4-line:before{content:""}.ri-brush-fill:before{content:""}.ri-brush-line:before{content:""}.ri-bubble-chart-fill:before{content:""}.ri-bubble-chart-line:before{content:""}.ri-bug-2-fill:before{content:""}.ri-bug-2-line:before{content:""}.ri-bug-fill:before{content:""}.ri-bug-line:before{content:""}.ri-building-2-fill:before{content:""}.ri-building-2-line:before{content:""}.ri-building-3-fill:before{content:""}.ri-building-3-line:before{content:""}.ri-building-4-fill:before{content:""}.ri-building-4-line:before{content:""}.ri-building-fill:before{content:""}.ri-building-line:before{content:""}.ri-bus-2-fill:before{content:""}.ri-bus-2-line:before{content:""}.ri-bus-fill:before{content:""}.ri-bus-line:before{content:""}.ri-bus-wifi-fill:before{content:""}.ri-bus-wifi-line:before{content:""}.ri-cactus-fill:before{content:""}.ri-cactus-line:before{content:""}.ri-cake-2-fill:before{content:""}.ri-cake-2-line:before{content:""}.ri-cake-3-fill:before{content:""}.ri-cake-3-line:before{content:""}.ri-cake-fill:before{content:""}.ri-cake-line:before{content:""}.ri-calculator-fill:before{content:""}.ri-calculator-line:before{content:""}.ri-calendar-2-fill:before{content:""}.ri-calendar-2-line:before{content:""}.ri-calendar-check-fill:before{content:""}.ri-calendar-check-line:before{content:""}.ri-calendar-event-fill:before{content:""}.ri-calendar-event-line:before{content:""}.ri-calendar-fill:before{content:""}.ri-calendar-line:before{content:""}.ri-calendar-todo-fill:before{content:""}.ri-calendar-todo-line:before{content:""}.ri-camera-2-fill:before{content:""}.ri-camera-2-line:before{content:""}.ri-camera-3-fill:before{content:""}.ri-camera-3-line:before{content:""}.ri-camera-fill:before{content:""}.ri-camera-lens-fill:before{content:""}.ri-camera-lens-line:before{content:""}.ri-camera-line:before{content:""}.ri-camera-off-fill:before{content:""}.ri-camera-off-line:before{content:""}.ri-camera-switch-fill:before{content:""}.ri-camera-switch-line:before{content:""}.ri-capsule-fill:before{content:""}.ri-capsule-line:before{content:""}.ri-car-fill:before{content:""}.ri-car-line:before{content:""}.ri-car-washing-fill:before{content:""}.ri-car-washing-line:before{content:""}.ri-caravan-fill:before{content:""}.ri-caravan-line:before{content:""}.ri-cast-fill:before{content:""}.ri-cast-line:before{content:""}.ri-cellphone-fill:before{content:""}.ri-cellphone-line:before{content:""}.ri-celsius-fill:before{content:""}.ri-celsius-line:before{content:""}.ri-centos-fill:before{content:""}.ri-centos-line:before{content:""}.ri-character-recognition-fill:before{content:""}.ri-character-recognition-line:before{content:""}.ri-charging-pile-2-fill:before{content:""}.ri-charging-pile-2-line:before{content:""}.ri-charging-pile-fill:before{content:""}.ri-charging-pile-line:before{content:""}.ri-chat-1-fill:before{content:""}.ri-chat-1-line:before{content:""}.ri-chat-2-fill:before{content:""}.ri-chat-2-line:before{content:""}.ri-chat-3-fill:before{content:""}.ri-chat-3-line:before{content:""}.ri-chat-4-fill:before{content:""}.ri-chat-4-line:before{content:""}.ri-chat-check-fill:before{content:""}.ri-chat-check-line:before{content:""}.ri-chat-delete-fill:before{content:""}.ri-chat-delete-line:before{content:""}.ri-chat-download-fill:before{content:""}.ri-chat-download-line:before{content:""}.ri-chat-follow-up-fill:before{content:""}.ri-chat-follow-up-line:before{content:""}.ri-chat-forward-fill:before{content:""}.ri-chat-forward-line:before{content:""}.ri-chat-heart-fill:before{content:""}.ri-chat-heart-line:before{content:""}.ri-chat-history-fill:before{content:""}.ri-chat-history-line:before{content:""}.ri-chat-new-fill:before{content:""}.ri-chat-new-line:before{content:""}.ri-chat-off-fill:before{content:""}.ri-chat-off-line:before{content:""}.ri-chat-poll-fill:before{content:""}.ri-chat-poll-line:before{content:""}.ri-chat-private-fill:before{content:""}.ri-chat-private-line:before{content:""}.ri-chat-quote-fill:before{content:""}.ri-chat-quote-line:before{content:""}.ri-chat-settings-fill:before{content:""}.ri-chat-settings-line:before{content:""}.ri-chat-smile-2-fill:before{content:""}.ri-chat-smile-2-line:before{content:""}.ri-chat-smile-3-fill:before{content:""}.ri-chat-smile-3-line:before{content:""}.ri-chat-smile-fill:before{content:""}.ri-chat-smile-line:before{content:""}.ri-chat-upload-fill:before{content:""}.ri-chat-upload-line:before{content:""}.ri-chat-voice-fill:before{content:""}.ri-chat-voice-line:before{content:""}.ri-check-double-fill:before{content:""}.ri-check-double-line:before{content:""}.ri-check-fill:before{content:""}.ri-check-line:before{content:""}.ri-checkbox-blank-circle-fill:before{content:""}.ri-checkbox-blank-circle-line:before{content:""}.ri-checkbox-blank-fill:before{content:""}.ri-checkbox-blank-line:before{content:""}.ri-checkbox-circle-fill:before{content:""}.ri-checkbox-circle-line:before{content:""}.ri-checkbox-fill:before{content:""}.ri-checkbox-indeterminate-fill:before{content:""}.ri-checkbox-indeterminate-line:before{content:""}.ri-checkbox-line:before{content:""}.ri-checkbox-multiple-blank-fill:before{content:""}.ri-checkbox-multiple-blank-line:before{content:""}.ri-checkbox-multiple-fill:before{content:""}.ri-checkbox-multiple-line:before{content:""}.ri-china-railway-fill:before{content:""}.ri-china-railway-line:before{content:""}.ri-chrome-fill:before{content:""}.ri-chrome-line:before{content:""}.ri-clapperboard-fill:before{content:""}.ri-clapperboard-line:before{content:""}.ri-clipboard-fill:before{content:""}.ri-clipboard-line:before{content:""}.ri-clockwise-2-fill:before{content:""}.ri-clockwise-2-line:before{content:""}.ri-clockwise-fill:before{content:""}.ri-clockwise-line:before{content:""}.ri-close-circle-fill:before{content:""}.ri-close-circle-line:before{content:""}.ri-close-fill:before{content:""}.ri-close-line:before{content:""}.ri-closed-captioning-fill:before{content:""}.ri-closed-captioning-line:before{content:""}.ri-cloud-fill:before{content:""}.ri-cloud-line:before{content:""}.ri-cloud-off-fill:before{content:""}.ri-cloud-off-line:before{content:""}.ri-cloud-windy-fill:before{content:""}.ri-cloud-windy-line:before{content:""}.ri-cloudy-2-fill:before{content:""}.ri-cloudy-2-line:before{content:""}.ri-cloudy-fill:before{content:""}.ri-cloudy-line:before{content:""}.ri-code-box-fill:before{content:""}.ri-code-box-line:before{content:""}.ri-code-fill:before{content:""}.ri-code-line:before{content:""}.ri-code-s-fill:before{content:""}.ri-code-s-line:before{content:""}.ri-code-s-slash-fill:before{content:""}.ri-code-s-slash-line:before{content:""}.ri-code-view:before{content:""}.ri-codepen-fill:before{content:""}.ri-codepen-line:before{content:""}.ri-coin-fill:before{content:""}.ri-coin-line:before{content:""}.ri-coins-fill:before{content:""}.ri-coins-line:before{content:""}.ri-collage-fill:before{content:""}.ri-collage-line:before{content:""}.ri-command-fill:before{content:""}.ri-command-line:before{content:""}.ri-community-fill:before{content:""}.ri-community-line:before{content:""}.ri-compass-2-fill:before{content:""}.ri-compass-2-line:before{content:""}.ri-compass-3-fill:before{content:""}.ri-compass-3-line:before{content:""}.ri-compass-4-fill:before{content:""}.ri-compass-4-line:before{content:""}.ri-compass-discover-fill:before{content:""}.ri-compass-discover-line:before{content:""}.ri-compass-fill:before{content:""}.ri-compass-line:before{content:""}.ri-compasses-2-fill:before{content:""}.ri-compasses-2-line:before{content:""}.ri-compasses-fill:before{content:""}.ri-compasses-line:before{content:""}.ri-computer-fill:before{content:""}.ri-computer-line:before{content:""}.ri-contacts-book-2-fill:before{content:""}.ri-contacts-book-2-line:before{content:""}.ri-contacts-book-fill:before{content:""}.ri-contacts-book-line:before{content:""}.ri-contacts-book-upload-fill:before{content:""}.ri-contacts-book-upload-line:before{content:""}.ri-contacts-fill:before{content:""}.ri-contacts-line:before{content:""}.ri-contrast-2-fill:before{content:""}.ri-contrast-2-line:before{content:""}.ri-contrast-drop-2-fill:before{content:""}.ri-contrast-drop-2-line:before{content:""}.ri-contrast-drop-fill:before{content:""}.ri-contrast-drop-line:before{content:""}.ri-contrast-fill:before{content:""}.ri-contrast-line:before{content:""}.ri-copper-coin-fill:before{content:""}.ri-copper-coin-line:before{content:""}.ri-copper-diamond-fill:before{content:""}.ri-copper-diamond-line:before{content:""}.ri-copyleft-fill:before{content:""}.ri-copyleft-line:before{content:""}.ri-copyright-fill:before{content:""}.ri-copyright-line:before{content:""}.ri-coreos-fill:before{content:""}.ri-coreos-line:before{content:""}.ri-coupon-2-fill:before{content:""}.ri-coupon-2-line:before{content:""}.ri-coupon-3-fill:before{content:""}.ri-coupon-3-line:before{content:""}.ri-coupon-4-fill:before{content:""}.ri-coupon-4-line:before{content:""}.ri-coupon-5-fill:before{content:""}.ri-coupon-5-line:before{content:""}.ri-coupon-fill:before{content:""}.ri-coupon-line:before{content:""}.ri-cpu-fill:before{content:""}.ri-cpu-line:before{content:""}.ri-creative-commons-by-fill:before{content:""}.ri-creative-commons-by-line:before{content:""}.ri-creative-commons-fill:before{content:""}.ri-creative-commons-line:before{content:""}.ri-creative-commons-nc-fill:before{content:""}.ri-creative-commons-nc-line:before{content:""}.ri-creative-commons-nd-fill:before{content:""}.ri-creative-commons-nd-line:before{content:""}.ri-creative-commons-sa-fill:before{content:""}.ri-creative-commons-sa-line:before{content:""}.ri-creative-commons-zero-fill:before{content:""}.ri-creative-commons-zero-line:before{content:""}.ri-criminal-fill:before{content:""}.ri-criminal-line:before{content:""}.ri-crop-2-fill:before{content:""}.ri-crop-2-line:before{content:""}.ri-crop-fill:before{content:""}.ri-crop-line:before{content:""}.ri-css3-fill:before{content:""}.ri-css3-line:before{content:""}.ri-cup-fill:before{content:""}.ri-cup-line:before{content:""}.ri-currency-fill:before{content:""}.ri-currency-line:before{content:""}.ri-cursor-fill:before{content:""}.ri-cursor-line:before{content:""}.ri-customer-service-2-fill:before{content:""}.ri-customer-service-2-line:before{content:""}.ri-customer-service-fill:before{content:""}.ri-customer-service-line:before{content:""}.ri-dashboard-2-fill:before{content:""}.ri-dashboard-2-line:before{content:""}.ri-dashboard-3-fill:before{content:""}.ri-dashboard-3-line:before{content:""}.ri-dashboard-fill:before{content:""}.ri-dashboard-line:before{content:""}.ri-database-2-fill:before{content:""}.ri-database-2-line:before{content:""}.ri-database-fill:before{content:""}.ri-database-line:before{content:""}.ri-delete-back-2-fill:before{content:""}.ri-delete-back-2-line:before{content:""}.ri-delete-back-fill:before{content:""}.ri-delete-back-line:before{content:""}.ri-delete-bin-2-fill:before{content:""}.ri-delete-bin-2-line:before{content:""}.ri-delete-bin-3-fill:before{content:""}.ri-delete-bin-3-line:before{content:""}.ri-delete-bin-4-fill:before{content:""}.ri-delete-bin-4-line:before{content:""}.ri-delete-bin-5-fill:before{content:""}.ri-delete-bin-5-line:before{content:""}.ri-delete-bin-6-fill:before{content:""}.ri-delete-bin-6-line:before{content:""}.ri-delete-bin-7-fill:before{content:""}.ri-delete-bin-7-line:before{content:""}.ri-delete-bin-fill:before{content:""}.ri-delete-bin-line:before{content:""}.ri-delete-column:before{content:""}.ri-delete-row:before{content:""}.ri-device-fill:before{content:""}.ri-device-line:before{content:""}.ri-device-recover-fill:before{content:""}.ri-device-recover-line:before{content:""}.ri-dingding-fill:before{content:""}.ri-dingding-line:before{content:""}.ri-direction-fill:before{content:""}.ri-direction-line:before{content:""}.ri-disc-fill:before{content:""}.ri-disc-line:before{content:""}.ri-discord-fill:before{content:""}.ri-discord-line:before{content:""}.ri-discuss-fill:before{content:""}.ri-discuss-line:before{content:""}.ri-dislike-fill:before{content:""}.ri-dislike-line:before{content:""}.ri-disqus-fill:before{content:""}.ri-disqus-line:before{content:""}.ri-divide-fill:before{content:""}.ri-divide-line:before{content:""}.ri-donut-chart-fill:before{content:""}.ri-donut-chart-line:before{content:""}.ri-door-closed-fill:before{content:""}.ri-door-closed-line:before{content:""}.ri-door-fill:before{content:""}.ri-door-line:before{content:""}.ri-door-lock-box-fill:before{content:""}.ri-door-lock-box-line:before{content:""}.ri-door-lock-fill:before{content:""}.ri-door-lock-line:before{content:""}.ri-door-open-fill:before{content:""}.ri-door-open-line:before{content:""}.ri-dossier-fill:before{content:""}.ri-dossier-line:before{content:""}.ri-douban-fill:before{content:""}.ri-douban-line:before{content:""}.ri-double-quotes-l:before{content:""}.ri-double-quotes-r:before{content:""}.ri-download-2-fill:before{content:""}.ri-download-2-line:before{content:""}.ri-download-cloud-2-fill:before{content:""}.ri-download-cloud-2-line:before{content:""}.ri-download-cloud-fill:before{content:""}.ri-download-cloud-line:before{content:""}.ri-download-fill:before{content:""}.ri-download-line:before{content:""}.ri-draft-fill:before{content:""}.ri-draft-line:before{content:""}.ri-drag-drop-fill:before{content:""}.ri-drag-drop-line:before{content:""}.ri-drag-move-2-fill:before{content:""}.ri-drag-move-2-line:before{content:""}.ri-drag-move-fill:before{content:""}.ri-drag-move-line:before{content:""}.ri-dribbble-fill:before{content:""}.ri-dribbble-line:before{content:""}.ri-drive-fill:before{content:""}.ri-drive-line:before{content:""}.ri-drizzle-fill:before{content:""}.ri-drizzle-line:before{content:""}.ri-drop-fill:before{content:""}.ri-drop-line:before{content:""}.ri-dropbox-fill:before{content:""}.ri-dropbox-line:before{content:""}.ri-dual-sim-1-fill:before{content:""}.ri-dual-sim-1-line:before{content:""}.ri-dual-sim-2-fill:before{content:""}.ri-dual-sim-2-line:before{content:""}.ri-dv-fill:before{content:""}.ri-dv-line:before{content:""}.ri-dvd-fill:before{content:""}.ri-dvd-line:before{content:""}.ri-e-bike-2-fill:before{content:""}.ri-e-bike-2-line:before{content:""}.ri-e-bike-fill:before{content:""}.ri-e-bike-line:before{content:""}.ri-earth-fill:before{content:""}.ri-earth-line:before{content:""}.ri-earthquake-fill:before{content:""}.ri-earthquake-line:before{content:""}.ri-edge-fill:before{content:""}.ri-edge-line:before{content:""}.ri-edit-2-fill:before{content:""}.ri-edit-2-line:before{content:""}.ri-edit-box-fill:before{content:""}.ri-edit-box-line:before{content:""}.ri-edit-circle-fill:before{content:""}.ri-edit-circle-line:before{content:""}.ri-edit-fill:before{content:""}.ri-edit-line:before{content:""}.ri-eject-fill:before{content:""}.ri-eject-line:before{content:""}.ri-emotion-2-fill:before{content:""}.ri-emotion-2-line:before{content:""}.ri-emotion-fill:before{content:""}.ri-emotion-happy-fill:before{content:""}.ri-emotion-happy-line:before{content:""}.ri-emotion-laugh-fill:before{content:""}.ri-emotion-laugh-line:before{content:""}.ri-emotion-line:before{content:""}.ri-emotion-normal-fill:before{content:""}.ri-emotion-normal-line:before{content:""}.ri-emotion-sad-fill:before{content:""}.ri-emotion-sad-line:before{content:""}.ri-emotion-unhappy-fill:before{content:""}.ri-emotion-unhappy-line:before{content:""}.ri-empathize-fill:before{content:""}.ri-empathize-line:before{content:""}.ri-emphasis-cn:before{content:""}.ri-emphasis:before{content:""}.ri-english-input:before{content:""}.ri-equalizer-fill:before{content:""}.ri-equalizer-line:before{content:""}.ri-eraser-fill:before{content:""}.ri-eraser-line:before{content:""}.ri-error-warning-fill:before{content:""}.ri-error-warning-line:before{content:""}.ri-evernote-fill:before{content:""}.ri-evernote-line:before{content:""}.ri-exchange-box-fill:before{content:""}.ri-exchange-box-line:before{content:""}.ri-exchange-cny-fill:before{content:""}.ri-exchange-cny-line:before{content:""}.ri-exchange-dollar-fill:before{content:""}.ri-exchange-dollar-line:before{content:""}.ri-exchange-fill:before{content:""}.ri-exchange-funds-fill:before{content:""}.ri-exchange-funds-line:before{content:""}.ri-exchange-line:before{content:""}.ri-external-link-fill:before{content:""}.ri-external-link-line:before{content:""}.ri-eye-2-fill:before{content:""}.ri-eye-2-line:before{content:""}.ri-eye-close-fill:before{content:""}.ri-eye-close-line:before{content:""}.ri-eye-fill:before{content:""}.ri-eye-line:before{content:""}.ri-eye-off-fill:before{content:""}.ri-eye-off-line:before{content:""}.ri-facebook-box-fill:before{content:""}.ri-facebook-box-line:before{content:""}.ri-facebook-circle-fill:before{content:""}.ri-facebook-circle-line:before{content:""}.ri-facebook-fill:before{content:""}.ri-facebook-line:before{content:""}.ri-fahrenheit-fill:before{content:""}.ri-fahrenheit-line:before{content:""}.ri-feedback-fill:before{content:""}.ri-feedback-line:before{content:""}.ri-file-2-fill:before{content:""}.ri-file-2-line:before{content:""}.ri-file-3-fill:before{content:""}.ri-file-3-line:before{content:""}.ri-file-4-fill:before{content:""}.ri-file-4-line:before{content:""}.ri-file-add-fill:before{content:""}.ri-file-add-line:before{content:""}.ri-file-chart-2-fill:before{content:""}.ri-file-chart-2-line:before{content:""}.ri-file-chart-fill:before{content:""}.ri-file-chart-line:before{content:""}.ri-file-cloud-fill:before{content:""}.ri-file-cloud-line:before{content:""}.ri-file-code-fill:before{content:""}.ri-file-code-line:before{content:""}.ri-file-copy-2-fill:before{content:""}.ri-file-copy-2-line:before{content:""}.ri-file-copy-fill:before{content:""}.ri-file-copy-line:before{content:""}.ri-file-damage-fill:before{content:""}.ri-file-damage-line:before{content:""}.ri-file-download-fill:before{content:""}.ri-file-download-line:before{content:""}.ri-file-edit-fill:before{content:""}.ri-file-edit-line:before{content:""}.ri-file-excel-2-fill:before{content:""}.ri-file-excel-2-line:before{content:""}.ri-file-excel-fill:before{content:""}.ri-file-excel-line:before{content:""}.ri-file-fill:before{content:""}.ri-file-forbid-fill:before{content:""}.ri-file-forbid-line:before{content:""}.ri-file-gif-fill:before{content:""}.ri-file-gif-line:before{content:""}.ri-file-history-fill:before{content:""}.ri-file-history-line:before{content:""}.ri-file-hwp-fill:before{content:""}.ri-file-hwp-line:before{content:""}.ri-file-info-fill:before{content:""}.ri-file-info-line:before{content:""}.ri-file-line:before{content:""}.ri-file-list-2-fill:before{content:""}.ri-file-list-2-line:before{content:""}.ri-file-list-3-fill:before{content:""}.ri-file-list-3-line:before{content:""}.ri-file-list-fill:before{content:""}.ri-file-list-line:before{content:""}.ri-file-lock-fill:before{content:""}.ri-file-lock-line:before{content:""}.ri-file-marked-fill:before{content:""}.ri-file-marked-line:before{content:""}.ri-file-music-fill:before{content:""}.ri-file-music-line:before{content:""}.ri-file-paper-2-fill:before{content:""}.ri-file-paper-2-line:before{content:""}.ri-file-paper-fill:before{content:""}.ri-file-paper-line:before{content:""}.ri-file-pdf-fill:before{content:""}.ri-file-pdf-line:before{content:""}.ri-file-ppt-2-fill:before{content:""}.ri-file-ppt-2-line:before{content:""}.ri-file-ppt-fill:before{content:""}.ri-file-ppt-line:before{content:""}.ri-file-reduce-fill:before{content:""}.ri-file-reduce-line:before{content:""}.ri-file-search-fill:before{content:""}.ri-file-search-line:before{content:""}.ri-file-settings-fill:before{content:""}.ri-file-settings-line:before{content:""}.ri-file-shield-2-fill:before{content:""}.ri-file-shield-2-line:before{content:""}.ri-file-shield-fill:before{content:""}.ri-file-shield-line:before{content:""}.ri-file-shred-fill:before{content:""}.ri-file-shred-line:before{content:""}.ri-file-text-fill:before{content:""}.ri-file-text-line:before{content:""}.ri-file-transfer-fill:before{content:""}.ri-file-transfer-line:before{content:""}.ri-file-unknow-fill:before{content:""}.ri-file-unknow-line:before{content:""}.ri-file-upload-fill:before{content:""}.ri-file-upload-line:before{content:""}.ri-file-user-fill:before{content:""}.ri-file-user-line:before{content:""}.ri-file-warning-fill:before{content:""}.ri-file-warning-line:before{content:""}.ri-file-word-2-fill:before{content:""}.ri-file-word-2-line:before{content:""}.ri-file-word-fill:before{content:""}.ri-file-word-line:before{content:""}.ri-file-zip-fill:before{content:""}.ri-file-zip-line:before{content:""}.ri-film-fill:before{content:""}.ri-film-line:before{content:""}.ri-filter-2-fill:before{content:""}.ri-filter-2-line:before{content:""}.ri-filter-3-fill:before{content:""}.ri-filter-3-line:before{content:""}.ri-filter-fill:before{content:""}.ri-filter-line:before{content:""}.ri-filter-off-fill:before{content:""}.ri-filter-off-line:before{content:""}.ri-find-replace-fill:before{content:""}.ri-find-replace-line:before{content:""}.ri-finder-fill:before{content:""}.ri-finder-line:before{content:""}.ri-fingerprint-2-fill:before{content:""}.ri-fingerprint-2-line:before{content:""}.ri-fingerprint-fill:before{content:""}.ri-fingerprint-line:before{content:""}.ri-fire-fill:before{content:""}.ri-fire-line:before{content:""}.ri-firefox-fill:before{content:""}.ri-firefox-line:before{content:""}.ri-first-aid-kit-fill:before{content:""}.ri-first-aid-kit-line:before{content:""}.ri-flag-2-fill:before{content:""}.ri-flag-2-line:before{content:""}.ri-flag-fill:before{content:""}.ri-flag-line:before{content:""}.ri-flashlight-fill:before{content:""}.ri-flashlight-line:before{content:""}.ri-flask-fill:before{content:""}.ri-flask-line:before{content:""}.ri-flight-land-fill:before{content:""}.ri-flight-land-line:before{content:""}.ri-flight-takeoff-fill:before{content:""}.ri-flight-takeoff-line:before{content:""}.ri-flood-fill:before{content:""}.ri-flood-line:before{content:""}.ri-flow-chart:before{content:""}.ri-flutter-fill:before{content:""}.ri-flutter-line:before{content:""}.ri-focus-2-fill:before{content:""}.ri-focus-2-line:before{content:""}.ri-focus-3-fill:before{content:""}.ri-focus-3-line:before{content:""}.ri-focus-fill:before{content:""}.ri-focus-line:before{content:""}.ri-foggy-fill:before{content:""}.ri-foggy-line:before{content:""}.ri-folder-2-fill:before{content:""}.ri-folder-2-line:before{content:""}.ri-folder-3-fill:before{content:""}.ri-folder-3-line:before{content:""}.ri-folder-4-fill:before{content:""}.ri-folder-4-line:before{content:""}.ri-folder-5-fill:before{content:""}.ri-folder-5-line:before{content:""}.ri-folder-add-fill:before{content:""}.ri-folder-add-line:before{content:""}.ri-folder-chart-2-fill:before{content:""}.ri-folder-chart-2-line:before{content:""}.ri-folder-chart-fill:before{content:""}.ri-folder-chart-line:before{content:""}.ri-folder-download-fill:before{content:""}.ri-folder-download-line:before{content:""}.ri-folder-fill:before{content:""}.ri-folder-forbid-fill:before{content:""}.ri-folder-forbid-line:before{content:""}.ri-folder-history-fill:before{content:""}.ri-folder-history-line:before{content:""}.ri-folder-info-fill:before{content:""}.ri-folder-info-line:before{content:""}.ri-folder-keyhole-fill:before{content:""}.ri-folder-keyhole-line:before{content:""}.ri-folder-line:before{content:""}.ri-folder-lock-fill:before{content:""}.ri-folder-lock-line:before{content:""}.ri-folder-music-fill:before{content:""}.ri-folder-music-line:before{content:""}.ri-folder-open-fill:before{content:""}.ri-folder-open-line:before{content:""}.ri-folder-received-fill:before{content:""}.ri-folder-received-line:before{content:""}.ri-folder-reduce-fill:before{content:""}.ri-folder-reduce-line:before{content:""}.ri-folder-settings-fill:before{content:""}.ri-folder-settings-line:before{content:""}.ri-folder-shared-fill:before{content:""}.ri-folder-shared-line:before{content:""}.ri-folder-shield-2-fill:before{content:""}.ri-folder-shield-2-line:before{content:""}.ri-folder-shield-fill:before{content:""}.ri-folder-shield-line:before{content:""}.ri-folder-transfer-fill:before{content:""}.ri-folder-transfer-line:before{content:""}.ri-folder-unknow-fill:before{content:""}.ri-folder-unknow-line:before{content:""}.ri-folder-upload-fill:before{content:""}.ri-folder-upload-line:before{content:""}.ri-folder-user-fill:before{content:""}.ri-folder-user-line:before{content:""}.ri-folder-warning-fill:before{content:""}.ri-folder-warning-line:before{content:""}.ri-folder-zip-fill:before{content:""}.ri-folder-zip-line:before{content:""}.ri-folders-fill:before{content:""}.ri-folders-line:before{content:""}.ri-font-color:before{content:""}.ri-font-size-2:before{content:""}.ri-font-size:before{content:""}.ri-football-fill:before{content:""}.ri-football-line:before{content:""}.ri-footprint-fill:before{content:""}.ri-footprint-line:before{content:""}.ri-forbid-2-fill:before{content:""}.ri-forbid-2-line:before{content:""}.ri-forbid-fill:before{content:""}.ri-forbid-line:before{content:""}.ri-format-clear:before{content:""}.ri-fridge-fill:before{content:""}.ri-fridge-line:before{content:""}.ri-fullscreen-exit-fill:before{content:""}.ri-fullscreen-exit-line:before{content:""}.ri-fullscreen-fill:before{content:""}.ri-fullscreen-line:before{content:""}.ri-function-fill:before{content:""}.ri-function-line:before{content:""}.ri-functions:before{content:""}.ri-funds-box-fill:before{content:""}.ri-funds-box-line:before{content:""}.ri-funds-fill:before{content:""}.ri-funds-line:before{content:""}.ri-gallery-fill:before{content:""}.ri-gallery-line:before{content:""}.ri-gallery-upload-fill:before{content:""}.ri-gallery-upload-line:before{content:""}.ri-game-fill:before{content:""}.ri-game-line:before{content:""}.ri-gamepad-fill:before{content:""}.ri-gamepad-line:before{content:""}.ri-gas-station-fill:before{content:""}.ri-gas-station-line:before{content:""}.ri-gatsby-fill:before{content:""}.ri-gatsby-line:before{content:""}.ri-genderless-fill:before{content:""}.ri-genderless-line:before{content:""}.ri-ghost-2-fill:before{content:""}.ri-ghost-2-line:before{content:""}.ri-ghost-fill:before{content:""}.ri-ghost-line:before{content:""}.ri-ghost-smile-fill:before{content:""}.ri-ghost-smile-line:before{content:""}.ri-gift-2-fill:before{content:""}.ri-gift-2-line:before{content:""}.ri-gift-fill:before{content:""}.ri-gift-line:before{content:""}.ri-git-branch-fill:before{content:""}.ri-git-branch-line:before{content:""}.ri-git-commit-fill:before{content:""}.ri-git-commit-line:before{content:""}.ri-git-merge-fill:before{content:""}.ri-git-merge-line:before{content:""}.ri-git-pull-request-fill:before{content:""}.ri-git-pull-request-line:before{content:""}.ri-git-repository-commits-fill:before{content:""}.ri-git-repository-commits-line:before{content:""}.ri-git-repository-fill:before{content:""}.ri-git-repository-line:before{content:""}.ri-git-repository-private-fill:before{content:""}.ri-git-repository-private-line:before{content:""}.ri-github-fill:before{content:""}.ri-github-line:before{content:""}.ri-gitlab-fill:before{content:""}.ri-gitlab-line:before{content:""}.ri-global-fill:before{content:""}.ri-global-line:before{content:""}.ri-globe-fill:before{content:""}.ri-globe-line:before{content:""}.ri-goblet-fill:before{content:""}.ri-goblet-line:before{content:""}.ri-google-fill:before{content:""}.ri-google-line:before{content:""}.ri-google-play-fill:before{content:""}.ri-google-play-line:before{content:""}.ri-government-fill:before{content:""}.ri-government-line:before{content:""}.ri-gps-fill:before{content:""}.ri-gps-line:before{content:""}.ri-gradienter-fill:before{content:""}.ri-gradienter-line:before{content:""}.ri-grid-fill:before{content:""}.ri-grid-line:before{content:""}.ri-group-2-fill:before{content:""}.ri-group-2-line:before{content:""}.ri-group-fill:before{content:""}.ri-group-line:before{content:""}.ri-guide-fill:before{content:""}.ri-guide-line:before{content:""}.ri-h-1:before{content:""}.ri-h-2:before{content:""}.ri-h-3:before{content:""}.ri-h-4:before{content:""}.ri-h-5:before{content:""}.ri-h-6:before{content:""}.ri-hail-fill:before{content:""}.ri-hail-line:before{content:""}.ri-hammer-fill:before{content:""}.ri-hammer-line:before{content:""}.ri-hand-coin-fill:before{content:""}.ri-hand-coin-line:before{content:""}.ri-hand-heart-fill:before{content:""}.ri-hand-heart-line:before{content:""}.ri-hand-sanitizer-fill:before{content:""}.ri-hand-sanitizer-line:before{content:""}.ri-handbag-fill:before{content:""}.ri-handbag-line:before{content:""}.ri-hard-drive-2-fill:before{content:""}.ri-hard-drive-2-line:before{content:""}.ri-hard-drive-fill:before{content:""}.ri-hard-drive-line:before{content:""}.ri-hashtag:before{content:""}.ri-haze-2-fill:before{content:""}.ri-haze-2-line:before{content:""}.ri-haze-fill:before{content:""}.ri-haze-line:before{content:""}.ri-hd-fill:before{content:""}.ri-hd-line:before{content:""}.ri-heading:before{content:""}.ri-headphone-fill:before{content:""}.ri-headphone-line:before{content:""}.ri-health-book-fill:before{content:""}.ri-health-book-line:before{content:""}.ri-heart-2-fill:before{content:""}.ri-heart-2-line:before{content:""}.ri-heart-3-fill:before{content:""}.ri-heart-3-line:before{content:""}.ri-heart-add-fill:before{content:""}.ri-heart-add-line:before{content:""}.ri-heart-fill:before{content:""}.ri-heart-line:before{content:""}.ri-heart-pulse-fill:before{content:""}.ri-heart-pulse-line:before{content:""}.ri-hearts-fill:before{content:""}.ri-hearts-line:before{content:""}.ri-heavy-showers-fill:before{content:""}.ri-heavy-showers-line:before{content:""}.ri-history-fill:before{content:""}.ri-history-line:before{content:""}.ri-home-2-fill:before{content:""}.ri-home-2-line:before{content:""}.ri-home-3-fill:before{content:""}.ri-home-3-line:before{content:""}.ri-home-4-fill:before{content:""}.ri-home-4-line:before{content:""}.ri-home-5-fill:before{content:""}.ri-home-5-line:before{content:""}.ri-home-6-fill:before{content:""}.ri-home-6-line:before{content:""}.ri-home-7-fill:before{content:""}.ri-home-7-line:before{content:""}.ri-home-8-fill:before{content:""}.ri-home-8-line:before{content:""}.ri-home-fill:before{content:""}.ri-home-gear-fill:before{content:""}.ri-home-gear-line:before{content:""}.ri-home-heart-fill:before{content:""}.ri-home-heart-line:before{content:""}.ri-home-line:before{content:""}.ri-home-smile-2-fill:before{content:""}.ri-home-smile-2-line:before{content:""}.ri-home-smile-fill:before{content:""}.ri-home-smile-line:before{content:""}.ri-home-wifi-fill:before{content:""}.ri-home-wifi-line:before{content:""}.ri-honor-of-kings-fill:before{content:""}.ri-honor-of-kings-line:before{content:""}.ri-honour-fill:before{content:""}.ri-honour-line:before{content:""}.ri-hospital-fill:before{content:""}.ri-hospital-line:before{content:""}.ri-hotel-bed-fill:before{content:""}.ri-hotel-bed-line:before{content:""}.ri-hotel-fill:before{content:""}.ri-hotel-line:before{content:""}.ri-hotspot-fill:before{content:""}.ri-hotspot-line:before{content:""}.ri-hq-fill:before{content:""}.ri-hq-line:before{content:""}.ri-html5-fill:before{content:""}.ri-html5-line:before{content:""}.ri-ie-fill:before{content:""}.ri-ie-line:before{content:""}.ri-image-2-fill:before{content:""}.ri-image-2-line:before{content:""}.ri-image-add-fill:before{content:""}.ri-image-add-line:before{content:""}.ri-image-edit-fill:before{content:""}.ri-image-edit-line:before{content:""}.ri-image-fill:before{content:""}.ri-image-line:before{content:""}.ri-inbox-archive-fill:before{content:""}.ri-inbox-archive-line:before{content:""}.ri-inbox-fill:before{content:""}.ri-inbox-line:before{content:""}.ri-inbox-unarchive-fill:before{content:""}.ri-inbox-unarchive-line:before{content:""}.ri-increase-decrease-fill:before{content:""}.ri-increase-decrease-line:before{content:""}.ri-indent-decrease:before{content:""}.ri-indent-increase:before{content:""}.ri-indeterminate-circle-fill:before{content:""}.ri-indeterminate-circle-line:before{content:""}.ri-information-fill:before{content:""}.ri-information-line:before{content:""}.ri-infrared-thermometer-fill:before{content:""}.ri-infrared-thermometer-line:before{content:""}.ri-ink-bottle-fill:before{content:""}.ri-ink-bottle-line:before{content:""}.ri-input-cursor-move:before{content:""}.ri-input-method-fill:before{content:""}.ri-input-method-line:before{content:""}.ri-insert-column-left:before{content:""}.ri-insert-column-right:before{content:""}.ri-insert-row-bottom:before{content:""}.ri-insert-row-top:before{content:""}.ri-instagram-fill:before{content:""}.ri-instagram-line:before{content:""}.ri-install-fill:before{content:""}.ri-install-line:before{content:""}.ri-invision-fill:before{content:""}.ri-invision-line:before{content:""}.ri-italic:before{content:""}.ri-kakao-talk-fill:before{content:""}.ri-kakao-talk-line:before{content:""}.ri-key-2-fill:before{content:""}.ri-key-2-line:before{content:""}.ri-key-fill:before{content:""}.ri-key-line:before{content:""}.ri-keyboard-box-fill:before{content:""}.ri-keyboard-box-line:before{content:""}.ri-keyboard-fill:before{content:""}.ri-keyboard-line:before{content:""}.ri-keynote-fill:before{content:""}.ri-keynote-line:before{content:""}.ri-knife-blood-fill:before{content:""}.ri-knife-blood-line:before{content:""}.ri-knife-fill:before{content:""}.ri-knife-line:before{content:""}.ri-landscape-fill:before{content:""}.ri-landscape-line:before{content:""}.ri-layout-2-fill:before{content:""}.ri-layout-2-line:before{content:""}.ri-layout-3-fill:before{content:""}.ri-layout-3-line:before{content:""}.ri-layout-4-fill:before{content:""}.ri-layout-4-line:before{content:""}.ri-layout-5-fill:before{content:""}.ri-layout-5-line:before{content:""}.ri-layout-6-fill:before{content:""}.ri-layout-6-line:before{content:""}.ri-layout-bottom-2-fill:before{content:""}.ri-layout-bottom-2-line:before{content:""}.ri-layout-bottom-fill:before{content:""}.ri-layout-bottom-line:before{content:""}.ri-layout-column-fill:before{content:""}.ri-layout-column-line:before{content:""}.ri-layout-fill:before{content:""}.ri-layout-grid-fill:before{content:""}.ri-layout-grid-line:before{content:""}.ri-layout-left-2-fill:before{content:""}.ri-layout-left-2-line:before{content:""}.ri-layout-left-fill:before{content:""}.ri-layout-left-line:before{content:""}.ri-layout-line:before{content:""}.ri-layout-masonry-fill:before{content:""}.ri-layout-masonry-line:before{content:""}.ri-layout-right-2-fill:before{content:""}.ri-layout-right-2-line:before{content:""}.ri-layout-right-fill:before{content:""}.ri-layout-right-line:before{content:""}.ri-layout-row-fill:before{content:""}.ri-layout-row-line:before{content:""}.ri-layout-top-2-fill:before{content:""}.ri-layout-top-2-line:before{content:""}.ri-layout-top-fill:before{content:""}.ri-layout-top-line:before{content:""}.ri-leaf-fill:before{content:""}.ri-leaf-line:before{content:""}.ri-lifebuoy-fill:before{content:""}.ri-lifebuoy-line:before{content:""}.ri-lightbulb-fill:before{content:""}.ri-lightbulb-flash-fill:before{content:""}.ri-lightbulb-flash-line:before{content:""}.ri-lightbulb-line:before{content:""}.ri-line-chart-fill:before{content:""}.ri-line-chart-line:before{content:""}.ri-line-fill:before{content:""}.ri-line-height:before{content:""}.ri-line-line:before{content:""}.ri-link-m:before{content:""}.ri-link-unlink-m:before{content:""}.ri-link-unlink:before{content:""}.ri-link:before{content:""}.ri-linkedin-box-fill:before{content:""}.ri-linkedin-box-line:before{content:""}.ri-linkedin-fill:before{content:""}.ri-linkedin-line:before{content:""}.ri-links-fill:before{content:""}.ri-links-line:before{content:""}.ri-list-check-2:before{content:""}.ri-list-check:before{content:""}.ri-list-ordered:before{content:""}.ri-list-settings-fill:before{content:""}.ri-list-settings-line:before{content:""}.ri-list-unordered:before{content:""}.ri-live-fill:before{content:""}.ri-live-line:before{content:""}.ri-loader-2-fill:before{content:""}.ri-loader-2-line:before{content:""}.ri-loader-3-fill:before{content:""}.ri-loader-3-line:before{content:""}.ri-loader-4-fill:before{content:""}.ri-loader-4-line:before{content:""}.ri-loader-5-fill:before{content:""}.ri-loader-5-line:before{content:""}.ri-loader-fill:before{content:""}.ri-loader-line:before{content:""}.ri-lock-2-fill:before{content:""}.ri-lock-2-line:before{content:""}.ri-lock-fill:before{content:""}.ri-lock-line:before{content:""}.ri-lock-password-fill:before{content:""}.ri-lock-password-line:before{content:""}.ri-lock-unlock-fill:before{content:""}.ri-lock-unlock-line:before{content:""}.ri-login-box-fill:before{content:""}.ri-login-box-line:before{content:""}.ri-login-circle-fill:before{content:""}.ri-login-circle-line:before{content:""}.ri-logout-box-fill:before{content:""}.ri-logout-box-line:before{content:""}.ri-logout-box-r-fill:before{content:""}.ri-logout-box-r-line:before{content:""}.ri-logout-circle-fill:before{content:""}.ri-logout-circle-line:before{content:""}.ri-logout-circle-r-fill:before{content:""}.ri-logout-circle-r-line:before{content:""}.ri-luggage-cart-fill:before{content:""}.ri-luggage-cart-line:before{content:""}.ri-luggage-deposit-fill:before{content:""}.ri-luggage-deposit-line:before{content:""}.ri-lungs-fill:before{content:""}.ri-lungs-line:before{content:""}.ri-mac-fill:before{content:""}.ri-mac-line:before{content:""}.ri-macbook-fill:before{content:""}.ri-macbook-line:before{content:""}.ri-magic-fill:before{content:""}.ri-magic-line:before{content:""}.ri-mail-add-fill:before{content:""}.ri-mail-add-line:before{content:""}.ri-mail-check-fill:before{content:""}.ri-mail-check-line:before{content:""}.ri-mail-close-fill:before{content:""}.ri-mail-close-line:before{content:""}.ri-mail-download-fill:before{content:""}.ri-mail-download-line:before{content:""}.ri-mail-fill:before{content:""}.ri-mail-forbid-fill:before{content:""}.ri-mail-forbid-line:before{content:""}.ri-mail-line:before{content:""}.ri-mail-lock-fill:before{content:""}.ri-mail-lock-line:before{content:""}.ri-mail-open-fill:before{content:""}.ri-mail-open-line:before{content:""}.ri-mail-send-fill:before{content:""}.ri-mail-send-line:before{content:""}.ri-mail-settings-fill:before{content:""}.ri-mail-settings-line:before{content:""}.ri-mail-star-fill:before{content:""}.ri-mail-star-line:before{content:""}.ri-mail-unread-fill:before{content:""}.ri-mail-unread-line:before{content:""}.ri-mail-volume-fill:before{content:""}.ri-mail-volume-line:before{content:""}.ri-map-2-fill:before{content:""}.ri-map-2-line:before{content:""}.ri-map-fill:before{content:""}.ri-map-line:before{content:""}.ri-map-pin-2-fill:before{content:""}.ri-map-pin-2-line:before{content:""}.ri-map-pin-3-fill:before{content:""}.ri-map-pin-3-line:before{content:""}.ri-map-pin-4-fill:before{content:""}.ri-map-pin-4-line:before{content:""}.ri-map-pin-5-fill:before{content:""}.ri-map-pin-5-line:before{content:""}.ri-map-pin-add-fill:before{content:""}.ri-map-pin-add-line:before{content:""}.ri-map-pin-fill:before{content:""}.ri-map-pin-line:before{content:""}.ri-map-pin-range-fill:before{content:""}.ri-map-pin-range-line:before{content:""}.ri-map-pin-time-fill:before{content:""}.ri-map-pin-time-line:before{content:""}.ri-map-pin-user-fill:before{content:""}.ri-map-pin-user-line:before{content:""}.ri-mark-pen-fill:before{content:""}.ri-mark-pen-line:before{content:""}.ri-markdown-fill:before{content:""}.ri-markdown-line:before{content:""}.ri-markup-fill:before{content:""}.ri-markup-line:before{content:""}.ri-mastercard-fill:before{content:""}.ri-mastercard-line:before{content:""}.ri-mastodon-fill:before{content:""}.ri-mastodon-line:before{content:""}.ri-medal-2-fill:before{content:""}.ri-medal-2-line:before{content:""}.ri-medal-fill:before{content:""}.ri-medal-line:before{content:""}.ri-medicine-bottle-fill:before{content:""}.ri-medicine-bottle-line:before{content:""}.ri-medium-fill:before{content:""}.ri-medium-line:before{content:""}.ri-men-fill:before{content:""}.ri-men-line:before{content:""}.ri-mental-health-fill:before{content:""}.ri-mental-health-line:before{content:""}.ri-menu-2-fill:before{content:""}.ri-menu-2-line:before{content:""}.ri-menu-3-fill:before{content:""}.ri-menu-3-line:before{content:""}.ri-menu-4-fill:before{content:""}.ri-menu-4-line:before{content:""}.ri-menu-5-fill:before{content:""}.ri-menu-5-line:before{content:""}.ri-menu-add-fill:before{content:""}.ri-menu-add-line:before{content:""}.ri-menu-fill:before{content:""}.ri-menu-fold-fill:before{content:""}.ri-menu-fold-line:before{content:""}.ri-menu-line:before{content:""}.ri-menu-unfold-fill:before{content:""}.ri-menu-unfold-line:before{content:""}.ri-merge-cells-horizontal:before{content:""}.ri-merge-cells-vertical:before{content:""}.ri-message-2-fill:before{content:""}.ri-message-2-line:before{content:""}.ri-message-3-fill:before{content:""}.ri-message-3-line:before{content:""}.ri-message-fill:before{content:""}.ri-message-line:before{content:""}.ri-messenger-fill:before{content:""}.ri-messenger-line:before{content:""}.ri-meteor-fill:before{content:""}.ri-meteor-line:before{content:""}.ri-mic-2-fill:before{content:""}.ri-mic-2-line:before{content:""}.ri-mic-fill:before{content:""}.ri-mic-line:before{content:""}.ri-mic-off-fill:before{content:""}.ri-mic-off-line:before{content:""}.ri-mickey-fill:before{content:""}.ri-mickey-line:before{content:""}.ri-microscope-fill:before{content:""}.ri-microscope-line:before{content:""}.ri-microsoft-fill:before{content:""}.ri-microsoft-line:before{content:""}.ri-mind-map:before{content:""}.ri-mini-program-fill:before{content:""}.ri-mini-program-line:before{content:""}.ri-mist-fill:before{content:""}.ri-mist-line:before{content:""}.ri-money-cny-box-fill:before{content:""}.ri-money-cny-box-line:before{content:""}.ri-money-cny-circle-fill:before{content:""}.ri-money-cny-circle-line:before{content:""}.ri-money-dollar-box-fill:before{content:""}.ri-money-dollar-box-line:before{content:""}.ri-money-dollar-circle-fill:before{content:""}.ri-money-dollar-circle-line:before{content:""}.ri-money-euro-box-fill:before{content:""}.ri-money-euro-box-line:before{content:""}.ri-money-euro-circle-fill:before{content:""}.ri-money-euro-circle-line:before{content:""}.ri-money-pound-box-fill:before{content:""}.ri-money-pound-box-line:before{content:""}.ri-money-pound-circle-fill:before{content:""}.ri-money-pound-circle-line:before{content:""}.ri-moon-clear-fill:before{content:""}.ri-moon-clear-line:before{content:""}.ri-moon-cloudy-fill:before{content:""}.ri-moon-cloudy-line:before{content:""}.ri-moon-fill:before{content:""}.ri-moon-foggy-fill:before{content:""}.ri-moon-foggy-line:before{content:""}.ri-moon-line:before{content:""}.ri-more-2-fill:before{content:""}.ri-more-2-line:before{content:""}.ri-more-fill:before{content:""}.ri-more-line:before{content:""}.ri-motorbike-fill:before{content:""}.ri-motorbike-line:before{content:""}.ri-mouse-fill:before{content:""}.ri-mouse-line:before{content:""}.ri-movie-2-fill:before{content:""}.ri-movie-2-line:before{content:""}.ri-movie-fill:before{content:""}.ri-movie-line:before{content:""}.ri-music-2-fill:before{content:""}.ri-music-2-line:before{content:""}.ri-music-fill:before{content:""}.ri-music-line:before{content:""}.ri-mv-fill:before{content:""}.ri-mv-line:before{content:""}.ri-navigation-fill:before{content:""}.ri-navigation-line:before{content:""}.ri-netease-cloud-music-fill:before{content:""}.ri-netease-cloud-music-line:before{content:""}.ri-netflix-fill:before{content:""}.ri-netflix-line:before{content:""}.ri-newspaper-fill:before{content:""}.ri-newspaper-line:before{content:""}.ri-node-tree:before{content:""}.ri-notification-2-fill:before{content:""}.ri-notification-2-line:before{content:""}.ri-notification-3-fill:before{content:""}.ri-notification-3-line:before{content:""}.ri-notification-4-fill:before{content:""}.ri-notification-4-line:before{content:""}.ri-notification-badge-fill:before{content:""}.ri-notification-badge-line:before{content:""}.ri-notification-fill:before{content:""}.ri-notification-line:before{content:""}.ri-notification-off-fill:before{content:""}.ri-notification-off-line:before{content:""}.ri-npmjs-fill:before{content:""}.ri-npmjs-line:before{content:""}.ri-number-0:before{content:""}.ri-number-1:before{content:""}.ri-number-2:before{content:""}.ri-number-3:before{content:""}.ri-number-4:before{content:""}.ri-number-5:before{content:""}.ri-number-6:before{content:""}.ri-number-7:before{content:""}.ri-number-8:before{content:""}.ri-number-9:before{content:""}.ri-numbers-fill:before{content:""}.ri-numbers-line:before{content:""}.ri-nurse-fill:before{content:""}.ri-nurse-line:before{content:""}.ri-oil-fill:before{content:""}.ri-oil-line:before{content:""}.ri-omega:before{content:""}.ri-open-arm-fill:before{content:""}.ri-open-arm-line:before{content:""}.ri-open-source-fill:before{content:""}.ri-open-source-line:before{content:""}.ri-opera-fill:before{content:""}.ri-opera-line:before{content:""}.ri-order-play-fill:before{content:""}.ri-order-play-line:before{content:""}.ri-organization-chart:before{content:""}.ri-outlet-2-fill:before{content:""}.ri-outlet-2-line:before{content:""}.ri-outlet-fill:before{content:""}.ri-outlet-line:before{content:""}.ri-page-separator:before{content:""}.ri-pages-fill:before{content:""}.ri-pages-line:before{content:""}.ri-paint-brush-fill:before{content:""}.ri-paint-brush-line:before{content:""}.ri-paint-fill:before{content:""}.ri-paint-line:before{content:""}.ri-palette-fill:before{content:""}.ri-palette-line:before{content:""}.ri-pantone-fill:before{content:""}.ri-pantone-line:before{content:""}.ri-paragraph:before{content:""}.ri-parent-fill:before{content:""}.ri-parent-line:before{content:""}.ri-parentheses-fill:before{content:""}.ri-parentheses-line:before{content:""}.ri-parking-box-fill:before{content:""}.ri-parking-box-line:before{content:""}.ri-parking-fill:before{content:""}.ri-parking-line:before{content:""}.ri-passport-fill:before{content:""}.ri-passport-line:before{content:""}.ri-patreon-fill:before{content:""}.ri-patreon-line:before{content:""}.ri-pause-circle-fill:before{content:""}.ri-pause-circle-line:before{content:""}.ri-pause-fill:before{content:""}.ri-pause-line:before{content:""}.ri-pause-mini-fill:before{content:""}.ri-pause-mini-line:before{content:""}.ri-paypal-fill:before{content:""}.ri-paypal-line:before{content:""}.ri-pen-nib-fill:before{content:""}.ri-pen-nib-line:before{content:""}.ri-pencil-fill:before{content:""}.ri-pencil-line:before{content:""}.ri-pencil-ruler-2-fill:before{content:""}.ri-pencil-ruler-2-line:before{content:""}.ri-pencil-ruler-fill:before{content:""}.ri-pencil-ruler-line:before{content:""}.ri-percent-fill:before{content:""}.ri-percent-line:before{content:""}.ri-phone-camera-fill:before{content:""}.ri-phone-camera-line:before{content:""}.ri-phone-fill:before{content:""}.ri-phone-find-fill:before{content:""}.ri-phone-find-line:before{content:""}.ri-phone-line:before{content:""}.ri-phone-lock-fill:before{content:""}.ri-phone-lock-line:before{content:""}.ri-picture-in-picture-2-fill:before{content:""}.ri-picture-in-picture-2-line:before{content:""}.ri-picture-in-picture-exit-fill:before{content:""}.ri-picture-in-picture-exit-line:before{content:""}.ri-picture-in-picture-fill:before{content:""}.ri-picture-in-picture-line:before{content:""}.ri-pie-chart-2-fill:before{content:""}.ri-pie-chart-2-line:before{content:""}.ri-pie-chart-box-fill:before{content:""}.ri-pie-chart-box-line:before{content:""}.ri-pie-chart-fill:before{content:""}.ri-pie-chart-line:before{content:""}.ri-pin-distance-fill:before{content:""}.ri-pin-distance-line:before{content:""}.ri-ping-pong-fill:before{content:""}.ri-ping-pong-line:before{content:""}.ri-pinterest-fill:before{content:""}.ri-pinterest-line:before{content:""}.ri-pinyin-input:before{content:""}.ri-pixelfed-fill:before{content:""}.ri-pixelfed-line:before{content:""}.ri-plane-fill:before{content:""}.ri-plane-line:before{content:""}.ri-plant-fill:before{content:""}.ri-plant-line:before{content:""}.ri-play-circle-fill:before{content:""}.ri-play-circle-line:before{content:""}.ri-play-fill:before{content:""}.ri-play-line:before{content:""}.ri-play-list-2-fill:before{content:""}.ri-play-list-2-line:before{content:""}.ri-play-list-add-fill:before{content:""}.ri-play-list-add-line:before{content:""}.ri-play-list-fill:before{content:""}.ri-play-list-line:before{content:""}.ri-play-mini-fill:before{content:""}.ri-play-mini-line:before{content:""}.ri-playstation-fill:before{content:""}.ri-playstation-line:before{content:""}.ri-plug-2-fill:before{content:""}.ri-plug-2-line:before{content:""}.ri-plug-fill:before{content:""}.ri-plug-line:before{content:""}.ri-polaroid-2-fill:before{content:""}.ri-polaroid-2-line:before{content:""}.ri-polaroid-fill:before{content:""}.ri-polaroid-line:before{content:""}.ri-police-car-fill:before{content:""}.ri-police-car-line:before{content:""}.ri-price-tag-2-fill:before{content:""}.ri-price-tag-2-line:before{content:""}.ri-price-tag-3-fill:before{content:""}.ri-price-tag-3-line:before{content:""}.ri-price-tag-fill:before{content:""}.ri-price-tag-line:before{content:""}.ri-printer-cloud-fill:before{content:""}.ri-printer-cloud-line:before{content:""}.ri-printer-fill:before{content:""}.ri-printer-line:before{content:""}.ri-product-hunt-fill:before{content:""}.ri-product-hunt-line:before{content:""}.ri-profile-fill:before{content:""}.ri-profile-line:before{content:""}.ri-projector-2-fill:before{content:""}.ri-projector-2-line:before{content:""}.ri-projector-fill:before{content:""}.ri-projector-line:before{content:""}.ri-psychotherapy-fill:before{content:""}.ri-psychotherapy-line:before{content:""}.ri-pulse-fill:before{content:""}.ri-pulse-line:before{content:""}.ri-pushpin-2-fill:before{content:""}.ri-pushpin-2-line:before{content:""}.ri-pushpin-fill:before{content:""}.ri-pushpin-line:before{content:""}.ri-qq-fill:before{content:""}.ri-qq-line:before{content:""}.ri-qr-code-fill:before{content:""}.ri-qr-code-line:before{content:""}.ri-qr-scan-2-fill:before{content:""}.ri-qr-scan-2-line:before{content:""}.ri-qr-scan-fill:before{content:""}.ri-qr-scan-line:before{content:""}.ri-question-answer-fill:before{content:""}.ri-question-answer-line:before{content:""}.ri-question-fill:before{content:""}.ri-question-line:before{content:""}.ri-question-mark:before{content:""}.ri-questionnaire-fill:before{content:""}.ri-questionnaire-line:before{content:""}.ri-quill-pen-fill:before{content:""}.ri-quill-pen-line:before{content:""}.ri-radar-fill:before{content:""}.ri-radar-line:before{content:""}.ri-radio-2-fill:before{content:""}.ri-radio-2-line:before{content:""}.ri-radio-button-fill:before{content:""}.ri-radio-button-line:before{content:""}.ri-radio-fill:before{content:""}.ri-radio-line:before{content:""}.ri-rainbow-fill:before{content:""}.ri-rainbow-line:before{content:""}.ri-rainy-fill:before{content:""}.ri-rainy-line:before{content:""}.ri-reactjs-fill:before{content:""}.ri-reactjs-line:before{content:""}.ri-record-circle-fill:before{content:""}.ri-record-circle-line:before{content:""}.ri-record-mail-fill:before{content:""}.ri-record-mail-line:before{content:""}.ri-recycle-fill:before{content:""}.ri-recycle-line:before{content:""}.ri-red-packet-fill:before{content:""}.ri-red-packet-line:before{content:""}.ri-reddit-fill:before{content:""}.ri-reddit-line:before{content:""}.ri-refresh-fill:before{content:""}.ri-refresh-line:before{content:""}.ri-refund-2-fill:before{content:""}.ri-refund-2-line:before{content:""}.ri-refund-fill:before{content:""}.ri-refund-line:before{content:""}.ri-registered-fill:before{content:""}.ri-registered-line:before{content:""}.ri-remixicon-fill:before{content:""}.ri-remixicon-line:before{content:""}.ri-remote-control-2-fill:before{content:""}.ri-remote-control-2-line:before{content:""}.ri-remote-control-fill:before{content:""}.ri-remote-control-line:before{content:""}.ri-repeat-2-fill:before{content:""}.ri-repeat-2-line:before{content:""}.ri-repeat-fill:before{content:""}.ri-repeat-line:before{content:""}.ri-repeat-one-fill:before{content:""}.ri-repeat-one-line:before{content:""}.ri-reply-all-fill:before{content:""}.ri-reply-all-line:before{content:""}.ri-reply-fill:before{content:""}.ri-reply-line:before{content:""}.ri-reserved-fill:before{content:""}.ri-reserved-line:before{content:""}.ri-rest-time-fill:before{content:""}.ri-rest-time-line:before{content:""}.ri-restart-fill:before{content:""}.ri-restart-line:before{content:""}.ri-restaurant-2-fill:before{content:""}.ri-restaurant-2-line:before{content:""}.ri-restaurant-fill:before{content:""}.ri-restaurant-line:before{content:""}.ri-rewind-fill:before{content:""}.ri-rewind-line:before{content:""}.ri-rewind-mini-fill:before{content:""}.ri-rewind-mini-line:before{content:""}.ri-rhythm-fill:before{content:""}.ri-rhythm-line:before{content:""}.ri-riding-fill:before{content:""}.ri-riding-line:before{content:""}.ri-road-map-fill:before{content:""}.ri-road-map-line:before{content:""}.ri-roadster-fill:before{content:""}.ri-roadster-line:before{content:""}.ri-robot-fill:before{content:""}.ri-robot-line:before{content:""}.ri-rocket-2-fill:before{content:""}.ri-rocket-2-line:before{content:""}.ri-rocket-fill:before{content:""}.ri-rocket-line:before{content:""}.ri-rotate-lock-fill:before{content:""}.ri-rotate-lock-line:before{content:""}.ri-rounded-corner:before{content:""}.ri-route-fill:before{content:""}.ri-route-line:before{content:""}.ri-router-fill:before{content:""}.ri-router-line:before{content:""}.ri-rss-fill:before{content:""}.ri-rss-line:before{content:""}.ri-ruler-2-fill:before{content:""}.ri-ruler-2-line:before{content:""}.ri-ruler-fill:before{content:""}.ri-ruler-line:before{content:""}.ri-run-fill:before{content:""}.ri-run-line:before{content:""}.ri-safari-fill:before{content:""}.ri-safari-line:before{content:""}.ri-safe-2-fill:before{content:""}.ri-safe-2-line:before{content:""}.ri-safe-fill:before{content:""}.ri-safe-line:before{content:""}.ri-sailboat-fill:before{content:""}.ri-sailboat-line:before{content:""}.ri-save-2-fill:before{content:""}.ri-save-2-line:before{content:""}.ri-save-3-fill:before{content:""}.ri-save-3-line:before{content:""}.ri-save-fill:before{content:""}.ri-save-line:before{content:""}.ri-scales-2-fill:before{content:""}.ri-scales-2-line:before{content:""}.ri-scales-3-fill:before{content:""}.ri-scales-3-line:before{content:""}.ri-scales-fill:before{content:""}.ri-scales-line:before{content:""}.ri-scan-2-fill:before{content:""}.ri-scan-2-line:before{content:""}.ri-scan-fill:before{content:""}.ri-scan-line:before{content:""}.ri-scissors-2-fill:before{content:""}.ri-scissors-2-line:before{content:""}.ri-scissors-cut-fill:before{content:""}.ri-scissors-cut-line:before{content:""}.ri-scissors-fill:before{content:""}.ri-scissors-line:before{content:""}.ri-screenshot-2-fill:before{content:""}.ri-screenshot-2-line:before{content:""}.ri-screenshot-fill:before{content:""}.ri-screenshot-line:before{content:""}.ri-sd-card-fill:before{content:""}.ri-sd-card-line:before{content:""}.ri-sd-card-mini-fill:before{content:""}.ri-sd-card-mini-line:before{content:""}.ri-search-2-fill:before{content:""}.ri-search-2-line:before{content:""}.ri-search-eye-fill:before{content:""}.ri-search-eye-line:before{content:""}.ri-search-fill:before{content:""}.ri-search-line:before{content:""}.ri-secure-payment-fill:before{content:""}.ri-secure-payment-line:before{content:""}.ri-seedling-fill:before{content:""}.ri-seedling-line:before{content:""}.ri-send-backward:before{content:""}.ri-send-plane-2-fill:before{content:""}.ri-send-plane-2-line:before{content:""}.ri-send-plane-fill:before{content:""}.ri-send-plane-line:before{content:""}.ri-send-to-back:before{content:""}.ri-sensor-fill:before{content:""}.ri-sensor-line:before{content:""}.ri-separator:before{content:""}.ri-server-fill:before{content:""}.ri-server-line:before{content:""}.ri-service-fill:before{content:""}.ri-service-line:before{content:""}.ri-settings-2-fill:before{content:""}.ri-settings-2-line:before{content:""}.ri-settings-3-fill:before{content:""}.ri-settings-3-line:before{content:""}.ri-settings-4-fill:before{content:""}.ri-settings-4-line:before{content:""}.ri-settings-5-fill:before{content:""}.ri-settings-5-line:before{content:""}.ri-settings-6-fill:before{content:""}.ri-settings-6-line:before{content:""}.ri-settings-fill:before{content:""}.ri-settings-line:before{content:""}.ri-shape-2-fill:before{content:""}.ri-shape-2-line:before{content:""}.ri-shape-fill:before{content:""}.ri-shape-line:before{content:""}.ri-share-box-fill:before{content:""}.ri-share-box-line:before{content:""}.ri-share-circle-fill:before{content:""}.ri-share-circle-line:before{content:""}.ri-share-fill:before{content:""}.ri-share-forward-2-fill:before{content:""}.ri-share-forward-2-line:before{content:""}.ri-share-forward-box-fill:before{content:""}.ri-share-forward-box-line:before{content:""}.ri-share-forward-fill:before{content:""}.ri-share-forward-line:before{content:""}.ri-share-line:before{content:""}.ri-shield-check-fill:before{content:""}.ri-shield-check-line:before{content:""}.ri-shield-cross-fill:before{content:""}.ri-shield-cross-line:before{content:""}.ri-shield-fill:before{content:""}.ri-shield-flash-fill:before{content:""}.ri-shield-flash-line:before{content:""}.ri-shield-keyhole-fill:before{content:""}.ri-shield-keyhole-line:before{content:""}.ri-shield-line:before{content:""}.ri-shield-star-fill:before{content:""}.ri-shield-star-line:before{content:""}.ri-shield-user-fill:before{content:""}.ri-shield-user-line:before{content:""}.ri-ship-2-fill:before{content:""}.ri-ship-2-line:before{content:""}.ri-ship-fill:before{content:""}.ri-ship-line:before{content:""}.ri-shirt-fill:before{content:""}.ri-shirt-line:before{content:""}.ri-shopping-bag-2-fill:before{content:""}.ri-shopping-bag-2-line:before{content:""}.ri-shopping-bag-3-fill:before{content:""}.ri-shopping-bag-3-line:before{content:""}.ri-shopping-bag-fill:before{content:""}.ri-shopping-bag-line:before{content:""}.ri-shopping-basket-2-fill:before{content:""}.ri-shopping-basket-2-line:before{content:""}.ri-shopping-basket-fill:before{content:""}.ri-shopping-basket-line:before{content:""}.ri-shopping-cart-2-fill:before{content:""}.ri-shopping-cart-2-line:before{content:""}.ri-shopping-cart-fill:before{content:""}.ri-shopping-cart-line:before{content:""}.ri-showers-fill:before{content:""}.ri-showers-line:before{content:""}.ri-shuffle-fill:before{content:""}.ri-shuffle-line:before{content:""}.ri-shut-down-fill:before{content:""}.ri-shut-down-line:before{content:""}.ri-side-bar-fill:before{content:""}.ri-side-bar-line:before{content:""}.ri-signal-tower-fill:before{content:""}.ri-signal-tower-line:before{content:""}.ri-signal-wifi-1-fill:before{content:""}.ri-signal-wifi-1-line:before{content:""}.ri-signal-wifi-2-fill:before{content:""}.ri-signal-wifi-2-line:before{content:""}.ri-signal-wifi-3-fill:before{content:""}.ri-signal-wifi-3-line:before{content:""}.ri-signal-wifi-error-fill:before{content:""}.ri-signal-wifi-error-line:before{content:""}.ri-signal-wifi-fill:before{content:""}.ri-signal-wifi-line:before{content:""}.ri-signal-wifi-off-fill:before{content:""}.ri-signal-wifi-off-line:before{content:""}.ri-sim-card-2-fill:before{content:""}.ri-sim-card-2-line:before{content:""}.ri-sim-card-fill:before{content:""}.ri-sim-card-line:before{content:""}.ri-single-quotes-l:before{content:""}.ri-single-quotes-r:before{content:""}.ri-sip-fill:before{content:""}.ri-sip-line:before{content:""}.ri-skip-back-fill:before{content:""}.ri-skip-back-line:before{content:""}.ri-skip-back-mini-fill:before{content:""}.ri-skip-back-mini-line:before{content:""}.ri-skip-forward-fill:before{content:""}.ri-skip-forward-line:before{content:""}.ri-skip-forward-mini-fill:before{content:""}.ri-skip-forward-mini-line:before{content:""}.ri-skull-2-fill:before{content:""}.ri-skull-2-line:before{content:""}.ri-skull-fill:before{content:""}.ri-skull-line:before{content:""}.ri-skype-fill:before{content:""}.ri-skype-line:before{content:""}.ri-slack-fill:before{content:""}.ri-slack-line:before{content:""}.ri-slice-fill:before{content:""}.ri-slice-line:before{content:""}.ri-slideshow-2-fill:before{content:""}.ri-slideshow-2-line:before{content:""}.ri-slideshow-3-fill:before{content:""}.ri-slideshow-3-line:before{content:""}.ri-slideshow-4-fill:before{content:""}.ri-slideshow-4-line:before{content:""}.ri-slideshow-fill:before{content:""}.ri-slideshow-line:before{content:""}.ri-smartphone-fill:before{content:""}.ri-smartphone-line:before{content:""}.ri-snapchat-fill:before{content:""}.ri-snapchat-line:before{content:""}.ri-snowy-fill:before{content:""}.ri-snowy-line:before{content:""}.ri-sort-asc:before{content:""}.ri-sort-desc:before{content:""}.ri-sound-module-fill:before{content:""}.ri-sound-module-line:before{content:""}.ri-soundcloud-fill:before{content:""}.ri-soundcloud-line:before{content:""}.ri-space-ship-fill:before{content:""}.ri-space-ship-line:before{content:""}.ri-space:before{content:""}.ri-spam-2-fill:before{content:""}.ri-spam-2-line:before{content:""}.ri-spam-3-fill:before{content:""}.ri-spam-3-line:before{content:""}.ri-spam-fill:before{content:""}.ri-spam-line:before{content:""}.ri-speaker-2-fill:before{content:""}.ri-speaker-2-line:before{content:""}.ri-speaker-3-fill:before{content:""}.ri-speaker-3-line:before{content:""}.ri-speaker-fill:before{content:""}.ri-speaker-line:before{content:""}.ri-spectrum-fill:before{content:""}.ri-spectrum-line:before{content:""}.ri-speed-fill:before{content:""}.ri-speed-line:before{content:""}.ri-speed-mini-fill:before{content:""}.ri-speed-mini-line:before{content:""}.ri-split-cells-horizontal:before{content:""}.ri-split-cells-vertical:before{content:""}.ri-spotify-fill:before{content:""}.ri-spotify-line:before{content:""}.ri-spy-fill:before{content:""}.ri-spy-line:before{content:""}.ri-stack-fill:before{content:""}.ri-stack-line:before{content:""}.ri-stack-overflow-fill:before{content:""}.ri-stack-overflow-line:before{content:""}.ri-stackshare-fill:before{content:""}.ri-stackshare-line:before{content:""}.ri-star-fill:before{content:""}.ri-star-half-fill:before{content:""}.ri-star-half-line:before{content:""}.ri-star-half-s-fill:before{content:""}.ri-star-half-s-line:before{content:""}.ri-star-line:before{content:""}.ri-star-s-fill:before{content:""}.ri-star-s-line:before{content:""}.ri-star-smile-fill:before{content:""}.ri-star-smile-line:before{content:""}.ri-steam-fill:before{content:""}.ri-steam-line:before{content:""}.ri-steering-2-fill:before{content:""}.ri-steering-2-line:before{content:""}.ri-steering-fill:before{content:""}.ri-steering-line:before{content:""}.ri-stethoscope-fill:before{content:""}.ri-stethoscope-line:before{content:""}.ri-sticky-note-2-fill:before{content:""}.ri-sticky-note-2-line:before{content:""}.ri-sticky-note-fill:before{content:""}.ri-sticky-note-line:before{content:""}.ri-stock-fill:before{content:""}.ri-stock-line:before{content:""}.ri-stop-circle-fill:before{content:""}.ri-stop-circle-line:before{content:""}.ri-stop-fill:before{content:""}.ri-stop-line:before{content:""}.ri-stop-mini-fill:before{content:""}.ri-stop-mini-line:before{content:""}.ri-store-2-fill:before{content:""}.ri-store-2-line:before{content:""}.ri-store-3-fill:before{content:""}.ri-store-3-line:before{content:""}.ri-store-fill:before{content:""}.ri-store-line:before{content:""}.ri-strikethrough-2:before{content:""}.ri-strikethrough:before{content:""}.ri-subscript-2:before{content:""}.ri-subscript:before{content:""}.ri-subtract-fill:before{content:""}.ri-subtract-line:before{content:""}.ri-subway-fill:before{content:""}.ri-subway-line:before{content:""}.ri-subway-wifi-fill:before{content:""}.ri-subway-wifi-line:before{content:""}.ri-suitcase-2-fill:before{content:""}.ri-suitcase-2-line:before{content:""}.ri-suitcase-3-fill:before{content:""}.ri-suitcase-3-line:before{content:""}.ri-suitcase-fill:before{content:""}.ri-suitcase-line:before{content:""}.ri-sun-cloudy-fill:before{content:""}.ri-sun-cloudy-line:before{content:""}.ri-sun-fill:before{content:""}.ri-sun-foggy-fill:before{content:""}.ri-sun-foggy-line:before{content:""}.ri-sun-line:before{content:""}.ri-superscript-2:before{content:""}.ri-superscript:before{content:""}.ri-surgical-mask-fill:before{content:""}.ri-surgical-mask-line:before{content:""}.ri-surround-sound-fill:before{content:""}.ri-surround-sound-line:before{content:""}.ri-survey-fill:before{content:""}.ri-survey-line:before{content:""}.ri-swap-box-fill:before{content:""}.ri-swap-box-line:before{content:""}.ri-swap-fill:before{content:""}.ri-swap-line:before{content:""}.ri-switch-fill:before{content:""}.ri-switch-line:before{content:""}.ri-sword-fill:before{content:""}.ri-sword-line:before{content:""}.ri-syringe-fill:before{content:""}.ri-syringe-line:before{content:""}.ri-t-box-fill:before{content:""}.ri-t-box-line:before{content:""}.ri-t-shirt-2-fill:before{content:""}.ri-t-shirt-2-line:before{content:""}.ri-t-shirt-air-fill:before{content:""}.ri-t-shirt-air-line:before{content:""}.ri-t-shirt-fill:before{content:""}.ri-t-shirt-line:before{content:""}.ri-table-2:before{content:""}.ri-table-alt-fill:before{content:""}.ri-table-alt-line:before{content:""}.ri-table-fill:before{content:""}.ri-table-line:before{content:""}.ri-tablet-fill:before{content:""}.ri-tablet-line:before{content:""}.ri-takeaway-fill:before{content:""}.ri-takeaway-line:before{content:""}.ri-taobao-fill:before{content:""}.ri-taobao-line:before{content:""}.ri-tape-fill:before{content:""}.ri-tape-line:before{content:""}.ri-task-fill:before{content:""}.ri-task-line:before{content:""}.ri-taxi-fill:before{content:""}.ri-taxi-line:before{content:""}.ri-taxi-wifi-fill:before{content:""}.ri-taxi-wifi-line:before{content:""}.ri-team-fill:before{content:""}.ri-team-line:before{content:""}.ri-telegram-fill:before{content:""}.ri-telegram-line:before{content:""}.ri-temp-cold-fill:before{content:""}.ri-temp-cold-line:before{content:""}.ri-temp-hot-fill:before{content:""}.ri-temp-hot-line:before{content:""}.ri-terminal-box-fill:before{content:""}.ri-terminal-box-line:before{content:""}.ri-terminal-fill:before{content:""}.ri-terminal-line:before{content:""}.ri-terminal-window-fill:before{content:""}.ri-terminal-window-line:before{content:""}.ri-test-tube-fill:before{content:""}.ri-test-tube-line:before{content:""}.ri-text-direction-l:before{content:""}.ri-text-direction-r:before{content:""}.ri-text-spacing:before{content:""}.ri-text-wrap:before{content:""}.ri-text:before{content:""}.ri-thermometer-fill:before{content:""}.ri-thermometer-line:before{content:""}.ri-thumb-down-fill:before{content:""}.ri-thumb-down-line:before{content:""}.ri-thumb-up-fill:before{content:""}.ri-thumb-up-line:before{content:""}.ri-thunderstorms-fill:before{content:""}.ri-thunderstorms-line:before{content:""}.ri-ticket-2-fill:before{content:""}.ri-ticket-2-line:before{content:""}.ri-ticket-fill:before{content:""}.ri-ticket-line:before{content:""}.ri-time-fill:before{content:""}.ri-time-line:before{content:""}.ri-timer-2-fill:before{content:""}.ri-timer-2-line:before{content:""}.ri-timer-fill:before{content:""}.ri-timer-flash-fill:before{content:""}.ri-timer-flash-line:before{content:""}.ri-timer-line:before{content:""}.ri-todo-fill:before{content:""}.ri-todo-line:before{content:""}.ri-toggle-fill:before{content:""}.ri-toggle-line:before{content:""}.ri-tools-fill:before{content:""}.ri-tools-line:before{content:""}.ri-tornado-fill:before{content:""}.ri-tornado-line:before{content:""}.ri-trademark-fill:before{content:""}.ri-trademark-line:before{content:""}.ri-traffic-light-fill:before{content:""}.ri-traffic-light-line:before{content:""}.ri-train-fill:before{content:""}.ri-train-line:before{content:""}.ri-train-wifi-fill:before{content:""}.ri-train-wifi-line:before{content:""}.ri-translate-2:before{content:""}.ri-translate:before{content:""}.ri-travesti-fill:before{content:""}.ri-travesti-line:before{content:""}.ri-treasure-map-fill:before{content:""}.ri-treasure-map-line:before{content:""}.ri-trello-fill:before{content:""}.ri-trello-line:before{content:""}.ri-trophy-fill:before{content:""}.ri-trophy-line:before{content:""}.ri-truck-fill:before{content:""}.ri-truck-line:before{content:""}.ri-tumblr-fill:before{content:""}.ri-tumblr-line:before{content:""}.ri-tv-2-fill:before{content:""}.ri-tv-2-line:before{content:""}.ri-tv-fill:before{content:""}.ri-tv-line:before{content:""}.ri-twitch-fill:before{content:""}.ri-twitch-line:before{content:""}.ri-twitter-fill:before{content:""}.ri-twitter-line:before{content:""}.ri-typhoon-fill:before{content:""}.ri-typhoon-line:before{content:""}.ri-u-disk-fill:before{content:""}.ri-u-disk-line:before{content:""}.ri-ubuntu-fill:before{content:""}.ri-ubuntu-line:before{content:""}.ri-umbrella-fill:before{content:""}.ri-umbrella-line:before{content:""}.ri-underline:before{content:""}.ri-uninstall-fill:before{content:""}.ri-uninstall-line:before{content:""}.ri-unsplash-fill:before{content:""}.ri-unsplash-line:before{content:""}.ri-upload-2-fill:before{content:""}.ri-upload-2-line:before{content:""}.ri-upload-cloud-2-fill:before{content:""}.ri-upload-cloud-2-line:before{content:""}.ri-upload-cloud-fill:before{content:""}.ri-upload-cloud-line:before{content:""}.ri-upload-fill:before{content:""}.ri-upload-line:before{content:""}.ri-usb-fill:before{content:""}.ri-usb-line:before{content:""}.ri-user-2-fill:before{content:""}.ri-user-2-line:before{content:""}.ri-user-3-fill:before{content:""}.ri-user-3-line:before{content:""}.ri-user-4-fill:before{content:""}.ri-user-4-line:before{content:""}.ri-user-5-fill:before{content:""}.ri-user-5-line:before{content:""}.ri-user-6-fill:before{content:""}.ri-user-6-line:before{content:""}.ri-user-add-fill:before{content:""}.ri-user-add-line:before{content:""}.ri-user-fill:before{content:""}.ri-user-follow-fill:before{content:""}.ri-user-follow-line:before{content:""}.ri-user-heart-fill:before{content:""}.ri-user-heart-line:before{content:""}.ri-user-line:before{content:""}.ri-user-location-fill:before{content:""}.ri-user-location-line:before{content:""}.ri-user-received-2-fill:before{content:""}.ri-user-received-2-line:before{content:""}.ri-user-received-fill:before{content:""}.ri-user-received-line:before{content:""}.ri-user-search-fill:before{content:""}.ri-user-search-line:before{content:""}.ri-user-settings-fill:before{content:""}.ri-user-settings-line:before{content:""}.ri-user-shared-2-fill:before{content:""}.ri-user-shared-2-line:before{content:""}.ri-user-shared-fill:before{content:""}.ri-user-shared-line:before{content:""}.ri-user-smile-fill:before{content:""}.ri-user-smile-line:before{content:""}.ri-user-star-fill:before{content:""}.ri-user-star-line:before{content:""}.ri-user-unfollow-fill:before{content:""}.ri-user-unfollow-line:before{content:""}.ri-user-voice-fill:before{content:""}.ri-user-voice-line:before{content:""}.ri-video-add-fill:before{content:""}.ri-video-add-line:before{content:""}.ri-video-chat-fill:before{content:""}.ri-video-chat-line:before{content:""}.ri-video-download-fill:before{content:""}.ri-video-download-line:before{content:""}.ri-video-fill:before{content:""}.ri-video-line:before{content:""}.ri-video-upload-fill:before{content:""}.ri-video-upload-line:before{content:""}.ri-vidicon-2-fill:before{content:""}.ri-vidicon-2-line:before{content:""}.ri-vidicon-fill:before{content:""}.ri-vidicon-line:before{content:""}.ri-vimeo-fill:before{content:""}.ri-vimeo-line:before{content:""}.ri-vip-crown-2-fill:before{content:""}.ri-vip-crown-2-line:before{content:""}.ri-vip-crown-fill:before{content:""}.ri-vip-crown-line:before{content:""}.ri-vip-diamond-fill:before{content:""}.ri-vip-diamond-line:before{content:""}.ri-vip-fill:before{content:""}.ri-vip-line:before{content:""}.ri-virus-fill:before{content:""}.ri-virus-line:before{content:""}.ri-visa-fill:before{content:""}.ri-visa-line:before{content:""}.ri-voice-recognition-fill:before{content:""}.ri-voice-recognition-line:before{content:""}.ri-voiceprint-fill:before{content:""}.ri-voiceprint-line:before{content:""}.ri-volume-down-fill:before{content:""}.ri-volume-down-line:before{content:""}.ri-volume-mute-fill:before{content:""}.ri-volume-mute-line:before{content:""}.ri-volume-off-vibrate-fill:before{content:""}.ri-volume-off-vibrate-line:before{content:""}.ri-volume-up-fill:before{content:""}.ri-volume-up-line:before{content:""}.ri-volume-vibrate-fill:before{content:""}.ri-volume-vibrate-line:before{content:""}.ri-vuejs-fill:before{content:""}.ri-vuejs-line:before{content:""}.ri-walk-fill:before{content:""}.ri-walk-line:before{content:""}.ri-wallet-2-fill:before{content:""}.ri-wallet-2-line:before{content:""}.ri-wallet-3-fill:before{content:""}.ri-wallet-3-line:before{content:""}.ri-wallet-fill:before{content:""}.ri-wallet-line:before{content:""}.ri-water-flash-fill:before{content:""}.ri-water-flash-line:before{content:""}.ri-webcam-fill:before{content:""}.ri-webcam-line:before{content:""}.ri-wechat-2-fill:before{content:""}.ri-wechat-2-line:before{content:""}.ri-wechat-fill:before{content:""}.ri-wechat-line:before{content:""}.ri-wechat-pay-fill:before{content:""}.ri-wechat-pay-line:before{content:""}.ri-weibo-fill:before{content:""}.ri-weibo-line:before{content:""}.ri-whatsapp-fill:before{content:""}.ri-whatsapp-line:before{content:""}.ri-wheelchair-fill:before{content:""}.ri-wheelchair-line:before{content:""}.ri-wifi-fill:before{content:""}.ri-wifi-line:before{content:""}.ri-wifi-off-fill:before{content:""}.ri-wifi-off-line:before{content:""}.ri-window-2-fill:before{content:""}.ri-window-2-line:before{content:""}.ri-window-fill:before{content:""}.ri-window-line:before{content:""}.ri-windows-fill:before{content:""}.ri-windows-line:before{content:""}.ri-windy-fill:before{content:""}.ri-windy-line:before{content:""}.ri-wireless-charging-fill:before{content:""}.ri-wireless-charging-line:before{content:""}.ri-women-fill:before{content:""}.ri-women-line:before{content:""}.ri-wubi-input:before{content:""}.ri-xbox-fill:before{content:""}.ri-xbox-line:before{content:""}.ri-xing-fill:before{content:""}.ri-xing-line:before{content:""}.ri-youtube-fill:before{content:""}.ri-youtube-line:before{content:""}.ri-zcool-fill:before{content:""}.ri-zcool-line:before{content:""}.ri-zhihu-fill:before{content:""}.ri-zhihu-line:before{content:""}.ri-zoom-in-fill:before{content:""}.ri-zoom-in-line:before{content:""}.ri-zoom-out-fill:before{content:""}.ri-zoom-out-line:before{content:""}.ri-zzz-fill:before{content:""}.ri-zzz-line:before{content:""}.ri-arrow-down-double-fill:before{content:""}.ri-arrow-down-double-line:before{content:""}.ri-arrow-left-double-fill:before{content:""}.ri-arrow-left-double-line:before{content:""}.ri-arrow-right-double-fill:before{content:""}.ri-arrow-right-double-line:before{content:""}.ri-arrow-turn-back-fill:before{content:""}.ri-arrow-turn-back-line:before{content:""}.ri-arrow-turn-forward-fill:before{content:""}.ri-arrow-turn-forward-line:before{content:""}.ri-arrow-up-double-fill:before{content:""}.ri-arrow-up-double-line:before{content:""}.ri-bard-fill:before{content:""}.ri-bard-line:before{content:""}.ri-bootstrap-fill:before{content:""}.ri-bootstrap-line:before{content:""}.ri-box-1-fill:before{content:""}.ri-box-1-line:before{content:""}.ri-box-2-fill:before{content:""}.ri-box-2-line:before{content:""}.ri-box-3-fill:before{content:""}.ri-box-3-line:before{content:""}.ri-brain-fill:before{content:""}.ri-brain-line:before{content:""}.ri-candle-fill:before{content:""}.ri-candle-line:before{content:""}.ri-cash-fill:before{content:""}.ri-cash-line:before{content:""}.ri-contract-left-fill:before{content:""}.ri-contract-left-line:before{content:""}.ri-contract-left-right-fill:before{content:""}.ri-contract-left-right-line:before{content:""}.ri-contract-right-fill:before{content:""}.ri-contract-right-line:before{content:""}.ri-contract-up-down-fill:before{content:""}.ri-contract-up-down-line:before{content:""}.ri-copilot-fill:before{content:""}.ri-copilot-line:before{content:""}.ri-corner-down-left-fill:before{content:""}.ri-corner-down-left-line:before{content:""}.ri-corner-down-right-fill:before{content:""}.ri-corner-down-right-line:before{content:""}.ri-corner-left-down-fill:before{content:""}.ri-corner-left-down-line:before{content:""}.ri-corner-left-up-fill:before{content:""}.ri-corner-left-up-line:before{content:""}.ri-corner-right-down-fill:before{content:""}.ri-corner-right-down-line:before{content:""}.ri-corner-right-up-fill:before{content:""}.ri-corner-right-up-line:before{content:""}.ri-corner-up-left-double-fill:before{content:""}.ri-corner-up-left-double-line:before{content:""}.ri-corner-up-left-fill:before{content:""}.ri-corner-up-left-line:before{content:""}.ri-corner-up-right-double-fill:before{content:""}.ri-corner-up-right-double-line:before{content:""}.ri-corner-up-right-fill:before{content:""}.ri-corner-up-right-line:before{content:""}.ri-cross-fill:before{content:""}.ri-cross-line:before{content:""}.ri-edge-new-fill:before{content:""}.ri-edge-new-line:before{content:""}.ri-equal-fill:before{content:""}.ri-equal-line:before{content:""}.ri-expand-left-fill:before{content:""}.ri-expand-left-line:before{content:""}.ri-expand-left-right-fill:before{content:""}.ri-expand-left-right-line:before{content:""}.ri-expand-right-fill:before{content:""}.ri-expand-right-line:before{content:""}.ri-expand-up-down-fill:before{content:""}.ri-expand-up-down-line:before{content:""}.ri-flickr-fill:before{content:""}.ri-flickr-line:before{content:""}.ri-forward-10-fill:before{content:""}.ri-forward-10-line:before{content:""}.ri-forward-15-fill:before{content:""}.ri-forward-15-line:before{content:""}.ri-forward-30-fill:before{content:""}.ri-forward-30-line:before{content:""}.ri-forward-5-fill:before{content:""}.ri-forward-5-line:before{content:""}.ri-graduation-cap-fill:before{content:""}.ri-graduation-cap-line:before{content:""}.ri-home-office-fill:before{content:""}.ri-home-office-line:before{content:""}.ri-hourglass-2-fill:before{content:""}.ri-hourglass-2-line:before{content:""}.ri-hourglass-fill:before{content:""}.ri-hourglass-line:before{content:""}.ri-javascript-fill:before{content:""}.ri-javascript-line:before{content:""}.ri-loop-left-fill:before{content:""}.ri-loop-left-line:before{content:""}.ri-loop-right-fill:before{content:""}.ri-loop-right-line:before{content:""}.ri-memories-fill:before{content:""}.ri-memories-line:before{content:""}.ri-meta-fill:before{content:""}.ri-meta-line:before{content:""}.ri-microsoft-loop-fill:before{content:""}.ri-microsoft-loop-line:before{content:""}.ri-nft-fill:before{content:""}.ri-nft-line:before{content:""}.ri-notion-fill:before{content:""}.ri-notion-line:before{content:""}.ri-openai-fill:before{content:""}.ri-openai-line:before{content:""}.ri-overline:before{content:""}.ri-p2p-fill:before{content:""}.ri-p2p-line:before{content:""}.ri-presentation-fill:before{content:""}.ri-presentation-line:before{content:""}.ri-replay-10-fill:before{content:""}.ri-replay-10-line:before{content:""}.ri-replay-15-fill:before{content:""}.ri-replay-15-line:before{content:""}.ri-replay-30-fill:before{content:""}.ri-replay-30-line:before{content:""}.ri-replay-5-fill:before{content:""}.ri-replay-5-line:before{content:""}.ri-school-fill:before{content:""}.ri-school-line:before{content:""}.ri-shining-2-fill:before{content:""}.ri-shining-2-line:before{content:""}.ri-shining-fill:before{content:""}.ri-shining-line:before{content:""}.ri-sketching:before{content:""}.ri-skip-down-fill:before{content:""}.ri-skip-down-line:before{content:""}.ri-skip-left-fill:before{content:""}.ri-skip-left-line:before{content:""}.ri-skip-right-fill:before{content:""}.ri-skip-right-line:before{content:""}.ri-skip-up-fill:before{content:""}.ri-skip-up-line:before{content:""}.ri-slow-down-fill:before{content:""}.ri-slow-down-line:before{content:""}.ri-sparkling-2-fill:before{content:""}.ri-sparkling-2-line:before{content:""}.ri-sparkling-fill:before{content:""}.ri-sparkling-line:before{content:""}.ri-speak-fill:before{content:""}.ri-speak-line:before{content:""}.ri-speed-up-fill:before{content:""}.ri-speed-up-line:before{content:""}.ri-tiktok-fill:before{content:""}.ri-tiktok-line:before{content:""}.ri-token-swap-fill:before{content:""}.ri-token-swap-line:before{content:""}.ri-unpin-fill:before{content:""}.ri-unpin-line:before{content:""}.ri-wechat-channels-fill:before{content:""}.ri-wechat-channels-line:before{content:""}.ri-wordpress-fill:before{content:""}.ri-wordpress-line:before{content:""}.ri-blender-fill:before{content:""}.ri-blender-line:before{content:""}.ri-emoji-sticker-fill:before{content:""}.ri-emoji-sticker-line:before{content:""}.ri-git-close-pull-request-fill:before{content:""}.ri-git-close-pull-request-line:before{content:""}.ri-instance-fill:before{content:""}.ri-instance-line:before{content:""}.ri-megaphone-fill:before{content:""}.ri-megaphone-line:before{content:""}.ri-pass-expired-fill:before{content:""}.ri-pass-expired-line:before{content:""}.ri-pass-pending-fill:before{content:""}.ri-pass-pending-line:before{content:""}.ri-pass-valid-fill:before{content:""}.ri-pass-valid-line:before{content:""}.ri-ai-generate:before{content:""}.ri-calendar-close-fill:before{content:""}.ri-calendar-close-line:before{content:""}.ri-draggable:before{content:""}.ri-font-family:before{content:""}.ri-font-mono:before{content:""}.ri-font-sans-serif:before{content:""}.ri-font-sans:before{content:""}.ri-hard-drive-3-fill:before{content:""}.ri-hard-drive-3-line:before{content:""}.ri-kick-fill:before{content:""}.ri-kick-line:before{content:""}.ri-list-check-3:before{content:""}.ri-list-indefinite:before{content:""}.ri-list-ordered-2:before{content:""}.ri-list-radio:before{content:""}.ri-openbase-fill:before{content:""}.ri-openbase-line:before{content:""}.ri-planet-fill:before{content:""}.ri-planet-line:before{content:""}.ri-prohibited-fill:before{content:""}.ri-prohibited-line:before{content:""}.ri-quote-text:before{content:""}.ri-seo-fill:before{content:""}.ri-seo-line:before{content:""}.ri-slash-commands:before{content:""}.ri-archive-2-fill:before{content:""}.ri-archive-2-line:before{content:""}.ri-inbox-2-fill:before{content:""}.ri-inbox-2-line:before{content:""}.ri-shake-hands-fill:before{content:""}.ri-shake-hands-line:before{content:""}.ri-supabase-fill:before{content:""}.ri-supabase-line:before{content:""}.ri-water-percent-fill:before{content:""}.ri-water-percent-line:before{content:""}.ri-yuque-fill:before{content:""}.ri-yuque-line:before{content:""}.ri-crosshair-2-fill:before{content:""}.ri-crosshair-2-line:before{content:""}.ri-crosshair-fill:before{content:""}.ri-crosshair-line:before{content:""}.ri-file-close-fill:before{content:""}.ri-file-close-line:before{content:""}.ri-infinity-fill:before{content:""}.ri-infinity-line:before{content:""}.ri-rfid-fill:before{content:""}.ri-rfid-line:before{content:""}.ri-slash-commands-2:before{content:""}.ri-user-forbid-fill:before{content:""}.ri-user-forbid-line:before{content:""}.ri-beer-fill:before{content:""}.ri-beer-line:before{content:""}.ri-circle-fill:before{content:""}.ri-circle-line:before{content:""}.ri-dropdown-list:before{content:""}.ri-file-image-fill:before{content:""}.ri-file-image-line:before{content:""}.ri-file-pdf-2-fill:before{content:""}.ri-file-pdf-2-line:before{content:""}.ri-file-video-fill:before{content:""}.ri-file-video-line:before{content:""}.ri-folder-image-fill:before{content:""}.ri-folder-image-line:before{content:""}.ri-folder-video-fill:before{content:""}.ri-folder-video-line:before{content:""}.ri-hexagon-fill:before{content:""}.ri-hexagon-line:before{content:""}.ri-menu-search-fill:before{content:""}.ri-menu-search-line:before{content:""}.ri-octagon-fill:before{content:""}.ri-octagon-line:before{content:""}.ri-pentagon-fill:before{content:""}.ri-pentagon-line:before{content:""}.ri-rectangle-fill:before{content:""}.ri-rectangle-line:before{content:""}.ri-robot-2-fill:before{content:""}.ri-robot-2-line:before{content:""}.ri-shapes-fill:before{content:""}.ri-shapes-line:before{content:""}.ri-square-fill:before{content:""}.ri-square-line:before{content:""}.ri-tent-fill:before{content:""}.ri-tent-line:before{content:""}.ri-threads-fill:before{content:""}.ri-threads-line:before{content:""}.ri-tree-fill:before{content:""}.ri-tree-line:before{content:""}.ri-triangle-fill:before{content:""}.ri-triangle-line:before{content:""}.ri-twitter-x-fill:before{content:""}.ri-twitter-x-line:before{content:""}.ri-verified-badge-fill:before{content:""}.ri-verified-badge-line:before{content:""}.ri-armchair-fill:before{content:""}.ri-armchair-line:before{content:""}.ri-bnb-fill:before{content:""}.ri-bnb-line:before{content:""}.ri-bread-fill:before{content:""}.ri-bread-line:before{content:""}.ri-btc-fill:before{content:""}.ri-btc-line:before{content:""}.ri-calendar-schedule-fill:before{content:""}.ri-calendar-schedule-line:before{content:""}.ri-dice-1-fill:before{content:""}.ri-dice-1-line:before{content:""}.ri-dice-2-fill:before{content:""}.ri-dice-2-line:before{content:""}.ri-dice-3-fill:before{content:""}.ri-dice-3-line:before{content:""}.ri-dice-4-fill:before{content:""}.ri-dice-4-line:before{content:""}.ri-dice-5-fill:before{content:""}.ri-dice-5-line:before{content:""}.ri-dice-6-fill:before{content:""}.ri-dice-6-line:before{content:""}.ri-dice-fill:before{content:""}.ri-dice-line:before{content:""}.ri-drinks-fill:before{content:""}.ri-drinks-line:before{content:""}.ri-equalizer-2-fill:before{content:""}.ri-equalizer-2-line:before{content:""}.ri-equalizer-3-fill:before{content:""}.ri-equalizer-3-line:before{content:""}.ri-eth-fill:before{content:""}.ri-eth-line:before{content:""}.ri-flower-fill:before{content:""}.ri-flower-line:before{content:""}.ri-glasses-2-fill:before{content:""}.ri-glasses-2-line:before{content:""}.ri-glasses-fill:before{content:""}.ri-glasses-line:before{content:""}.ri-goggles-fill:before{content:""}.ri-goggles-line:before{content:""}.ri-image-circle-fill:before{content:""}.ri-image-circle-line:before{content:""}.ri-info-i:before{content:""}.ri-money-rupee-circle-fill:before{content:""}.ri-money-rupee-circle-line:before{content:""}.ri-news-fill:before{content:""}.ri-news-line:before{content:""}.ri-robot-3-fill:before{content:""}.ri-robot-3-line:before{content:""}.ri-share-2-fill:before{content:""}.ri-share-2-line:before{content:""}.ri-sofa-fill:before{content:""}.ri-sofa-line:before{content:""}.ri-svelte-fill:before{content:""}.ri-svelte-line:before{content:""}.ri-vk-fill:before{content:""}.ri-vk-line:before{content:""}.ri-xrp-fill:before{content:""}.ri-xrp-line:before{content:""}.ri-xtz-fill:before{content:""}.ri-xtz-line:before{content:""}.ri-archive-stack-fill:before{content:""}.ri-archive-stack-line:before{content:""}.ri-bowl-fill:before{content:""}.ri-bowl-line:before{content:""}.ri-calendar-view:before{content:""}.ri-carousel-view:before{content:""}.ri-code-block:before{content:""}.ri-color-filter-fill:before{content:""}.ri-color-filter-line:before{content:""}.ri-contacts-book-3-fill:before{content:""}.ri-contacts-book-3-line:before{content:""}.ri-contract-fill:before{content:""}.ri-contract-line:before{content:""}.ri-drinks-2-fill:before{content:""}.ri-drinks-2-line:before{content:""}.ri-export-fill:before{content:""}.ri-export-line:before{content:""}.ri-file-check-fill:before{content:""}.ri-file-check-line:before{content:""}.ri-focus-mode:before{content:""}.ri-folder-6-fill:before{content:""}.ri-folder-6-line:before{content:""}.ri-folder-check-fill:before{content:""}.ri-folder-check-line:before{content:""}.ri-folder-close-fill:before{content:""}.ri-folder-close-line:before{content:""}.ri-folder-cloud-fill:before{content:""}.ri-folder-cloud-line:before{content:""}.ri-gallery-view-2:before{content:""}.ri-gallery-view:before{content:""}.ri-hand:before{content:""}.ri-import-fill:before{content:""}.ri-import-line:before{content:""}.ri-information-2-fill:before{content:""}.ri-information-2-line:before{content:""}.ri-kanban-view-2:before{content:""}.ri-kanban-view:before{content:""}.ri-list-view:before{content:""}.ri-lock-star-fill:before{content:""}.ri-lock-star-line:before{content:""}.ri-puzzle-2-fill:before{content:""}.ri-puzzle-2-line:before{content:""}.ri-puzzle-fill:before{content:""}.ri-puzzle-line:before{content:""}.ri-ram-2-fill:before{content:""}.ri-ram-2-line:before{content:""}.ri-ram-fill:before{content:""}.ri-ram-line:before{content:""}.ri-receipt-fill:before{content:""}.ri-receipt-line:before{content:""}.ri-shadow-fill:before{content:""}.ri-shadow-line:before{content:""}.ri-sidebar-fold-fill:before{content:""}.ri-sidebar-fold-line:before{content:""}.ri-sidebar-unfold-fill:before{content:""}.ri-sidebar-unfold-line:before{content:""}.ri-slideshow-view:before{content:""}.ri-sort-alphabet-asc:before{content:""}.ri-sort-alphabet-desc:before{content:""}.ri-sort-number-asc:before{content:""}.ri-sort-number-desc:before{content:""}.ri-stacked-view:before{content:""}.ri-sticky-note-add-fill:before{content:""}.ri-sticky-note-add-line:before{content:""}.ri-swap-2-fill:before{content:""}.ri-swap-2-line:before{content:""}.ri-swap-3-fill:before{content:""}.ri-swap-3-line:before{content:""}.ri-table-3:before{content:""}.ri-table-view:before{content:""}.ri-text-block:before{content:""}.ri-text-snippet:before{content:""}.ri-timeline-view:before{content:""}.ri-blogger-fill:before{content:""}.ri-blogger-line:before{content:""}.ri-chat-thread-fill:before{content:""}.ri-chat-thread-line:before{content:""}.ri-discount-percent-fill:before{content:""}.ri-discount-percent-line:before{content:""}.ri-exchange-2-fill:before{content:""}.ri-exchange-2-line:before{content:""}.ri-git-fork-fill:before{content:""}.ri-git-fork-line:before{content:""}.ri-input-field:before{content:""}.ri-progress-1-fill:before{content:""}.ri-progress-1-line:before{content:""}.ri-progress-2-fill:before{content:""}.ri-progress-2-line:before{content:""}.ri-progress-3-fill:before{content:""}.ri-progress-3-line:before{content:""}.ri-progress-4-fill:before{content:""}.ri-progress-4-line:before{content:""}.ri-progress-5-fill:before{content:""}.ri-progress-5-line:before{content:""}.ri-progress-6-fill:before{content:""}.ri-progress-6-line:before{content:""}.ri-progress-7-fill:before{content:""}.ri-progress-7-line:before{content:""}.ri-progress-8-fill:before{content:""}.ri-progress-8-line:before{content:""}.ri-remix-run-fill:before{content:""}.ri-remix-run-line:before{content:""}.ri-signpost-fill:before{content:""}.ri-signpost-line:before{content:""}.ri-time-zone-fill:before{content:""}.ri-time-zone-line:before{content:""}.ri-arrow-down-wide-fill:before{content:""}.ri-arrow-down-wide-line:before{content:""}.ri-arrow-left-wide-fill:before{content:""}.ri-arrow-left-wide-line:before{content:""}.ri-arrow-right-wide-fill:before{content:""}.ri-arrow-right-wide-line:before{content:""}.ri-arrow-up-wide-fill:before{content:""}.ri-arrow-up-wide-line:before{content:""}.ri-bluesky-fill:before{content:""}.ri-bluesky-line:before{content:""}.ri-expand-height-fill:before{content:""}.ri-expand-height-line:before{content:""}.ri-expand-width-fill:before{content:""}.ri-expand-width-line:before{content:""}.ri-forward-end-fill:before{content:""}.ri-forward-end-line:before{content:""}.ri-forward-end-mini-fill:before{content:""}.ri-forward-end-mini-line:before{content:""}.ri-friendica-fill:before{content:""}.ri-friendica-line:before{content:""}.ri-git-pr-draft-fill:before{content:""}.ri-git-pr-draft-line:before{content:""}.ri-play-reverse-fill:before{content:""}.ri-play-reverse-line:before{content:""}.ri-play-reverse-mini-fill:before{content:""}.ri-play-reverse-mini-line:before{content:""}.ri-rewind-start-fill:before{content:""}.ri-rewind-start-line:before{content:""}.ri-rewind-start-mini-fill:before{content:""}.ri-rewind-start-mini-line:before{content:""}.ri-scroll-to-bottom-fill:before{content:""}.ri-scroll-to-bottom-line:before{content:""}.ri-add-large-fill:before{content:""}.ri-add-large-line:before{content:""}.ri-aed-electrodes-fill:before{content:""}.ri-aed-electrodes-line:before{content:""}.ri-aed-fill:before{content:""}.ri-aed-line:before{content:""}.ri-alibaba-cloud-fill:before{content:""}.ri-alibaba-cloud-line:before{content:""}.ri-align-item-bottom-fill:before{content:""}.ri-align-item-bottom-line:before{content:""}.ri-align-item-horizontal-center-fill:before{content:""}.ri-align-item-horizontal-center-line:before{content:""}.ri-align-item-left-fill:before{content:""}.ri-align-item-left-line:before{content:""}.ri-align-item-right-fill:before{content:""}.ri-align-item-right-line:before{content:""}.ri-align-item-top-fill:before{content:""}.ri-align-item-top-line:before{content:""}.ri-align-item-vertical-center-fill:before{content:""}.ri-align-item-vertical-center-line:before{content:""}.ri-apps-2-add-fill:before{content:""}.ri-apps-2-add-line:before{content:""}.ri-close-large-fill:before{content:""}.ri-close-large-line:before{content:""}.ri-collapse-diagonal-2-fill:before{content:""}.ri-collapse-diagonal-2-line:before{content:""}.ri-collapse-diagonal-fill:before{content:""}.ri-collapse-diagonal-line:before{content:""}.ri-dashboard-horizontal-fill:before{content:""}.ri-dashboard-horizontal-line:before{content:""}.ri-expand-diagonal-2-fill:before{content:""}.ri-expand-diagonal-2-line:before{content:""}.ri-expand-diagonal-fill:before{content:""}.ri-expand-diagonal-line:before{content:""}.ri-firebase-fill:before{content:""}.ri-firebase-line:before{content:""}.ri-flip-horizontal-2-fill:before{content:""}.ri-flip-horizontal-2-line:before{content:""}.ri-flip-horizontal-fill:before{content:""}.ri-flip-horizontal-line:before{content:""}.ri-flip-vertical-2-fill:before{content:""}.ri-flip-vertical-2-line:before{content:""}.ri-flip-vertical-fill:before{content:""}.ri-flip-vertical-line:before{content:""}.ri-formula:before{content:""}.ri-function-add-fill:before{content:""}.ri-function-add-line:before{content:""}.ri-goblet-2-fill:before{content:""}.ri-goblet-2-line:before{content:""}.ri-golf-ball-fill:before{content:""}.ri-golf-ball-line:before{content:""}.ri-group-3-fill:before{content:""}.ri-group-3-line:before{content:""}.ri-heart-add-2-fill:before{content:""}.ri-heart-add-2-line:before{content:""}.ri-id-card-fill:before{content:""}.ri-id-card-line:before{content:""}.ri-information-off-fill:before{content:""}.ri-information-off-line:before{content:""}.ri-java-fill:before{content:""}.ri-java-line:before{content:""}.ri-layout-grid-2-fill:before{content:""}.ri-layout-grid-2-line:before{content:""}.ri-layout-horizontal-fill:before{content:""}.ri-layout-horizontal-line:before{content:""}.ri-layout-vertical-fill:before{content:""}.ri-layout-vertical-line:before{content:""}.ri-menu-fold-2-fill:before{content:""}.ri-menu-fold-2-line:before{content:""}.ri-menu-fold-3-fill:before{content:""}.ri-menu-fold-3-line:before{content:""}.ri-menu-fold-4-fill:before{content:""}.ri-menu-fold-4-line:before{content:""}.ri-menu-unfold-2-fill:before{content:""}.ri-menu-unfold-2-line:before{content:""}.ri-menu-unfold-3-fill:before{content:""}.ri-menu-unfold-3-line:before{content:""}.ri-menu-unfold-4-fill:before{content:""}.ri-menu-unfold-4-line:before{content:""}.ri-mobile-download-fill:before{content:""}.ri-mobile-download-line:before{content:""}.ri-nextjs-fill:before{content:""}.ri-nextjs-line:before{content:""}.ri-nodejs-fill:before{content:""}.ri-nodejs-line:before{content:""}.ri-pause-large-fill:before{content:""}.ri-pause-large-line:before{content:""}.ri-play-large-fill:before{content:""}.ri-play-large-line:before{content:""}.ri-play-reverse-large-fill:before{content:""}.ri-play-reverse-large-line:before{content:""}.ri-police-badge-fill:before{content:""}.ri-police-badge-line:before{content:""}.ri-prohibited-2-fill:before{content:""}.ri-prohibited-2-line:before{content:""}.ri-shopping-bag-4-fill:before{content:""}.ri-shopping-bag-4-line:before{content:""}.ri-snowflake-fill:before{content:""}.ri-snowflake-line:before{content:""}.ri-square-root:before{content:""}.ri-stop-large-fill:before{content:""}.ri-stop-large-line:before{content:""}.ri-tailwind-css-fill:before{content:""}.ri-tailwind-css-line:before{content:""}.ri-tooth-fill:before{content:""}.ri-tooth-line:before{content:""}.ri-video-off-fill:before{content:""}.ri-video-off-line:before{content:""}.ri-video-on-fill:before{content:""}.ri-video-on-line:before{content:""}.ri-webhook-fill:before{content:""}.ri-webhook-line:before{content:""}.ri-weight-fill:before{content:""}.ri-weight-line:before{content:""}.ri-book-shelf-fill:before{content:""}.ri-book-shelf-line:before{content:""}.ri-brain-2-fill:before{content:""}.ri-brain-2-line:before{content:""}.ri-chat-search-fill:before{content:""}.ri-chat-search-line:before{content:""}.ri-chat-unread-fill:before{content:""}.ri-chat-unread-line:before{content:""}.ri-collapse-horizontal-fill:before{content:""}.ri-collapse-horizontal-line:before{content:""}.ri-collapse-vertical-fill:before{content:""}.ri-collapse-vertical-line:before{content:""}.ri-dna-fill:before{content:""}.ri-dna-line:before{content:""}.ri-dropper-fill:before{content:""}.ri-dropper-line:before{content:""}.ri-expand-diagonal-s-2-fill:before{content:""}.ri-expand-diagonal-s-2-line:before{content:""}.ri-expand-diagonal-s-fill:before{content:""}.ri-expand-diagonal-s-line:before{content:""}.ri-expand-horizontal-fill:before{content:""}.ri-expand-horizontal-line:before{content:""}.ri-expand-horizontal-s-fill:before{content:""}.ri-expand-horizontal-s-line:before{content:""}.ri-expand-vertical-fill:before{content:""}.ri-expand-vertical-line:before{content:""}.ri-expand-vertical-s-fill:before{content:""}.ri-expand-vertical-s-line:before{content:""}.ri-gemini-fill:before{content:""}.ri-gemini-line:before{content:""}.ri-reset-left-fill:before{content:""}.ri-reset-left-line:before{content:""}.ri-reset-right-fill:before{content:""}.ri-reset-right-line:before{content:""}.ri-stairs-fill:before{content:""}.ri-stairs-line:before{content:""}.ri-telegram-2-fill:before{content:""}.ri-telegram-2-line:before{content:""}.ri-triangular-flag-fill:before{content:""}.ri-triangular-flag-line:before{content:""}.ri-user-minus-fill:before{content:""}.ri-user-minus-line:before{content:""}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes expand{0%{transform:rotateY(90deg)}to{opacity:1;transform:rotateY(0)}}@keyframes slideIn{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0;visibility:hidden}to{opacity:1;visibility:visible}}@keyframes shine{to{background-position-x:-200%}}@keyframes loaderShow{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@keyframes entranceLeft{0%{opacity:0;transform:translate(-5px)}to{opacity:1;transform:translate(0)}}@keyframes entranceRight{0%{opacity:0;transform:translate(5px)}to{opacity:1;transform:translate(0)}}@keyframes entranceTop{0%{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}@keyframes entranceBottom{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@media screen and (min-width: 550px){::-webkit-scrollbar{width:8px;height:8px;border-radius:var(--baseRadius)}::-webkit-scrollbar-track{background:transparent;border-radius:var(--baseRadius)}::-webkit-scrollbar-thumb{background-color:var(--baseAlt2Color);border-radius:15px;border:2px solid transparent;background-clip:padding-box}::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background-color:var(--baseAlt3Color)}html{scrollbar-color:var(--baseAlt2Color) transparent;scrollbar-width:thin;scroll-behavior:smooth}html *{scrollbar-width:inherit}}:focus-visible{outline-color:var(--primaryColor);outline-style:solid}html,body{line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);font-size:var(--baseFontSize);color:var(--txtPrimaryColor);background:var(--bodyColor)}#app{overflow:auto;display:block;width:100%;height:100vh}.schema-field,.flatpickr-inline-container,.accordion .accordion-content,.accordion,.tabs,.tabs-content,.select .txt-missing,.form-field .form-field-block,.list,.skeleton-loader,.clearfix,.content,.form-field .help-block,.overlay-panel .panel-content,.sub-panel,.panel,.block,.code-block,blockquote,p{display:block;width:100%}h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6{margin:0;font-weight:400}h1{font-size:22px;line-height:28px}h2,.breadcrumbs .breadcrumb-item{font-size:20px;line-height:26px}h3{font-size:19px;line-height:24px}h4{font-size:18px;line-height:24px}h5{font-size:17px;line-height:24px}h6{font-size:16px;line-height:22px}em{font-style:italic}ins{color:var(--txtPrimaryColor);background:var(--successAltColor);text-decoration:none}del{color:var(--txtPrimaryColor);background:var(--dangerAltColor);text-decoration:none}strong{font-weight:600}small{font-size:var(--smFontSize);line-height:var(--smLineHeight)}sub,sup{position:relative;font-size:.75em;line-height:1}sup{vertical-align:top}sub{vertical-align:bottom}p{margin:5px 0}blockquote{position:relative;padding-left:var(--smSpacing);font-style:italic;color:var(--txtHintColor)}blockquote:before{content:"";position:absolute;top:0;left:0;width:2px;height:100%;background:var(--baseColor)}code{display:inline-block;font-family:var(--monospaceFontFamily);font-style:normal;font-size:1em;line-height:1.379rem;padding:0 4px;white-space:nowrap;color:inherit;background:var(--baseAlt2Color);border-radius:var(--baseRadius)}.code-block{overflow:auto;padding:var(--xsSpacing);white-space:pre-wrap;background:var(--baseAlt1Color)}ol,ul{margin:10px 0;list-style:decimal;padding-left:var(--baseSpacing)}ol li,ul li{margin-top:5px;margin-bottom:5px}ul{list-style:disc}img{max-width:100%;vertical-align:top}hr{display:block;border:0;height:1px;width:100%;background:var(--baseAlt1Color);margin:var(--baseSpacing) 0}hr.dark{background:var(--baseAlt2Color)}a{color:inherit}a:hover{text-decoration:none}a i,a .txt{display:inline-block;vertical-align:top}.txt-mono{font-family:var(--monospaceFontFamily)}.txt-nowrap{white-space:nowrap}.txt-ellipsis{display:inline-block;vertical-align:top;flex-shrink:1;min-width:0;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.txt-base{font-size:var(--baseFontSize)!important}.txt-xs{font-size:var(--xsFontSize)!important;line-height:var(--smLineHeight)}.txt-sm{font-size:var(--smFontSize)!important;line-height:var(--smLineHeight)}.txt-lg{font-size:var(--lgFontSize)!important}.txt-xl{font-size:var(--xlFontSize)!important}.txt-bold{font-weight:600!important}.txt-strikethrough{text-decoration:line-through!important}.txt-break{white-space:pre-wrap!important}.txt-center{text-align:center!important}.txt-justify{text-align:justify!important}.txt-left{text-align:left!important}.txt-right{text-align:right!important}.txt-main{color:var(--txtPrimaryColor)!important}.txt-hint{color:var(--txtHintColor)!important}.txt-disabled{color:var(--txtDisabledColor)!important}.link-hint{-webkit-user-select:none;user-select:none;cursor:pointer;color:var(--txtHintColor)!important;text-decoration:none;transition:color var(--baseAnimationSpeed)}.link-hint:hover,.link-hint:focus-visible,.link-hint:active{color:var(--txtPrimaryColor)!important}.link-fade{opacity:1;-webkit-user-select:none;user-select:none;cursor:pointer;text-decoration:none;color:var(--txtPrimaryColor);transition:opacity var(--baseAnimationSpeed)}.link-fade:focus-visible,.link-fade:hover,.link-fade:active{opacity:.8}.txt-primary{color:var(--primaryColor)!important}.bg-primary{background:var(--primaryColor)!important}.link-primary{cursor:pointer;color:var(--primaryColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-primary:focus-visible,.link-primary:hover,.link-primary:active{opacity:.8}.txt-info{color:var(--infoColor)!important}.bg-info{background:var(--infoColor)!important}.link-info{cursor:pointer;color:var(--infoColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info:focus-visible,.link-info:hover,.link-info:active{opacity:.8}.txt-info-alt{color:var(--infoAltColor)!important}.bg-info-alt{background:var(--infoAltColor)!important}.link-info-alt{cursor:pointer;color:var(--infoAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info-alt:focus-visible,.link-info-alt:hover,.link-info-alt:active{opacity:.8}.txt-success{color:var(--successColor)!important}.bg-success{background:var(--successColor)!important}.link-success{cursor:pointer;color:var(--successColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success:focus-visible,.link-success:hover,.link-success:active{opacity:.8}.txt-success-alt{color:var(--successAltColor)!important}.bg-success-alt{background:var(--successAltColor)!important}.link-success-alt{cursor:pointer;color:var(--successAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success-alt:focus-visible,.link-success-alt:hover,.link-success-alt:active{opacity:.8}.txt-danger{color:var(--dangerColor)!important}.bg-danger{background:var(--dangerColor)!important}.link-danger{cursor:pointer;color:var(--dangerColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger:focus-visible,.link-danger:hover,.link-danger:active{opacity:.8}.txt-danger-alt{color:var(--dangerAltColor)!important}.bg-danger-alt{background:var(--dangerAltColor)!important}.link-danger-alt{cursor:pointer;color:var(--dangerAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger-alt:focus-visible,.link-danger-alt:hover,.link-danger-alt:active{opacity:.8}.txt-warning{color:var(--warningColor)!important}.bg-warning{background:var(--warningColor)!important}.link-warning{cursor:pointer;color:var(--warningColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning:focus-visible,.link-warning:hover,.link-warning:active{opacity:.8}.txt-warning-alt{color:var(--warningAltColor)!important}.bg-warning-alt{background:var(--warningAltColor)!important}.link-warning-alt{cursor:pointer;color:var(--warningAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning-alt:focus-visible,.link-warning-alt:hover,.link-warning-alt:active{opacity:.8}.fade{opacity:.6}a.fade,.btn.fade,[tabindex].fade,[class*=link-].fade,.handle.fade{transition:all var(--baseAnimationSpeed)}a.fade:hover,.btn.fade:hover,[tabindex].fade:hover,[class*=link-].fade:hover,.handle.fade:hover{opacity:1}.noborder{border:0px!important}.hidden{display:none!important}.hidden-empty:empty{display:none!important}.v-align-top{vertical-align:top}.v-align-middle{vertical-align:middle}.v-align-bottom{vertical-align:bottom}.scrollbar-gutter-stable{scrollbar-gutter:stable}.no-pointer-events{pointer-events:none}.content,.form-field .help-block,.overlay-panel .panel-content,.sub-panel,.panel{min-width:0}.content>:first-child,.form-field .help-block>:first-child,.overlay-panel .panel-content>:first-child,.sub-panel>:first-child,.panel>:first-child{margin-top:0}.content>:last-child,.form-field .help-block>:last-child,.overlay-panel .panel-content>:last-child,.sub-panel>:last-child,.panel>:last-child{margin-bottom:0}.panel{background:var(--baseColor);border-radius:var(--lgRadius);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);box-shadow:0 2px 5px 0 var(--shadowColor)}.sub-panel{background:var(--baseColor);border-radius:var(--baseRadius);padding:calc(var(--smSpacing) - 5px) var(--smSpacing);border:1px solid var(--baseAlt1Color)}.shadowize{box-shadow:0 2px 5px 0 var(--shadowColor)}.clearfix{clear:both}.clearfix:after{content:"";display:table;clear:both}.flex{position:relative;display:flex;align-items:center;width:100%;min-width:0;gap:var(--smSpacing)}.flex-fill{flex:1 1 auto!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.inline-flex{position:relative;display:inline-flex;vertical-align:top;align-items:center;flex-wrap:wrap;min-width:0;gap:10px}.flex-order-0{order:0}.flex-order-1{order:1}.flex-order-2{order:2}.flex-order-3{order:3}.flex-order-4{order:4}.flex-order-5{order:5}.flex-order-6{order:6}.flex-order-7{order:7}.flex-order-8{order:8}.flex-order-9{order:9}.flex-order-10{order:10}.flex-gap-base{gap:var(--baseSpacing)!important}.flex-gap-xs{gap:var(--xsSpacing)!important}.flex-gap-sm{gap:var(--smSpacing)!important}.flex-gap-lg{gap:var(--lgSpacing)!important}.flex-gap-xl{gap:var(--xlSpacing)!important}.flex-gap-0{gap:0px!important}.flex-gap-5{gap:5px!important}.flex-gap-10{gap:10px!important}.flex-gap-15{gap:15px!important}.flex-gap-20{gap:20px!important}.flex-gap-25{gap:25px!important}.flex-gap-30{gap:30px!important}.flex-gap-35{gap:35px!important}.flex-gap-40{gap:40px!important}.flex-gap-45{gap:45px!important}.flex-gap-50{gap:50px!important}.flex-gap-55{gap:55px!important}.flex-gap-60{gap:60px!important}.m-base{margin:var(--baseSpacing)!important}.p-base{padding:var(--baseSpacing)!important}.m-xs{margin:var(--xsSpacing)!important}.p-xs{padding:var(--xsSpacing)!important}.m-sm{margin:var(--smSpacing)!important}.p-sm{padding:var(--smSpacing)!important}.m-lg{margin:var(--lgSpacing)!important}.p-lg{padding:var(--lgSpacing)!important}.m-xl{margin:var(--xlSpacing)!important}.p-xl{padding:var(--xlSpacing)!important}.m-t-auto{margin-top:auto!important}.p-t-auto{padding-top:auto!important}.m-t-base{margin-top:var(--baseSpacing)!important}.p-t-base{padding-top:var(--baseSpacing)!important}.m-t-xs{margin-top:var(--xsSpacing)!important}.p-t-xs{padding-top:var(--xsSpacing)!important}.m-t-sm{margin-top:var(--smSpacing)!important}.p-t-sm{padding-top:var(--smSpacing)!important}.m-t-lg{margin-top:var(--lgSpacing)!important}.p-t-lg{padding-top:var(--lgSpacing)!important}.m-t-xl{margin-top:var(--xlSpacing)!important}.p-t-xl{padding-top:var(--xlSpacing)!important}.m-r-auto{margin-right:auto!important}.p-r-auto{padding-right:auto!important}.m-r-base{margin-right:var(--baseSpacing)!important}.p-r-base{padding-right:var(--baseSpacing)!important}.m-r-xs{margin-right:var(--xsSpacing)!important}.p-r-xs{padding-right:var(--xsSpacing)!important}.m-r-sm{margin-right:var(--smSpacing)!important}.p-r-sm{padding-right:var(--smSpacing)!important}.m-r-lg{margin-right:var(--lgSpacing)!important}.p-r-lg{padding-right:var(--lgSpacing)!important}.m-r-xl{margin-right:var(--xlSpacing)!important}.p-r-xl{padding-right:var(--xlSpacing)!important}.m-b-auto{margin-bottom:auto!important}.p-b-auto{padding-bottom:auto!important}.m-b-base{margin-bottom:var(--baseSpacing)!important}.p-b-base{padding-bottom:var(--baseSpacing)!important}.m-b-xs{margin-bottom:var(--xsSpacing)!important}.p-b-xs{padding-bottom:var(--xsSpacing)!important}.m-b-sm{margin-bottom:var(--smSpacing)!important}.p-b-sm{padding-bottom:var(--smSpacing)!important}.m-b-lg{margin-bottom:var(--lgSpacing)!important}.p-b-lg{padding-bottom:var(--lgSpacing)!important}.m-b-xl{margin-bottom:var(--xlSpacing)!important}.p-b-xl{padding-bottom:var(--xlSpacing)!important}.m-l-auto{margin-left:auto!important}.p-l-auto{padding-left:auto!important}.m-l-base{margin-left:var(--baseSpacing)!important}.p-l-base{padding-left:var(--baseSpacing)!important}.m-l-xs{margin-left:var(--xsSpacing)!important}.p-l-xs{padding-left:var(--xsSpacing)!important}.m-l-sm{margin-left:var(--smSpacing)!important}.p-l-sm{padding-left:var(--smSpacing)!important}.m-l-lg{margin-left:var(--lgSpacing)!important}.p-l-lg{padding-left:var(--lgSpacing)!important}.m-l-xl{margin-left:var(--xlSpacing)!important}.p-l-xl{padding-left:var(--xlSpacing)!important}.m-0{margin:0!important}.p-0{padding:0!important}.m-t-0{margin-top:0!important}.p-t-0{padding-top:0!important}.m-r-0{margin-right:0!important}.p-r-0{padding-right:0!important}.m-b-0{margin-bottom:0!important}.p-b-0{padding-bottom:0!important}.m-l-0{margin-left:0!important}.p-l-0{padding-left:0!important}.m-5{margin:5px!important}.p-5{padding:5px!important}.m-t-5{margin-top:5px!important}.p-t-5{padding-top:5px!important}.m-r-5{margin-right:5px!important}.p-r-5{padding-right:5px!important}.m-b-5{margin-bottom:5px!important}.p-b-5{padding-bottom:5px!important}.m-l-5{margin-left:5px!important}.p-l-5{padding-left:5px!important}.m-10{margin:10px!important}.p-10{padding:10px!important}.m-t-10{margin-top:10px!important}.p-t-10{padding-top:10px!important}.m-r-10{margin-right:10px!important}.p-r-10{padding-right:10px!important}.m-b-10{margin-bottom:10px!important}.p-b-10{padding-bottom:10px!important}.m-l-10{margin-left:10px!important}.p-l-10{padding-left:10px!important}.m-15{margin:15px!important}.p-15{padding:15px!important}.m-t-15{margin-top:15px!important}.p-t-15{padding-top:15px!important}.m-r-15{margin-right:15px!important}.p-r-15{padding-right:15px!important}.m-b-15{margin-bottom:15px!important}.p-b-15{padding-bottom:15px!important}.m-l-15{margin-left:15px!important}.p-l-15{padding-left:15px!important}.m-20{margin:20px!important}.p-20{padding:20px!important}.m-t-20{margin-top:20px!important}.p-t-20{padding-top:20px!important}.m-r-20{margin-right:20px!important}.p-r-20{padding-right:20px!important}.m-b-20{margin-bottom:20px!important}.p-b-20{padding-bottom:20px!important}.m-l-20{margin-left:20px!important}.p-l-20{padding-left:20px!important}.m-25{margin:25px!important}.p-25{padding:25px!important}.m-t-25{margin-top:25px!important}.p-t-25{padding-top:25px!important}.m-r-25{margin-right:25px!important}.p-r-25{padding-right:25px!important}.m-b-25{margin-bottom:25px!important}.p-b-25{padding-bottom:25px!important}.m-l-25{margin-left:25px!important}.p-l-25{padding-left:25px!important}.m-30{margin:30px!important}.p-30{padding:30px!important}.m-t-30{margin-top:30px!important}.p-t-30{padding-top:30px!important}.m-r-30{margin-right:30px!important}.p-r-30{padding-right:30px!important}.m-b-30{margin-bottom:30px!important}.p-b-30{padding-bottom:30px!important}.m-l-30{margin-left:30px!important}.p-l-30{padding-left:30px!important}.m-35{margin:35px!important}.p-35{padding:35px!important}.m-t-35{margin-top:35px!important}.p-t-35{padding-top:35px!important}.m-r-35{margin-right:35px!important}.p-r-35{padding-right:35px!important}.m-b-35{margin-bottom:35px!important}.p-b-35{padding-bottom:35px!important}.m-l-35{margin-left:35px!important}.p-l-35{padding-left:35px!important}.m-40{margin:40px!important}.p-40{padding:40px!important}.m-t-40{margin-top:40px!important}.p-t-40{padding-top:40px!important}.m-r-40{margin-right:40px!important}.p-r-40{padding-right:40px!important}.m-b-40{margin-bottom:40px!important}.p-b-40{padding-bottom:40px!important}.m-l-40{margin-left:40px!important}.p-l-40{padding-left:40px!important}.m-45{margin:45px!important}.p-45{padding:45px!important}.m-t-45{margin-top:45px!important}.p-t-45{padding-top:45px!important}.m-r-45{margin-right:45px!important}.p-r-45{padding-right:45px!important}.m-b-45{margin-bottom:45px!important}.p-b-45{padding-bottom:45px!important}.m-l-45{margin-left:45px!important}.p-l-45{padding-left:45px!important}.m-50{margin:50px!important}.p-50{padding:50px!important}.m-t-50{margin-top:50px!important}.p-t-50{padding-top:50px!important}.m-r-50{margin-right:50px!important}.p-r-50{padding-right:50px!important}.m-b-50{margin-bottom:50px!important}.p-b-50{padding-bottom:50px!important}.m-l-50{margin-left:50px!important}.p-l-50{padding-left:50px!important}.m-55{margin:55px!important}.p-55{padding:55px!important}.m-t-55{margin-top:55px!important}.p-t-55{padding-top:55px!important}.m-r-55{margin-right:55px!important}.p-r-55{padding-right:55px!important}.m-b-55{margin-bottom:55px!important}.p-b-55{padding-bottom:55px!important}.m-l-55{margin-left:55px!important}.p-l-55{padding-left:55px!important}.m-60{margin:60px!important}.p-60{padding:60px!important}.m-t-60{margin-top:60px!important}.p-t-60{padding-top:60px!important}.m-r-60{margin-right:60px!important}.p-r-60{padding-right:60px!important}.m-b-60{margin-bottom:60px!important}.p-b-60{padding-bottom:60px!important}.m-l-60{margin-left:60px!important}.p-l-60{padding-left:60px!important}.no-min-width{min-width:0!important}.wrapper{position:relative;width:var(--wrapperWidth);margin:0 auto;max-width:100%}.wrapper.wrapper-sm{width:var(--smWrapperWidth)}.wrapper.wrapper-lg{width:var(--lgWrapperWidth)}.thumb{--thumbSize: 40px;display:inline-flex;vertical-align:top;position:relative;flex-shrink:0;align-items:center;justify-content:center;line-height:1;width:var(--thumbSize);height:var(--thumbSize);aspect-ratio:1;background:var(--baseAlt2Color);border-radius:var(--baseRadius);color:var(--txtPrimaryColor);outline-offset:-2px;outline:2px solid transparent;box-shadow:0 2px 5px 0 var(--shadowColor)}.thumb i{font-size:inherit}.thumb img{width:100%;height:100%;border-radius:inherit;overflow:hidden}.thumb .initials{text-transform:uppercase;margin-top:-2px}.thumb.thumb-xs{--thumbSize: 24px;font-size:.85rem}.thumb.thumb-sm{--thumbSize: 32px;font-size:.92rem}.thumb.thumb-lg{--thumbSize: 60px;font-size:1.3rem}.thumb.thumb-xl{--thumbSize: 80px;font-size:1.5rem}.thumb.thumb-circle{border-radius:50%}.thumb.thumb-primary{outline-color:var(--primaryColor)}.thumb.thumb-info{outline-color:var(--infoColor)}.thumb.thumb-info-alt{outline-color:var(--infoAltColor)}.thumb.thumb-success{outline-color:var(--successColor)}.thumb.thumb-success-alt{outline-color:var(--successAltColor)}.thumb.thumb-danger{outline-color:var(--dangerColor)}.thumb.thumb-danger-alt{outline-color:var(--dangerAltColor)}.thumb.thumb-warning{outline-color:var(--warningColor)}.thumb.thumb-warning-alt{outline-color:var(--warningAltColor)}.handle.thumb:not(.thumb-active),a.thumb:not(.thumb-active){cursor:pointer;transition:opacity var(--baseAnimationSpeed),outline-color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.handle.thumb:not(.thumb-active):hover,.handle.thumb:not(.thumb-active):focus-visible,.handle.thumb:not(.thumb-active):active,a.thumb:not(.thumb-active):hover,a.thumb:not(.thumb-active):focus-visible,a.thumb:not(.thumb-active):active{opacity:.8;box-shadow:0 2px 5px 0 var(--shadowColor),0 2px 4px 1px var(--shadowColor)}.handle.thumb:not(.thumb-active):active,a.thumb:not(.thumb-active):active{transition-duration:var(--activeAnimationSpeed);transform:scale(.97)}.label{--labelVPadding: 3px;--labelHPadding: 9px;display:inline-flex;align-items:center;justify-content:center;vertical-align:top;gap:5px;padding:var(--labelVPadding) var(--labelHPadding);min-height:24px;max-width:100%;text-align:center;line-height:var(--smLineHeight);font-weight:400;font-size:var(--smFontSize);background:var(--baseAlt2Color);color:var(--txtPrimaryColor);white-space:nowrap;border-radius:15px}.label .btn:last-child{margin-right:calc(-.5 * var(--labelHPadding))}.label .btn:first-child{margin-left:calc(-.5 * var(--labelHPadding))}.label .thumb{box-shadow:none}.label.label-sm{--labelHPadding: 5px;font-size:var(--xsFontSize);min-height:18px;line-height:1}.label.label-primary{color:var(--baseColor);background:var(--primaryColor)}.label.label-info{background:var(--infoAltColor)}.label.label-success{background:var(--successAltColor)}.label.label-danger{background:var(--dangerAltColor)}.label.label-warning{background:var(--warningAltColor)}.section-title{display:flex;align-items:center;width:100%;column-gap:10px;row-gap:5px;margin:0 0 var(--xsSpacing);font-weight:600;font-size:var(--baseFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor)}.logo{position:relative;vertical-align:top;display:inline-flex;align-items:center;gap:10px;font-size:23px;text-decoration:none;color:inherit;-webkit-user-select:none;user-select:none}.logo strong{font-weight:700}.logo .version{position:absolute;right:0;top:-5px;line-height:1;font-size:10px;font-weight:400;padding:2px 4px;border-radius:var(--baseRadius);background:var(--dangerAltColor);color:var(--txtPrimaryColor)}.logo.logo-sm{font-size:20px}.drag-handle{position:relative;display:inline-flex;align-items:center;justify-content:center;text-align:center;flex-shrink:0;color:var(--txtDisabledColor);-webkit-user-select:none;user-select:none;cursor:pointer;transition:color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed)}.drag-handle:before{content:"";line-height:1;font-family:var(--iconFontFamily);padding-right:5px;text-shadow:5px 0px currentColor}.drag-handle:hover,.drag-handle:focus-visible{color:var(--txtHintColor)}.drag-handle:active{transition-duration:var(--activeAnimationSpeed);color:var(--txtPrimaryColor)}.loader{--loaderSize: 32px;position:relative;display:inline-flex;vertical-align:top;flex-direction:column;align-items:center;justify-content:center;row-gap:10px;margin:0;color:var(--txtDisabledColor);text-align:center;font-weight:400}.loader:before{content:"";display:inline-block;vertical-align:top;clear:both;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);font-weight:400;font-family:var(--iconFontFamily);color:inherit;text-align:center;animation:loaderShow var(--activeAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.loader.loader-primary{color:var(--primaryColor)}.loader.loader-info{color:var(--infoColor)}.loader.loader-info-alt{color:var(--infoAltColor)}.loader.loader-success{color:var(--successColor)}.loader.loader-success-alt{color:var(--successAltColor)}.loader.loader-danger{color:var(--dangerColor)}.loader.loader-danger-alt{color:var(--dangerAltColor)}.loader.loader-warning{color:var(--warningColor)}.loader.loader-warning-alt{color:var(--warningAltColor)}.loader.loader-xs{--loaderSize: 18px}.loader.loader-sm{--loaderSize: 24px}.loader.loader-lg{--loaderSize: 42px}.skeleton-loader{position:relative;height:12px;margin:5px 0;border-radius:var(--baseRadius);background:var(--baseAlt1Color);animation:fadeIn .4s}.skeleton-loader:before{content:"";width:100%;height:100%;display:block;border-radius:inherit;background:linear-gradient(90deg,var(--baseAlt1Color) 8%,var(--bodyColor) 18%,var(--baseAlt1Color) 33%);background-size:200% 100%;animation:shine 1s linear infinite}.placeholder-section{display:flex;width:100%;align-items:center;justify-content:center;text-align:center;flex-direction:column;gap:var(--smSpacing);color:var(--txtHintColor)}.placeholder-section .icon{font-size:50px;height:50px;line-height:1;opacity:.3}.placeholder-section .icon i{font-size:inherit;vertical-align:top}.list{position:relative;overflow:auto;overflow:overlay;border:1px solid var(--baseAlt2Color);border-radius:var(--baseRadius)}.list .list-item{word-break:break-word;position:relative;display:flex;align-items:center;width:100%;gap:var(--xsSpacing);outline:0;padding:10px var(--xsSpacing);min-height:50px;border-top:1px solid var(--baseAlt2Color);transition:background var(--baseAnimationSpeed)}.list .list-item:first-child{border-top:0}.list .list-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list .list-item .content,.list .list-item .form-field .help-block,.form-field .list .list-item .help-block,.list .list-item .overlay-panel .panel-content,.overlay-panel .list .list-item .panel-content,.list .list-item .panel,.list .list-item .sub-panel{display:flex;align-items:center;gap:5px;min-width:0;max-width:100%;-webkit-user-select:text;user-select:text}.list .list-item .actions{gap:10px;flex-shrink:0;display:inline-flex;align-items:center;margin:-1px -5px -1px 0}.list .list-item .actions.nonintrusive{opacity:0;transform:translate(5px);transition:transform var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed)}.list .list-item:hover,.list .list-item:focus-visible,.list .list-item:focus-within,.list .list-item:active{background:var(--bodyColor)}.list .list-item:hover .actions.nonintrusive,.list .list-item:focus-visible .actions.nonintrusive,.list .list-item:focus-within .actions.nonintrusive,.list .list-item:active .actions.nonintrusive{opacity:1;transform:translate(0)}.list .list-item.selected{background:var(--bodyColor)}.list .list-item.handle:not(.disabled){cursor:pointer;-webkit-user-select:none;user-select:none}.list .list-item.handle:not(.disabled):hover,.list .list-item.handle:not(.disabled):focus-visible{background:var(--baseAlt1Color)}.list .list-item.handle:not(.disabled):active{background:var(--baseAlt2Color)}.list .list-item.disabled:not(.selected){cursor:default;opacity:.6}.list .list-item-placeholder{color:var(--txtHintColor)}.list .list-item-btn{padding:5px;min-height:auto}.list .list-item-placeholder:hover,.list .list-item-placeholder:focus-visible,.list .list-item-placeholder:focus-within,.list .list-item-placeholder:active,.list .list-item-btn:hover,.list .list-item-btn:focus-visible,.list .list-item-btn:focus-within,.list .list-item-btn:active{background:none}.list.list-compact .list-item{gap:10px;min-height:40px}.entrance-top{animation:entranceTop var(--entranceAnimationSpeed)}.entrance-bottom{animation:entranceBottom var(--entranceAnimationSpeed)}.entrance-left{animation:entranceLeft var(--entranceAnimationSpeed)}.entrance-right{animation:entranceRight var(--entranceAnimationSpeed)}.entrance-fade{animation:fadeIn var(--entranceAnimationSpeed)}.provider-logo{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:32px;height:32px;border-radius:var(--baseRadius);background:var(--bodyColor);padding:0;gap:0}.provider-logo img{max-width:20px;max-height:20px;height:auto;flex-shrink:0}.provider-card{display:flex;align-items:center;width:100%;height:100%;gap:10px;padding:5px 10px;min-height:var(--lgBtnHeight);border-radius:var(--baseRadius);border:1px solid var(--baseAlt1Color)}.provider-card .content,.provider-card .form-field .help-block,.form-field .provider-card .help-block,.provider-card .overlay-panel .panel-content,.overlay-panel .provider-card .panel-content,.provider-card .panel,.provider-card .sub-panel{line-height:var(--smLineHeight)}.provider-card.handle{cursor:pointer;transition:background var(--baseAnimationSpeed),border var(--baseAnimationSpeed)}.provider-card.handle:hover,.provider-card.handle:focus-within{background:var(--bodyColor)}.provider-card.handle:active{transition-duration:var(--activeAnimationSpeed);border-color:var(--baseAlt2Color)}.provider-card.error{border-color:var(--dangerColor)}.sidebar-menu{--sidebarListItemMargin: 10px;z-index:0;display:flex;flex-direction:column;width:200px;flex-shrink:0;flex-grow:0;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);padding:calc(var(--baseSpacing) - 5px) 0 var(--smSpacing)}.sidebar-menu>*{padding:0 var(--smSpacing)}.sidebar-menu .sidebar-content{overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.sidebar-menu .sidebar-content>:first-child{margin-top:0}.sidebar-menu .sidebar-content>:last-child{margin-bottom:0}.sidebar-menu .sidebar-footer{margin-top:var(--smSpacing)}.sidebar-menu .search{display:flex;align-items:center;width:auto;column-gap:5px;margin:0 0 var(--xsSpacing);color:var(--txtHintColor);opacity:.7;transition:opacity var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.sidebar-menu .search input{border:0;background:var(--baseColor);transition:box-shadow var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.sidebar-menu .search .btn-clear{margin-right:-8px}.sidebar-menu .search:hover,.sidebar-menu .search:focus-within,.sidebar-menu .search.active{opacity:1;color:var(--txtPrimaryColor)}.sidebar-menu .search:hover input,.sidebar-menu .search:focus-within input,.sidebar-menu .search.active input{background:var(--baseAlt2Color)}.sidebar-menu .sidebar-title{display:flex;align-items:center;gap:5px;width:100%;margin:var(--baseSpacing) 0 var(--xsSpacing);font-weight:600;font-size:1rem;line-height:var(--smLineHeight);color:var(--txtHintColor)}.sidebar-menu .sidebar-title .label{font-weight:400}.sidebar-menu .sidebar-list-item{cursor:pointer;outline:0;text-decoration:none;position:relative;display:flex;width:100%;align-items:center;column-gap:10px;margin:var(--sidebarListItemMargin) 0;padding:3px 10px;font-size:var(--xlFontSize);min-height:var(--btnHeight);min-width:0;color:var(--txtHintColor);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.sidebar-menu .sidebar-list-item i{font-size:18px}.sidebar-menu .sidebar-list-item .txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-menu .sidebar-list-item:focus-visible,.sidebar-menu .sidebar-list-item:hover,.sidebar-menu .sidebar-list-item:active,.sidebar-menu .sidebar-list-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.sidebar-menu .sidebar-list-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.sidebar-menu .sidebar-content-compact .sidebar-list-item{--sidebarListItemMargin: 5px}@media screen and (max-height: 600px){.sidebar-menu{--sidebarListItemMargin: 5px}}@media screen and (max-width: 1100px){.sidebar-menu{min-width:190px}.sidebar-menu>*{padding-left:10px;padding-right:10px}}.grid{--gridGap: var(--baseSpacing);position:relative;display:flex;flex-grow:1;flex-wrap:wrap;row-gap:var(--gridGap);margin:0 calc(-.5 * var(--gridGap))}.grid.grid-center{align-items:center}.grid.grid-sm{--gridGap: var(--smSpacing)}.grid .form-field{margin-bottom:0}.grid>*{margin:0 calc(.5 * var(--gridGap))}.col-xxl-1,.col-xxl-2,.col-xxl-3,.col-xxl-4,.col-xxl-5,.col-xxl-6,.col-xxl-7,.col-xxl-8,.col-xxl-9,.col-xxl-10,.col-xxl-11,.col-xxl-12,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{position:relative;width:100%;min-height:1px}.col-auto{flex:0 0 auto;width:auto}.col-12{width:calc(100% - var(--gridGap))}.col-11{width:calc(91.6666666667% - var(--gridGap))}.col-10{width:calc(83.3333333333% - var(--gridGap))}.col-9{width:calc(75% - var(--gridGap))}.col-8{width:calc(66.6666666667% - var(--gridGap))}.col-7{width:calc(58.3333333333% - var(--gridGap))}.col-6{width:calc(50% - var(--gridGap))}.col-5{width:calc(41.6666666667% - var(--gridGap))}.col-4{width:calc(33.3333333333% - var(--gridGap))}.col-3{width:calc(25% - var(--gridGap))}.col-2{width:calc(16.6666666667% - var(--gridGap))}.col-1{width:calc(8.3333333333% - var(--gridGap))}@media (min-width: 576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-12{width:calc(100% - var(--gridGap))}.col-sm-11{width:calc(91.6666666667% - var(--gridGap))}.col-sm-10{width:calc(83.3333333333% - var(--gridGap))}.col-sm-9{width:calc(75% - var(--gridGap))}.col-sm-8{width:calc(66.6666666667% - var(--gridGap))}.col-sm-7{width:calc(58.3333333333% - var(--gridGap))}.col-sm-6{width:calc(50% - var(--gridGap))}.col-sm-5{width:calc(41.6666666667% - var(--gridGap))}.col-sm-4{width:calc(33.3333333333% - var(--gridGap))}.col-sm-3{width:calc(25% - var(--gridGap))}.col-sm-2{width:calc(16.6666666667% - var(--gridGap))}.col-sm-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-12{width:calc(100% - var(--gridGap))}.col-md-11{width:calc(91.6666666667% - var(--gridGap))}.col-md-10{width:calc(83.3333333333% - var(--gridGap))}.col-md-9{width:calc(75% - var(--gridGap))}.col-md-8{width:calc(66.6666666667% - var(--gridGap))}.col-md-7{width:calc(58.3333333333% - var(--gridGap))}.col-md-6{width:calc(50% - var(--gridGap))}.col-md-5{width:calc(41.6666666667% - var(--gridGap))}.col-md-4{width:calc(33.3333333333% - var(--gridGap))}.col-md-3{width:calc(25% - var(--gridGap))}.col-md-2{width:calc(16.6666666667% - var(--gridGap))}.col-md-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-12{width:calc(100% - var(--gridGap))}.col-lg-11{width:calc(91.6666666667% - var(--gridGap))}.col-lg-10{width:calc(83.3333333333% - var(--gridGap))}.col-lg-9{width:calc(75% - var(--gridGap))}.col-lg-8{width:calc(66.6666666667% - var(--gridGap))}.col-lg-7{width:calc(58.3333333333% - var(--gridGap))}.col-lg-6{width:calc(50% - var(--gridGap))}.col-lg-5{width:calc(41.6666666667% - var(--gridGap))}.col-lg-4{width:calc(33.3333333333% - var(--gridGap))}.col-lg-3{width:calc(25% - var(--gridGap))}.col-lg-2{width:calc(16.6666666667% - var(--gridGap))}.col-lg-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-12{width:calc(100% - var(--gridGap))}.col-xl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xl-9{width:calc(75% - var(--gridGap))}.col-xl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xl-6{width:calc(50% - var(--gridGap))}.col-xl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xl-3{width:calc(25% - var(--gridGap))}.col-xl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xl-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-12{width:calc(100% - var(--gridGap))}.col-xxl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xxl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xxl-9{width:calc(75% - var(--gridGap))}.col-xxl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xxl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xxl-6{width:calc(50% - var(--gridGap))}.col-xxl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xxl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xxl-3{width:calc(25% - var(--gridGap))}.col-xxl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xxl-1{width:calc(8.3333333333% - var(--gridGap))}}.app-tooltip{position:fixed;z-index:999999;top:0;left:0;display:inline-block;vertical-align:top;max-width:275px;padding:3px 5px;color:#fff;text-align:center;font-family:var(--baseFontFamily);font-size:var(--smFontSize);line-height:var(--smLineHeight);border-radius:var(--baseRadius);background:var(--tooltipColor);pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed),transform var(--baseAnimationSpeed);transform:translateY(1px);backface-visibility:hidden;white-space:pre-line;word-break:break-word;opacity:0;visibility:hidden}.app-tooltip.code{font-family:monospace;white-space:pre-wrap;text-align:left;min-width:150px;max-width:340px}.app-tooltip.active{transform:scale(1);opacity:1;visibility:visible}.dropdown{position:absolute;z-index:99;right:0;left:auto;top:100%;cursor:default;display:inline-block;vertical-align:top;padding:5px;margin:5px 0 0;width:auto;min-width:140px;max-width:450px;max-height:330px;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);border-radius:var(--baseRadius);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor)}.dropdown hr{margin:5px 0}.dropdown .dropdown-item{border:0;background:none;position:relative;outline:0;display:flex;align-items:center;column-gap:8px;width:100%;height:auto;min-height:0;text-align:left;padding:8px 10px;margin:0 0 5px;cursor:pointer;color:var(--txtPrimaryColor);font-weight:400;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);line-height:var(--baseLineHeight);border-radius:var(--baseRadius);text-decoration:none;word-break:break-word;-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.dropdown .dropdown-item:last-child{margin-bottom:0}.dropdown .dropdown-item.selected{background:var(--baseAlt2Color)}.dropdown .dropdown-item:focus-visible,.dropdown .dropdown-item:hover{background:var(--baseAlt1Color)}.dropdown .dropdown-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.dropdown .dropdown-item.disabled{color:var(--txtDisabledColor);background:none;pointer-events:none}.dropdown .dropdown-item.separator{cursor:default;background:none;text-transform:uppercase;padding-top:0;padding-bottom:0;margin-top:15px;color:var(--txtDisabledColor);font-weight:600;font-size:var(--smFontSize)}.dropdown.dropdown-upside{top:auto;bottom:100%;margin:0 0 5px}.dropdown.dropdown-left{right:auto;left:0}.dropdown.dropdown-center{right:auto;left:50%;transform:translate(-50%)}.dropdown.dropdown-sm{margin-top:5px;min-width:100px}.dropdown.dropdown-sm .dropdown-item{column-gap:7px;font-size:var(--smFontSize);margin:0 0 2px;padding:5px 7px}.dropdown.dropdown-sm .dropdown-item:last-child{margin-bottom:0}.dropdown.dropdown-sm.dropdown-upside{margin-top:0;margin-bottom:5px}.dropdown.dropdown-block{width:100%;min-width:130px;max-width:100%}.dropdown.dropdown-nowrap{white-space:nowrap}.toggler-container{outline:0}.overlay-panel{position:relative;z-index:1;display:flex;flex-direction:column;align-self:flex-end;margin-left:auto;background:var(--baseColor);height:100%;width:580px;max-width:100%;word-wrap:break-word;box-shadow:0 2px 5px 0 var(--shadowColor)}.overlay-panel .overlay-panel-section{position:relative;width:100%;margin:0;padding:var(--baseSpacing);transition:box-shadow var(--baseAnimationSpeed)}.overlay-panel .overlay-panel-section:empty{display:none}.overlay-panel .overlay-panel-section:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.overlay-panel .overlay-panel-section:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.overlay-panel .overlay-panel-section .btn{flex-grow:0}.overlay-panel img{max-width:100%}.overlay-panel .panel-header{position:relative;z-index:2;display:flex;flex-wrap:wrap;align-items:center;column-gap:10px;row-gap:var(--baseSpacing);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel .panel-header>*{margin-top:0;margin-bottom:0}.overlay-panel .panel-header .btn-back{margin-left:-10px}.overlay-panel .panel-header .overlay-close{z-index:3;outline:0;position:absolute;right:100%;top:20px;margin:0;display:inline-flex;align-items:center;justify-content:center;width:35px;height:35px;cursor:pointer;text-align:center;font-size:1.6rem;line-height:1;border-radius:50% 0 0 50%;color:#fff;background:var(--primaryColor);opacity:.5;transition:opacity var(--baseAnimationSpeed);-webkit-user-select:none;user-select:none}.overlay-panel .panel-header .overlay-close i{font-size:inherit}.overlay-panel .panel-header .overlay-close:hover,.overlay-panel .panel-header .overlay-close:focus-visible,.overlay-panel .panel-header .overlay-close:active{opacity:.7}.overlay-panel .panel-header .overlay-close:active{transition-duration:var(--activeAnimationSpeed);opacity:1}.overlay-panel .panel-header .btn-close{margin-right:-10px}.overlay-panel .panel-header .tabs-header{margin-bottom:-24px}.overlay-panel .panel-content{z-index:auto;flex-grow:1;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;scroll-behavior:smooth}.tox-fullscreen .overlay-panel .panel-content{z-index:9}.overlay-panel .panel-header~.panel-content{padding-top:5px}.overlay-panel .panel-footer{z-index:2;column-gap:var(--smSpacing);display:flex;align-items:center;justify-content:flex-end;border-top:1px solid var(--baseAlt2Color);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel.scrollable .panel-header{box-shadow:0 4px 5px #0000000d}.overlay-panel.scrollable .panel-footer{box-shadow:0 -4px 5px #0000000d}.overlay-panel.scrollable.scroll-top-reached .panel-header,.overlay-panel.scrollable.scroll-bottom-reached .panel-footer{box-shadow:none}.overlay-panel.overlay-panel-xl{width:850px}.overlay-panel.overlay-panel-lg{width:700px}.overlay-panel.overlay-panel-sm{width:460px}.overlay-panel.popup{height:auto;max-height:100%;align-self:center;border-radius:var(--baseRadius);margin:0 auto}.overlay-panel.popup .panel-footer{background:var(--bodyColor)}.overlay-panel.hide-content .panel-content{display:none}.overlay-panel.colored-header .panel-header{background:var(--bodyColor);border-bottom:1px solid var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header .tabs-header{border-bottom:0}.overlay-panel.colored-header .panel-header .tabs-header .tab-item{border:1px solid transparent;border-bottom:0}.overlay-panel.colored-header .panel-header .tabs-header .tab-item:hover,.overlay-panel.colored-header .panel-header .tabs-header .tab-item:focus-visible{background:var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header .tabs-header .tab-item:after{content:none;display:none}.overlay-panel.colored-header .panel-header .tabs-header .tab-item.active{background:var(--baseColor);border-color:var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header~.panel-content{padding-top:calc(var(--baseSpacing) - 5px)}.overlay-panel.compact-header .panel-header{row-gap:var(--smSpacing)}.overlay-panel.full-width-popup{width:100%}.overlay-panel.preview .panel-header{position:absolute;z-index:99;box-shadow:none}.overlay-panel.preview .panel-header .overlay-close{left:100%;right:auto;border-radius:0 50% 50% 0}.overlay-panel.preview .panel-header .overlay-close i{margin-right:5px}.overlay-panel.preview .panel-header,.overlay-panel.preview .panel-footer{padding:10px 15px}.overlay-panel.preview .panel-content{padding:0;text-align:center;display:flex;align-items:center;justify-content:center}.overlay-panel.preview img{max-width:100%;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.overlay-panel.preview object{position:absolute;z-index:1;left:0;top:0;width:100%;height:100%}.overlay-panel.preview.preview-image{width:auto;min-width:320px;min-height:300px;max-width:75%;max-height:90%}.overlay-panel.preview.preview-image img{align-self:flex-start;margin:auto}.overlay-panel.preview.preview-document,.overlay-panel.preview.preview-video{width:75%;height:90%}.overlay-panel.preview.preview-audio{min-width:320px;min-height:300px;max-width:90%;max-height:90%}@media (max-width: 900px){.overlay-panel .overlay-panel-section{padding:var(--smSpacing)}}.overlay-panel-container{display:flex;position:fixed;z-index:1000;flex-direction:row;align-items:center;top:0;left:0;width:100%;height:100%;overflow:hidden;margin:0;padding:0;outline:0}.overlay-panel-container .overlay{position:absolute;z-index:0;left:0;top:0;width:100%;height:100%;-webkit-user-select:none;user-select:none;background:var(--overlayColor)}.overlay-panel-container.padded{padding:10px}.overlay-panel-wrapper{position:relative;z-index:1000;outline:0}.alert{position:relative;display:flex;column-gap:15px;align-items:center;width:100%;min-height:50px;max-width:100%;word-break:break-word;margin:0 0 var(--baseSpacing);border-radius:var(--baseRadius);padding:12px 15px;background:var(--baseAlt1Color);color:var(--txtAltColor)}.alert .content,.alert .form-field .help-block,.form-field .alert .help-block,.alert .panel,.alert .sub-panel,.alert .overlay-panel .panel-content,.overlay-panel .alert .panel-content{flex-grow:1}.alert .icon,.alert .close{display:inline-flex;align-items:center;justify-content:center;flex-grow:0;flex-shrink:0;text-align:center}.alert .icon{align-self:stretch;font-size:1.2em;padding-right:15px;font-weight:400;border-right:1px solid rgba(0,0,0,.05);color:var(--txtHintColor)}.alert .close{display:inline-flex;margin-right:-5px;width:28px;height:28px;outline:0;cursor:pointer;text-align:center;font-size:var(--smFontSize);line-height:28px;border-radius:28px;text-decoration:none;color:inherit;opacity:.5;transition:opacity var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.alert .close:hover,.alert .close:focus{opacity:1;background:#fff3}.alert .close:active{opacity:1;background:#ffffff4d;transition-duration:var(--activeAnimationSpeed)}.alert code,.alert hr{background:#0000001a}.alert.alert-info{background:var(--infoAltColor)}.alert.alert-info .icon{color:var(--infoColor)}.alert.alert-warning{background:var(--warningAltColor)}.alert.alert-warning .icon{color:var(--warningColor)}.alert.alert-success{background:var(--successAltColor)}.alert.alert-success .icon{color:var(--successColor)}.alert.alert-danger{background:var(--dangerAltColor)}.alert.alert-danger .icon{color:var(--dangerColor)}.toasts-wrapper{position:fixed;z-index:999999;bottom:0;left:0;right:0;padding:0 var(--smSpacing);width:auto;display:block;text-align:center;pointer-events:none}.toasts-wrapper .alert{text-align:left;pointer-events:auto;width:var(--smWrapperWidth);margin:var(--baseSpacing) auto;box-shadow:0 2px 5px 0 var(--shadowColor)}@media screen and (min-width: 980px){body:not(.overlay-active):has(.app-sidebar) .toasts-wrapper{left:var(--appSidebarWidth)}body:not(.overlay-active):has(.page-sidebar) .toasts-wrapper{left:calc(var(--appSidebarWidth) + var(--pageSidebarWidth))}}button{outline:0;border:0;background:none;padding:0;text-align:left;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit}.btn{position:relative;z-index:1;display:inline-flex;vertical-align:top;align-items:center;justify-content:center;outline:0;border:0;margin:0;flex-shrink:0;cursor:pointer;padding:5px 20px;column-gap:7px;-webkit-user-select:none;user-select:none;min-width:var(--btnHeight);min-height:var(--btnHeight);text-align:center;text-decoration:none;line-height:1;font-weight:600;color:#fff;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);border-radius:var(--btnRadius);background:none;transition:color var(--baseAnimationSpeed)}.btn i{font-size:1.1428em;vertical-align:middle;display:inline-block}.btn .dropdown{-webkit-user-select:text;user-select:text}.btn:before{content:"";border-radius:inherit;position:absolute;left:0;top:0;z-index:-1;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;user-select:none;backface-visibility:hidden;background:var(--primaryColor);transition:filter var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.btn:hover:before,.btn:focus-visible:before{opacity:.9}.btn.active,.btn:active{z-index:999}.btn.active:before,.btn:active:before{opacity:.8;transition-duration:var(--activeAnimationSpeed)}.btn.btn-info:before{background:var(--infoColor)}.btn.btn-info:hover:before,.btn.btn-info:focus-visible:before{opacity:.8}.btn.btn-info:active:before{opacity:.7}.btn.btn-success:before{background:var(--successColor)}.btn.btn-success:hover:before,.btn.btn-success:focus-visible:before{opacity:.8}.btn.btn-success:active:before{opacity:.7}.btn.btn-danger:before{background:var(--dangerColor)}.btn.btn-danger:hover:before,.btn.btn-danger:focus-visible:before{opacity:.8}.btn.btn-danger:active:before{opacity:.7}.btn.btn-warning:before{background:var(--warningColor)}.btn.btn-warning:hover:before,.btn.btn-warning:focus-visible:before{opacity:.8}.btn.btn-warning:active:before{opacity:.7}.btn.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-hint:hover:before,.btn.btn-hint:focus-visible:before{opacity:.8}.btn.btn-hint:active:before{opacity:.7}.btn.btn-outline{border:2px solid currentColor;background:#fff}.btn.btn-secondary,.btn.btn-transparent,.btn.btn-outline{box-shadow:none;color:var(--txtPrimaryColor)}.btn.btn-secondary:before,.btn.btn-transparent:before,.btn.btn-outline:before{opacity:0}.btn.btn-secondary:focus-visible:before,.btn.btn-secondary:hover:before,.btn.btn-transparent:focus-visible:before,.btn.btn-transparent:hover:before,.btn.btn-outline:focus-visible:before,.btn.btn-outline:hover:before{opacity:.3}.btn.btn-secondary.active:before,.btn.btn-secondary:active:before,.btn.btn-transparent.active:before,.btn.btn-transparent:active:before,.btn.btn-outline.active:before,.btn.btn-outline:active:before{opacity:.45}.btn.btn-secondary:before,.btn.btn-transparent:before,.btn.btn-outline:before{background:var(--baseAlt3Color)}.btn.btn-secondary.btn-info,.btn.btn-transparent.btn-info,.btn.btn-outline.btn-info{color:var(--infoColor)}.btn.btn-secondary.btn-info:before,.btn.btn-transparent.btn-info:before,.btn.btn-outline.btn-info:before{opacity:0}.btn.btn-secondary.btn-info:focus-visible:before,.btn.btn-secondary.btn-info:hover:before,.btn.btn-transparent.btn-info:focus-visible:before,.btn.btn-transparent.btn-info:hover:before,.btn.btn-outline.btn-info:focus-visible:before,.btn.btn-outline.btn-info:hover:before{opacity:.15}.btn.btn-secondary.btn-info.active:before,.btn.btn-secondary.btn-info:active:before,.btn.btn-transparent.btn-info.active:before,.btn.btn-transparent.btn-info:active:before,.btn.btn-outline.btn-info.active:before,.btn.btn-outline.btn-info:active:before{opacity:.25}.btn.btn-secondary.btn-info:before,.btn.btn-transparent.btn-info:before,.btn.btn-outline.btn-info:before{background:var(--infoColor)}.btn.btn-secondary.btn-success,.btn.btn-transparent.btn-success,.btn.btn-outline.btn-success{color:var(--successColor)}.btn.btn-secondary.btn-success:before,.btn.btn-transparent.btn-success:before,.btn.btn-outline.btn-success:before{opacity:0}.btn.btn-secondary.btn-success:focus-visible:before,.btn.btn-secondary.btn-success:hover:before,.btn.btn-transparent.btn-success:focus-visible:before,.btn.btn-transparent.btn-success:hover:before,.btn.btn-outline.btn-success:focus-visible:before,.btn.btn-outline.btn-success:hover:before{opacity:.15}.btn.btn-secondary.btn-success.active:before,.btn.btn-secondary.btn-success:active:before,.btn.btn-transparent.btn-success.active:before,.btn.btn-transparent.btn-success:active:before,.btn.btn-outline.btn-success.active:before,.btn.btn-outline.btn-success:active:before{opacity:.25}.btn.btn-secondary.btn-success:before,.btn.btn-transparent.btn-success:before,.btn.btn-outline.btn-success:before{background:var(--successColor)}.btn.btn-secondary.btn-danger,.btn.btn-transparent.btn-danger,.btn.btn-outline.btn-danger{color:var(--dangerColor)}.btn.btn-secondary.btn-danger:before,.btn.btn-transparent.btn-danger:before,.btn.btn-outline.btn-danger:before{opacity:0}.btn.btn-secondary.btn-danger:focus-visible:before,.btn.btn-secondary.btn-danger:hover:before,.btn.btn-transparent.btn-danger:focus-visible:before,.btn.btn-transparent.btn-danger:hover:before,.btn.btn-outline.btn-danger:focus-visible:before,.btn.btn-outline.btn-danger:hover:before{opacity:.15}.btn.btn-secondary.btn-danger.active:before,.btn.btn-secondary.btn-danger:active:before,.btn.btn-transparent.btn-danger.active:before,.btn.btn-transparent.btn-danger:active:before,.btn.btn-outline.btn-danger.active:before,.btn.btn-outline.btn-danger:active:before{opacity:.25}.btn.btn-secondary.btn-danger:before,.btn.btn-transparent.btn-danger:before,.btn.btn-outline.btn-danger:before{background:var(--dangerColor)}.btn.btn-secondary.btn-warning,.btn.btn-transparent.btn-warning,.btn.btn-outline.btn-warning{color:var(--warningColor)}.btn.btn-secondary.btn-warning:before,.btn.btn-transparent.btn-warning:before,.btn.btn-outline.btn-warning:before{opacity:0}.btn.btn-secondary.btn-warning:focus-visible:before,.btn.btn-secondary.btn-warning:hover:before,.btn.btn-transparent.btn-warning:focus-visible:before,.btn.btn-transparent.btn-warning:hover:before,.btn.btn-outline.btn-warning:focus-visible:before,.btn.btn-outline.btn-warning:hover:before{opacity:.15}.btn.btn-secondary.btn-warning.active:before,.btn.btn-secondary.btn-warning:active:before,.btn.btn-transparent.btn-warning.active:before,.btn.btn-transparent.btn-warning:active:before,.btn.btn-outline.btn-warning.active:before,.btn.btn-outline.btn-warning:active:before{opacity:.25}.btn.btn-secondary.btn-warning:before,.btn.btn-transparent.btn-warning:before,.btn.btn-outline.btn-warning:before{background:var(--warningColor)}.btn.btn-secondary.btn-hint,.btn.btn-transparent.btn-hint,.btn.btn-outline.btn-hint{color:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint:before,.btn.btn-transparent.btn-hint:before,.btn.btn-outline.btn-hint:before{opacity:0}.btn.btn-secondary.btn-hint:focus-visible:before,.btn.btn-secondary.btn-hint:hover:before,.btn.btn-transparent.btn-hint:focus-visible:before,.btn.btn-transparent.btn-hint:hover:before,.btn.btn-outline.btn-hint:focus-visible:before,.btn.btn-outline.btn-hint:hover:before{opacity:.15}.btn.btn-secondary.btn-hint.active:before,.btn.btn-secondary.btn-hint:active:before,.btn.btn-transparent.btn-hint.active:before,.btn.btn-transparent.btn-hint:active:before,.btn.btn-outline.btn-hint.active:before,.btn.btn-outline.btn-hint:active:before{opacity:.25}.btn.btn-secondary.btn-hint:before,.btn.btn-transparent.btn-hint:before,.btn.btn-outline.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint,.btn.btn-transparent.btn-hint,.btn.btn-outline.btn-hint{color:var(--txtHintColor)}.btn.btn-secondary.btn-hint:focus-visible,.btn.btn-secondary.btn-hint:hover,.btn.btn-secondary.btn-hint:active,.btn.btn-secondary.btn-hint.active,.btn.btn-transparent.btn-hint:focus-visible,.btn.btn-transparent.btn-hint:hover,.btn.btn-transparent.btn-hint:active,.btn.btn-transparent.btn-hint.active,.btn.btn-outline.btn-hint:focus-visible,.btn.btn-outline.btn-hint:hover,.btn.btn-outline.btn-hint:active,.btn.btn-outline.btn-hint.active{color:var(--txtPrimaryColor)}.btn.btn-secondary:before{opacity:.35}.btn.btn-secondary:focus-visible:before,.btn.btn-secondary:hover:before{opacity:.5}.btn.btn-secondary.active:before,.btn.btn-secondary:active:before{opacity:.7}.btn.btn-secondary.btn-info:before{opacity:.15}.btn.btn-secondary.btn-info:focus-visible:before,.btn.btn-secondary.btn-info:hover:before{opacity:.25}.btn.btn-secondary.btn-info.active:before,.btn.btn-secondary.btn-info:active:before{opacity:.3}.btn.btn-secondary.btn-success:before{opacity:.15}.btn.btn-secondary.btn-success:focus-visible:before,.btn.btn-secondary.btn-success:hover:before{opacity:.25}.btn.btn-secondary.btn-success.active:before,.btn.btn-secondary.btn-success:active:before{opacity:.3}.btn.btn-secondary.btn-danger:before{opacity:.15}.btn.btn-secondary.btn-danger:focus-visible:before,.btn.btn-secondary.btn-danger:hover:before{opacity:.25}.btn.btn-secondary.btn-danger.active:before,.btn.btn-secondary.btn-danger:active:before{opacity:.3}.btn.btn-secondary.btn-warning:before{opacity:.15}.btn.btn-secondary.btn-warning:focus-visible:before,.btn.btn-secondary.btn-warning:hover:before{opacity:.25}.btn.btn-secondary.btn-warning.active:before,.btn.btn-secondary.btn-warning:active:before{opacity:.3}.btn.btn-secondary.btn-hint:before{opacity:.15}.btn.btn-secondary.btn-hint:focus-visible:before,.btn.btn-secondary.btn-hint:hover:before{opacity:.25}.btn.btn-secondary.btn-hint.active:before,.btn.btn-secondary.btn-hint:active:before{opacity:.3}.btn.btn-disabled,.btn[disabled]{box-shadow:none;cursor:default;background:var(--baseAlt1Color);color:var(--txtDisabledColor)!important}.btn.btn-disabled:before,.btn[disabled]:before{display:none}.btn.btn-disabled.btn-transparent,.btn[disabled].btn-transparent{background:none}.btn.btn-disabled.btn-outline,.btn[disabled].btn-outline{border-color:var(--baseAlt2Color)}.btn.txt-left{text-align:left;justify-content:flex-start}.btn.txt-right{text-align:right;justify-content:flex-end}.btn.btn-expanded{min-width:150px}.btn.btn-expanded-sm{min-width:90px}.btn.btn-expanded-lg{min-width:170px}.btn.btn-lg{column-gap:10px;font-size:var(--lgFontSize);min-height:var(--lgBtnHeight);min-width:var(--lgBtnHeight);padding-left:30px;padding-right:30px}.btn.btn-lg i{font-size:1.2666em}.btn.btn-lg.btn-expanded{min-width:240px}.btn.btn-lg.btn-expanded-sm{min-width:160px}.btn.btn-lg.btn-expanded-lg{min-width:300px}.btn.btn-sm,.btn.btn-xs{column-gap:5px;font-size:var(--smFontSize);min-height:var(--smBtnHeight);min-width:var(--smBtnHeight);padding-left:12px;padding-right:12px}.btn.btn-sm i,.btn.btn-xs i{font-size:1rem}.btn.btn-sm.btn-expanded,.btn.btn-xs.btn-expanded{min-width:100px}.btn.btn-sm.btn-expanded-sm,.btn.btn-xs.btn-expanded-sm{min-width:80px}.btn.btn-sm.btn-expanded-lg,.btn.btn-xs.btn-expanded-lg{min-width:130px}.btn.btn-xs{padding-left:7px;padding-right:7px;min-width:var(--xsBtnHeight);min-height:var(--xsBtnHeight)}.btn.btn-block{display:flex;width:100%}.btn.btn-pill{border-radius:30px}.btn.btn-circle{border-radius:50%;padding:0;gap:0}.btn.btn-circle i{font-size:1.2857rem;text-align:center;width:19px;height:19px;line-height:19px}.btn.btn-circle i:before{margin:0;display:block}.btn.btn-circle.btn-sm i{font-size:1.1rem}.btn.btn-circle.btn-xs i{font-size:1.05rem}.btn.btn-loading{--loaderSize: 24px;cursor:default;pointer-events:none}.btn.btn-loading:after{content:"";position:absolute;display:inline-block;vertical-align:top;left:50%;top:50%;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);color:inherit;text-align:center;font-weight:400;margin-left:calc(var(--loaderSize) * -.5);margin-top:calc(var(--loaderSize) * -.5);font-family:var(--iconFontFamily);animation:loaderShow var(--baseAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.btn.btn-loading>*{opacity:0;transform:scale(.9)}.btn.btn-loading.btn-sm,.btn.btn-loading.btn-xs{--loaderSize: 20px}.btn.btn-loading.btn-lg{--loaderSize: 28px}.btn.btn-prev i,.btn.btn-next i{transition:transform var(--baseAnimationSpeed)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i,.btn.btn-next:hover i,.btn.btn-next:focus-within i{transform:translate(3px)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i{transform:translate(-3px)}.btn.btn-horizontal-sticky{position:sticky;left:var(--xsSpacing);right:var(--xsSpacing)}.btns-group{display:inline-flex;align-items:center;gap:var(--xsSpacing)}.btns-group.no-gap{gap:0}.btns-group.no-gap>*{border-radius:0;min-width:0;box-shadow:-1px 0 #ffffff1a}.btns-group.no-gap>*:first-child{border-top-left-radius:var(--btnRadius);border-bottom-left-radius:var(--btnRadius);box-shadow:none}.btns-group.no-gap>*:last-child{border-top-right-radius:var(--btnRadius);border-bottom-right-radius:var(--btnRadius)}.tinymce-wrapper,.code-editor,.select .selected-container,input,select,textarea{display:block;width:100%;outline:0;border:0;margin:0;background:none;padding:5px 10px;line-height:20px;min-width:0;min-height:var(--inputHeight);background:var(--baseAlt1Color);color:var(--txtPrimaryColor);font-size:var(--baseFontSize);font-family:var(--baseFontFamily);font-weight:400;border-radius:var(--baseRadius);overflow:auto;overflow:overlay}.tinymce-wrapper::placeholder,.code-editor::placeholder,.select .selected-container::placeholder,input::placeholder,select::placeholder,textarea::placeholder{color:var(--txtDisabledColor)}@media screen and (min-width: 550px){.tinymce-wrapper:focus::-webkit-scrollbar,.code-editor:focus::-webkit-scrollbar,.select .selected-container:focus::-webkit-scrollbar,input:focus::-webkit-scrollbar,select:focus::-webkit-scrollbar,textarea:focus::-webkit-scrollbar,.tinymce-wrapper:focus-within::-webkit-scrollbar,.code-editor:focus-within::-webkit-scrollbar,.select .selected-container:focus-within::-webkit-scrollbar,input:focus-within::-webkit-scrollbar,select:focus-within::-webkit-scrollbar,textarea:focus-within::-webkit-scrollbar{width:8px;height:8px;border-radius:var(--baseRadius)}.tinymce-wrapper:focus::-webkit-scrollbar-track,.code-editor:focus::-webkit-scrollbar-track,.select .selected-container:focus::-webkit-scrollbar-track,input:focus::-webkit-scrollbar-track,select:focus::-webkit-scrollbar-track,textarea:focus::-webkit-scrollbar-track,.tinymce-wrapper:focus-within::-webkit-scrollbar-track,.code-editor:focus-within::-webkit-scrollbar-track,.select .selected-container:focus-within::-webkit-scrollbar-track,input:focus-within::-webkit-scrollbar-track,select:focus-within::-webkit-scrollbar-track,textarea:focus-within::-webkit-scrollbar-track{background:transparent;border-radius:var(--baseRadius)}.tinymce-wrapper:focus::-webkit-scrollbar-thumb,.code-editor:focus::-webkit-scrollbar-thumb,.select .selected-container:focus::-webkit-scrollbar-thumb,input:focus::-webkit-scrollbar-thumb,select:focus::-webkit-scrollbar-thumb,textarea:focus::-webkit-scrollbar-thumb,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb,.code-editor:focus-within::-webkit-scrollbar-thumb,.select .selected-container:focus-within::-webkit-scrollbar-thumb,input:focus-within::-webkit-scrollbar-thumb,select:focus-within::-webkit-scrollbar-thumb,textarea:focus-within::-webkit-scrollbar-thumb{background-color:var(--baseAlt3Color);border-radius:15px;border:2px solid transparent;background-clip:padding-box}.tinymce-wrapper:focus::-webkit-scrollbar-thumb:hover,.code-editor:focus::-webkit-scrollbar-thumb:hover,.select .selected-container:focus::-webkit-scrollbar-thumb:hover,input:focus::-webkit-scrollbar-thumb:hover,select:focus::-webkit-scrollbar-thumb:hover,textarea:focus::-webkit-scrollbar-thumb:hover,.tinymce-wrapper:focus::-webkit-scrollbar-thumb:active,.code-editor:focus::-webkit-scrollbar-thumb:active,.select .selected-container:focus::-webkit-scrollbar-thumb:active,input:focus::-webkit-scrollbar-thumb:active,select:focus::-webkit-scrollbar-thumb:active,textarea:focus::-webkit-scrollbar-thumb:active,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb:hover,.code-editor:focus-within::-webkit-scrollbar-thumb:hover,.select .selected-container:focus-within::-webkit-scrollbar-thumb:hover,input:focus-within::-webkit-scrollbar-thumb:hover,select:focus-within::-webkit-scrollbar-thumb:hover,textarea:focus-within::-webkit-scrollbar-thumb:hover,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb:active,.code-editor:focus-within::-webkit-scrollbar-thumb:active,.select .selected-container:focus-within::-webkit-scrollbar-thumb:active,input:focus-within::-webkit-scrollbar-thumb:active,select:focus-within::-webkit-scrollbar-thumb:active,textarea:focus-within::-webkit-scrollbar-thumb:active{background-color:var(--baseAlt4Color)}.tinymce-wrapper:focus,.code-editor:focus,.select .selected-container:focus,input:focus,select:focus,textarea:focus,.tinymce-wrapper:focus-within,.code-editor:focus-within,.select .selected-container:focus-within,input:focus-within,select:focus-within,textarea:focus-within{scrollbar-color:var(--baseAlt3Color) transparent;scrollbar-width:thin;scroll-behavior:smooth}}[readonly].tinymce-wrapper,[readonly].code-editor,.select [readonly].selected-container,input[readonly],select[readonly],textarea[readonly],.readonly.tinymce-wrapper,.readonly.code-editor,.select .readonly.selected-container,input.readonly,select.readonly,textarea.readonly{cursor:default;color:var(--txtHintColor)}[disabled].tinymce-wrapper,[disabled].code-editor,.select [disabled].selected-container,input[disabled],select[disabled],textarea[disabled],.disabled.tinymce-wrapper,.disabled.code-editor,.select .disabled.selected-container,input.disabled,select.disabled,textarea.disabled{cursor:default;color:var(--txtDisabledColor)}.txt-mono.tinymce-wrapper,.txt-mono.code-editor,.select .txt-mono.selected-container,input.txt-mono,select.txt-mono,textarea.txt-mono{line-height:var(--smLineHeight)}.code.tinymce-wrapper,.code.code-editor,.select .code.selected-container,input.code,select.code,textarea.code{font-size:15px;line-height:1.379rem;font-family:var(--monospaceFontFamily)}input{height:var(--inputHeight)}input[list]::-webkit-calendar-picker-indicator{display:none!important}input:-webkit-autofill{-webkit-text-fill-color:var(--txtPrimaryColor);-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt1Color)}.form-field:focus-within input:-webkit-autofill,input:-webkit-autofill:focus{-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt2Color)}input[type=file]{padding:9px}input[type=checkbox],input[type=radio]{width:auto;height:auto;display:inline}input[type=number]{-moz-appearance:textfield;-webkit-appearance:textfield;appearance:textfield}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none}textarea{min-height:80px;resize:vertical}select{padding-left:8px}.form-field{--hPadding: 15px;position:relative;display:block;width:100%;margin-bottom:var(--baseSpacing)}.form-field .tinymce-wrapper,.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea{z-index:0;padding-left:var(--hPadding);padding-right:var(--hPadding)}.form-field select{padding-left:8px}.form-field label{display:flex;width:100%;column-gap:5px;align-items:center;-webkit-user-select:none;user-select:none;font-weight:600;font-size:var(--smFontSize);letter-spacing:.1px;color:var(--txtHintColor);line-height:1;padding-top:12px;padding-bottom:3px;padding-left:var(--hPadding);padding-right:var(--hPadding);border:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.form-field label~.tinymce-wrapper,.form-field label~.code-editor,.form-field .select label~.selected-container,.select .form-field label~.selected-container,.form-field label~input,.form-field label~select,.form-field label~textarea,.form-field label~div .tinymce-wrapper,.form-field label~div .code-editor,.form-field label~div .select .selected-container,.select .form-field label~div .selected-container,.form-field label~div input,.form-field label~div select,.form-field label~div textarea{border-top:0;padding-top:2px;padding-bottom:8px;border-top-left-radius:0;border-top-right-radius:0}.form-field label i{font-size:.96rem;margin-bottom:-1px}.form-field label i:before{margin:0}.form-field .tinymce-wrapper,.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea,.form-field label{background:var(--baseAlt1Color);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.form-field:focus-within .tinymce-wrapper,.form-field:focus-within .code-editor,.form-field:focus-within .select .selected-container,.select .form-field:focus-within .selected-container,.form-field:focus-within input,.form-field:focus-within select,.form-field:focus-within textarea,.form-field:focus-within label{background:var(--baseAlt2Color)}.form-field:focus-within label{color:var(--txtPrimaryColor)}.form-field .form-field-addon{position:absolute;display:inline-flex;align-items:center;z-index:1;top:0;right:var(--hPadding);min-height:var(--inputHeight);color:var(--txtHintColor)}.form-field .form-field-addon .btn{margin-right:-5px}.form-field .form-field-addon:not(.prefix)~.tinymce-wrapper,.form-field .form-field-addon:not(.prefix)~.code-editor,.form-field .select .form-field-addon:not(.prefix)~.selected-container,.select .form-field .form-field-addon:not(.prefix)~.selected-container,.form-field .form-field-addon:not(.prefix)~input,.form-field .form-field-addon:not(.prefix)~select,.form-field .form-field-addon:not(.prefix)~textarea{padding-right:45px}.form-field .form-field-addon.prefix{right:auto;left:var(--hPadding)}.form-field .form-field-addon.prefix~.tinymce-wrapper,.form-field .form-field-addon.prefix~.code-editor,.form-field .select .form-field-addon.prefix~.selected-container,.select .form-field .form-field-addon.prefix~.selected-container,.form-field .form-field-addon.prefix~input,.form-field .form-field-addon.prefix~select,.form-field .form-field-addon.prefix~textarea{padding-left:45px}.form-field label~.form-field-addon{min-height:calc(26px + var(--inputHeight))}.form-field .help-block{position:relative;margin-top:8px;font-size:var(--smFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor);word-break:break-word}.form-field .help-block pre{white-space:pre-wrap}.form-field .help-block-error{color:var(--dangerColor)}.form-field.error>label,.form-field.invalid>label{color:var(--dangerColor)}.form-field.invalid label,.form-field.invalid .tinymce-wrapper,.form-field.invalid .code-editor,.form-field.invalid .select .selected-container,.select .form-field.invalid .selected-container,.form-field.invalid input,.form-field.invalid select,.form-field.invalid textarea{background:var(--dangerAltColor)}.form-field.required:not(.form-field-toggle)>label:after{content:"*";color:var(--dangerColor);margin-top:-2px;margin-left:-2px}.form-field.readonly label,.form-field.readonly .tinymce-wrapper,.form-field.readonly .code-editor,.form-field.readonly .select .selected-container,.select .form-field.readonly .selected-container,.form-field.readonly input,.form-field.readonly select,.form-field.readonly textarea,.form-field.disabled label,.form-field.disabled .tinymce-wrapper,.form-field.disabled .code-editor,.form-field.disabled .select .selected-container,.select .form-field.disabled .selected-container,.form-field.disabled input,.form-field.disabled select,.form-field.disabled textarea{background:var(--baseAlt1Color)}.form-field.readonly>label,.form-field.disabled>label{color:var(--txtHintColor)}.form-field.readonly.required>label:after,.form-field.disabled.required>label:after{opacity:.5}.form-field.disabled label,.form-field.disabled .tinymce-wrapper,.form-field.disabled .code-editor,.form-field.disabled .select .selected-container,.select .form-field.disabled .selected-container,.form-field.disabled input,.form-field.disabled select,.form-field.disabled textarea{box-shadow:inset 0 0 0 var(--btnHeight) #ffffff73}.form-field.disabled>label{color:var(--txtDisabledColor)}.form-field input[type=radio],.form-field input[type=checkbox]{position:absolute;z-index:-1;left:0;width:0;height:0;min-height:0;min-width:0;border:0;background:none;-webkit-user-select:none;user-select:none;pointer-events:none;box-shadow:none;opacity:0}.form-field input[type=radio]~label,.form-field input[type=checkbox]~label{border:0;margin:0;outline:0;background:none;display:inline-flex;vertical-align:top;align-items:center;width:auto;column-gap:5px;-webkit-user-select:none;user-select:none;padding:0 0 0 27px;line-height:20px;min-height:20px;font-weight:400;font-size:var(--baseFontSize);text-transform:none;color:var(--txtPrimaryColor)}.form-field input[type=radio]~label:before,.form-field input[type=checkbox]~label:before{content:"";display:inline-block;vertical-align:top;position:absolute;z-index:0;left:0;top:0;width:20px;height:20px;line-height:16px;font-family:var(--iconFontFamily);font-size:1.2rem;text-align:center;color:var(--baseColor);cursor:pointer;background:var(--baseColor);border-radius:var(--baseRadius);border:2px solid var(--baseAlt3Color);transition:transform var(--baseAnimationSpeed),border-color var(--baseAnimationSpeed),color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.form-field input[type=radio]~label:active:before,.form-field input[type=checkbox]~label:active:before{transform:scale(.9)}.form-field input[type=radio]:focus~label:before,.form-field input[type=radio]~label:hover:before,.form-field input[type=checkbox]:focus~label:before,.form-field input[type=checkbox]~label:hover:before{border-color:var(--baseAlt4Color)}.form-field input[type=radio]:checked~label:before,.form-field input[type=checkbox]:checked~label:before{content:"";box-shadow:none;mix-blend-mode:unset;background:var(--successColor);border-color:var(--successColor)}.form-field input[type=radio]:disabled~label,.form-field input[type=checkbox]:disabled~label{pointer-events:none;cursor:not-allowed;color:var(--txtDisabledColor)}.form-field input[type=radio]:disabled~label:before,.form-field input[type=checkbox]:disabled~label:before{opacity:.5}.form-field input[type=radio]~label:before{border-radius:50%;font-size:1rem}.form-field .form-field-block{position:relative;margin:0 0 var(--xsSpacing)}.form-field .form-field-block:last-child{margin-bottom:0}.form-field.form-field-toggle .form-field-addon{position:relative;right:auto;left:auto;top:auto;bottom:auto;min-height:0;vertical-align:middle;margin-left:5px}.form-field.form-field-toggle input[type=radio]~label,.form-field.form-field-toggle input[type=checkbox]~label{position:relative}.form-field.form-field-toggle input[type=radio]~label:before,.form-field.form-field-toggle input[type=checkbox]~label:before{content:"";border:0;box-shadow:none;background:var(--baseAlt3Color);transition:background var(--activeAnimationSpeed)}.form-field.form-field-toggle input[type=radio]~label:after,.form-field.form-field-toggle input[type=checkbox]~label:after{content:"";position:absolute;z-index:1;cursor:pointer;background:var(--baseColor);transition:left var(--activeAnimationSpeed),transform var(--activeAnimationSpeed),background var(--activeAnimationSpeed);box-shadow:0 2px 5px 0 var(--shadowColor)}.form-field.form-field-toggle input[type=radio]~label:active:before,.form-field.form-field-toggle input[type=checkbox]~label:active:before{transform:none}.form-field.form-field-toggle input[type=radio]~label:active:after,.form-field.form-field-toggle input[type=checkbox]~label:active:after{transform:scale(.9)}.form-field.form-field-toggle input[type=radio]:focus-visible~label:before,.form-field.form-field-toggle input[type=checkbox]:focus-visible~label:before{box-shadow:0 0 0 2px var(--baseAlt2Color)}.form-field.form-field-toggle input[type=radio]~label:hover:before,.form-field.form-field-toggle input[type=checkbox]~label:hover:before{background:var(--baseAlt4Color)}.form-field.form-field-toggle input[type=radio]:checked~label:before,.form-field.form-field-toggle input[type=checkbox]:checked~label:before{background:var(--successColor)}.form-field.form-field-toggle input[type=radio]:checked~label:after,.form-field.form-field-toggle input[type=checkbox]:checked~label:after{background:var(--baseColor)}.form-field.form-field-toggle input[type=radio]~label,.form-field.form-field-toggle input[type=checkbox]~label{min-height:24px;padding-left:47px}.form-field.form-field-toggle input[type=radio]~label:empty,.form-field.form-field-toggle input[type=checkbox]~label:empty{padding-left:40px}.form-field.form-field-toggle input[type=radio]~label:before,.form-field.form-field-toggle input[type=checkbox]~label:before{width:40px;height:24px;border-radius:24px}.form-field.form-field-toggle input[type=radio]~label:after,.form-field.form-field-toggle input[type=checkbox]~label:after{top:4px;left:4px;width:16px;height:16px;border-radius:16px}.form-field.form-field-toggle input[type=radio]:checked~label:after,.form-field.form-field-toggle input[type=checkbox]:checked~label:after{left:20px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label{min-height:20px;padding-left:39px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:empty,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:empty{padding-left:32px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:before,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:before{width:32px;height:20px;border-radius:20px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:after,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:after{top:4px;left:4px;width:12px;height:12px;border-radius:12px}.form-field.form-field-toggle.form-field-sm input[type=radio]:checked~label:after,.form-field.form-field-toggle.form-field-sm input[type=checkbox]:checked~label:after{left:16px}.form-field-group{display:flex;width:100%;align-items:center}.form-field-group>.form-field{flex-grow:1;border-left:1px solid var(--baseAlt2Color)}.form-field-group>.form-field:first-child{border-left:0}.form-field-group>.form-field:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.form-field-group>.form-field:not(:first-child)>label,.form-field-group>.form-field:not(:first-child)>.tinymce-wrapper,.form-field-group>.form-field:not(:first-child)>.code-editor,.select .form-field-group>.form-field:not(:first-child)>.selected-container,.form-field-group>.form-field:not(:first-child)>input,.form-field-group>.form-field:not(:first-child)>select,.form-field-group>.form-field:not(:first-child)>textarea,.form-field-group>.form-field:not(:first-child)>.select .selected-container{border-top-left-radius:0;border-bottom-left-radius:0}.form-field-group>.form-field:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.form-field-group>.form-field:not(:last-child)>label,.form-field-group>.form-field:not(:last-child)>.tinymce-wrapper,.form-field-group>.form-field:not(:last-child)>.code-editor,.select .form-field-group>.form-field:not(:last-child)>.selected-container,.form-field-group>.form-field:not(:last-child)>input,.form-field-group>.form-field:not(:last-child)>select,.form-field-group>.form-field:not(:last-child)>textarea,.form-field-group>.form-field:not(:last-child)>.select .selected-container{border-top-right-radius:0;border-bottom-right-radius:0}.form-field-group .form-field.col-12{width:100%}.form-field-group .form-field.col-11{width:91.6666666667%}.form-field-group .form-field.col-10{width:83.3333333333%}.form-field-group .form-field.col-9{width:75%}.form-field-group .form-field.col-8{width:66.6666666667%}.form-field-group .form-field.col-7{width:58.3333333333%}.form-field-group .form-field.col-6{width:50%}.form-field-group .form-field.col-5{width:41.6666666667%}.form-field-group .form-field.col-4{width:33.3333333333%}.form-field-group .form-field.col-3{width:25%}.form-field-group .form-field.col-2{width:16.6666666667%}.form-field-group .form-field.col-1{width:8.3333333333%}.select{position:relative;display:block;outline:0}.select .option{-webkit-user-select:none;user-select:none;column-gap:5px}.select .option .icon{min-width:20px;text-align:center;line-height:inherit}.select .option .icon i{vertical-align:middle;line-height:inherit}.select .txt-placeholder{color:var(--txtHintColor)}label~.select .selected-container{border-top:0}.select .selected-container{position:relative;display:flex;flex-wrap:wrap;width:100%;align-items:center;padding-top:0;padding-bottom:0;padding-right:35px!important;-webkit-user-select:none;user-select:none}.select .selected-container:after{content:"";position:absolute;right:5px;top:50%;width:20px;height:20px;line-height:20px;text-align:center;margin-top:-10px;display:inline-block;vertical-align:top;font-size:1rem;font-family:var(--iconFontFamily);align-self:flex-end;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed)}.select .selected-container:active,.select .selected-container.active{border-bottom-left-radius:0;border-bottom-right-radius:0}.select .selected-container:active:after,.select .selected-container.active:after{color:var(--txtPrimaryColor);transform:rotate(180deg)}.select .selected-container .option{display:flex;width:100%;align-items:center;max-width:100%;-webkit-user-select:text;user-select:text}.select .selected-container .clear{margin-left:auto;cursor:pointer;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed)}.select .selected-container .clear i{display:inline-block;vertical-align:middle;line-height:1}.select .selected-container .clear:hover{color:var(--txtPrimaryColor)}.select.multiple .selected-container{display:flex;align-items:center;padding-left:2px;row-gap:3px;column-gap:4px}.select.multiple .selected-container .txt-placeholder{margin-left:5px}.select.multiple .selected-container .option{display:inline-flex;width:auto;padding:3px 5px;line-height:1;border-radius:var(--baseRadius);background:var(--baseColor)}.select:not(.multiple) .selected-container .label{margin-left:-2px}.select:not(.multiple) .selected-container .option .txt{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;max-width:100%;line-height:normal}.select:not(.disabled) .selected-container:hover{cursor:pointer}.select.readonly,.select.disabled{color:var(--txtHintColor);pointer-events:none}.select.readonly .txt-placeholder,.select.disabled .txt-placeholder,.select.readonly .selected-container,.select.disabled .selected-container{color:inherit}.select.readonly .selected-container .link-hint,.select.disabled .selected-container .link-hint{pointer-events:auto}.select.readonly .selected-container *:not(.link-hint),.select.disabled .selected-container *:not(.link-hint){color:inherit!important}.select.readonly .selected-container:after,.select.readonly .selected-container .clear,.select.disabled .selected-container:after,.select.disabled .selected-container .clear{display:none}.select.readonly .selected-container:hover,.select.disabled .selected-container:hover{cursor:inherit}.select.disabled{color:var(--txtDisabledColor)}.select .txt-missing{color:var(--txtHintColor);padding:5px 12px;margin:0}.select .options-dropdown{max-height:none;border:0;overflow:auto;border-top-left-radius:0;border-top-right-radius:0;margin-top:-2px;box-shadow:0 2px 5px 0 var(--shadowColor),inset 0 0 0 2px var(--baseAlt2Color)}.select .options-dropdown .input-group:focus-within{box-shadow:none}.select .options-dropdown .form-field.options-search{margin:0 0 5px;padding:0 0 2px;color:var(--txtHintColor);border-bottom:1px solid var(--baseAlt2Color)}.select .options-dropdown .form-field.options-search .input-group{border-radius:0;padding:0 0 0 10px;margin:0;background:none;column-gap:0;border:0}.select .options-dropdown .form-field.options-search input{border:0;padding-left:9px;padding-right:9px;background:none}.select .options-dropdown .options-list{overflow:auto;max-height:240px;width:auto;margin-left:0;margin-right:-5px;padding-right:5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty){margin:5px -5px -5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty) .btn-block{border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}label~.select .selected-container{padding-bottom:4px;border-top-left-radius:0;border-top-right-radius:0}label~.select.multiple .selected-container{padding-top:3px;padding-bottom:3px;padding-left:10px}.select.block-options.multiple .selected-container .option{width:100%;box-shadow:0 2px 5px 0 var(--shadowColor)}.select.upside .selected-container.active{border-radius:0 0 var(--baseRadius) var(--baseRadius)}.select.upside .options-dropdown{border-radius:var(--baseRadius) var(--baseRadius) 0 0;margin:0}.field-type-select .options-dropdown{padding:2px 1px 1px 2px}.field-type-select .options-dropdown .form-field.options-search{margin:0}.field-type-select .options-dropdown .options-list{max-height:490px;display:flex;flex-direction:row;flex-wrap:wrap;width:100%;padding:0}.field-type-select .options-dropdown .dropdown-item{width:50%;margin:0;padding-left:12px;border-radius:0;border-bottom:1px solid var(--baseAlt2Color);border-right:1px solid var(--baseAlt2Color)}.field-type-select .options-dropdown .dropdown-item.selected{background:var(--baseAlt1Color)}.form-field-list{border-radius:var(--baseRadius);transition:box-shadow var(--baseAnimationSpeed)}.form-field-list label{padding-bottom:10px}.form-field-list .list{background:var(--baseAlt1Color);border:0;border-radius:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius);transition:background var(--baseAnimationSpeed)}.form-field-list .list .list-item{border-top:1px solid var(--baseAlt2Color)}.form-field-list .list .list-item:hover,.form-field-list .list .list-item:focus,.form-field-list .list .list-item:focus-within,.form-field-list .list .list-item:focus-visible,.form-field-list .list .list-item:active{background:none}.form-field-list .list .list-item.selected{background:var(--baseAlt2Color)}.form-field-list .list .list-item.handle:not(.disabled):hover,.form-field-list .list .list-item.handle:not(.disabled):focus-visible{background:var(--baseAlt2Color)}.form-field-list .list .list-item.handle:not(.disabled):active{background:var(--baseAlt3Color)}.form-field-list .list .list-item.dragging{z-index:9;box-shadow:inset 0 0 0 1px var(--baseAlt3Color)}.form-field-list .list .list-item.dragover{background:var(--baseAlt2Color)}.form-field-list:focus-within .list,.form-field-list:focus-within label{background:var(--baseAlt1Color)}.form-field-list.dragover:not(:has(.dragging)){box-shadow:0 0 0 2px var(--warningColor)}.code-editor{display:flex;flex-direction:column;width:100%}.form-field label~.code-editor{padding-bottom:6px;padding-top:4px}.code-editor .cm-editor{flex-grow:1;border:0!important;outline:none!important}.code-editor .cm-editor .cm-line{padding-left:0;padding-right:0}.code-editor .cm-editor .cm-tooltip-autocomplete{box-shadow:0 2px 5px 0 var(--shadowColor);border-radius:var(--baseRadius);background:var(--baseColor);border:0;z-index:9999;padding:0 3px;font-size:.92rem}.code-editor .cm-editor .cm-tooltip-autocomplete ul{margin:0;border-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul li[aria-selected]{background:var(--infoColor)}.code-editor .cm-editor .cm-scroller{flex-grow:1;outline:0!important;font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.code-editor .cm-editor .cm-cursorLayer .cm-cursor{margin-left:0!important}.code-editor .cm-editor .cm-placeholder{color:var(--txtDisabledColor);font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.code-editor .cm-editor .cm-selectionMatch{background:var(--infoAltColor)}.code-editor .cm-editor.cm-focused .cm-matchingBracket{background-color:#328c821a}.code-editor .ͼf{color:var(--dangerColor)}.tinymce-wrapper{min-height:277px}.tinymce-wrapper .tox-tinymce{border-radius:var(--baseRadius);border:0}.form-field label~.tinymce-wrapper{position:relative;z-index:auto;padding:5px 2px 2px}.form-field label~.tinymce-wrapper:before{content:"";position:absolute;z-index:-1;top:5px;left:2px;right:2px;bottom:2px;background:#fff;border-radius:var(--baseRadius)}body .tox .tox-dialog{border:0;border-radius:var(--baseRadius)}body .tox .tox-dialog-wrap__backdrop{background:var(--overlayColor)}body .tox .tox-tbtn{height:30px}body .tox .tox-tbtn svg{transform:scale(.85)}body .tox .tox-collection__item-checkmark,body .tox .tox-collection__item-icon{width:22px;height:22px;transform:scale(.85)}body .tox .tox-tbtn:not(.tox-tbtn--select){width:30px}body .tox .tox-button,body .tox .tox-button--secondary{font-size:var(--smFontSize)}body .tox .tox-toolbar-overlord{box-shadow:0 2px 5px 0 var(--shadowColor)}body .tox .tox-listboxfield .tox-listbox--select,body .tox .tox-textarea,body .tox .tox-textfield,body .tox .tox-toolbar-textfield{padding:3px 5px}body .tox-swatch:not(.tox-swatch--remove):not(.tox-collection__item--enabled) svg{display:none}body .tox .tox-textarea-wrap{display:flex;flex:1}body.tox-fullscreen .overlay-panel-section{overflow:hidden}.main-menu{--menuItemSize: 45px;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;row-gap:var(--smSpacing);font-size:var(--xlFontSize);color:var(--txtPrimaryColor)}.main-menu i{font-size:24px;line-height:1}.main-menu .menu-item{position:relative;outline:0;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;text-align:center;justify-content:center;-webkit-user-select:none;user-select:none;color:inherit;min-width:var(--menuItemSize);min-height:var(--menuItemSize);border:2px solid transparent;border-radius:var(--lgRadius);transition:background var(--baseAnimationSpeed),border var(--baseAnimationSpeed)}.main-menu .menu-item:focus-visible,.main-menu .menu-item:hover{background:var(--baseAlt1Color)}.main-menu .menu-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.main-menu .menu-item.active,.main-menu .menu-item.current-route{background:var(--baseColor);border-color:var(--primaryColor)}.app-sidebar{position:relative;z-index:1;display:flex;flex-grow:0;flex-shrink:0;flex-direction:column;align-items:center;width:var(--appSidebarWidth);padding:var(--smSpacing) 0px var(--smSpacing);background:var(--baseColor);border-right:1px solid var(--baseAlt2Color)}.app-sidebar .main-menu{flex-grow:1;justify-content:flex-start;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;margin-top:34px;margin-bottom:var(--baseSpacing)}.app-layout{display:flex;width:100%;height:100vh}.app-layout .app-body{flex-grow:1;min-width:0;height:100%;display:flex;align-items:stretch}.app-layout .app-sidebar~.app-body{min-width:650px}.page-sidebar{--sidebarListItemMargin: 10px;position:relative;z-index:0;display:flex;flex-direction:column;width:var(--pageSidebarWidth);min-width:var(--pageSidebarWidth);max-width:400px;flex-shrink:0;flex-grow:0;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);padding:calc(var(--baseSpacing) - 5px) 0 var(--smSpacing);border-right:1px solid var(--baseAlt2Color)}.page-sidebar>*{padding:0 var(--xsSpacing)}.page-sidebar .sidebar-content{overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.page-sidebar .sidebar-content>:first-child{margin-top:0}.page-sidebar .sidebar-content>:last-child{margin-bottom:0}.page-sidebar .sidebar-footer{margin-top:var(--smSpacing)}.page-sidebar .search{display:flex;align-items:center;width:auto;column-gap:5px;margin:0 0 var(--xsSpacing);color:var(--txtHintColor);opacity:.7;transition:opacity var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .search input{border:0;background:var(--baseColor);transition:box-shadow var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.page-sidebar .search .btn-clear{margin-right:-8px}.page-sidebar .search:hover,.page-sidebar .search:focus-within,.page-sidebar .search.active{opacity:1;color:var(--txtPrimaryColor)}.page-sidebar .search:hover input,.page-sidebar .search:focus-within input,.page-sidebar .search.active input{background:var(--baseAlt2Color)}.page-sidebar .sidebar-title{display:flex;align-items:center;gap:5px;width:100%;margin:var(--baseSpacing) 5px var(--xsSpacing);font-weight:600;font-size:1rem;line-height:var(--smLineHeight);color:var(--txtHintColor)}.page-sidebar .sidebar-title .label{font-weight:400}.page-sidebar .sidebar-list-item{cursor:pointer;outline:0;text-decoration:none;position:relative;display:flex;width:100%;align-items:center;column-gap:10px;margin:var(--sidebarListItemMargin) 0;padding:3px 10px;font-size:var(--xlFontSize);min-height:var(--btnHeight);min-width:0;color:var(--txtHintColor);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .sidebar-list-item i{font-size:18px}.page-sidebar .sidebar-list-item .txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.page-sidebar .sidebar-list-item:focus-visible,.page-sidebar .sidebar-list-item:hover,.page-sidebar .sidebar-list-item:active,.page-sidebar .sidebar-list-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.page-sidebar .sidebar-list-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.page-sidebar .sidebar-content-compact .sidebar-list-item{--sidebarListItemMargin: 5px}@media screen and (max-height: 600px){.page-sidebar{--sidebarListItemMargin: 5px}}@media screen and (max-width: 1100px){.page-sidebar{min-width:200px}.page-sidebar>*{padding-left:10px;padding-right:10px}}.page-header{display:flex;flex-shrink:0;align-items:center;width:100%;min-height:var(--btnHeight);gap:var(--xsSpacing);margin:0 0 var(--baseSpacing)}.page-header .btns-group{margin-left:auto;justify-content:end}@media screen and (max-width: 1050px){.page-header{flex-wrap:wrap}.page-header .btns-group{width:100%}.page-header .btns-group .btn{flex-grow:1;flex-basis:0}}.page-header-wrapper{background:var(--baseColor);width:auto;margin-top:calc(-1 * (var(--baseSpacing) - 5px));margin-left:calc(-1 * var(--baseSpacing));margin-right:calc(-1 * var(--baseSpacing));margin-bottom:var(--baseSpacing);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);border-bottom:1px solid var(--baseAlt2Color)}.breadcrumbs{display:flex;align-items:center;gap:30px;color:var(--txtDisabledColor)}.breadcrumbs .breadcrumb-item{position:relative;margin:0;line-height:1;font-weight:400}.breadcrumbs .breadcrumb-item:after{content:"/";position:absolute;right:-20px;top:0;width:10px;text-align:center;pointer-events:none;opacity:.4}.breadcrumbs .breadcrumb-item:last-child{word-break:break-word;color:var(--txtPrimaryColor)}.breadcrumbs .breadcrumb-item:last-child:after{content:none;display:none}.breadcrumbs a{text-decoration:none;color:inherit;transition:color var(--baseAnimationSpeed)}.breadcrumbs a:hover{color:var(--txtPrimaryColor)}.page-content{position:relative;z-index:0;display:block;width:100%;flex-grow:1;padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing) var(--smSpacing)}.page-footer{display:flex;gap:5px;align-items:center;justify-content:right;padding:0px var(--baseSpacing) var(--smSpacing);color:var(--txtDisabledColor);font-size:var(--xsFontSize);line-height:var(--smLineHeight)}.page-footer i{font-size:1.2em}.page-footer a{color:inherit;text-decoration:none;transition:color var(--baseAnimationSpeed)}.page-footer a:focus-visible,.page-footer a:hover,.page-footer a:active{color:var(--txtPrimaryColor)}.page-wrapper{display:flex;flex-direction:column;flex-grow:1;width:100%;overflow-x:hidden;overflow-y:auto;scroll-behavior:smooth;scrollbar-gutter:stable}.overlay-active .page-wrapper{overflow-y:hidden}.page-wrapper.full-page{scrollbar-gutter:auto;background:var(--baseColor)}.page-wrapper.center-content .page-content{display:flex;align-items:center}.page-wrapper.flex-content{scrollbar-gutter:auto}.page-wrapper.flex-content .page-content{display:flex;min-height:0;flex-direction:column}@keyframes tabChange{0%{opacity:.7}to{opacity:1}}.tabs-header{display:flex;align-items:stretch;justify-content:flex-start;column-gap:10px;width:100%;min-height:50px;-webkit-user-select:none;user-select:none;margin:0 0 var(--baseSpacing);border-bottom:2px solid var(--baseAlt2Color)}.tabs-header .tab-item{position:relative;outline:0;border:0;background:none;display:inline-flex;align-items:center;justify-content:center;min-width:70px;gap:5px;padding:10px;margin:0;font-size:var(--lgFontSize);line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);color:var(--txtHintColor);text-align:center;text-decoration:none;cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.tabs-header .tab-item:after{content:"";position:absolute;display:block;left:0;bottom:-2px;width:100%;height:2px;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);background:var(--primaryColor);transform:rotateY(90deg);transition:transform .2s}.tabs-header .tab-item .txt,.tabs-header .tab-item i{display:inline-block;vertical-align:top}.tabs-header .tab-item:hover,.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{color:var(--txtPrimaryColor)}.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.tabs-header .tab-item.active{color:var(--txtPrimaryColor)}.tabs-header .tab-item.active:after{transform:rotateY(0)}.tabs-header .tab-item.disabled{pointer-events:none;color:var(--txtDisabledColor)}.tabs-header .tab-item.disabled:after{display:none}.tabs-header.right{justify-content:flex-end}.tabs-header.center{justify-content:center}.tabs-header.stretched .tab-item{flex-grow:1;flex-basis:0}.tabs-header.compact{min-height:30px;margin-bottom:var(--smSpacing)}.tabs-header.combined{border:0;margin-bottom:-2px}.tabs-header.combined .tab-item:after{content:none;display:none}.tabs-header.combined .tab-item.active{background:var(--baseAlt1Color)}.tabs-content{position:relative}.tabs-content>.tab-item{width:100%;display:none}.tabs-content>.tab-item.active{display:block;opacity:0;animation:tabChange .2s forwards}.tabs-content>.tab-item>:first-child{margin-top:0}.tabs-content>.tab-item>:last-child{margin-bottom:0}.tabs-content.no-animations>.tab-item.active{opacity:1;animation:none}.tabs{position:relative}.accordion{outline:0;position:relative;border-radius:var(--baseRadius);background:var(--baseColor);border:1px solid var(--baseAlt2Color);transition:border-radius var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed),margin var(--baseAnimationSpeed)}.accordion .accordion-header{outline:0;position:relative;display:flex;min-height:52px;align-items:center;row-gap:10px;column-gap:var(--smSpacing);padding:12px 20px;width:100%;-webkit-user-select:none;user-select:none;color:var(--txtPrimaryColor);border-radius:inherit;transition:border-radius var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.accordion .accordion-header .icon{width:18px;text-align:center}.accordion .accordion-header .icon i{display:inline-block;vertical-align:top;font-size:1.1rem}.accordion .accordion-header.interactive{padding-right:50px;cursor:pointer}.accordion .accordion-header.interactive:after{content:"";position:absolute;right:15px;top:50%;margin-top:-12.5px;width:25px;height:25px;line-height:25px;color:var(--txtHintColor);font-family:var(--iconFontFamily);font-size:1.3em;text-align:center;transition:color var(--baseAnimationSpeed)}.accordion .accordion-header:hover:after,.accordion .accordion-header.focus:after,.accordion .accordion-header:focus-visible:after{color:var(--txtPrimaryColor)}.accordion .accordion-header:active{transition-duration:var(--activeAnimationSpeed)}.accordion .accordion-content{padding:20px}.accordion:hover,.accordion:focus-visible,.accordion.active{z-index:9}.accordion:hover .accordion-header.interactive,.accordion:focus-visible .accordion-header.interactive,.accordion.active .accordion-header.interactive{background:var(--baseAlt1Color)}.accordion.drag-over .accordion-header{background:var(--bodyColor)}.accordion.active{box-shadow:0 2px 5px 0 var(--shadowColor)}.accordion.active .accordion-header{position:relative;top:0;z-index:9;box-shadow:0 0 0 1px var(--baseAlt2Color);border-bottom-left-radius:0;border-bottom-right-radius:0;background:var(--bodyColor)}.accordion.active .accordion-header.interactive{background:var(--bodyColor)}.accordion.active .accordion-header.interactive:after{color:inherit;content:""}.accordion.disabled{z-index:0;border-color:var(--baseAlt1Color)}.accordion.disabled .accordion-header{color:var(--txtDisabledColor)}.accordions .accordion{border-radius:0;margin:-1px 0 0}.accordions .accordion:has(+.accordion.active){border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.accordions>.accordion.active,.accordions>.accordion-wrapper>.accordion.active{margin:var(--xsSpacing) 0;border-radius:var(--baseRadius)}.accordions>.accordion.active+.accordion,.accordions>.accordion-wrapper>.accordion.active+.accordion{border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions>.accordion:first-child,.accordions>.accordion-wrapper:first-child>.accordion{margin-top:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions>.accordion:last-child,.accordions>.accordion-wrapper:last-child>.accordion{margin-bottom:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}table{--entranceAnimationSpeed: .3s;border-collapse:separate;min-width:100%;transition:opacity var(--baseAnimationSpeed)}table .form-field{margin:0;line-height:1;text-align:left}table td,table th{outline:0;vertical-align:middle;position:relative;text-align:left;padding:10px;border-bottom:1px solid var(--baseAlt2Color)}table td:first-child,table th:first-child{padding-left:20px}table td:last-child,table th:last-child{padding-right:20px}table th{color:var(--txtHintColor);font-weight:600;font-size:1rem;-webkit-user-select:none;user-select:none;height:50px;line-height:var(--smLineHeight)}table th i{font-size:inherit}table td{height:56px;word-break:break-word}table .min-width{width:1%!important;white-space:nowrap}table .nowrap{white-space:nowrap}table .col-sort{cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);padding-right:30px;transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}table .col-sort:after{content:"";position:absolute;right:10px;top:50%;margin-top:-12.5px;line-height:25px;height:25px;font-family:var(--iconFontFamily);font-weight:400;color:var(--txtHintColor);opacity:0;transition:color var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed)}table .col-sort.sort-desc:after{content:""}table .col-sort.sort-asc:after{content:""}table .col-sort.sort-active:after{opacity:1}table .col-sort:hover,table .col-sort:focus-visible{background:var(--baseAlt1Color)}table .col-sort:hover:after,table .col-sort:focus-visible:after{opacity:1}table .col-sort:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}table .col-sort.col-sort-disabled{cursor:default;background:none}table .col-sort.col-sort-disabled:after{display:none}table .col-header-content{display:inline-flex;align-items:center;flex-wrap:nowrap;gap:5px}table .col-header-content .txt{max-width:140px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table td.col-field-username,table .col-field-created,table .col-field-updated,table .col-type-action{width:1%!important;white-space:nowrap}table .col-type-action{white-space:nowrap;text-align:right;color:var(--txtHintColor)}table .col-type-action i{display:inline-block;vertical-align:top;transition:transform var(--baseAnimationSpeed)}table td.col-type-json{font-family:monospace;font-size:var(--smFontSize);line-height:var(--smLineHeight);max-width:300px}table .col-type-text{max-width:300px}table .col-type-editor{min-width:300px}table .col-type-select{min-width:150px}table .col-type-email{min-width:120px;white-space:nowrap}table .col-type-file{min-width:100px}table .col-type-number{white-space:nowrap}table td.col-field-id{width:175px;white-space:nowrap}table tr{outline:0;background:var(--bodyColor);transition:background var(--baseAnimationSpeed)}table tr.row-handle{cursor:pointer;-webkit-user-select:none;user-select:none}table tr.row-handle:focus-visible,table tr.row-handle:hover,table tr.row-handle:active{background:var(--baseAlt1Color)}table tr.row-handle:focus-visible .action-col,table tr.row-handle:hover .action-col,table tr.row-handle:active .action-col{color:var(--txtPrimaryColor)}table tr.row-handle:focus-visible .action-col i,table tr.row-handle:hover .action-col i,table tr.row-handle:active .action-col i{transform:translate(3px)}table tr.row-handle:active{transition-duration:var(--activeAnimationSpeed)}table.table-border{border:1px solid var(--baseAlt2Color);border-radius:var(--baseRadius)}table.table-border tr{background:var(--baseColor)}table.table-border td,table.table-border th{height:45px}table.table-border th{background:var(--baseAlt1Color)}table.table-border>:last-child>:last-child th,table.table-border>:last-child>:last-child td{border-bottom:0}table.table-border>tr:first-child>:first-child,table.table-border>:first-child>tr:first-child>:first-child{border-top-left-radius:var(--baseRadius)}table.table-border>tr:first-child>:last-child,table.table-border>:first-child>tr:first-child>:last-child{border-top-right-radius:var(--baseRadius)}table.table-border>tr:last-child>:first-child,table.table-border>:last-child>tr:last-child>:first-child{border-bottom-left-radius:var(--baseRadius)}table.table-border>tr:last-child>:last-child,table.table-border>:last-child>tr:last-child>:last-child{border-bottom-right-radius:var(--baseRadius)}table.table-compact td,table.table-compact th{height:auto}table.table-animate tr{animation:entranceTop var(--entranceAnimationSpeed)}table.table-loading{pointer-events:none;opacity:.7}.table-wrapper{width:auto;padding:0;max-height:100%;max-width:calc(100% + 2 * var(--baseSpacing));margin-left:calc(var(--baseSpacing) * -1);margin-right:calc(var(--baseSpacing) * -1);border-bottom:1px solid var(--baseAlt2Color)}.table-wrapper .bulk-select-col{min-width:70px}.table-wrapper td,.table-wrapper th{position:relative}.table-wrapper td:first-child,.table-wrapper th:first-child{padding-left:calc(var(--baseSpacing) + 3px)}.table-wrapper td:last-child,.table-wrapper th:last-child{padding-right:calc(var(--baseSpacing) + 3px)}.table-wrapper thead{position:sticky;top:0;z-index:100;transition:box-shadow var(--baseAnimationSpeed)}.table-wrapper tbody{position:relative;z-index:0}.table-wrapper tbody tr:last-child td,.table-wrapper tbody tr:last-child th{border-bottom:0}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{position:sticky;z-index:99;transition:box-shadow var(--baseAnimationSpeed)}.table-wrapper .bulk-select-col{left:0}.table-wrapper .col-type-action{right:0}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{background:inherit}.table-wrapper th.bulk-select-col,.table-wrapper th.col-type-action{background:var(--bodyColor)}.table-wrapper.h-scroll .bulk-select-col{box-shadow:3px 0 5px 0 var(--shadowColor)}.table-wrapper.h-scroll .col-type-action{box-shadow:-3px 0 5px 0 var(--shadowColor)}.table-wrapper.h-scroll.h-scroll-start .bulk-select-col,.table-wrapper.h-scroll.h-scroll-end .col-type-action{box-shadow:none}.table-wrapper.v-scroll:not(.v-scroll-start) thead{box-shadow:0 2px 5px 0 var(--shadowColor)}.searchbar{--searchHeight: 44px;outline:0;display:flex;align-items:center;width:100%;min-height:var(--searchHeight);padding:5px 7px;margin:0;white-space:nowrap;color:var(--txtHintColor);background:var(--baseAlt1Color);border-radius:var(--btnHeight);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.searchbar>:first-child{border-top-left-radius:var(--btnHeight);border-bottom-left-radius:var(--btnHeight)}.searchbar>:last-child{border-top-right-radius:var(--btnHeight);border-bottom-right-radius:var(--btnHeight)}.searchbar .btn{border-radius:var(--btnHeight)}.searchbar .code-editor,.searchbar input,.searchbar input:focus{font-size:var(--baseFontSize);font-family:var(--monospaceFontFamily);border:0;background:none;min-height:0;height:100%;max-height:100px;padding-top:0;padding-bottom:0}.searchbar .cm-editor{flex-grow:0;margin-top:auto;margin-bottom:auto}.searchbar label>i{line-height:inherit}.searchbar .search-options{flex-shrink:0;width:90px}.searchbar .search-options .selected-container{border-radius:inherit;background:none;padding-right:25px!important}.searchbar .search-options:not(:focus-within) .selected-container{color:var(--txtHintColor)}.searchbar:focus-within{color:var(--txtPrimaryColor);background:var(--baseAlt2Color)}.bulkbar{position:absolute;bottom:var(--baseSpacing);left:50%;z-index:101;gap:10px;display:flex;justify-content:center;align-items:center;width:var(--smWrapperWidth);max-width:100%;margin-bottom:10px;padding:10px var(--smSpacing);border-radius:var(--btnHeight);background:var(--baseColor);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor);transform:translate(-50%)}.flatpickr-calendar{opacity:0;display:none;text-align:center;visibility:hidden;padding:0;animation:none;direction:ltr;border:0;font-size:1rem;line-height:24px;position:absolute;width:298px;box-sizing:border-box;-webkit-user-select:none;user-select:none;color:var(--txtPrimaryColor);background:var(--baseColor);border-radius:var(--baseRadius);box-shadow:0 2px 5px 0 var(--shadowColor),0 0 0 1px var(--baseAlt2Color)}.flatpickr-calendar input,.flatpickr-calendar select{box-shadow:none;min-height:0;height:var(--smBtnHeight);padding-top:3px;padding-bottom:3px;background:none;border-radius:var(--baseRadius);border:1px solid var(--baseAlt1Color)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:0;width:100%}.flatpickr-calendar.static{position:absolute;top:100%;margin-top:2px;margin-bottom:10px;width:100%}.flatpickr-calendar.static .flatpickr-days{width:100%}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none!important;box-shadow:none!important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color);box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid var(--baseAlt2Color)}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:"";height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowTop:after{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:after{border-top-color:var(--baseColor)}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative}.flatpickr-months{display:flex;align-items:center;padding:5px 0}.flatpickr-months .flatpickr-month{display:flex;align-items:center;justify-content:center;background:transparent;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor);line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{display:flex;align-items:center;text-decoration:none;cursor:pointer;height:34px;padding:5px 12px;z-index:3;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{left:0}.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{right:0}.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover,.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:var(--txtHintColor)}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto;border-radius:var(--baseRadius)}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.15);box-sizing:border-box}.numInputWrapper span:hover{background:#0000001a}.numInputWrapper span:active{background:#0003}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:#00000080}.numInputWrapper:hover{background:var(--baseAlt1Color)}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{line-height:inherit;color:inherit;width:85%;padding:1px 0;line-height:1;display:flex;gap:10px;align-items:center;justify-content:center;text-align:center}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:var(--baseAlt1Color)}.flatpickr-current-month .numInputWrapper{display:inline-flex;align-items:center;justify-content:center;width:62px}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-current-month input.cur-year{background:transparent;box-sizing:border-box;color:inherit;cursor:text;margin:0;display:inline-block;font-size:inherit;font-family:inherit;line-height:inherit;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{color:var(--txtDisabledColor);background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;line-height:inherit;outline:none;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:var(--baseAlt1Color)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{display:block;flex:1;margin:0;cursor:default;line-height:1;background:transparent;color:var(--txtHintColor);text-align:center;font-weight:bolder;font-size:var(--smFontSize)}.dayContainer,.flatpickr-weeks{padding:1px 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:100%;box-sizing:border-box;display:inline-block;display:flex;flex-wrap:wrap;transform:translateZ(0);opacity:1;gap:2px}.dayContainer+.dayContainer{-webkit-box-shadow:-1px 0 0 var(--baseAlt2Color);box-shadow:-1px 0 0 var(--baseAlt2Color)}.flatpickr-day{background:none;border:1px solid transparent;border-radius:var(--baseRadius);box-sizing:border-box;color:var(--txtPrimaryColor);cursor:pointer;font-weight:400;width:calc(14.2857143% - 2px);flex-basis:calc(14.2857143% - 2px);height:39px;display:inline-flex;align-items:center;justify-content:center;position:relative;text-align:center;flex-direction:column}.flatpickr-day.weekend,.flatpickr-day:nth-child(7n+6),.flatpickr-day:nth-child(7n+7){color:var(--dangerColor)}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:var(--baseAlt2Color);border-color:var(--baseAlt2Color)}.flatpickr-day.today{border-color:var(--baseColor)}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:var(--primaryColor);background:var(--primaryColor);color:var(--baseColor)}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:var(--primaryColor);-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:var(--primaryColor)}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 var(--primaryColor);box-shadow:-10px 0 0 var(--primaryColor)}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;box-shadow:-5px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:var(--txtDisabledColor);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:var(--txtDisabledColor);background:var(--baseAlt2Color)}.flatpickr-day.week.selected{border-radius:0;box-shadow:-5px 0 0 var(--primaryColor),5px 0 0 var(--primaryColor)}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 var(--baseAlt2Color);box-shadow:1px 0 0 var(--baseAlt2Color)}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:var(--txtHintColor);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:flex;box-sizing:border-box;overflow:hidden;padding:5px}.flatpickr-rContainer{display:inline-block;padding:0;width:100%;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;box-shadow:none;border:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:var(--txtPrimaryColor);font-size:14px;position:relative;box-sizing:border-box;background:var(--baseColor);-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:var(--txtPrimaryColor);font-weight:700;width:2%;-webkit-user-select:none;user-select:none;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:var(--baseAlt1Color)}.flatpickr-input[readonly]{cursor:pointer}@keyframes fpFadeInDown{0%{opacity:0;transform:translate3d(0,10px,0)}to{opacity:1;transform:translateZ(0)}}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .prevMonthDay{visibility:hidden}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .nextMonthDay,.flatpickr-inline-container .flatpickr-input{display:none}.flatpickr-inline-container .flatpickr-calendar{margin:0;box-shadow:none;border:1px solid var(--baseAlt2Color)}.docs-sidebar{--itemsSpacing: 10px;--itemsHeight: 40px;position:relative;min-width:180px;max-width:300px;height:100%;flex-shrink:0;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;background:var(--bodyColor);padding:var(--smSpacing) var(--xsSpacing);border-right:1px solid var(--baseAlt1Color)}.docs-sidebar .sidebar-content{display:block;width:100%}.docs-sidebar .sidebar-item{position:relative;outline:0;cursor:pointer;text-decoration:none;display:flex;width:100%;gap:10px;align-items:center;text-align:right;justify-content:start;padding:5px 15px;margin:0 0 var(--itemsSpacing) 0;font-size:var(--lgFontSize);min-height:var(--itemsHeight);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;color:var(--txtHintColor);transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.docs-sidebar .sidebar-item:last-child{margin-bottom:0}.docs-sidebar .sidebar-item:focus-visible,.docs-sidebar .sidebar-item:hover,.docs-sidebar .sidebar-item:active,.docs-sidebar .sidebar-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.docs-sidebar .sidebar-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.docs-sidebar.compact .sidebar-item{--itemsSpacing: 7px}.docs-content{width:100%;display:block;padding:calc(var(--baseSpacing) - 3px) var(--baseSpacing);overflow:auto}.docs-content-wrapper{display:flex;width:100%;height:100%}.docs-panel{width:960px;height:100%}.docs-panel .overlay-panel-section.panel-header{padding:0;border:0;box-shadow:none}.docs-panel .overlay-panel-section.panel-content{padding:0!important}.docs-panel .overlay-panel-section.panel-footer{display:none}@media screen and (max-width: 1000px){.docs-panel .overlay-panel-section.panel-footer{display:flex}}.schema-field-header{position:relative;display:flex;width:100%;min-height:42px;gap:5px;padding:0 5px;align-items:center;justify-content:stretch;background:var(--baseAlt1Color);transition:border-radius var(--baseAnimationSpeed)}.schema-field-header .form-field{margin:0}.schema-field-header .form-field .form-field-addon.prefix{left:10px}.schema-field-header .form-field .form-field-addon.prefix~input,.schema-field-header .form-field .form-field-addon.prefix~select,.schema-field-header .form-field .form-field-addon.prefix~textarea,.schema-field-header .form-field .select .form-field-addon.prefix~.selected-container,.select .schema-field-header .form-field .form-field-addon.prefix~.selected-container,.schema-field-header .form-field .form-field-addon.prefix~.code-editor,.schema-field-header .form-field .form-field-addon.prefix~.tinymce-wrapper{padding-left:37px}.schema-field-header .options-trigger{padding:2px;margin:0 3px}.schema-field-header .options-trigger i{transition:transform var(--baseAnimationSpeed)}.schema-field-header .separator{flex-shrink:0;width:1px;align-self:stretch;background:#0000000d}.schema-field-header .drag-handle-wrapper{position:absolute;top:0;left:auto;right:100%;height:100%;display:flex;align-items:center}.schema-field-header .drag-handle{padding:0 5px;transform:translate(5px);opacity:0;visibility:hidden}.schema-field-header .form-field-single-multiple-select{width:135px;flex-shrink:0}.schema-field-header .form-field-single-multiple-select .selected-container{padding-left:10px}.schema-field-header .form-field-single-multiple-select .dropdown{min-width:0}.schema-field-header .field-labels{position:absolute;z-index:1;right:0;top:0;gap:2px;display:inline-flex;align-items:center;transition:opacity var(--baseAnimationSpeed)}.schema-field-header .field-labels .label{min-height:0;font-size:inherit;padding:0 2px;font-size:.7rem;line-height:.75rem;border-radius:var(--baseRadius)}.schema-field-header .field-labels~.inline-error-icon{margin-top:4px}.schema-field-header .field-labels~.inline-error-icon i{font-size:1rem}.schema-field-header .form-field:focus-within .field-labels{opacity:.2}.schema-field-options{background:#fff;padding:var(--xsSpacing);border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius);border-top:2px solid transparent;transition:border-color var(--baseAnimationSpeed)}.schema-field-options-footer{display:flex;flex-wrap:wrap;align-items:center;width:100%;min-width:0;gap:var(--baseSpacing)}.schema-field-options-footer .form-field{margin:0;width:auto}.schema-field{position:relative;border-radius:var(--baseRadius);background:var(--baseAlt1Color);border:1px solid var(--baseAlt1Color);transition:border-radius var(--baseAnimationSpeed),margin var(--baseAnimationSpeed)}.schema-field:not(.deleted):hover .drag-handle{transform:translate(0);opacity:1;visibility:visible}.dragover .schema-field,.schema-field.dragover{opacity:.5}.schema-field.expanded{box-shadow:0 2px 5px 0 var(--shadowColor);border-color:var(--baseAlt2Color)}.draggable:first-child .schema-field.expanded{margin-top:0}.schema-field.expanded .schema-field-header{border-bottom-left-radius:0;border-bottom-right-radius:0}.schema-field.expanded .schema-field-header .options-trigger i{transform:rotate(-60deg)}.schema-field.expanded .schema-field-options{border-top-color:var(--baseAlt2Color)}.schema-field.deleted .schema-field-header{background:var(--bodyColor)}.schema-field.deleted .markers,.schema-field.deleted .separator,.schema-field.deleted .field-labels{opacity:.5}.schema-field.deleted input,.schema-field.deleted select,.schema-field.deleted textarea,.schema-field.deleted .select .selected-container,.select .schema-field.deleted .selected-container,.schema-field.deleted .code-editor,.schema-field.deleted .tinymce-wrapper{background:none;box-shadow:none}.schema-fields{margin:0 0 var(--xsSpacing)}.schema-fields .schema-field{border-radius:0;box-shadow:0 0 0 1px var(--baseAlt2Color)}.schema-fields .draggable:has(+.draggable .schema-field.expanded) .schema-field{border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.schema-fields .draggable:has(.schema-field.expanded)+.draggable .schema-field{border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.schema-fields>.schema-field.expanded,.schema-fields>.draggable>.schema-field.expanded{margin:var(--xsSpacing) 0;border-radius:var(--baseRadius)}.schema-fields>.schema-field:first-child,.schema-fields>.draggable:first-child>.schema-field{margin-top:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.schema-fields>.schema-field:last-child,.schema-fields>.draggable:last-child>.schema-field{margin-bottom:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.file-picker-sidebar{flex-shrink:0;width:180px;text-align:right;max-height:100%;overflow:auto}.file-picker-sidebar .sidebar-item{outline:0;cursor:pointer;text-decoration:none;display:flex;width:100%;align-items:center;text-align:left;gap:10px;font-weight:600;padding:5px 10px;margin:0 0 10px;color:var(--txtHintColor);min-height:var(--btnHeight);border-radius:var(--baseRadius);word-break:break-word;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.file-picker-sidebar .sidebar-item:last-child{margin-bottom:0}.file-picker-sidebar .sidebar-item:hover,.file-picker-sidebar .sidebar-item:focus-visible,.file-picker-sidebar .sidebar-item:active,.file-picker-sidebar .sidebar-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.file-picker-sidebar .sidebar-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.files-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--xsSpacing);flex-grow:1;min-height:0;max-height:100%;overflow:auto;scrollbar-gutter:stable}.files-list .list-item{cursor:pointer;outline:0;transition:box-shadow var(--baseAnimationSpeed)}.file-picker-size-select{width:170px;margin:0}.file-picker-size-select .selected-container{min-height:var(--btnHeight)}.file-picker-content{position:relative;display:flex;flex-direction:column;width:100%;flex-grow:1;min-width:0;min-height:0;height:100%}.file-picker-content .thumb{--thumbSize: 14.6%}.file-picker{display:flex;height:420px;max-height:100%;align-items:stretch;gap:var(--baseSpacing)}.overlay-panel.file-picker-popup{width:930px}.export-list{display:flex;flex-direction:column;gap:15px;width:220px;min-height:0;flex-shrink:0;overflow:auto;padding:var(--xsSpacing);background:var(--baseAlt1Color);border-radius:var(--baseRadius)}.export-list .list-item{margin:0;width:100%}.export-list .form-field{margin:0}.export-list .form-field label{width:100%;display:block!important;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.export-preview{position:relative;flex-grow:1;border-radius:var(--baseRadius);overflow:hidden}.export-preview .copy-schema{position:absolute;right:15px;top:10px}.export-preview .code-wrapper{height:100%;width:100%;padding:var(--xsSpacing);overflow:auto;background:var(--baseAlt1Color);font-family:var(--monospaceFontFamily)}.export-panel{display:flex;width:100%;height:550px;align-items:stretch}.export-panel>*{border-radius:0;border-left:1px solid var(--baseAlt2Color)}.export-panel>:first-child{border-top-left-radius:var(--baseRadius);border-bottom-left-radius:var(--baseRadius);border-left:0}.export-panel>:last-child{border-top-right-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.rate-limit-table{background:none}.rate-limit-table tr,.rate-limit-table td,.rate-limit-table th{background:none;padding:0;border:0;min-height:0;height:auto}.rate-limit-table tr:first-child,.rate-limit-table td:first-child,.rate-limit-table th:first-child{padding-left:0}.rate-limit-table tr:last-child,.rate-limit-table td:last-child,.rate-limit-table th:last-child{padding-right:0}.rate-limit-table th{padding:10px 0}.rate-limit-table .rate-limit-row input,.rate-limit-table .rate-limit-row select,.rate-limit-table .rate-limit-row textarea,.rate-limit-table .rate-limit-row .select .selected-container,.select .rate-limit-table .rate-limit-row .selected-container,.rate-limit-table .rate-limit-row .code-editor,.rate-limit-table .rate-limit-row .tinymce-wrapper{border-radius:inherit}.rate-limit-table .rate-limit-row td{background:var(--baseAlt1Color);border-left:1px solid var(--baseAlt2Color);border-top:1px solid var(--baseAlt2Color)}.rate-limit-table .rate-limit-row td:first-child{border-left:0px}.rate-limit-table .rate-limit-row:first-child td{border-top:0px}.rate-limit-table .rate-limit-row:first-child td:first-child,.rate-limit-table .rate-limit-row:first-child td:first-child input,.rate-limit-table .rate-limit-row:first-child td:first-child select,.rate-limit-table .rate-limit-row:first-child td:first-child textarea,.rate-limit-table .rate-limit-row:first-child td:first-child .select .selected-container,.select .rate-limit-table .rate-limit-row:first-child td:first-child .selected-container,.rate-limit-table .rate-limit-row:first-child td:first-child .code-editor,.rate-limit-table .rate-limit-row:first-child td:first-child .tinymce-wrapper{border-top-left-radius:var(--baseRadius)}.rate-limit-table .rate-limit-row:first-child td:last-child,.rate-limit-table .rate-limit-row:first-child td:last-child input,.rate-limit-table .rate-limit-row:first-child td:last-child select,.rate-limit-table .rate-limit-row:first-child td:last-child textarea,.rate-limit-table .rate-limit-row:first-child td:last-child .select .selected-container,.select .rate-limit-table .rate-limit-row:first-child td:last-child .selected-container,.rate-limit-table .rate-limit-row:first-child td:last-child .code-editor,.rate-limit-table .rate-limit-row:first-child td:last-child .tinymce-wrapper{border-top-right-radius:var(--baseRadius)}.rate-limit-table .rate-limit-row:last-child td:first-child,.rate-limit-table .rate-limit-row:last-child td:first-child input,.rate-limit-table .rate-limit-row:last-child td:first-child select,.rate-limit-table .rate-limit-row:last-child td:first-child textarea,.rate-limit-table .rate-limit-row:last-child td:first-child .select .selected-container,.select .rate-limit-table .rate-limit-row:last-child td:first-child .selected-container,.rate-limit-table .rate-limit-row:last-child td:first-child .code-editor,.rate-limit-table .rate-limit-row:last-child td:first-child .tinymce-wrapper{border-bottom-left-radius:var(--baseRadius)}.rate-limit-table .rate-limit-row:last-child td:last-child,.rate-limit-table .rate-limit-row:last-child td:last-child input,.rate-limit-table .rate-limit-row:last-child td:last-child select,.rate-limit-table .rate-limit-row:last-child td:last-child textarea,.rate-limit-table .rate-limit-row:last-child td:last-child .select .selected-container,.select .rate-limit-table .rate-limit-row:last-child td:last-child .selected-container,.rate-limit-table .rate-limit-row:last-child td:last-child .code-editor,.rate-limit-table .rate-limit-row:last-child td:last-child .tinymce-wrapper{border-bottom-right-radius:var(--baseRadius)}.rate-limit-table .form-field{margin:0}.rate-limit-table .col-tag{width:50%}.rate-limit-table .col-requests,.rate-limit-table .col-burst{width:25%}.rate-limit-table .col-action{width:1px;min-width:0;white-space:nowrap;padding:0 5px!important}.panel-wrapper.svelte-lxxzfu{animation:slideIn .2s}@keyframes svelte-1bvelc2-refresh{to{transform:rotate(180deg)}}.btn.refreshing.svelte-1bvelc2 i.svelte-1bvelc2{animation:svelte-1bvelc2-refresh .15s ease-out}.scroller.svelte-3a0gfs{width:auto;min-height:0;overflow:auto}.scroller-wrapper.svelte-3a0gfs{position:relative;min-height:0}.scroller-wrapper .columns-dropdown{top:40px;z-index:101;max-height:340px}.log-level-label.svelte-ha6hme{min-width:75px;font-weight:600;font-size:var(--xsFontSize)}.log-level-label.svelte-ha6hme:before{content:"";width:5px;height:5px;border-radius:5px;background:var(--baseAlt4Color)}.log-level-label.level--8.svelte-ha6hme:before{background:var(--primaryColor)}.log-level-label.level-0.svelte-ha6hme:before{background:var(--infoColor)}.log-level-label.level-4.svelte-ha6hme:before{background:var(--warningColor)}.log-level-label.level-8.svelte-ha6hme:before{background:var(--dangerColor)}.bulkbar.svelte-91v05h{position:sticky;margin-top:var(--smSpacing);bottom:var(--baseSpacing)}.col-field-level.svelte-91v05h{min-width:100px}.col-field-message.svelte-91v05h{min-width:600px}.chart-wrapper.svelte-kfnurg.svelte-kfnurg{position:relative;display:block;width:100%;height:170px}.chart-wrapper.loading.svelte-kfnurg .chart-canvas.svelte-kfnurg{pointer-events:none;opacity:.5}.chart-loader.svelte-kfnurg.svelte-kfnurg{position:absolute;z-index:999;top:50%;left:50%;transform:translate(-50%,-50%)}.total-logs.svelte-kfnurg.svelte-kfnurg{position:absolute;right:0;top:-50px;font-size:var(--smFontSize);color:var(--txtHintColor)}.btn-chart-zoom.svelte-kfnurg.svelte-kfnurg{position:absolute;right:10px;top:20px}code.svelte-s3jkbp.svelte-s3jkbp{display:block;width:100%;padding:10px 15px;white-space:pre-wrap;word-break:break-word}.code-wrapper.svelte-s3jkbp.svelte-s3jkbp{display:block;width:100%}.prism-light.svelte-s3jkbp code.svelte-s3jkbp{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.log-error-label.svelte-1c23bpt.svelte-1c23bpt{white-space:normal}.copy-icon-wrapper.svelte-1c23bpt.svelte-1c23bpt{position:absolute;right:12px;top:12px;opacity:0;transition:opacity var(--baseAnimationSpeed)}tr.svelte-1c23bpt:hover .copy-icon-wrapper.svelte-1c23bpt{opacity:1}td.svelte-1c23bpt.svelte-1c23bpt:has(.copy-icon-wrapper){padding-right:30px}.lock-toggle.svelte-dnx4io.svelte-dnx4io{position:absolute;right:0;top:0;min-width:135px;padding:10px;border-top-left-radius:0;border-bottom-right-radius:0;background:#35476817}.rule-field .code-editor .cm-placeholder{font-family:var(--baseFontFamily)}.input-wrapper.svelte-dnx4io.svelte-dnx4io{position:relative}.unlock-overlay.svelte-dnx4io.svelte-dnx4io{--hoverAnimationSpeed:.2s;position:absolute;z-index:1;left:0;top:0;width:100%;height:100%;display:flex;padding:20px;gap:10px;align-items:center;justify-content:end;text-align:center;border-radius:var(--baseRadius);outline:0;cursor:pointer;text-decoration:none;color:var(--successColor);border:2px solid var(--baseAlt1Color);transition:border-color var(--baseAnimationSpeed)}.unlock-overlay.svelte-dnx4io i.svelte-dnx4io{font-size:inherit}.unlock-overlay.svelte-dnx4io .icon.svelte-dnx4io{color:var(--successColor);font-size:1.15rem;line-height:1;font-weight:400;transition:transform var(--hoverAnimationSpeed)}.unlock-overlay.svelte-dnx4io .txt.svelte-dnx4io{opacity:0;font-size:var(--xsFontSize);font-weight:600;line-height:var(--smLineHeight);transform:translate(5px);transition:transform var(--hoverAnimationSpeed),opacity var(--hoverAnimationSpeed)}.unlock-overlay.svelte-dnx4io.svelte-dnx4io:hover,.unlock-overlay.svelte-dnx4io.svelte-dnx4io:focus-visible,.unlock-overlay.svelte-dnx4io.svelte-dnx4io:active{border-color:var(--baseAlt3Color)}.unlock-overlay.svelte-dnx4io:hover .icon.svelte-dnx4io,.unlock-overlay.svelte-dnx4io:focus-visible .icon.svelte-dnx4io,.unlock-overlay.svelte-dnx4io:active .icon.svelte-dnx4io{transform:scale(1.1)}.unlock-overlay.svelte-dnx4io:hover .txt.svelte-dnx4io,.unlock-overlay.svelte-dnx4io:focus-visible .txt.svelte-dnx4io,.unlock-overlay.svelte-dnx4io:active .txt.svelte-dnx4io{opacity:1;transform:scale(1)}.unlock-overlay.svelte-dnx4io.svelte-dnx4io:active{transition-duration:var(--activeAnimationSpeed);border-color:var(--baseAlt3Color)}.unlock-overlay[disabled].svelte-dnx4io.svelte-dnx4io{cursor:not-allowed}.draggable.svelte-19c69j7{-webkit-user-select:text;user-select:text;outline:0;min-width:0}.indexes-list.svelte-167lbwu{display:flex;flex-wrap:wrap;width:100%;gap:10px}.label.svelte-167lbwu{overflow:hidden;min-width:50px}.field-types-btn.active.svelte-1gz9b6p{border-bottom-left-radius:0;border-bottom-right-radius:0}.field-types-dropdown{display:flex;flex-wrap:wrap;width:100%;max-width:none;padding:10px;margin-top:2px;border:0;box-shadow:0 0 0 2px var(--primaryColor);border-top-left-radius:0;border-top-right-radius:0}.field-types-dropdown .dropdown-item.svelte-1gz9b6p{width:25%}.form-field-file-max-select{width:100px;flex-shrink:0}.changes-list.svelte-xqpcsf.svelte-xqpcsf{word-break:break-word;line-height:var(--smLineHeight)}.changes-list.svelte-xqpcsf li.svelte-xqpcsf{margin-top:10px;margin-bottom:10px}.upsert-panel-title.svelte-xyiw1b{display:inline-flex;align-items:center;min-height:var(--smBtnHeight)}.tabs-content.svelte-xyiw1b:focus-within{z-index:9}.collection-panel .panel-content{scrollbar-gutter:stable;padding-right:calc(var(--baseSpacing) - 5px)}.dragline.svelte-y9un12{position:relative;z-index:101;left:0;top:0;height:100%;width:5px;padding:0;margin:0 -3px 0 -1px;background:none;cursor:ew-resize;box-sizing:content-box;-webkit-user-select:none;user-select:none;transition:box-shadow var(--activeAnimationSpeed);box-shadow:inset 1px 0 0 0 var(--baseAlt2Color)}.dragline.svelte-y9un12:hover,.dragline.dragging.svelte-y9un12{box-shadow:inset 3px 0 0 0 var(--baseAlt2Color)}.btn-pin-collection.svelte-5oh3nd.svelte-5oh3nd{margin:0 -7px 0 -15px;opacity:0;transition:opacity var(--baseAnimationSpeed)}.btn-pin-collection.svelte-5oh3nd i.svelte-5oh3nd{font-size:inherit}a.svelte-5oh3nd:hover .btn-pin-collection.svelte-5oh3nd{opacity:.4}a.svelte-5oh3nd:hover .btn-pin-collection.svelte-5oh3nd:hover{opacity:1}.datetime.svelte-5pjd03{display:inline-block;vertical-align:top;white-space:nowrap;line-height:var(--smLineHeight)}.time.svelte-5pjd03{font-size:var(--smFontSize);color:var(--txtHintColor)}.record-info.svelte-69icne{display:inline-flex;vertical-align:top;align-items:center;justify-content:center;max-width:100%;min-width:0;gap:5px;padding-left:1px}.fallback-block.svelte-jdf51v{max-height:100px;overflow:auto}.col-field.svelte-1nt58f7{max-width:1px}.secret.svelte-1md8247{font-family:monospace;font-weight:400;-webkit-user-select:all;user-select:all}.email-visibility-addon.svelte-1751a4d~input.svelte-1751a4d{padding-right:100px}.clear-btn.svelte-11df51y{margin-top:20px}.json-state.svelte-p6ecb8{position:absolute;right:10px}.picker-list.svelte-1u8jhky{max-height:380px}.selected-list.svelte-1u8jhky{display:flex;flex-wrap:wrap;align-items:center;gap:10px;max-height:220px;overflow:auto}.relations-list.svelte-1ynw0pc{max-height:300px;overflow:auto;overflow:overlay}textarea.svelte-1x1pbts{resize:none;padding-top:4px!important;padding-bottom:5px!important;min-height:var(--inputHeight);height:var(--inputHeight)}.sdk-tabs.svelte-1maocj6 .tabs-header .tab-item.svelte-1maocj6{min-width:100px}.token-holder.svelte-1i56uix{-webkit-user-select:all;user-select:all}.panel-title.svelte-qc5ngu{line-height:var(--smBtnHeight)}.popup-title.svelte-1fcgldh{max-width:80%}.list-content.svelte-1ulbkf5.svelte-1ulbkf5{overflow:auto;max-height:342px}.list-content.svelte-1ulbkf5 .list-item.svelte-1ulbkf5{min-height:49px}.backup-name.svelte-1ulbkf5.svelte-1ulbkf5{max-width:300px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.collections-diff-table.svelte-qs0w8h.svelte-qs0w8h{color:var(--txtHintColor);border:2px solid var(--primaryColor)}.collections-diff-table.svelte-qs0w8h tr.svelte-qs0w8h{background:none}.collections-diff-table.svelte-qs0w8h th.svelte-qs0w8h,.collections-diff-table.svelte-qs0w8h td.svelte-qs0w8h{height:auto;padding:2px 15px;border-bottom:1px solid rgba(0,0,0,.07)}.collections-diff-table.svelte-qs0w8h th.svelte-qs0w8h{height:35px;padding:4px 15px;color:var(--txtPrimaryColor)}.collections-diff-table.svelte-qs0w8h thead tr.svelte-qs0w8h{background:var(--primaryColor)}.collections-diff-table.svelte-qs0w8h thead tr th.svelte-qs0w8h{color:var(--baseColor);background:none}.collections-diff-table.svelte-qs0w8h .label.svelte-qs0w8h{font-weight:400}.collections-diff-table.svelte-qs0w8h .changed-none-col.svelte-qs0w8h{color:var(--txtDisabledColor);background:var(--baseAlt1Color)}.collections-diff-table.svelte-qs0w8h .changed-old-col.svelte-qs0w8h{color:var(--txtPrimaryColor);background:var(--dangerAltColor)}.collections-diff-table.svelte-qs0w8h .changed-new-col.svelte-qs0w8h{color:var(--txtPrimaryColor);background:var(--successAltColor)}.collections-diff-table.svelte-qs0w8h .field-key-col.svelte-qs0w8h{padding-left:30px}.collections-diff-table.svelte-qs0w8h .diff-value.svelte-qs0w8h{white-space:break-spaces}.list-label.svelte-1jx20fl{min-width:65px}.current-superuser.svelte-1ahgi3o{padding:10px;max-width:200px;color:var(--txtHintColor)} diff --git a/ui/dist/assets/index-DpAp7TiX.css b/ui/dist/assets/index-DpAp7TiX.css deleted file mode 100644 index 3a63b4ee..00000000 --- a/ui/dist/assets/index-DpAp7TiX.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";@font-face{font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?v=1) format("woff2");font-display:swap}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:400;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:400;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:600;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:600;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:700;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2) format("woff2")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:700;src:url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2) format("woff2")}@font-face{font-display:swap;font-family:Ubuntu Mono;font-style:normal;font-weight:400;src:url(../fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2) format("woff2")}@font-face{font-display:swap;font-family:Ubuntu Mono;font-style:normal;font-weight:700;src:url(../fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2) format("woff2")}:root{--baseFontFamily: "Source Sans Pro", sans-serif, emoji;--monospaceFontFamily: "Ubuntu Mono", monospace, emoji;--iconFontFamily: "remixicon";--txtPrimaryColor: #16161a;--txtHintColor: #666f75;--txtDisabledColor: #a0a6ac;--primaryColor: #16161a;--bodyColor: #f8f9fa;--baseColor: #ffffff;--baseAlt1Color: #e4e9ec;--baseAlt2Color: #d7dde4;--baseAlt3Color: #c6cdd7;--baseAlt4Color: #a5b0c0;--infoColor: #5499e8;--infoAltColor: #cee2f8;--successColor: #32ad84;--successAltColor: #c4eedc;--dangerColor: #e34562;--dangerAltColor: #f7cad2;--warningColor: #ff944d;--warningAltColor: #ffd4b8;--overlayColor: rgba(53, 71, 104, .28);--tooltipColor: rgba(0, 0, 0, .85);--shadowColor: rgba(0, 0, 0, .06);--baseFontSize: 14.5px;--xsFontSize: 12px;--smFontSize: 13px;--lgFontSize: 15px;--xlFontSize: 16px;--baseLineHeight: 22px;--smLineHeight: 16px;--lgLineHeight: 24px;--inputHeight: 34px;--btnHeight: 40px;--xsBtnHeight: 22px;--smBtnHeight: 30px;--lgBtnHeight: 54px;--baseSpacing: 30px;--xsSpacing: 15px;--smSpacing: 20px;--lgSpacing: 50px;--xlSpacing: 60px;--wrapperWidth: 850px;--smWrapperWidth: 420px;--lgWrapperWidth: 1200px;--appSidebarWidth: 75px;--pageSidebarWidth: 235px;--baseAnimationSpeed: .15s;--activeAnimationSpeed: 70ms;--entranceAnimationSpeed: .25s;--baseRadius: 4px;--lgRadius: 12px;--btnRadius: 4px;accent-color:var(--primaryColor)}html,body,div,span,applet,object,iframe,h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}i{font-family:remixicon!important;font-style:normal;font-weight:400;font-size:1.1238rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}i:before{vertical-align:top;margin-top:1px;display:inline-block}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-arrow-left-down-fill:before{content:""}.ri-arrow-left-down-line:before{content:""}.ri-arrow-left-fill:before{content:""}.ri-arrow-left-line:before{content:""}.ri-arrow-left-right-fill:before{content:""}.ri-arrow-left-right-line:before{content:""}.ri-arrow-left-s-fill:before{content:""}.ri-arrow-left-s-line:before{content:""}.ri-arrow-left-up-fill:before{content:""}.ri-arrow-left-up-line:before{content:""}.ri-arrow-right-circle-fill:before{content:""}.ri-arrow-right-circle-line:before{content:""}.ri-arrow-right-down-fill:before{content:""}.ri-arrow-right-down-line:before{content:""}.ri-arrow-right-fill:before{content:""}.ri-arrow-right-line:before{content:""}.ri-arrow-right-s-fill:before{content:""}.ri-arrow-right-s-line:before{content:""}.ri-arrow-right-up-fill:before{content:""}.ri-arrow-right-up-line:before{content:""}.ri-arrow-up-circle-fill:before{content:""}.ri-arrow-up-circle-line:before{content:""}.ri-arrow-up-down-fill:before{content:""}.ri-arrow-up-down-line:before{content:""}.ri-arrow-up-fill:before{content:""}.ri-arrow-up-line:before{content:""}.ri-arrow-up-s-fill:before{content:""}.ri-arrow-up-s-line:before{content:""}.ri-artboard-2-fill:before{content:""}.ri-artboard-2-line:before{content:""}.ri-artboard-fill:before{content:""}.ri-artboard-line:before{content:""}.ri-article-fill:before{content:""}.ri-article-line:before{content:""}.ri-aspect-ratio-fill:before{content:""}.ri-aspect-ratio-line:before{content:""}.ri-asterisk:before{content:""}.ri-at-fill:before{content:""}.ri-at-line:before{content:""}.ri-attachment-2:before{content:""}.ri-attachment-fill:before{content:""}.ri-attachment-line:before{content:""}.ri-auction-fill:before{content:""}.ri-auction-line:before{content:""}.ri-award-fill:before{content:""}.ri-award-line:before{content:""}.ri-baidu-fill:before{content:""}.ri-baidu-line:before{content:""}.ri-ball-pen-fill:before{content:""}.ri-ball-pen-line:before{content:""}.ri-bank-card-2-fill:before{content:""}.ri-bank-card-2-line:before{content:""}.ri-bank-card-fill:before{content:""}.ri-bank-card-line:before{content:""}.ri-bank-fill:before{content:""}.ri-bank-line:before{content:""}.ri-bar-chart-2-fill:before{content:""}.ri-bar-chart-2-line:before{content:""}.ri-bar-chart-box-fill:before{content:""}.ri-bar-chart-box-line:before{content:""}.ri-bar-chart-fill:before{content:""}.ri-bar-chart-grouped-fill:before{content:""}.ri-bar-chart-grouped-line:before{content:""}.ri-bar-chart-horizontal-fill:before{content:""}.ri-bar-chart-horizontal-line:before{content:""}.ri-bar-chart-line:before{content:""}.ri-barcode-box-fill:before{content:""}.ri-barcode-box-line:before{content:""}.ri-barcode-fill:before{content:""}.ri-barcode-line:before{content:""}.ri-barricade-fill:before{content:""}.ri-barricade-line:before{content:""}.ri-base-station-fill:before{content:""}.ri-base-station-line:before{content:""}.ri-basketball-fill:before{content:""}.ri-basketball-line:before{content:""}.ri-battery-2-charge-fill:before{content:""}.ri-battery-2-charge-line:before{content:""}.ri-battery-2-fill:before{content:""}.ri-battery-2-line:before{content:""}.ri-battery-charge-fill:before{content:""}.ri-battery-charge-line:before{content:""}.ri-battery-fill:before{content:""}.ri-battery-line:before{content:""}.ri-battery-low-fill:before{content:""}.ri-battery-low-line:before{content:""}.ri-battery-saver-fill:before{content:""}.ri-battery-saver-line:before{content:""}.ri-battery-share-fill:before{content:""}.ri-battery-share-line:before{content:""}.ri-bear-smile-fill:before{content:""}.ri-bear-smile-line:before{content:""}.ri-behance-fill:before{content:""}.ri-behance-line:before{content:""}.ri-bell-fill:before{content:""}.ri-bell-line:before{content:""}.ri-bike-fill:before{content:""}.ri-bike-line:before{content:""}.ri-bilibili-fill:before{content:""}.ri-bilibili-line:before{content:""}.ri-bill-fill:before{content:""}.ri-bill-line:before{content:""}.ri-billiards-fill:before{content:""}.ri-billiards-line:before{content:""}.ri-bit-coin-fill:before{content:""}.ri-bit-coin-line:before{content:""}.ri-blaze-fill:before{content:""}.ri-blaze-line:before{content:""}.ri-bluetooth-connect-fill:before{content:""}.ri-bluetooth-connect-line:before{content:""}.ri-bluetooth-fill:before{content:""}.ri-bluetooth-line:before{content:""}.ri-blur-off-fill:before{content:""}.ri-blur-off-line:before{content:""}.ri-body-scan-fill:before{content:""}.ri-body-scan-line:before{content:""}.ri-bold:before{content:""}.ri-book-2-fill:before{content:""}.ri-book-2-line:before{content:""}.ri-book-3-fill:before{content:""}.ri-book-3-line:before{content:""}.ri-book-fill:before{content:""}.ri-book-line:before{content:""}.ri-book-mark-fill:before{content:""}.ri-book-mark-line:before{content:""}.ri-book-open-fill:before{content:""}.ri-book-open-line:before{content:""}.ri-book-read-fill:before{content:""}.ri-book-read-line:before{content:""}.ri-booklet-fill:before{content:""}.ri-booklet-line:before{content:""}.ri-bookmark-2-fill:before{content:""}.ri-bookmark-2-line:before{content:""}.ri-bookmark-3-fill:before{content:""}.ri-bookmark-3-line:before{content:""}.ri-bookmark-fill:before{content:""}.ri-bookmark-line:before{content:""}.ri-boxing-fill:before{content:""}.ri-boxing-line:before{content:""}.ri-braces-fill:before{content:""}.ri-braces-line:before{content:""}.ri-brackets-fill:before{content:""}.ri-brackets-line:before{content:""}.ri-briefcase-2-fill:before{content:""}.ri-briefcase-2-line:before{content:""}.ri-briefcase-3-fill:before{content:""}.ri-briefcase-3-line:before{content:""}.ri-briefcase-4-fill:before{content:""}.ri-briefcase-4-line:before{content:""}.ri-briefcase-5-fill:before{content:""}.ri-briefcase-5-line:before{content:""}.ri-briefcase-fill:before{content:""}.ri-briefcase-line:before{content:""}.ri-bring-forward:before{content:""}.ri-bring-to-front:before{content:""}.ri-broadcast-fill:before{content:""}.ri-broadcast-line:before{content:""}.ri-brush-2-fill:before{content:""}.ri-brush-2-line:before{content:""}.ri-brush-3-fill:before{content:""}.ri-brush-3-line:before{content:""}.ri-brush-4-fill:before{content:""}.ri-brush-4-line:before{content:""}.ri-brush-fill:before{content:""}.ri-brush-line:before{content:""}.ri-bubble-chart-fill:before{content:""}.ri-bubble-chart-line:before{content:""}.ri-bug-2-fill:before{content:""}.ri-bug-2-line:before{content:""}.ri-bug-fill:before{content:""}.ri-bug-line:before{content:""}.ri-building-2-fill:before{content:""}.ri-building-2-line:before{content:""}.ri-building-3-fill:before{content:""}.ri-building-3-line:before{content:""}.ri-building-4-fill:before{content:""}.ri-building-4-line:before{content:""}.ri-building-fill:before{content:""}.ri-building-line:before{content:""}.ri-bus-2-fill:before{content:""}.ri-bus-2-line:before{content:""}.ri-bus-fill:before{content:""}.ri-bus-line:before{content:""}.ri-bus-wifi-fill:before{content:""}.ri-bus-wifi-line:before{content:""}.ri-cactus-fill:before{content:""}.ri-cactus-line:before{content:""}.ri-cake-2-fill:before{content:""}.ri-cake-2-line:before{content:""}.ri-cake-3-fill:before{content:""}.ri-cake-3-line:before{content:""}.ri-cake-fill:before{content:""}.ri-cake-line:before{content:""}.ri-calculator-fill:before{content:""}.ri-calculator-line:before{content:""}.ri-calendar-2-fill:before{content:""}.ri-calendar-2-line:before{content:""}.ri-calendar-check-fill:before{content:""}.ri-calendar-check-line:before{content:""}.ri-calendar-event-fill:before{content:""}.ri-calendar-event-line:before{content:""}.ri-calendar-fill:before{content:""}.ri-calendar-line:before{content:""}.ri-calendar-todo-fill:before{content:""}.ri-calendar-todo-line:before{content:""}.ri-camera-2-fill:before{content:""}.ri-camera-2-line:before{content:""}.ri-camera-3-fill:before{content:""}.ri-camera-3-line:before{content:""}.ri-camera-fill:before{content:""}.ri-camera-lens-fill:before{content:""}.ri-camera-lens-line:before{content:""}.ri-camera-line:before{content:""}.ri-camera-off-fill:before{content:""}.ri-camera-off-line:before{content:""}.ri-camera-switch-fill:before{content:""}.ri-camera-switch-line:before{content:""}.ri-capsule-fill:before{content:""}.ri-capsule-line:before{content:""}.ri-car-fill:before{content:""}.ri-car-line:before{content:""}.ri-car-washing-fill:before{content:""}.ri-car-washing-line:before{content:""}.ri-caravan-fill:before{content:""}.ri-caravan-line:before{content:""}.ri-cast-fill:before{content:""}.ri-cast-line:before{content:""}.ri-cellphone-fill:before{content:""}.ri-cellphone-line:before{content:""}.ri-celsius-fill:before{content:""}.ri-celsius-line:before{content:""}.ri-centos-fill:before{content:""}.ri-centos-line:before{content:""}.ri-character-recognition-fill:before{content:""}.ri-character-recognition-line:before{content:""}.ri-charging-pile-2-fill:before{content:""}.ri-charging-pile-2-line:before{content:""}.ri-charging-pile-fill:before{content:""}.ri-charging-pile-line:before{content:""}.ri-chat-1-fill:before{content:""}.ri-chat-1-line:before{content:""}.ri-chat-2-fill:before{content:""}.ri-chat-2-line:before{content:""}.ri-chat-3-fill:before{content:""}.ri-chat-3-line:before{content:""}.ri-chat-4-fill:before{content:""}.ri-chat-4-line:before{content:""}.ri-chat-check-fill:before{content:""}.ri-chat-check-line:before{content:""}.ri-chat-delete-fill:before{content:""}.ri-chat-delete-line:before{content:""}.ri-chat-download-fill:before{content:""}.ri-chat-download-line:before{content:""}.ri-chat-follow-up-fill:before{content:""}.ri-chat-follow-up-line:before{content:""}.ri-chat-forward-fill:before{content:""}.ri-chat-forward-line:before{content:""}.ri-chat-heart-fill:before{content:""}.ri-chat-heart-line:before{content:""}.ri-chat-history-fill:before{content:""}.ri-chat-history-line:before{content:""}.ri-chat-new-fill:before{content:""}.ri-chat-new-line:before{content:""}.ri-chat-off-fill:before{content:""}.ri-chat-off-line:before{content:""}.ri-chat-poll-fill:before{content:""}.ri-chat-poll-line:before{content:""}.ri-chat-private-fill:before{content:""}.ri-chat-private-line:before{content:""}.ri-chat-quote-fill:before{content:""}.ri-chat-quote-line:before{content:""}.ri-chat-settings-fill:before{content:""}.ri-chat-settings-line:before{content:""}.ri-chat-smile-2-fill:before{content:""}.ri-chat-smile-2-line:before{content:""}.ri-chat-smile-3-fill:before{content:""}.ri-chat-smile-3-line:before{content:""}.ri-chat-smile-fill:before{content:""}.ri-chat-smile-line:before{content:""}.ri-chat-upload-fill:before{content:""}.ri-chat-upload-line:before{content:""}.ri-chat-voice-fill:before{content:""}.ri-chat-voice-line:before{content:""}.ri-check-double-fill:before{content:""}.ri-check-double-line:before{content:""}.ri-check-fill:before{content:""}.ri-check-line:before{content:""}.ri-checkbox-blank-circle-fill:before{content:""}.ri-checkbox-blank-circle-line:before{content:""}.ri-checkbox-blank-fill:before{content:""}.ri-checkbox-blank-line:before{content:""}.ri-checkbox-circle-fill:before{content:""}.ri-checkbox-circle-line:before{content:""}.ri-checkbox-fill:before{content:""}.ri-checkbox-indeterminate-fill:before{content:""}.ri-checkbox-indeterminate-line:before{content:""}.ri-checkbox-line:before{content:""}.ri-checkbox-multiple-blank-fill:before{content:""}.ri-checkbox-multiple-blank-line:before{content:""}.ri-checkbox-multiple-fill:before{content:""}.ri-checkbox-multiple-line:before{content:""}.ri-china-railway-fill:before{content:""}.ri-china-railway-line:before{content:""}.ri-chrome-fill:before{content:""}.ri-chrome-line:before{content:""}.ri-clapperboard-fill:before{content:""}.ri-clapperboard-line:before{content:""}.ri-clipboard-fill:before{content:""}.ri-clipboard-line:before{content:""}.ri-clockwise-2-fill:before{content:""}.ri-clockwise-2-line:before{content:""}.ri-clockwise-fill:before{content:""}.ri-clockwise-line:before{content:""}.ri-close-circle-fill:before{content:""}.ri-close-circle-line:before{content:""}.ri-close-fill:before{content:""}.ri-close-line:before{content:""}.ri-closed-captioning-fill:before{content:""}.ri-closed-captioning-line:before{content:""}.ri-cloud-fill:before{content:""}.ri-cloud-line:before{content:""}.ri-cloud-off-fill:before{content:""}.ri-cloud-off-line:before{content:""}.ri-cloud-windy-fill:before{content:""}.ri-cloud-windy-line:before{content:""}.ri-cloudy-2-fill:before{content:""}.ri-cloudy-2-line:before{content:""}.ri-cloudy-fill:before{content:""}.ri-cloudy-line:before{content:""}.ri-code-box-fill:before{content:""}.ri-code-box-line:before{content:""}.ri-code-fill:before{content:""}.ri-code-line:before{content:""}.ri-code-s-fill:before{content:""}.ri-code-s-line:before{content:""}.ri-code-s-slash-fill:before{content:""}.ri-code-s-slash-line:before{content:""}.ri-code-view:before{content:""}.ri-codepen-fill:before{content:""}.ri-codepen-line:before{content:""}.ri-coin-fill:before{content:""}.ri-coin-line:before{content:""}.ri-coins-fill:before{content:""}.ri-coins-line:before{content:""}.ri-collage-fill:before{content:""}.ri-collage-line:before{content:""}.ri-command-fill:before{content:""}.ri-command-line:before{content:""}.ri-community-fill:before{content:""}.ri-community-line:before{content:""}.ri-compass-2-fill:before{content:""}.ri-compass-2-line:before{content:""}.ri-compass-3-fill:before{content:""}.ri-compass-3-line:before{content:""}.ri-compass-4-fill:before{content:""}.ri-compass-4-line:before{content:""}.ri-compass-discover-fill:before{content:""}.ri-compass-discover-line:before{content:""}.ri-compass-fill:before{content:""}.ri-compass-line:before{content:""}.ri-compasses-2-fill:before{content:""}.ri-compasses-2-line:before{content:""}.ri-compasses-fill:before{content:""}.ri-compasses-line:before{content:""}.ri-computer-fill:before{content:""}.ri-computer-line:before{content:""}.ri-contacts-book-2-fill:before{content:""}.ri-contacts-book-2-line:before{content:""}.ri-contacts-book-fill:before{content:""}.ri-contacts-book-line:before{content:""}.ri-contacts-book-upload-fill:before{content:""}.ri-contacts-book-upload-line:before{content:""}.ri-contacts-fill:before{content:""}.ri-contacts-line:before{content:""}.ri-contrast-2-fill:before{content:""}.ri-contrast-2-line:before{content:""}.ri-contrast-drop-2-fill:before{content:""}.ri-contrast-drop-2-line:before{content:""}.ri-contrast-drop-fill:before{content:""}.ri-contrast-drop-line:before{content:""}.ri-contrast-fill:before{content:""}.ri-contrast-line:before{content:""}.ri-copper-coin-fill:before{content:""}.ri-copper-coin-line:before{content:""}.ri-copper-diamond-fill:before{content:""}.ri-copper-diamond-line:before{content:""}.ri-copyleft-fill:before{content:""}.ri-copyleft-line:before{content:""}.ri-copyright-fill:before{content:""}.ri-copyright-line:before{content:""}.ri-coreos-fill:before{content:""}.ri-coreos-line:before{content:""}.ri-coupon-2-fill:before{content:""}.ri-coupon-2-line:before{content:""}.ri-coupon-3-fill:before{content:""}.ri-coupon-3-line:before{content:""}.ri-coupon-4-fill:before{content:""}.ri-coupon-4-line:before{content:""}.ri-coupon-5-fill:before{content:""}.ri-coupon-5-line:before{content:""}.ri-coupon-fill:before{content:""}.ri-coupon-line:before{content:""}.ri-cpu-fill:before{content:""}.ri-cpu-line:before{content:""}.ri-creative-commons-by-fill:before{content:""}.ri-creative-commons-by-line:before{content:""}.ri-creative-commons-fill:before{content:""}.ri-creative-commons-line:before{content:""}.ri-creative-commons-nc-fill:before{content:""}.ri-creative-commons-nc-line:before{content:""}.ri-creative-commons-nd-fill:before{content:""}.ri-creative-commons-nd-line:before{content:""}.ri-creative-commons-sa-fill:before{content:""}.ri-creative-commons-sa-line:before{content:""}.ri-creative-commons-zero-fill:before{content:""}.ri-creative-commons-zero-line:before{content:""}.ri-criminal-fill:before{content:""}.ri-criminal-line:before{content:""}.ri-crop-2-fill:before{content:""}.ri-crop-2-line:before{content:""}.ri-crop-fill:before{content:""}.ri-crop-line:before{content:""}.ri-css3-fill:before{content:""}.ri-css3-line:before{content:""}.ri-cup-fill:before{content:""}.ri-cup-line:before{content:""}.ri-currency-fill:before{content:""}.ri-currency-line:before{content:""}.ri-cursor-fill:before{content:""}.ri-cursor-line:before{content:""}.ri-customer-service-2-fill:before{content:""}.ri-customer-service-2-line:before{content:""}.ri-customer-service-fill:before{content:""}.ri-customer-service-line:before{content:""}.ri-dashboard-2-fill:before{content:""}.ri-dashboard-2-line:before{content:""}.ri-dashboard-3-fill:before{content:""}.ri-dashboard-3-line:before{content:""}.ri-dashboard-fill:before{content:""}.ri-dashboard-line:before{content:""}.ri-database-2-fill:before{content:""}.ri-database-2-line:before{content:""}.ri-database-fill:before{content:""}.ri-database-line:before{content:""}.ri-delete-back-2-fill:before{content:""}.ri-delete-back-2-line:before{content:""}.ri-delete-back-fill:before{content:""}.ri-delete-back-line:before{content:""}.ri-delete-bin-2-fill:before{content:""}.ri-delete-bin-2-line:before{content:""}.ri-delete-bin-3-fill:before{content:""}.ri-delete-bin-3-line:before{content:""}.ri-delete-bin-4-fill:before{content:""}.ri-delete-bin-4-line:before{content:""}.ri-delete-bin-5-fill:before{content:""}.ri-delete-bin-5-line:before{content:""}.ri-delete-bin-6-fill:before{content:""}.ri-delete-bin-6-line:before{content:""}.ri-delete-bin-7-fill:before{content:""}.ri-delete-bin-7-line:before{content:""}.ri-delete-bin-fill:before{content:""}.ri-delete-bin-line:before{content:""}.ri-delete-column:before{content:""}.ri-delete-row:before{content:""}.ri-device-fill:before{content:""}.ri-device-line:before{content:""}.ri-device-recover-fill:before{content:""}.ri-device-recover-line:before{content:""}.ri-dingding-fill:before{content:""}.ri-dingding-line:before{content:""}.ri-direction-fill:before{content:""}.ri-direction-line:before{content:""}.ri-disc-fill:before{content:""}.ri-disc-line:before{content:""}.ri-discord-fill:before{content:""}.ri-discord-line:before{content:""}.ri-discuss-fill:before{content:""}.ri-discuss-line:before{content:""}.ri-dislike-fill:before{content:""}.ri-dislike-line:before{content:""}.ri-disqus-fill:before{content:""}.ri-disqus-line:before{content:""}.ri-divide-fill:before{content:""}.ri-divide-line:before{content:""}.ri-donut-chart-fill:before{content:""}.ri-donut-chart-line:before{content:""}.ri-door-closed-fill:before{content:""}.ri-door-closed-line:before{content:""}.ri-door-fill:before{content:""}.ri-door-line:before{content:""}.ri-door-lock-box-fill:before{content:""}.ri-door-lock-box-line:before{content:""}.ri-door-lock-fill:before{content:""}.ri-door-lock-line:before{content:""}.ri-door-open-fill:before{content:""}.ri-door-open-line:before{content:""}.ri-dossier-fill:before{content:""}.ri-dossier-line:before{content:""}.ri-douban-fill:before{content:""}.ri-douban-line:before{content:""}.ri-double-quotes-l:before{content:""}.ri-double-quotes-r:before{content:""}.ri-download-2-fill:before{content:""}.ri-download-2-line:before{content:""}.ri-download-cloud-2-fill:before{content:""}.ri-download-cloud-2-line:before{content:""}.ri-download-cloud-fill:before{content:""}.ri-download-cloud-line:before{content:""}.ri-download-fill:before{content:""}.ri-download-line:before{content:""}.ri-draft-fill:before{content:""}.ri-draft-line:before{content:""}.ri-drag-drop-fill:before{content:""}.ri-drag-drop-line:before{content:""}.ri-drag-move-2-fill:before{content:""}.ri-drag-move-2-line:before{content:""}.ri-drag-move-fill:before{content:""}.ri-drag-move-line:before{content:""}.ri-dribbble-fill:before{content:""}.ri-dribbble-line:before{content:""}.ri-drive-fill:before{content:""}.ri-drive-line:before{content:""}.ri-drizzle-fill:before{content:""}.ri-drizzle-line:before{content:""}.ri-drop-fill:before{content:""}.ri-drop-line:before{content:""}.ri-dropbox-fill:before{content:""}.ri-dropbox-line:before{content:""}.ri-dual-sim-1-fill:before{content:""}.ri-dual-sim-1-line:before{content:""}.ri-dual-sim-2-fill:before{content:""}.ri-dual-sim-2-line:before{content:""}.ri-dv-fill:before{content:""}.ri-dv-line:before{content:""}.ri-dvd-fill:before{content:""}.ri-dvd-line:before{content:""}.ri-e-bike-2-fill:before{content:""}.ri-e-bike-2-line:before{content:""}.ri-e-bike-fill:before{content:""}.ri-e-bike-line:before{content:""}.ri-earth-fill:before{content:""}.ri-earth-line:before{content:""}.ri-earthquake-fill:before{content:""}.ri-earthquake-line:before{content:""}.ri-edge-fill:before{content:""}.ri-edge-line:before{content:""}.ri-edit-2-fill:before{content:""}.ri-edit-2-line:before{content:""}.ri-edit-box-fill:before{content:""}.ri-edit-box-line:before{content:""}.ri-edit-circle-fill:before{content:""}.ri-edit-circle-line:before{content:""}.ri-edit-fill:before{content:""}.ri-edit-line:before{content:""}.ri-eject-fill:before{content:""}.ri-eject-line:before{content:""}.ri-emotion-2-fill:before{content:""}.ri-emotion-2-line:before{content:""}.ri-emotion-fill:before{content:""}.ri-emotion-happy-fill:before{content:""}.ri-emotion-happy-line:before{content:""}.ri-emotion-laugh-fill:before{content:""}.ri-emotion-laugh-line:before{content:""}.ri-emotion-line:before{content:""}.ri-emotion-normal-fill:before{content:""}.ri-emotion-normal-line:before{content:""}.ri-emotion-sad-fill:before{content:""}.ri-emotion-sad-line:before{content:""}.ri-emotion-unhappy-fill:before{content:""}.ri-emotion-unhappy-line:before{content:""}.ri-empathize-fill:before{content:""}.ri-empathize-line:before{content:""}.ri-emphasis-cn:before{content:""}.ri-emphasis:before{content:""}.ri-english-input:before{content:""}.ri-equalizer-fill:before{content:""}.ri-equalizer-line:before{content:""}.ri-eraser-fill:before{content:""}.ri-eraser-line:before{content:""}.ri-error-warning-fill:before{content:""}.ri-error-warning-line:before{content:""}.ri-evernote-fill:before{content:""}.ri-evernote-line:before{content:""}.ri-exchange-box-fill:before{content:""}.ri-exchange-box-line:before{content:""}.ri-exchange-cny-fill:before{content:""}.ri-exchange-cny-line:before{content:""}.ri-exchange-dollar-fill:before{content:""}.ri-exchange-dollar-line:before{content:""}.ri-exchange-fill:before{content:""}.ri-exchange-funds-fill:before{content:""}.ri-exchange-funds-line:before{content:""}.ri-exchange-line:before{content:""}.ri-external-link-fill:before{content:""}.ri-external-link-line:before{content:""}.ri-eye-2-fill:before{content:""}.ri-eye-2-line:before{content:""}.ri-eye-close-fill:before{content:""}.ri-eye-close-line:before{content:""}.ri-eye-fill:before{content:""}.ri-eye-line:before{content:""}.ri-eye-off-fill:before{content:""}.ri-eye-off-line:before{content:""}.ri-facebook-box-fill:before{content:""}.ri-facebook-box-line:before{content:""}.ri-facebook-circle-fill:before{content:""}.ri-facebook-circle-line:before{content:""}.ri-facebook-fill:before{content:""}.ri-facebook-line:before{content:""}.ri-fahrenheit-fill:before{content:""}.ri-fahrenheit-line:before{content:""}.ri-feedback-fill:before{content:""}.ri-feedback-line:before{content:""}.ri-file-2-fill:before{content:""}.ri-file-2-line:before{content:""}.ri-file-3-fill:before{content:""}.ri-file-3-line:before{content:""}.ri-file-4-fill:before{content:""}.ri-file-4-line:before{content:""}.ri-file-add-fill:before{content:""}.ri-file-add-line:before{content:""}.ri-file-chart-2-fill:before{content:""}.ri-file-chart-2-line:before{content:""}.ri-file-chart-fill:before{content:""}.ri-file-chart-line:before{content:""}.ri-file-cloud-fill:before{content:""}.ri-file-cloud-line:before{content:""}.ri-file-code-fill:before{content:""}.ri-file-code-line:before{content:""}.ri-file-copy-2-fill:before{content:""}.ri-file-copy-2-line:before{content:""}.ri-file-copy-fill:before{content:""}.ri-file-copy-line:before{content:""}.ri-file-damage-fill:before{content:""}.ri-file-damage-line:before{content:""}.ri-file-download-fill:before{content:""}.ri-file-download-line:before{content:""}.ri-file-edit-fill:before{content:""}.ri-file-edit-line:before{content:""}.ri-file-excel-2-fill:before{content:""}.ri-file-excel-2-line:before{content:""}.ri-file-excel-fill:before{content:""}.ri-file-excel-line:before{content:""}.ri-file-fill:before{content:""}.ri-file-forbid-fill:before{content:""}.ri-file-forbid-line:before{content:""}.ri-file-gif-fill:before{content:""}.ri-file-gif-line:before{content:""}.ri-file-history-fill:before{content:""}.ri-file-history-line:before{content:""}.ri-file-hwp-fill:before{content:""}.ri-file-hwp-line:before{content:""}.ri-file-info-fill:before{content:""}.ri-file-info-line:before{content:""}.ri-file-line:before{content:""}.ri-file-list-2-fill:before{content:""}.ri-file-list-2-line:before{content:""}.ri-file-list-3-fill:before{content:""}.ri-file-list-3-line:before{content:""}.ri-file-list-fill:before{content:""}.ri-file-list-line:before{content:""}.ri-file-lock-fill:before{content:""}.ri-file-lock-line:before{content:""}.ri-file-mark-fill:before{content:""}.ri-file-mark-line:before{content:""}.ri-file-music-fill:before{content:""}.ri-file-music-line:before{content:""}.ri-file-paper-2-fill:before{content:""}.ri-file-paper-2-line:before{content:""}.ri-file-paper-fill:before{content:""}.ri-file-paper-line:before{content:""}.ri-file-pdf-fill:before{content:""}.ri-file-pdf-line:before{content:""}.ri-file-ppt-2-fill:before{content:""}.ri-file-ppt-2-line:before{content:""}.ri-file-ppt-fill:before{content:""}.ri-file-ppt-line:before{content:""}.ri-file-reduce-fill:before{content:""}.ri-file-reduce-line:before{content:""}.ri-file-search-fill:before{content:""}.ri-file-search-line:before{content:""}.ri-file-settings-fill:before{content:""}.ri-file-settings-line:before{content:""}.ri-file-shield-2-fill:before{content:""}.ri-file-shield-2-line:before{content:""}.ri-file-shield-fill:before{content:""}.ri-file-shield-line:before{content:""}.ri-file-shred-fill:before{content:""}.ri-file-shred-line:before{content:""}.ri-file-text-fill:before{content:""}.ri-file-text-line:before{content:""}.ri-file-transfer-fill:before{content:""}.ri-file-transfer-line:before{content:""}.ri-file-unknow-fill:before{content:""}.ri-file-unknow-line:before{content:""}.ri-file-upload-fill:before{content:""}.ri-file-upload-line:before{content:""}.ri-file-user-fill:before{content:""}.ri-file-user-line:before{content:""}.ri-file-warning-fill:before{content:""}.ri-file-warning-line:before{content:""}.ri-file-word-2-fill:before{content:""}.ri-file-word-2-line:before{content:""}.ri-file-word-fill:before{content:""}.ri-file-word-line:before{content:""}.ri-file-zip-fill:before{content:""}.ri-file-zip-line:before{content:""}.ri-film-fill:before{content:""}.ri-film-line:before{content:""}.ri-filter-2-fill:before{content:""}.ri-filter-2-line:before{content:""}.ri-filter-3-fill:before{content:""}.ri-filter-3-line:before{content:""}.ri-filter-fill:before{content:""}.ri-filter-line:before{content:""}.ri-filter-off-fill:before{content:""}.ri-filter-off-line:before{content:""}.ri-find-replace-fill:before{content:""}.ri-find-replace-line:before{content:""}.ri-finder-fill:before{content:""}.ri-finder-line:before{content:""}.ri-fingerprint-2-fill:before{content:""}.ri-fingerprint-2-line:before{content:""}.ri-fingerprint-fill:before{content:""}.ri-fingerprint-line:before{content:""}.ri-fire-fill:before{content:""}.ri-fire-line:before{content:""}.ri-firefox-fill:before{content:""}.ri-firefox-line:before{content:""}.ri-first-aid-kit-fill:before{content:""}.ri-first-aid-kit-line:before{content:""}.ri-flag-2-fill:before{content:""}.ri-flag-2-line:before{content:""}.ri-flag-fill:before{content:""}.ri-flag-line:before{content:""}.ri-flashlight-fill:before{content:""}.ri-flashlight-line:before{content:""}.ri-flask-fill:before{content:""}.ri-flask-line:before{content:""}.ri-flight-land-fill:before{content:""}.ri-flight-land-line:before{content:""}.ri-flight-takeoff-fill:before{content:""}.ri-flight-takeoff-line:before{content:""}.ri-flood-fill:before{content:""}.ri-flood-line:before{content:""}.ri-flow-chart:before{content:""}.ri-flutter-fill:before{content:""}.ri-flutter-line:before{content:""}.ri-focus-2-fill:before{content:""}.ri-focus-2-line:before{content:""}.ri-focus-3-fill:before{content:""}.ri-focus-3-line:before{content:""}.ri-focus-fill:before{content:""}.ri-focus-line:before{content:""}.ri-foggy-fill:before{content:""}.ri-foggy-line:before{content:""}.ri-folder-2-fill:before{content:""}.ri-folder-2-line:before{content:""}.ri-folder-3-fill:before{content:""}.ri-folder-3-line:before{content:""}.ri-folder-4-fill:before{content:""}.ri-folder-4-line:before{content:""}.ri-folder-5-fill:before{content:""}.ri-folder-5-line:before{content:""}.ri-folder-add-fill:before{content:""}.ri-folder-add-line:before{content:""}.ri-folder-chart-2-fill:before{content:""}.ri-folder-chart-2-line:before{content:""}.ri-folder-chart-fill:before{content:""}.ri-folder-chart-line:before{content:""}.ri-folder-download-fill:before{content:""}.ri-folder-download-line:before{content:""}.ri-folder-fill:before{content:""}.ri-folder-forbid-fill:before{content:""}.ri-folder-forbid-line:before{content:""}.ri-folder-history-fill:before{content:""}.ri-folder-history-line:before{content:""}.ri-folder-info-fill:before{content:""}.ri-folder-info-line:before{content:""}.ri-folder-keyhole-fill:before{content:""}.ri-folder-keyhole-line:before{content:""}.ri-folder-line:before{content:""}.ri-folder-lock-fill:before{content:""}.ri-folder-lock-line:before{content:""}.ri-folder-music-fill:before{content:""}.ri-folder-music-line:before{content:""}.ri-folder-open-fill:before{content:""}.ri-folder-open-line:before{content:""}.ri-folder-received-fill:before{content:""}.ri-folder-received-line:before{content:""}.ri-folder-reduce-fill:before{content:""}.ri-folder-reduce-line:before{content:""}.ri-folder-settings-fill:before{content:""}.ri-folder-settings-line:before{content:""}.ri-folder-shared-fill:before{content:""}.ri-folder-shared-line:before{content:""}.ri-folder-shield-2-fill:before{content:""}.ri-folder-shield-2-line:before{content:""}.ri-folder-shield-fill:before{content:""}.ri-folder-shield-line:before{content:""}.ri-folder-transfer-fill:before{content:""}.ri-folder-transfer-line:before{content:""}.ri-folder-unknow-fill:before{content:""}.ri-folder-unknow-line:before{content:""}.ri-folder-upload-fill:before{content:""}.ri-folder-upload-line:before{content:""}.ri-folder-user-fill:before{content:""}.ri-folder-user-line:before{content:""}.ri-folder-warning-fill:before{content:""}.ri-folder-warning-line:before{content:""}.ri-folder-zip-fill:before{content:""}.ri-folder-zip-line:before{content:""}.ri-folders-fill:before{content:""}.ri-folders-line:before{content:""}.ri-font-color:before{content:""}.ri-font-size-2:before{content:""}.ri-font-size:before{content:""}.ri-football-fill:before{content:""}.ri-football-line:before{content:""}.ri-footprint-fill:before{content:""}.ri-footprint-line:before{content:""}.ri-forbid-2-fill:before{content:""}.ri-forbid-2-line:before{content:""}.ri-forbid-fill:before{content:""}.ri-forbid-line:before{content:""}.ri-format-clear:before{content:""}.ri-fridge-fill:before{content:""}.ri-fridge-line:before{content:""}.ri-fullscreen-exit-fill:before{content:""}.ri-fullscreen-exit-line:before{content:""}.ri-fullscreen-fill:before{content:""}.ri-fullscreen-line:before{content:""}.ri-function-fill:before{content:""}.ri-function-line:before{content:""}.ri-functions:before{content:""}.ri-funds-box-fill:before{content:""}.ri-funds-box-line:before{content:""}.ri-funds-fill:before{content:""}.ri-funds-line:before{content:""}.ri-gallery-fill:before{content:""}.ri-gallery-line:before{content:""}.ri-gallery-upload-fill:before{content:""}.ri-gallery-upload-line:before{content:""}.ri-game-fill:before{content:""}.ri-game-line:before{content:""}.ri-gamepad-fill:before{content:""}.ri-gamepad-line:before{content:""}.ri-gas-station-fill:before{content:""}.ri-gas-station-line:before{content:""}.ri-gatsby-fill:before{content:""}.ri-gatsby-line:before{content:""}.ri-genderless-fill:before{content:""}.ri-genderless-line:before{content:""}.ri-ghost-2-fill:before{content:""}.ri-ghost-2-line:before{content:""}.ri-ghost-fill:before{content:""}.ri-ghost-line:before{content:""}.ri-ghost-smile-fill:before{content:""}.ri-ghost-smile-line:before{content:""}.ri-gift-2-fill:before{content:""}.ri-gift-2-line:before{content:""}.ri-gift-fill:before{content:""}.ri-gift-line:before{content:""}.ri-git-branch-fill:before{content:""}.ri-git-branch-line:before{content:""}.ri-git-commit-fill:before{content:""}.ri-git-commit-line:before{content:""}.ri-git-merge-fill:before{content:""}.ri-git-merge-line:before{content:""}.ri-git-pull-request-fill:before{content:""}.ri-git-pull-request-line:before{content:""}.ri-git-repository-commits-fill:before{content:""}.ri-git-repository-commits-line:before{content:""}.ri-git-repository-fill:before{content:""}.ri-git-repository-line:before{content:""}.ri-git-repository-private-fill:before{content:""}.ri-git-repository-private-line:before{content:""}.ri-github-fill:before{content:""}.ri-github-line:before{content:""}.ri-gitlab-fill:before{content:""}.ri-gitlab-line:before{content:""}.ri-global-fill:before{content:""}.ri-global-line:before{content:""}.ri-globe-fill:before{content:""}.ri-globe-line:before{content:""}.ri-goblet-fill:before{content:""}.ri-goblet-line:before{content:""}.ri-google-fill:before{content:""}.ri-google-line:before{content:""}.ri-google-play-fill:before{content:""}.ri-google-play-line:before{content:""}.ri-government-fill:before{content:""}.ri-government-line:before{content:""}.ri-gps-fill:before{content:""}.ri-gps-line:before{content:""}.ri-gradienter-fill:before{content:""}.ri-gradienter-line:before{content:""}.ri-grid-fill:before{content:""}.ri-grid-line:before{content:""}.ri-group-2-fill:before{content:""}.ri-group-2-line:before{content:""}.ri-group-fill:before{content:""}.ri-group-line:before{content:""}.ri-guide-fill:before{content:""}.ri-guide-line:before{content:""}.ri-h-1:before{content:""}.ri-h-2:before{content:""}.ri-h-3:before{content:""}.ri-h-4:before{content:""}.ri-h-5:before{content:""}.ri-h-6:before{content:""}.ri-hail-fill:before{content:""}.ri-hail-line:before{content:""}.ri-hammer-fill:before{content:""}.ri-hammer-line:before{content:""}.ri-hand-coin-fill:before{content:""}.ri-hand-coin-line:before{content:""}.ri-hand-heart-fill:before{content:""}.ri-hand-heart-line:before{content:""}.ri-hand-sanitizer-fill:before{content:""}.ri-hand-sanitizer-line:before{content:""}.ri-handbag-fill:before{content:""}.ri-handbag-line:before{content:""}.ri-hard-drive-2-fill:before{content:""}.ri-hard-drive-2-line:before{content:""}.ri-hard-drive-fill:before{content:""}.ri-hard-drive-line:before{content:""}.ri-hashtag:before{content:""}.ri-haze-2-fill:before{content:""}.ri-haze-2-line:before{content:""}.ri-haze-fill:before{content:""}.ri-haze-line:before{content:""}.ri-hd-fill:before{content:""}.ri-hd-line:before{content:""}.ri-heading:before{content:""}.ri-headphone-fill:before{content:""}.ri-headphone-line:before{content:""}.ri-health-book-fill:before{content:""}.ri-health-book-line:before{content:""}.ri-heart-2-fill:before{content:""}.ri-heart-2-line:before{content:""}.ri-heart-3-fill:before{content:""}.ri-heart-3-line:before{content:""}.ri-heart-add-fill:before{content:""}.ri-heart-add-line:before{content:""}.ri-heart-fill:before{content:""}.ri-heart-line:before{content:""}.ri-heart-pulse-fill:before{content:""}.ri-heart-pulse-line:before{content:""}.ri-hearts-fill:before{content:""}.ri-hearts-line:before{content:""}.ri-heavy-showers-fill:before{content:""}.ri-heavy-showers-line:before{content:""}.ri-history-fill:before{content:""}.ri-history-line:before{content:""}.ri-home-2-fill:before{content:""}.ri-home-2-line:before{content:""}.ri-home-3-fill:before{content:""}.ri-home-3-line:before{content:""}.ri-home-4-fill:before{content:""}.ri-home-4-line:before{content:""}.ri-home-5-fill:before{content:""}.ri-home-5-line:before{content:""}.ri-home-6-fill:before{content:""}.ri-home-6-line:before{content:""}.ri-home-7-fill:before{content:""}.ri-home-7-line:before{content:""}.ri-home-8-fill:before{content:""}.ri-home-8-line:before{content:""}.ri-home-fill:before{content:""}.ri-home-gear-fill:before{content:""}.ri-home-gear-line:before{content:""}.ri-home-heart-fill:before{content:""}.ri-home-heart-line:before{content:""}.ri-home-line:before{content:""}.ri-home-smile-2-fill:before{content:""}.ri-home-smile-2-line:before{content:""}.ri-home-smile-fill:before{content:""}.ri-home-smile-line:before{content:""}.ri-home-wifi-fill:before{content:""}.ri-home-wifi-line:before{content:""}.ri-honor-of-kings-fill:before{content:""}.ri-honor-of-kings-line:before{content:""}.ri-honour-fill:before{content:""}.ri-honour-line:before{content:""}.ri-hospital-fill:before{content:""}.ri-hospital-line:before{content:""}.ri-hotel-bed-fill:before{content:""}.ri-hotel-bed-line:before{content:""}.ri-hotel-fill:before{content:""}.ri-hotel-line:before{content:""}.ri-hotspot-fill:before{content:""}.ri-hotspot-line:before{content:""}.ri-hq-fill:before{content:""}.ri-hq-line:before{content:""}.ri-html5-fill:before{content:""}.ri-html5-line:before{content:""}.ri-ie-fill:before{content:""}.ri-ie-line:before{content:""}.ri-image-2-fill:before{content:""}.ri-image-2-line:before{content:""}.ri-image-add-fill:before{content:""}.ri-image-add-line:before{content:""}.ri-image-edit-fill:before{content:""}.ri-image-edit-line:before{content:""}.ri-image-fill:before{content:""}.ri-image-line:before{content:""}.ri-inbox-archive-fill:before{content:""}.ri-inbox-archive-line:before{content:""}.ri-inbox-fill:before{content:""}.ri-inbox-line:before{content:""}.ri-inbox-unarchive-fill:before{content:""}.ri-inbox-unarchive-line:before{content:""}.ri-increase-decrease-fill:before{content:""}.ri-increase-decrease-line:before{content:""}.ri-indent-decrease:before{content:""}.ri-indent-increase:before{content:""}.ri-indeterminate-circle-fill:before{content:""}.ri-indeterminate-circle-line:before{content:""}.ri-information-fill:before{content:""}.ri-information-line:before{content:""}.ri-infrared-thermometer-fill:before{content:""}.ri-infrared-thermometer-line:before{content:""}.ri-ink-bottle-fill:before{content:""}.ri-ink-bottle-line:before{content:""}.ri-input-cursor-move:before{content:""}.ri-input-method-fill:before{content:""}.ri-input-method-line:before{content:""}.ri-insert-column-left:before{content:""}.ri-insert-column-right:before{content:""}.ri-insert-row-bottom:before{content:""}.ri-insert-row-top:before{content:""}.ri-instagram-fill:before{content:""}.ri-instagram-line:before{content:""}.ri-install-fill:before{content:""}.ri-install-line:before{content:""}.ri-invision-fill:before{content:""}.ri-invision-line:before{content:""}.ri-italic:before{content:""}.ri-kakao-talk-fill:before{content:""}.ri-kakao-talk-line:before{content:""}.ri-key-2-fill:before{content:""}.ri-key-2-line:before{content:""}.ri-key-fill:before{content:""}.ri-key-line:before{content:""}.ri-keyboard-box-fill:before{content:""}.ri-keyboard-box-line:before{content:""}.ri-keyboard-fill:before{content:""}.ri-keyboard-line:before{content:""}.ri-keynote-fill:before{content:""}.ri-keynote-line:before{content:""}.ri-knife-blood-fill:before{content:""}.ri-knife-blood-line:before{content:""}.ri-knife-fill:before{content:""}.ri-knife-line:before{content:""}.ri-landscape-fill:before{content:""}.ri-landscape-line:before{content:""}.ri-layout-2-fill:before{content:""}.ri-layout-2-line:before{content:""}.ri-layout-3-fill:before{content:""}.ri-layout-3-line:before{content:""}.ri-layout-4-fill:before{content:""}.ri-layout-4-line:before{content:""}.ri-layout-5-fill:before{content:""}.ri-layout-5-line:before{content:""}.ri-layout-6-fill:before{content:""}.ri-layout-6-line:before{content:""}.ri-layout-bottom-2-fill:before{content:""}.ri-layout-bottom-2-line:before{content:""}.ri-layout-bottom-fill:before{content:""}.ri-layout-bottom-line:before{content:""}.ri-layout-column-fill:before{content:""}.ri-layout-column-line:before{content:""}.ri-layout-fill:before{content:""}.ri-layout-grid-fill:before{content:""}.ri-layout-grid-line:before{content:""}.ri-layout-left-2-fill:before{content:""}.ri-layout-left-2-line:before{content:""}.ri-layout-left-fill:before{content:""}.ri-layout-left-line:before{content:""}.ri-layout-line:before{content:""}.ri-layout-masonry-fill:before{content:""}.ri-layout-masonry-line:before{content:""}.ri-layout-right-2-fill:before{content:""}.ri-layout-right-2-line:before{content:""}.ri-layout-right-fill:before{content:""}.ri-layout-right-line:before{content:""}.ri-layout-row-fill:before{content:""}.ri-layout-row-line:before{content:""}.ri-layout-top-2-fill:before{content:""}.ri-layout-top-2-line:before{content:""}.ri-layout-top-fill:before{content:""}.ri-layout-top-line:before{content:""}.ri-leaf-fill:before{content:""}.ri-leaf-line:before{content:""}.ri-lifebuoy-fill:before{content:""}.ri-lifebuoy-line:before{content:""}.ri-lightbulb-fill:before{content:""}.ri-lightbulb-flash-fill:before{content:""}.ri-lightbulb-flash-line:before{content:""}.ri-lightbulb-line:before{content:""}.ri-line-chart-fill:before{content:""}.ri-line-chart-line:before{content:""}.ri-line-fill:before{content:""}.ri-line-height:before{content:""}.ri-line-line:before{content:""}.ri-link-m:before{content:""}.ri-link-unlink-m:before{content:""}.ri-link-unlink:before{content:""}.ri-link:before{content:""}.ri-linkedin-box-fill:before{content:""}.ri-linkedin-box-line:before{content:""}.ri-linkedin-fill:before{content:""}.ri-linkedin-line:before{content:""}.ri-links-fill:before{content:""}.ri-links-line:before{content:""}.ri-list-check-2:before{content:""}.ri-list-check:before{content:""}.ri-list-ordered:before{content:""}.ri-list-settings-fill:before{content:""}.ri-list-settings-line:before{content:""}.ri-list-unordered:before{content:""}.ri-live-fill:before{content:""}.ri-live-line:before{content:""}.ri-loader-2-fill:before{content:""}.ri-loader-2-line:before{content:""}.ri-loader-3-fill:before{content:""}.ri-loader-3-line:before{content:""}.ri-loader-4-fill:before{content:""}.ri-loader-4-line:before{content:""}.ri-loader-5-fill:before{content:""}.ri-loader-5-line:before{content:""}.ri-loader-fill:before{content:""}.ri-loader-line:before{content:""}.ri-lock-2-fill:before{content:""}.ri-lock-2-line:before{content:""}.ri-lock-fill:before{content:""}.ri-lock-line:before{content:""}.ri-lock-password-fill:before{content:""}.ri-lock-password-line:before{content:""}.ri-lock-unlock-fill:before{content:""}.ri-lock-unlock-line:before{content:""}.ri-login-box-fill:before{content:""}.ri-login-box-line:before{content:""}.ri-login-circle-fill:before{content:""}.ri-login-circle-line:before{content:""}.ri-logout-box-fill:before{content:""}.ri-logout-box-line:before{content:""}.ri-logout-box-r-fill:before{content:""}.ri-logout-box-r-line:before{content:""}.ri-logout-circle-fill:before{content:""}.ri-logout-circle-line:before{content:""}.ri-logout-circle-r-fill:before{content:""}.ri-logout-circle-r-line:before{content:""}.ri-luggage-cart-fill:before{content:""}.ri-luggage-cart-line:before{content:""}.ri-luggage-deposit-fill:before{content:""}.ri-luggage-deposit-line:before{content:""}.ri-lungs-fill:before{content:""}.ri-lungs-line:before{content:""}.ri-mac-fill:before{content:""}.ri-mac-line:before{content:""}.ri-macbook-fill:before{content:""}.ri-macbook-line:before{content:""}.ri-magic-fill:before{content:""}.ri-magic-line:before{content:""}.ri-mail-add-fill:before{content:""}.ri-mail-add-line:before{content:""}.ri-mail-check-fill:before{content:""}.ri-mail-check-line:before{content:""}.ri-mail-close-fill:before{content:""}.ri-mail-close-line:before{content:""}.ri-mail-download-fill:before{content:""}.ri-mail-download-line:before{content:""}.ri-mail-fill:before{content:""}.ri-mail-forbid-fill:before{content:""}.ri-mail-forbid-line:before{content:""}.ri-mail-line:before{content:""}.ri-mail-lock-fill:before{content:""}.ri-mail-lock-line:before{content:""}.ri-mail-open-fill:before{content:""}.ri-mail-open-line:before{content:""}.ri-mail-send-fill:before{content:""}.ri-mail-send-line:before{content:""}.ri-mail-settings-fill:before{content:""}.ri-mail-settings-line:before{content:""}.ri-mail-star-fill:before{content:""}.ri-mail-star-line:before{content:""}.ri-mail-unread-fill:before{content:""}.ri-mail-unread-line:before{content:""}.ri-mail-volume-fill:before{content:""}.ri-mail-volume-line:before{content:""}.ri-map-2-fill:before{content:""}.ri-map-2-line:before{content:""}.ri-map-fill:before{content:""}.ri-map-line:before{content:""}.ri-map-pin-2-fill:before{content:""}.ri-map-pin-2-line:before{content:""}.ri-map-pin-3-fill:before{content:""}.ri-map-pin-3-line:before{content:""}.ri-map-pin-4-fill:before{content:""}.ri-map-pin-4-line:before{content:""}.ri-map-pin-5-fill:before{content:""}.ri-map-pin-5-line:before{content:""}.ri-map-pin-add-fill:before{content:""}.ri-map-pin-add-line:before{content:""}.ri-map-pin-fill:before{content:""}.ri-map-pin-line:before{content:""}.ri-map-pin-range-fill:before{content:""}.ri-map-pin-range-line:before{content:""}.ri-map-pin-time-fill:before{content:""}.ri-map-pin-time-line:before{content:""}.ri-map-pin-user-fill:before{content:""}.ri-map-pin-user-line:before{content:""}.ri-mark-pen-fill:before{content:""}.ri-mark-pen-line:before{content:""}.ri-markdown-fill:before{content:""}.ri-markdown-line:before{content:""}.ri-markup-fill:before{content:""}.ri-markup-line:before{content:""}.ri-mastercard-fill:before{content:""}.ri-mastercard-line:before{content:""}.ri-mastodon-fill:before{content:""}.ri-mastodon-line:before{content:""}.ri-medal-2-fill:before{content:""}.ri-medal-2-line:before{content:""}.ri-medal-fill:before{content:""}.ri-medal-line:before{content:""}.ri-medicine-bottle-fill:before{content:""}.ri-medicine-bottle-line:before{content:""}.ri-medium-fill:before{content:""}.ri-medium-line:before{content:""}.ri-men-fill:before{content:""}.ri-men-line:before{content:""}.ri-mental-health-fill:before{content:""}.ri-mental-health-line:before{content:""}.ri-menu-2-fill:before{content:""}.ri-menu-2-line:before{content:""}.ri-menu-3-fill:before{content:""}.ri-menu-3-line:before{content:""}.ri-menu-4-fill:before{content:""}.ri-menu-4-line:before{content:""}.ri-menu-5-fill:before{content:""}.ri-menu-5-line:before{content:""}.ri-menu-add-fill:before{content:""}.ri-menu-add-line:before{content:""}.ri-menu-fill:before{content:""}.ri-menu-fold-fill:before{content:""}.ri-menu-fold-line:before{content:""}.ri-menu-line:before{content:""}.ri-menu-unfold-fill:before{content:""}.ri-menu-unfold-line:before{content:""}.ri-merge-cells-horizontal:before{content:""}.ri-merge-cells-vertical:before{content:""}.ri-message-2-fill:before{content:""}.ri-message-2-line:before{content:""}.ri-message-3-fill:before{content:""}.ri-message-3-line:before{content:""}.ri-message-fill:before{content:""}.ri-message-line:before{content:""}.ri-messenger-fill:before{content:""}.ri-messenger-line:before{content:""}.ri-meteor-fill:before{content:""}.ri-meteor-line:before{content:""}.ri-mic-2-fill:before{content:""}.ri-mic-2-line:before{content:""}.ri-mic-fill:before{content:""}.ri-mic-line:before{content:""}.ri-mic-off-fill:before{content:""}.ri-mic-off-line:before{content:""}.ri-mickey-fill:before{content:""}.ri-mickey-line:before{content:""}.ri-microscope-fill:before{content:""}.ri-microscope-line:before{content:""}.ri-microsoft-fill:before{content:""}.ri-microsoft-line:before{content:""}.ri-mind-map:before{content:""}.ri-mini-program-fill:before{content:""}.ri-mini-program-line:before{content:""}.ri-mist-fill:before{content:""}.ri-mist-line:before{content:""}.ri-money-cny-box-fill:before{content:""}.ri-money-cny-box-line:before{content:""}.ri-money-cny-circle-fill:before{content:""}.ri-money-cny-circle-line:before{content:""}.ri-money-dollar-box-fill:before{content:""}.ri-money-dollar-box-line:before{content:""}.ri-money-dollar-circle-fill:before{content:""}.ri-money-dollar-circle-line:before{content:""}.ri-money-euro-box-fill:before{content:""}.ri-money-euro-box-line:before{content:""}.ri-money-euro-circle-fill:before{content:""}.ri-money-euro-circle-line:before{content:""}.ri-money-pound-box-fill:before{content:""}.ri-money-pound-box-line:before{content:""}.ri-money-pound-circle-fill:before{content:""}.ri-money-pound-circle-line:before{content:""}.ri-moon-clear-fill:before{content:""}.ri-moon-clear-line:before{content:""}.ri-moon-cloudy-fill:before{content:""}.ri-moon-cloudy-line:before{content:""}.ri-moon-fill:before{content:""}.ri-moon-foggy-fill:before{content:""}.ri-moon-foggy-line:before{content:""}.ri-moon-line:before{content:""}.ri-more-2-fill:before{content:""}.ri-more-2-line:before{content:""}.ri-more-fill:before{content:""}.ri-more-line:before{content:""}.ri-motorbike-fill:before{content:""}.ri-motorbike-line:before{content:""}.ri-mouse-fill:before{content:""}.ri-mouse-line:before{content:""}.ri-movie-2-fill:before{content:""}.ri-movie-2-line:before{content:""}.ri-movie-fill:before{content:""}.ri-movie-line:before{content:""}.ri-music-2-fill:before{content:""}.ri-music-2-line:before{content:""}.ri-music-fill:before{content:""}.ri-music-line:before{content:""}.ri-mv-fill:before{content:""}.ri-mv-line:before{content:""}.ri-navigation-fill:before{content:""}.ri-navigation-line:before{content:""}.ri-netease-cloud-music-fill:before{content:""}.ri-netease-cloud-music-line:before{content:""}.ri-netflix-fill:before{content:""}.ri-netflix-line:before{content:""}.ri-newspaper-fill:before{content:""}.ri-newspaper-line:before{content:""}.ri-node-tree:before{content:""}.ri-notification-2-fill:before{content:""}.ri-notification-2-line:before{content:""}.ri-notification-3-fill:before{content:""}.ri-notification-3-line:before{content:""}.ri-notification-4-fill:before{content:""}.ri-notification-4-line:before{content:""}.ri-notification-badge-fill:before{content:""}.ri-notification-badge-line:before{content:""}.ri-notification-fill:before{content:""}.ri-notification-line:before{content:""}.ri-notification-off-fill:before{content:""}.ri-notification-off-line:before{content:""}.ri-npmjs-fill:before{content:""}.ri-npmjs-line:before{content:""}.ri-number-0:before{content:""}.ri-number-1:before{content:""}.ri-number-2:before{content:""}.ri-number-3:before{content:""}.ri-number-4:before{content:""}.ri-number-5:before{content:""}.ri-number-6:before{content:""}.ri-number-7:before{content:""}.ri-number-8:before{content:""}.ri-number-9:before{content:""}.ri-numbers-fill:before{content:""}.ri-numbers-line:before{content:""}.ri-nurse-fill:before{content:""}.ri-nurse-line:before{content:""}.ri-oil-fill:before{content:""}.ri-oil-line:before{content:""}.ri-omega:before{content:""}.ri-open-arm-fill:before{content:""}.ri-open-arm-line:before{content:""}.ri-open-source-fill:before{content:""}.ri-open-source-line:before{content:""}.ri-opera-fill:before{content:""}.ri-opera-line:before{content:""}.ri-order-play-fill:before{content:""}.ri-order-play-line:before{content:""}.ri-organization-chart:before{content:""}.ri-outlet-2-fill:before{content:""}.ri-outlet-2-line:before{content:""}.ri-outlet-fill:before{content:""}.ri-outlet-line:before{content:""}.ri-page-separator:before{content:""}.ri-pages-fill:before{content:""}.ri-pages-line:before{content:""}.ri-paint-brush-fill:before{content:""}.ri-paint-brush-line:before{content:""}.ri-paint-fill:before{content:""}.ri-paint-line:before{content:""}.ri-palette-fill:before{content:""}.ri-palette-line:before{content:""}.ri-pantone-fill:before{content:""}.ri-pantone-line:before{content:""}.ri-paragraph:before{content:""}.ri-parent-fill:before{content:""}.ri-parent-line:before{content:""}.ri-parentheses-fill:before{content:""}.ri-parentheses-line:before{content:""}.ri-parking-box-fill:before{content:""}.ri-parking-box-line:before{content:""}.ri-parking-fill:before{content:""}.ri-parking-line:before{content:""}.ri-passport-fill:before{content:""}.ri-passport-line:before{content:""}.ri-patreon-fill:before{content:""}.ri-patreon-line:before{content:""}.ri-pause-circle-fill:before{content:""}.ri-pause-circle-line:before{content:""}.ri-pause-fill:before{content:""}.ri-pause-line:before{content:""}.ri-pause-mini-fill:before{content:""}.ri-pause-mini-line:before{content:""}.ri-paypal-fill:before{content:""}.ri-paypal-line:before{content:""}.ri-pen-nib-fill:before{content:""}.ri-pen-nib-line:before{content:""}.ri-pencil-fill:before{content:""}.ri-pencil-line:before{content:""}.ri-pencil-ruler-2-fill:before{content:""}.ri-pencil-ruler-2-line:before{content:""}.ri-pencil-ruler-fill:before{content:""}.ri-pencil-ruler-line:before{content:""}.ri-percent-fill:before{content:""}.ri-percent-line:before{content:""}.ri-phone-camera-fill:before{content:""}.ri-phone-camera-line:before{content:""}.ri-phone-fill:before{content:""}.ri-phone-find-fill:before{content:""}.ri-phone-find-line:before{content:""}.ri-phone-line:before{content:""}.ri-phone-lock-fill:before{content:""}.ri-phone-lock-line:before{content:""}.ri-picture-in-picture-2-fill:before{content:""}.ri-picture-in-picture-2-line:before{content:""}.ri-picture-in-picture-exit-fill:before{content:""}.ri-picture-in-picture-exit-line:before{content:""}.ri-picture-in-picture-fill:before{content:""}.ri-picture-in-picture-line:before{content:""}.ri-pie-chart-2-fill:before{content:""}.ri-pie-chart-2-line:before{content:""}.ri-pie-chart-box-fill:before{content:""}.ri-pie-chart-box-line:before{content:""}.ri-pie-chart-fill:before{content:""}.ri-pie-chart-line:before{content:""}.ri-pin-distance-fill:before{content:""}.ri-pin-distance-line:before{content:""}.ri-ping-pong-fill:before{content:""}.ri-ping-pong-line:before{content:""}.ri-pinterest-fill:before{content:""}.ri-pinterest-line:before{content:""}.ri-pinyin-input:before{content:""}.ri-pixelfed-fill:before{content:""}.ri-pixelfed-line:before{content:""}.ri-plane-fill:before{content:""}.ri-plane-line:before{content:""}.ri-plant-fill:before{content:""}.ri-plant-line:before{content:""}.ri-play-circle-fill:before{content:""}.ri-play-circle-line:before{content:""}.ri-play-fill:before{content:""}.ri-play-line:before{content:""}.ri-play-list-2-fill:before{content:""}.ri-play-list-2-line:before{content:""}.ri-play-list-add-fill:before{content:""}.ri-play-list-add-line:before{content:""}.ri-play-list-fill:before{content:""}.ri-play-list-line:before{content:""}.ri-play-mini-fill:before{content:""}.ri-play-mini-line:before{content:""}.ri-playstation-fill:before{content:""}.ri-playstation-line:before{content:""}.ri-plug-2-fill:before{content:""}.ri-plug-2-line:before{content:""}.ri-plug-fill:before{content:""}.ri-plug-line:before{content:""}.ri-polaroid-2-fill:before{content:""}.ri-polaroid-2-line:before{content:""}.ri-polaroid-fill:before{content:""}.ri-polaroid-line:before{content:""}.ri-police-car-fill:before{content:""}.ri-police-car-line:before{content:""}.ri-price-tag-2-fill:before{content:""}.ri-price-tag-2-line:before{content:""}.ri-price-tag-3-fill:before{content:""}.ri-price-tag-3-line:before{content:""}.ri-price-tag-fill:before{content:""}.ri-price-tag-line:before{content:""}.ri-printer-cloud-fill:before{content:""}.ri-printer-cloud-line:before{content:""}.ri-printer-fill:before{content:""}.ri-printer-line:before{content:""}.ri-product-hunt-fill:before{content:""}.ri-product-hunt-line:before{content:""}.ri-profile-fill:before{content:""}.ri-profile-line:before{content:""}.ri-projector-2-fill:before{content:""}.ri-projector-2-line:before{content:""}.ri-projector-fill:before{content:""}.ri-projector-line:before{content:""}.ri-psychotherapy-fill:before{content:""}.ri-psychotherapy-line:before{content:""}.ri-pulse-fill:before{content:""}.ri-pulse-line:before{content:""}.ri-pushpin-2-fill:before{content:""}.ri-pushpin-2-line:before{content:""}.ri-pushpin-fill:before{content:""}.ri-pushpin-line:before{content:""}.ri-qq-fill:before{content:""}.ri-qq-line:before{content:""}.ri-qr-code-fill:before{content:""}.ri-qr-code-line:before{content:""}.ri-qr-scan-2-fill:before{content:""}.ri-qr-scan-2-line:before{content:""}.ri-qr-scan-fill:before{content:""}.ri-qr-scan-line:before{content:""}.ri-question-answer-fill:before{content:""}.ri-question-answer-line:before{content:""}.ri-question-fill:before{content:""}.ri-question-line:before{content:""}.ri-question-mark:before{content:""}.ri-questionnaire-fill:before{content:""}.ri-questionnaire-line:before{content:""}.ri-quill-pen-fill:before{content:""}.ri-quill-pen-line:before{content:""}.ri-radar-fill:before{content:""}.ri-radar-line:before{content:""}.ri-radio-2-fill:before{content:""}.ri-radio-2-line:before{content:""}.ri-radio-button-fill:before{content:""}.ri-radio-button-line:before{content:""}.ri-radio-fill:before{content:""}.ri-radio-line:before{content:""}.ri-rainbow-fill:before{content:""}.ri-rainbow-line:before{content:""}.ri-rainy-fill:before{content:""}.ri-rainy-line:before{content:""}.ri-reactjs-fill:before{content:""}.ri-reactjs-line:before{content:""}.ri-record-circle-fill:before{content:""}.ri-record-circle-line:before{content:""}.ri-record-mail-fill:before{content:""}.ri-record-mail-line:before{content:""}.ri-recycle-fill:before{content:""}.ri-recycle-line:before{content:""}.ri-red-packet-fill:before{content:""}.ri-red-packet-line:before{content:""}.ri-reddit-fill:before{content:""}.ri-reddit-line:before{content:""}.ri-refresh-fill:before{content:""}.ri-refresh-line:before{content:""}.ri-refund-2-fill:before{content:""}.ri-refund-2-line:before{content:""}.ri-refund-fill:before{content:""}.ri-refund-line:before{content:""}.ri-registered-fill:before{content:""}.ri-registered-line:before{content:""}.ri-remixicon-fill:before{content:""}.ri-remixicon-line:before{content:""}.ri-remote-control-2-fill:before{content:""}.ri-remote-control-2-line:before{content:""}.ri-remote-control-fill:before{content:""}.ri-remote-control-line:before{content:""}.ri-repeat-2-fill:before{content:""}.ri-repeat-2-line:before{content:""}.ri-repeat-fill:before{content:""}.ri-repeat-line:before{content:""}.ri-repeat-one-fill:before{content:""}.ri-repeat-one-line:before{content:""}.ri-reply-all-fill:before{content:""}.ri-reply-all-line:before{content:""}.ri-reply-fill:before{content:""}.ri-reply-line:before{content:""}.ri-reserved-fill:before{content:""}.ri-reserved-line:before{content:""}.ri-rest-time-fill:before{content:""}.ri-rest-time-line:before{content:""}.ri-restart-fill:before{content:""}.ri-restart-line:before{content:""}.ri-restaurant-2-fill:before{content:""}.ri-restaurant-2-line:before{content:""}.ri-restaurant-fill:before{content:""}.ri-restaurant-line:before{content:""}.ri-rewind-fill:before{content:""}.ri-rewind-line:before{content:""}.ri-rewind-mini-fill:before{content:""}.ri-rewind-mini-line:before{content:""}.ri-rhythm-fill:before{content:""}.ri-rhythm-line:before{content:""}.ri-riding-fill:before{content:""}.ri-riding-line:before{content:""}.ri-road-map-fill:before{content:""}.ri-road-map-line:before{content:""}.ri-roadster-fill:before{content:""}.ri-roadster-line:before{content:""}.ri-robot-fill:before{content:""}.ri-robot-line:before{content:""}.ri-rocket-2-fill:before{content:""}.ri-rocket-2-line:before{content:""}.ri-rocket-fill:before{content:""}.ri-rocket-line:before{content:""}.ri-rotate-lock-fill:before{content:""}.ri-rotate-lock-line:before{content:""}.ri-rounded-corner:before{content:""}.ri-route-fill:before{content:""}.ri-route-line:before{content:""}.ri-router-fill:before{content:""}.ri-router-line:before{content:""}.ri-rss-fill:before{content:""}.ri-rss-line:before{content:""}.ri-ruler-2-fill:before{content:""}.ri-ruler-2-line:before{content:""}.ri-ruler-fill:before{content:""}.ri-ruler-line:before{content:""}.ri-run-fill:before{content:""}.ri-run-line:before{content:""}.ri-safari-fill:before{content:""}.ri-safari-line:before{content:""}.ri-safe-2-fill:before{content:""}.ri-safe-2-line:before{content:""}.ri-safe-fill:before{content:""}.ri-safe-line:before{content:""}.ri-sailboat-fill:before{content:""}.ri-sailboat-line:before{content:""}.ri-save-2-fill:before{content:""}.ri-save-2-line:before{content:""}.ri-save-3-fill:before{content:""}.ri-save-3-line:before{content:""}.ri-save-fill:before{content:""}.ri-save-line:before{content:""}.ri-scales-2-fill:before{content:""}.ri-scales-2-line:before{content:""}.ri-scales-3-fill:before{content:""}.ri-scales-3-line:before{content:""}.ri-scales-fill:before{content:""}.ri-scales-line:before{content:""}.ri-scan-2-fill:before{content:""}.ri-scan-2-line:before{content:""}.ri-scan-fill:before{content:""}.ri-scan-line:before{content:""}.ri-scissors-2-fill:before{content:""}.ri-scissors-2-line:before{content:""}.ri-scissors-cut-fill:before{content:""}.ri-scissors-cut-line:before{content:""}.ri-scissors-fill:before{content:""}.ri-scissors-line:before{content:""}.ri-screenshot-2-fill:before{content:""}.ri-screenshot-2-line:before{content:""}.ri-screenshot-fill:before{content:""}.ri-screenshot-line:before{content:""}.ri-sd-card-fill:before{content:""}.ri-sd-card-line:before{content:""}.ri-sd-card-mini-fill:before{content:""}.ri-sd-card-mini-line:before{content:""}.ri-search-2-fill:before{content:""}.ri-search-2-line:before{content:""}.ri-search-eye-fill:before{content:""}.ri-search-eye-line:before{content:""}.ri-search-fill:before{content:""}.ri-search-line:before{content:""}.ri-secure-payment-fill:before{content:""}.ri-secure-payment-line:before{content:""}.ri-seedling-fill:before{content:""}.ri-seedling-line:before{content:""}.ri-send-backward:before{content:""}.ri-send-plane-2-fill:before{content:""}.ri-send-plane-2-line:before{content:""}.ri-send-plane-fill:before{content:""}.ri-send-plane-line:before{content:""}.ri-send-to-back:before{content:""}.ri-sensor-fill:before{content:""}.ri-sensor-line:before{content:""}.ri-separator:before{content:""}.ri-server-fill:before{content:""}.ri-server-line:before{content:""}.ri-service-fill:before{content:""}.ri-service-line:before{content:""}.ri-settings-2-fill:before{content:""}.ri-settings-2-line:before{content:""}.ri-settings-3-fill:before{content:""}.ri-settings-3-line:before{content:""}.ri-settings-4-fill:before{content:""}.ri-settings-4-line:before{content:""}.ri-settings-5-fill:before{content:""}.ri-settings-5-line:before{content:""}.ri-settings-6-fill:before{content:""}.ri-settings-6-line:before{content:""}.ri-settings-fill:before{content:""}.ri-settings-line:before{content:""}.ri-shape-2-fill:before{content:""}.ri-shape-2-line:before{content:""}.ri-shape-fill:before{content:""}.ri-shape-line:before{content:""}.ri-share-box-fill:before{content:""}.ri-share-box-line:before{content:""}.ri-share-circle-fill:before{content:""}.ri-share-circle-line:before{content:""}.ri-share-fill:before{content:""}.ri-share-forward-2-fill:before{content:""}.ri-share-forward-2-line:before{content:""}.ri-share-forward-box-fill:before{content:""}.ri-share-forward-box-line:before{content:""}.ri-share-forward-fill:before{content:""}.ri-share-forward-line:before{content:""}.ri-share-line:before{content:""}.ri-shield-check-fill:before{content:""}.ri-shield-check-line:before{content:""}.ri-shield-cross-fill:before{content:""}.ri-shield-cross-line:before{content:""}.ri-shield-fill:before{content:""}.ri-shield-flash-fill:before{content:""}.ri-shield-flash-line:before{content:""}.ri-shield-keyhole-fill:before{content:""}.ri-shield-keyhole-line:before{content:""}.ri-shield-line:before{content:""}.ri-shield-star-fill:before{content:""}.ri-shield-star-line:before{content:""}.ri-shield-user-fill:before{content:""}.ri-shield-user-line:before{content:""}.ri-ship-2-fill:before{content:""}.ri-ship-2-line:before{content:""}.ri-ship-fill:before{content:""}.ri-ship-line:before{content:""}.ri-shirt-fill:before{content:""}.ri-shirt-line:before{content:""}.ri-shopping-bag-2-fill:before{content:""}.ri-shopping-bag-2-line:before{content:""}.ri-shopping-bag-3-fill:before{content:""}.ri-shopping-bag-3-line:before{content:""}.ri-shopping-bag-fill:before{content:""}.ri-shopping-bag-line:before{content:""}.ri-shopping-basket-2-fill:before{content:""}.ri-shopping-basket-2-line:before{content:""}.ri-shopping-basket-fill:before{content:""}.ri-shopping-basket-line:before{content:""}.ri-shopping-cart-2-fill:before{content:""}.ri-shopping-cart-2-line:before{content:""}.ri-shopping-cart-fill:before{content:""}.ri-shopping-cart-line:before{content:""}.ri-showers-fill:before{content:""}.ri-showers-line:before{content:""}.ri-shuffle-fill:before{content:""}.ri-shuffle-line:before{content:""}.ri-shut-down-fill:before{content:""}.ri-shut-down-line:before{content:""}.ri-side-bar-fill:before{content:""}.ri-side-bar-line:before{content:""}.ri-signal-tower-fill:before{content:""}.ri-signal-tower-line:before{content:""}.ri-signal-wifi-1-fill:before{content:""}.ri-signal-wifi-1-line:before{content:""}.ri-signal-wifi-2-fill:before{content:""}.ri-signal-wifi-2-line:before{content:""}.ri-signal-wifi-3-fill:before{content:""}.ri-signal-wifi-3-line:before{content:""}.ri-signal-wifi-error-fill:before{content:""}.ri-signal-wifi-error-line:before{content:""}.ri-signal-wifi-fill:before{content:""}.ri-signal-wifi-line:before{content:""}.ri-signal-wifi-off-fill:before{content:""}.ri-signal-wifi-off-line:before{content:""}.ri-sim-card-2-fill:before{content:""}.ri-sim-card-2-line:before{content:""}.ri-sim-card-fill:before{content:""}.ri-sim-card-line:before{content:""}.ri-single-quotes-l:before{content:""}.ri-single-quotes-r:before{content:""}.ri-sip-fill:before{content:""}.ri-sip-line:before{content:""}.ri-skip-back-fill:before{content:""}.ri-skip-back-line:before{content:""}.ri-skip-back-mini-fill:before{content:""}.ri-skip-back-mini-line:before{content:""}.ri-skip-forward-fill:before{content:""}.ri-skip-forward-line:before{content:""}.ri-skip-forward-mini-fill:before{content:""}.ri-skip-forward-mini-line:before{content:""}.ri-skull-2-fill:before{content:""}.ri-skull-2-line:before{content:""}.ri-skull-fill:before{content:""}.ri-skull-line:before{content:""}.ri-skype-fill:before{content:""}.ri-skype-line:before{content:""}.ri-slack-fill:before{content:""}.ri-slack-line:before{content:""}.ri-slice-fill:before{content:""}.ri-slice-line:before{content:""}.ri-slideshow-2-fill:before{content:""}.ri-slideshow-2-line:before{content:""}.ri-slideshow-3-fill:before{content:""}.ri-slideshow-3-line:before{content:""}.ri-slideshow-4-fill:before{content:""}.ri-slideshow-4-line:before{content:""}.ri-slideshow-fill:before{content:""}.ri-slideshow-line:before{content:""}.ri-smartphone-fill:before{content:""}.ri-smartphone-line:before{content:""}.ri-snapchat-fill:before{content:""}.ri-snapchat-line:before{content:""}.ri-snowy-fill:before{content:""}.ri-snowy-line:before{content:""}.ri-sort-asc:before{content:""}.ri-sort-desc:before{content:""}.ri-sound-module-fill:before{content:""}.ri-sound-module-line:before{content:""}.ri-soundcloud-fill:before{content:""}.ri-soundcloud-line:before{content:""}.ri-space-ship-fill:before{content:""}.ri-space-ship-line:before{content:""}.ri-space:before{content:""}.ri-spam-2-fill:before{content:""}.ri-spam-2-line:before{content:""}.ri-spam-3-fill:before{content:""}.ri-spam-3-line:before{content:""}.ri-spam-fill:before{content:""}.ri-spam-line:before{content:""}.ri-speaker-2-fill:before{content:""}.ri-speaker-2-line:before{content:""}.ri-speaker-3-fill:before{content:""}.ri-speaker-3-line:before{content:""}.ri-speaker-fill:before{content:""}.ri-speaker-line:before{content:""}.ri-spectrum-fill:before{content:""}.ri-spectrum-line:before{content:""}.ri-speed-fill:before{content:""}.ri-speed-line:before{content:""}.ri-speed-mini-fill:before{content:""}.ri-speed-mini-line:before{content:""}.ri-split-cells-horizontal:before{content:""}.ri-split-cells-vertical:before{content:""}.ri-spotify-fill:before{content:""}.ri-spotify-line:before{content:""}.ri-spy-fill:before{content:""}.ri-spy-line:before{content:""}.ri-stack-fill:before{content:""}.ri-stack-line:before{content:""}.ri-stack-overflow-fill:before{content:""}.ri-stack-overflow-line:before{content:""}.ri-stackshare-fill:before{content:""}.ri-stackshare-line:before{content:""}.ri-star-fill:before{content:""}.ri-star-half-fill:before{content:""}.ri-star-half-line:before{content:""}.ri-star-half-s-fill:before{content:""}.ri-star-half-s-line:before{content:""}.ri-star-line:before{content:""}.ri-star-s-fill:before{content:""}.ri-star-s-line:before{content:""}.ri-star-smile-fill:before{content:""}.ri-star-smile-line:before{content:""}.ri-steam-fill:before{content:""}.ri-steam-line:before{content:""}.ri-steering-2-fill:before{content:""}.ri-steering-2-line:before{content:""}.ri-steering-fill:before{content:""}.ri-steering-line:before{content:""}.ri-stethoscope-fill:before{content:""}.ri-stethoscope-line:before{content:""}.ri-sticky-note-2-fill:before{content:""}.ri-sticky-note-2-line:before{content:""}.ri-sticky-note-fill:before{content:""}.ri-sticky-note-line:before{content:""}.ri-stock-fill:before{content:""}.ri-stock-line:before{content:""}.ri-stop-circle-fill:before{content:""}.ri-stop-circle-line:before{content:""}.ri-stop-fill:before{content:""}.ri-stop-line:before{content:""}.ri-stop-mini-fill:before{content:""}.ri-stop-mini-line:before{content:""}.ri-store-2-fill:before{content:""}.ri-store-2-line:before{content:""}.ri-store-3-fill:before{content:""}.ri-store-3-line:before{content:""}.ri-store-fill:before{content:""}.ri-store-line:before{content:""}.ri-strikethrough-2:before{content:""}.ri-strikethrough:before{content:""}.ri-subscript-2:before{content:""}.ri-subscript:before{content:""}.ri-subtract-fill:before{content:""}.ri-subtract-line:before{content:""}.ri-subway-fill:before{content:""}.ri-subway-line:before{content:""}.ri-subway-wifi-fill:before{content:""}.ri-subway-wifi-line:before{content:""}.ri-suitcase-2-fill:before{content:""}.ri-suitcase-2-line:before{content:""}.ri-suitcase-3-fill:before{content:""}.ri-suitcase-3-line:before{content:""}.ri-suitcase-fill:before{content:""}.ri-suitcase-line:before{content:""}.ri-sun-cloudy-fill:before{content:""}.ri-sun-cloudy-line:before{content:""}.ri-sun-fill:before{content:""}.ri-sun-foggy-fill:before{content:""}.ri-sun-foggy-line:before{content:""}.ri-sun-line:before{content:""}.ri-superscript-2:before{content:""}.ri-superscript:before{content:""}.ri-surgical-mask-fill:before{content:""}.ri-surgical-mask-line:before{content:""}.ri-surround-sound-fill:before{content:""}.ri-surround-sound-line:before{content:""}.ri-survey-fill:before{content:""}.ri-survey-line:before{content:""}.ri-swap-box-fill:before{content:""}.ri-swap-box-line:before{content:""}.ri-swap-fill:before{content:""}.ri-swap-line:before{content:""}.ri-switch-fill:before{content:""}.ri-switch-line:before{content:""}.ri-sword-fill:before{content:""}.ri-sword-line:before{content:""}.ri-syringe-fill:before{content:""}.ri-syringe-line:before{content:""}.ri-t-box-fill:before{content:""}.ri-t-box-line:before{content:""}.ri-t-shirt-2-fill:before{content:""}.ri-t-shirt-2-line:before{content:""}.ri-t-shirt-air-fill:before{content:""}.ri-t-shirt-air-line:before{content:""}.ri-t-shirt-fill:before{content:""}.ri-t-shirt-line:before{content:""}.ri-table-2:before{content:""}.ri-table-alt-fill:before{content:""}.ri-table-alt-line:before{content:""}.ri-table-fill:before{content:""}.ri-table-line:before{content:""}.ri-tablet-fill:before{content:""}.ri-tablet-line:before{content:""}.ri-takeaway-fill:before{content:""}.ri-takeaway-line:before{content:""}.ri-taobao-fill:before{content:""}.ri-taobao-line:before{content:""}.ri-tape-fill:before{content:""}.ri-tape-line:before{content:""}.ri-task-fill:before{content:""}.ri-task-line:before{content:""}.ri-taxi-fill:before{content:""}.ri-taxi-line:before{content:""}.ri-taxi-wifi-fill:before{content:""}.ri-taxi-wifi-line:before{content:""}.ri-team-fill:before{content:""}.ri-team-line:before{content:""}.ri-telegram-fill:before{content:""}.ri-telegram-line:before{content:""}.ri-temp-cold-fill:before{content:""}.ri-temp-cold-line:before{content:""}.ri-temp-hot-fill:before{content:""}.ri-temp-hot-line:before{content:""}.ri-terminal-box-fill:before{content:""}.ri-terminal-box-line:before{content:""}.ri-terminal-fill:before{content:""}.ri-terminal-line:before{content:""}.ri-terminal-window-fill:before{content:""}.ri-terminal-window-line:before{content:""}.ri-test-tube-fill:before{content:""}.ri-test-tube-line:before{content:""}.ri-text-direction-l:before{content:""}.ri-text-direction-r:before{content:""}.ri-text-spacing:before{content:""}.ri-text-wrap:before{content:""}.ri-text:before{content:""}.ri-thermometer-fill:before{content:""}.ri-thermometer-line:before{content:""}.ri-thumb-down-fill:before{content:""}.ri-thumb-down-line:before{content:""}.ri-thumb-up-fill:before{content:""}.ri-thumb-up-line:before{content:""}.ri-thunderstorms-fill:before{content:""}.ri-thunderstorms-line:before{content:""}.ri-ticket-2-fill:before{content:""}.ri-ticket-2-line:before{content:""}.ri-ticket-fill:before{content:""}.ri-ticket-line:before{content:""}.ri-time-fill:before{content:""}.ri-time-line:before{content:""}.ri-timer-2-fill:before{content:""}.ri-timer-2-line:before{content:""}.ri-timer-fill:before{content:""}.ri-timer-flash-fill:before{content:""}.ri-timer-flash-line:before{content:""}.ri-timer-line:before{content:""}.ri-todo-fill:before{content:""}.ri-todo-line:before{content:""}.ri-toggle-fill:before{content:""}.ri-toggle-line:before{content:""}.ri-tools-fill:before{content:""}.ri-tools-line:before{content:""}.ri-tornado-fill:before{content:""}.ri-tornado-line:before{content:""}.ri-trademark-fill:before{content:""}.ri-trademark-line:before{content:""}.ri-traffic-light-fill:before{content:""}.ri-traffic-light-line:before{content:""}.ri-train-fill:before{content:""}.ri-train-line:before{content:""}.ri-train-wifi-fill:before{content:""}.ri-train-wifi-line:before{content:""}.ri-translate-2:before{content:""}.ri-translate:before{content:""}.ri-travesti-fill:before{content:""}.ri-travesti-line:before{content:""}.ri-treasure-map-fill:before{content:""}.ri-treasure-map-line:before{content:""}.ri-trello-fill:before{content:""}.ri-trello-line:before{content:""}.ri-trophy-fill:before{content:""}.ri-trophy-line:before{content:""}.ri-truck-fill:before{content:""}.ri-truck-line:before{content:""}.ri-tumblr-fill:before{content:""}.ri-tumblr-line:before{content:""}.ri-tv-2-fill:before{content:""}.ri-tv-2-line:before{content:""}.ri-tv-fill:before{content:""}.ri-tv-line:before{content:""}.ri-twitch-fill:before{content:""}.ri-twitch-line:before{content:""}.ri-twitter-fill:before{content:""}.ri-twitter-line:before{content:""}.ri-typhoon-fill:before{content:""}.ri-typhoon-line:before{content:""}.ri-u-disk-fill:before{content:""}.ri-u-disk-line:before{content:""}.ri-ubuntu-fill:before{content:""}.ri-ubuntu-line:before{content:""}.ri-umbrella-fill:before{content:""}.ri-umbrella-line:before{content:""}.ri-underline:before{content:""}.ri-uninstall-fill:before{content:""}.ri-uninstall-line:before{content:""}.ri-unsplash-fill:before{content:""}.ri-unsplash-line:before{content:""}.ri-upload-2-fill:before{content:""}.ri-upload-2-line:before{content:""}.ri-upload-cloud-2-fill:before{content:""}.ri-upload-cloud-2-line:before{content:""}.ri-upload-cloud-fill:before{content:""}.ri-upload-cloud-line:before{content:""}.ri-upload-fill:before{content:""}.ri-upload-line:before{content:""}.ri-usb-fill:before{content:""}.ri-usb-line:before{content:""}.ri-user-2-fill:before{content:""}.ri-user-2-line:before{content:""}.ri-user-3-fill:before{content:""}.ri-user-3-line:before{content:""}.ri-user-4-fill:before{content:""}.ri-user-4-line:before{content:""}.ri-user-5-fill:before{content:""}.ri-user-5-line:before{content:""}.ri-user-6-fill:before{content:""}.ri-user-6-line:before{content:""}.ri-user-add-fill:before{content:""}.ri-user-add-line:before{content:""}.ri-user-fill:before{content:""}.ri-user-follow-fill:before{content:""}.ri-user-follow-line:before{content:""}.ri-user-heart-fill:before{content:""}.ri-user-heart-line:before{content:""}.ri-user-line:before{content:""}.ri-user-location-fill:before{content:""}.ri-user-location-line:before{content:""}.ri-user-received-2-fill:before{content:""}.ri-user-received-2-line:before{content:""}.ri-user-received-fill:before{content:""}.ri-user-received-line:before{content:""}.ri-user-search-fill:before{content:""}.ri-user-search-line:before{content:""}.ri-user-settings-fill:before{content:""}.ri-user-settings-line:before{content:""}.ri-user-shared-2-fill:before{content:""}.ri-user-shared-2-line:before{content:""}.ri-user-shared-fill:before{content:""}.ri-user-shared-line:before{content:""}.ri-user-smile-fill:before{content:""}.ri-user-smile-line:before{content:""}.ri-user-star-fill:before{content:""}.ri-user-star-line:before{content:""}.ri-user-unfollow-fill:before{content:""}.ri-user-unfollow-line:before{content:""}.ri-user-voice-fill:before{content:""}.ri-user-voice-line:before{content:""}.ri-video-add-fill:before{content:""}.ri-video-add-line:before{content:""}.ri-video-chat-fill:before{content:""}.ri-video-chat-line:before{content:""}.ri-video-download-fill:before{content:""}.ri-video-download-line:before{content:""}.ri-video-fill:before{content:""}.ri-video-line:before{content:""}.ri-video-upload-fill:before{content:""}.ri-video-upload-line:before{content:""}.ri-vidicon-2-fill:before{content:""}.ri-vidicon-2-line:before{content:""}.ri-vidicon-fill:before{content:""}.ri-vidicon-line:before{content:""}.ri-vimeo-fill:before{content:""}.ri-vimeo-line:before{content:""}.ri-vip-crown-2-fill:before{content:""}.ri-vip-crown-2-line:before{content:""}.ri-vip-crown-fill:before{content:""}.ri-vip-crown-line:before{content:""}.ri-vip-diamond-fill:before{content:""}.ri-vip-diamond-line:before{content:""}.ri-vip-fill:before{content:""}.ri-vip-line:before{content:""}.ri-virus-fill:before{content:""}.ri-virus-line:before{content:""}.ri-visa-fill:before{content:""}.ri-visa-line:before{content:""}.ri-voice-recognition-fill:before{content:""}.ri-voice-recognition-line:before{content:""}.ri-voiceprint-fill:before{content:""}.ri-voiceprint-line:before{content:""}.ri-volume-down-fill:before{content:""}.ri-volume-down-line:before{content:""}.ri-volume-mute-fill:before{content:""}.ri-volume-mute-line:before{content:""}.ri-volume-off-vibrate-fill:before{content:""}.ri-volume-off-vibrate-line:before{content:""}.ri-volume-up-fill:before{content:""}.ri-volume-up-line:before{content:""}.ri-volume-vibrate-fill:before{content:""}.ri-volume-vibrate-line:before{content:""}.ri-vuejs-fill:before{content:""}.ri-vuejs-line:before{content:""}.ri-walk-fill:before{content:""}.ri-walk-line:before{content:""}.ri-wallet-2-fill:before{content:""}.ri-wallet-2-line:before{content:""}.ri-wallet-3-fill:before{content:""}.ri-wallet-3-line:before{content:""}.ri-wallet-fill:before{content:""}.ri-wallet-line:before{content:""}.ri-water-flash-fill:before{content:""}.ri-water-flash-line:before{content:""}.ri-webcam-fill:before{content:""}.ri-webcam-line:before{content:""}.ri-wechat-2-fill:before{content:""}.ri-wechat-2-line:before{content:""}.ri-wechat-fill:before{content:""}.ri-wechat-line:before{content:""}.ri-wechat-pay-fill:before{content:""}.ri-wechat-pay-line:before{content:""}.ri-weibo-fill:before{content:""}.ri-weibo-line:before{content:""}.ri-whatsapp-fill:before{content:""}.ri-whatsapp-line:before{content:""}.ri-wheelchair-fill:before{content:""}.ri-wheelchair-line:before{content:""}.ri-wifi-fill:before{content:""}.ri-wifi-line:before{content:""}.ri-wifi-off-fill:before{content:""}.ri-wifi-off-line:before{content:""}.ri-window-2-fill:before{content:""}.ri-window-2-line:before{content:""}.ri-window-fill:before{content:""}.ri-window-line:before{content:""}.ri-windows-fill:before{content:""}.ri-windows-line:before{content:""}.ri-windy-fill:before{content:""}.ri-windy-line:before{content:""}.ri-wireless-charging-fill:before{content:""}.ri-wireless-charging-line:before{content:""}.ri-women-fill:before{content:""}.ri-women-line:before{content:""}.ri-wubi-input:before{content:""}.ri-xbox-fill:before{content:""}.ri-xbox-line:before{content:""}.ri-xing-fill:before{content:""}.ri-xing-line:before{content:""}.ri-youtube-fill:before{content:""}.ri-youtube-line:before{content:""}.ri-zcool-fill:before{content:""}.ri-zcool-line:before{content:""}.ri-zhihu-fill:before{content:""}.ri-zhihu-line:before{content:""}.ri-zoom-in-fill:before{content:""}.ri-zoom-in-line:before{content:""}.ri-zoom-out-fill:before{content:""}.ri-zoom-out-line:before{content:""}.ri-zzz-fill:before{content:""}.ri-zzz-line:before{content:""}.ri-arrow-down-double-fill:before{content:""}.ri-arrow-down-double-line:before{content:""}.ri-arrow-left-double-fill:before{content:""}.ri-arrow-left-double-line:before{content:""}.ri-arrow-right-double-fill:before{content:""}.ri-arrow-right-double-line:before{content:""}.ri-arrow-turn-back-fill:before{content:""}.ri-arrow-turn-back-line:before{content:""}.ri-arrow-turn-forward-fill:before{content:""}.ri-arrow-turn-forward-line:before{content:""}.ri-arrow-up-double-fill:before{content:""}.ri-arrow-up-double-line:before{content:""}.ri-bard-fill:before{content:""}.ri-bard-line:before{content:""}.ri-bootstrap-fill:before{content:""}.ri-bootstrap-line:before{content:""}.ri-box-1-fill:before{content:""}.ri-box-1-line:before{content:""}.ri-box-2-fill:before{content:""}.ri-box-2-line:before{content:""}.ri-box-3-fill:before{content:""}.ri-box-3-line:before{content:""}.ri-brain-fill:before{content:""}.ri-brain-line:before{content:""}.ri-candle-fill:before{content:""}.ri-candle-line:before{content:""}.ri-cash-fill:before{content:""}.ri-cash-line:before{content:""}.ri-contract-left-fill:before{content:""}.ri-contract-left-line:before{content:""}.ri-contract-left-right-fill:before{content:""}.ri-contract-left-right-line:before{content:""}.ri-contract-right-fill:before{content:""}.ri-contract-right-line:before{content:""}.ri-contract-up-down-fill:before{content:""}.ri-contract-up-down-line:before{content:""}.ri-copilot-fill:before{content:""}.ri-copilot-line:before{content:""}.ri-corner-down-left-fill:before{content:""}.ri-corner-down-left-line:before{content:""}.ri-corner-down-right-fill:before{content:""}.ri-corner-down-right-line:before{content:""}.ri-corner-left-down-fill:before{content:""}.ri-corner-left-down-line:before{content:""}.ri-corner-left-up-fill:before{content:""}.ri-corner-left-up-line:before{content:""}.ri-corner-right-down-fill:before{content:""}.ri-corner-right-down-line:before{content:""}.ri-corner-right-up-fill:before{content:""}.ri-corner-right-up-line:before{content:""}.ri-corner-up-left-double-fill:before{content:""}.ri-corner-up-left-double-line:before{content:""}.ri-corner-up-left-fill:before{content:""}.ri-corner-up-left-line:before{content:""}.ri-corner-up-right-double-fill:before{content:""}.ri-corner-up-right-double-line:before{content:""}.ri-corner-up-right-fill:before{content:""}.ri-corner-up-right-line:before{content:""}.ri-cross-fill:before{content:""}.ri-cross-line:before{content:""}.ri-edge-new-fill:before{content:""}.ri-edge-new-line:before{content:""}.ri-equal-fill:before{content:""}.ri-equal-line:before{content:""}.ri-expand-left-fill:before{content:""}.ri-expand-left-line:before{content:""}.ri-expand-left-right-fill:before{content:""}.ri-expand-left-right-line:before{content:""}.ri-expand-right-fill:before{content:""}.ri-expand-right-line:before{content:""}.ri-expand-up-down-fill:before{content:""}.ri-expand-up-down-line:before{content:""}.ri-flickr-fill:before{content:""}.ri-flickr-line:before{content:""}.ri-forward-10-fill:before{content:""}.ri-forward-10-line:before{content:""}.ri-forward-15-fill:before{content:""}.ri-forward-15-line:before{content:""}.ri-forward-30-fill:before{content:""}.ri-forward-30-line:before{content:""}.ri-forward-5-fill:before{content:""}.ri-forward-5-line:before{content:""}.ri-graduation-cap-fill:before{content:""}.ri-graduation-cap-line:before{content:""}.ri-home-office-fill:before{content:""}.ri-home-office-line:before{content:""}.ri-hourglass-2-fill:before{content:""}.ri-hourglass-2-line:before{content:""}.ri-hourglass-fill:before{content:""}.ri-hourglass-line:before{content:""}.ri-javascript-fill:before{content:""}.ri-javascript-line:before{content:""}.ri-loop-left-fill:before{content:""}.ri-loop-left-line:before{content:""}.ri-loop-right-fill:before{content:""}.ri-loop-right-line:before{content:""}.ri-memories-fill:before{content:""}.ri-memories-line:before{content:""}.ri-meta-fill:before{content:""}.ri-meta-line:before{content:""}.ri-microsoft-loop-fill:before{content:""}.ri-microsoft-loop-line:before{content:""}.ri-nft-fill:before{content:""}.ri-nft-line:before{content:""}.ri-notion-fill:before{content:""}.ri-notion-line:before{content:""}.ri-openai-fill:before{content:""}.ri-openai-line:before{content:""}.ri-overline:before{content:""}.ri-p2p-fill:before{content:""}.ri-p2p-line:before{content:""}.ri-presentation-fill:before{content:""}.ri-presentation-line:before{content:""}.ri-replay-10-fill:before{content:""}.ri-replay-10-line:before{content:""}.ri-replay-15-fill:before{content:""}.ri-replay-15-line:before{content:""}.ri-replay-30-fill:before{content:""}.ri-replay-30-line:before{content:""}.ri-replay-5-fill:before{content:""}.ri-replay-5-line:before{content:""}.ri-school-fill:before{content:""}.ri-school-line:before{content:""}.ri-shining-2-fill:before{content:""}.ri-shining-2-line:before{content:""}.ri-shining-fill:before{content:""}.ri-shining-line:before{content:""}.ri-sketching:before{content:""}.ri-skip-down-fill:before{content:""}.ri-skip-down-line:before{content:""}.ri-skip-left-fill:before{content:""}.ri-skip-left-line:before{content:""}.ri-skip-right-fill:before{content:""}.ri-skip-right-line:before{content:""}.ri-skip-up-fill:before{content:""}.ri-skip-up-line:before{content:""}.ri-slow-down-fill:before{content:""}.ri-slow-down-line:before{content:""}.ri-sparkling-2-fill:before{content:""}.ri-sparkling-2-line:before{content:""}.ri-sparkling-fill:before{content:""}.ri-sparkling-line:before{content:""}.ri-speak-fill:before{content:""}.ri-speak-line:before{content:""}.ri-speed-up-fill:before{content:""}.ri-speed-up-line:before{content:""}.ri-tiktok-fill:before{content:""}.ri-tiktok-line:before{content:""}.ri-token-swap-fill:before{content:""}.ri-token-swap-line:before{content:""}.ri-unpin-fill:before{content:""}.ri-unpin-line:before{content:""}.ri-wechat-channels-fill:before{content:""}.ri-wechat-channels-line:before{content:""}.ri-wordpress-fill:before{content:""}.ri-wordpress-line:before{content:""}.ri-blender-fill:before{content:""}.ri-blender-line:before{content:""}.ri-emoji-sticker-fill:before{content:""}.ri-emoji-sticker-line:before{content:""}.ri-git-close-pull-request-fill:before{content:""}.ri-git-close-pull-request-line:before{content:""}.ri-instance-fill:before{content:""}.ri-instance-line:before{content:""}.ri-megaphone-fill:before{content:""}.ri-megaphone-line:before{content:""}.ri-pass-expired-fill:before{content:""}.ri-pass-expired-line:before{content:""}.ri-pass-pending-fill:before{content:""}.ri-pass-pending-line:before{content:""}.ri-pass-valid-fill:before{content:""}.ri-pass-valid-line:before{content:""}.ri-ai-generate:before{content:""}.ri-calendar-close-fill:before{content:""}.ri-calendar-close-line:before{content:""}.ri-draggable:before{content:""}.ri-font-family:before{content:""}.ri-font-mono:before{content:""}.ri-font-sans-serif:before{content:""}.ri-font-sans:before{content:""}.ri-hard-drive-3-fill:before{content:""}.ri-hard-drive-3-line:before{content:""}.ri-kick-fill:before{content:""}.ri-kick-line:before{content:""}.ri-list-check-3:before{content:""}.ri-list-indefinite:before{content:""}.ri-list-ordered-2:before{content:""}.ri-list-radio:before{content:""}.ri-openbase-fill:before{content:""}.ri-openbase-line:before{content:""}.ri-planet-fill:before{content:""}.ri-planet-line:before{content:""}.ri-prohibited-fill:before{content:""}.ri-prohibited-line:before{content:""}.ri-quote-text:before{content:""}.ri-seo-fill:before{content:""}.ri-seo-line:before{content:""}.ri-slash-commands:before{content:""}.ri-archive-2-fill:before{content:""}.ri-archive-2-line:before{content:""}.ri-inbox-2-fill:before{content:""}.ri-inbox-2-line:before{content:""}.ri-shake-hands-fill:before{content:""}.ri-shake-hands-line:before{content:""}.ri-supabase-fill:before{content:""}.ri-supabase-line:before{content:""}.ri-water-percent-fill:before{content:""}.ri-water-percent-line:before{content:""}.ri-yuque-fill:before{content:""}.ri-yuque-line:before{content:""}.ri-crosshair-2-fill:before{content:""}.ri-crosshair-2-line:before{content:""}.ri-crosshair-fill:before{content:""}.ri-crosshair-line:before{content:""}.ri-file-close-fill:before{content:""}.ri-file-close-line:before{content:""}.ri-infinity-fill:before{content:""}.ri-infinity-line:before{content:""}.ri-rfid-fill:before{content:""}.ri-rfid-line:before{content:""}.ri-slash-commands-2:before{content:""}.ri-user-forbid-fill:before{content:""}.ri-user-forbid-line:before{content:""}.ri-beer-fill:before{content:""}.ri-beer-line:before{content:""}.ri-circle-fill:before{content:""}.ri-circle-line:before{content:""}.ri-dropdown-list:before{content:""}.ri-file-image-fill:before{content:""}.ri-file-image-line:before{content:""}.ri-file-pdf-2-fill:before{content:""}.ri-file-pdf-2-line:before{content:""}.ri-file-video-fill:before{content:""}.ri-file-video-line:before{content:""}.ri-folder-image-fill:before{content:""}.ri-folder-image-line:before{content:""}.ri-folder-video-fill:before{content:""}.ri-folder-video-line:before{content:""}.ri-hexagon-fill:before{content:""}.ri-hexagon-line:before{content:""}.ri-menu-search-fill:before{content:""}.ri-menu-search-line:before{content:""}.ri-octagon-fill:before{content:""}.ri-octagon-line:before{content:""}.ri-pentagon-fill:before{content:""}.ri-pentagon-line:before{content:""}.ri-rectangle-fill:before{content:""}.ri-rectangle-line:before{content:""}.ri-robot-2-fill:before{content:""}.ri-robot-2-line:before{content:""}.ri-shapes-fill:before{content:""}.ri-shapes-line:before{content:""}.ri-square-fill:before{content:""}.ri-square-line:before{content:""}.ri-tent-fill:before{content:""}.ri-tent-line:before{content:""}.ri-threads-fill:before{content:""}.ri-threads-line:before{content:""}.ri-tree-fill:before{content:""}.ri-tree-line:before{content:""}.ri-triangle-fill:before{content:""}.ri-triangle-line:before{content:""}.ri-twitter-x-fill:before{content:""}.ri-twitter-x-line:before{content:""}.ri-verified-badge-fill:before{content:""}.ri-verified-badge-line:before{content:""}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes expand{0%{transform:rotateY(90deg)}to{opacity:1;transform:rotateY(0)}}@keyframes slideIn{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0;visibility:hidden}to{opacity:1;visibility:visible}}@keyframes shine{to{background-position-x:-200%}}@keyframes loaderShow{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@keyframes entranceLeft{0%{opacity:0;transform:translate(-5px)}to{opacity:1;transform:translate(0)}}@keyframes entranceRight{0%{opacity:0;transform:translate(5px)}to{opacity:1;transform:translate(0)}}@keyframes entranceTop{0%{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}@keyframes entranceBottom{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@media screen and (min-width: 550px){::-webkit-scrollbar{width:8px;height:8px;border-radius:var(--baseRadius)}::-webkit-scrollbar-track{background:transparent;border-radius:var(--baseRadius)}::-webkit-scrollbar-thumb{background-color:var(--baseAlt2Color);border-radius:15px;border:2px solid transparent;background-clip:padding-box}::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background-color:var(--baseAlt3Color)}html{scrollbar-color:var(--baseAlt2Color) transparent;scrollbar-width:thin;scroll-behavior:smooth}html *{scrollbar-width:inherit}}:focus-visible{outline-color:var(--primaryColor);outline-style:solid}html,body{line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);font-size:var(--baseFontSize);color:var(--txtPrimaryColor);background:var(--bodyColor)}#app{overflow:auto;display:block;width:100%;height:100vh}.schema-field,.flatpickr-inline-container,.accordion .accordion-content,.accordion,.tabs,.tabs-content,.select .txt-missing,.form-field .form-field-block,.list,.skeleton-loader,.clearfix,.content,.form-field .help-block,.overlay-panel .panel-content,.sub-panel,.panel,.block,.code-block,blockquote,p{display:block;width:100%}h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6{margin:0;font-weight:400}h1{font-size:22px;line-height:28px}h2,.breadcrumbs .breadcrumb-item{font-size:20px;line-height:26px}h3{font-size:19px;line-height:24px}h4{font-size:18px;line-height:24px}h5{font-size:17px;line-height:24px}h6{font-size:16px;line-height:22px}em{font-style:italic}ins{color:var(--txtPrimaryColor);background:var(--successAltColor);text-decoration:none}del{color:var(--txtPrimaryColor);background:var(--dangerAltColor);text-decoration:none}strong{font-weight:600}small{font-size:var(--smFontSize);line-height:var(--smLineHeight)}sub,sup{position:relative;font-size:.75em;line-height:1}sup{vertical-align:top}sub{vertical-align:bottom}p{margin:5px 0}blockquote{position:relative;padding-left:var(--smSpacing);font-style:italic;color:var(--txtHintColor)}blockquote:before{content:"";position:absolute;top:0;left:0;width:2px;height:100%;background:var(--baseColor)}code{display:inline-block;font-family:var(--monospaceFontFamily);font-style:normal;font-size:1em;line-height:1.379rem;padding:0 4px;white-space:nowrap;color:inherit;background:var(--baseAlt2Color);border-radius:var(--baseRadius)}.code-block{overflow:auto;padding:var(--xsSpacing);white-space:pre-wrap;background:var(--baseAlt1Color)}ol,ul{margin:10px 0;list-style:decimal;padding-left:var(--baseSpacing)}ol li,ul li{margin-top:5px;margin-bottom:5px}ul{list-style:disc}img{max-width:100%;vertical-align:top}hr{display:block;border:0;height:1px;width:100%;background:var(--baseAlt1Color);margin:var(--baseSpacing) 0}hr.dark{background:var(--baseAlt2Color)}a{color:inherit}a:hover{text-decoration:none}a i,a .txt{display:inline-block;vertical-align:top}.txt-mono{font-family:var(--monospaceFontFamily)}.txt-nowrap{white-space:nowrap}.txt-ellipsis{display:inline-block;vertical-align:top;flex-shrink:1;min-width:0;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.txt-base{font-size:var(--baseFontSize)!important}.txt-xs{font-size:var(--xsFontSize)!important;line-height:var(--smLineHeight)}.txt-sm{font-size:var(--smFontSize)!important;line-height:var(--smLineHeight)}.txt-lg{font-size:var(--lgFontSize)!important}.txt-xl{font-size:var(--xlFontSize)!important}.txt-bold{font-weight:600!important}.txt-strikethrough{text-decoration:line-through!important}.txt-break{white-space:pre-wrap!important}.txt-center{text-align:center!important}.txt-justify{text-align:justify!important}.txt-left{text-align:left!important}.txt-right{text-align:right!important}.txt-main{color:var(--txtPrimaryColor)!important}.txt-hint{color:var(--txtHintColor)!important}.txt-disabled{color:var(--txtDisabledColor)!important}.link-hint{-webkit-user-select:none;user-select:none;cursor:pointer;color:var(--txtHintColor)!important;text-decoration:none;transition:color var(--baseAnimationSpeed)}.link-hint:hover,.link-hint:focus-visible,.link-hint:active{color:var(--txtPrimaryColor)!important}.link-fade{opacity:1;-webkit-user-select:none;user-select:none;cursor:pointer;text-decoration:none;color:var(--txtPrimaryColor);transition:opacity var(--baseAnimationSpeed)}.link-fade:focus-visible,.link-fade:hover,.link-fade:active{opacity:.8}.txt-primary{color:var(--primaryColor)!important}.bg-primary{background:var(--primaryColor)!important}.link-primary{cursor:pointer;color:var(--primaryColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-primary:focus-visible,.link-primary:hover,.link-primary:active{opacity:.8}.txt-info{color:var(--infoColor)!important}.bg-info{background:var(--infoColor)!important}.link-info{cursor:pointer;color:var(--infoColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info:focus-visible,.link-info:hover,.link-info:active{opacity:.8}.txt-info-alt{color:var(--infoAltColor)!important}.bg-info-alt{background:var(--infoAltColor)!important}.link-info-alt{cursor:pointer;color:var(--infoAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info-alt:focus-visible,.link-info-alt:hover,.link-info-alt:active{opacity:.8}.txt-success{color:var(--successColor)!important}.bg-success{background:var(--successColor)!important}.link-success{cursor:pointer;color:var(--successColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success:focus-visible,.link-success:hover,.link-success:active{opacity:.8}.txt-success-alt{color:var(--successAltColor)!important}.bg-success-alt{background:var(--successAltColor)!important}.link-success-alt{cursor:pointer;color:var(--successAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success-alt:focus-visible,.link-success-alt:hover,.link-success-alt:active{opacity:.8}.txt-danger{color:var(--dangerColor)!important}.bg-danger{background:var(--dangerColor)!important}.link-danger{cursor:pointer;color:var(--dangerColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger:focus-visible,.link-danger:hover,.link-danger:active{opacity:.8}.txt-danger-alt{color:var(--dangerAltColor)!important}.bg-danger-alt{background:var(--dangerAltColor)!important}.link-danger-alt{cursor:pointer;color:var(--dangerAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger-alt:focus-visible,.link-danger-alt:hover,.link-danger-alt:active{opacity:.8}.txt-warning{color:var(--warningColor)!important}.bg-warning{background:var(--warningColor)!important}.link-warning{cursor:pointer;color:var(--warningColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning:focus-visible,.link-warning:hover,.link-warning:active{opacity:.8}.txt-warning-alt{color:var(--warningAltColor)!important}.bg-warning-alt{background:var(--warningAltColor)!important}.link-warning-alt{cursor:pointer;color:var(--warningAltColor)!important;text-decoration:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning-alt:focus-visible,.link-warning-alt:hover,.link-warning-alt:active{opacity:.8}.fade{opacity:.6}a.fade,.btn.fade,[tabindex].fade,[class*=link-].fade,.handle.fade{transition:all var(--baseAnimationSpeed)}a.fade:hover,.btn.fade:hover,[tabindex].fade:hover,[class*=link-].fade:hover,.handle.fade:hover{opacity:1}.noborder{border:0px!important}.hidden{display:none!important}.hidden-empty:empty{display:none!important}.v-align-top{vertical-align:top}.v-align-middle{vertical-align:middle}.v-align-bottom{vertical-align:bottom}.scrollbar-gutter-stable{scrollbar-gutter:stable}.no-pointer-events{pointer-events:none}.content,.form-field .help-block,.overlay-panel .panel-content,.sub-panel,.panel{min-width:0}.content>:first-child,.form-field .help-block>:first-child,.overlay-panel .panel-content>:first-child,.sub-panel>:first-child,.panel>:first-child{margin-top:0}.content>:last-child,.form-field .help-block>:last-child,.overlay-panel .panel-content>:last-child,.sub-panel>:last-child,.panel>:last-child{margin-bottom:0}.panel{background:var(--baseColor);border-radius:var(--lgRadius);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);box-shadow:0 2px 5px 0 var(--shadowColor)}.sub-panel{background:var(--baseColor);border-radius:var(--baseRadius);padding:calc(var(--smSpacing) - 5px) var(--smSpacing);border:1px solid var(--baseAlt1Color)}.shadowize{box-shadow:0 2px 5px 0 var(--shadowColor)}.clearfix{clear:both}.clearfix:after{content:"";display:table;clear:both}.flex{position:relative;display:flex;align-items:center;width:100%;min-width:0;gap:var(--smSpacing)}.flex-fill{flex:1 1 auto!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.inline-flex{position:relative;display:inline-flex;vertical-align:top;align-items:center;flex-wrap:wrap;min-width:0;gap:10px}.flex-order-0{order:0}.flex-order-1{order:1}.flex-order-2{order:2}.flex-order-3{order:3}.flex-order-4{order:4}.flex-order-5{order:5}.flex-order-6{order:6}.flex-order-7{order:7}.flex-order-8{order:8}.flex-order-9{order:9}.flex-order-10{order:10}.flex-gap-base{gap:var(--baseSpacing)!important}.flex-gap-xs{gap:var(--xsSpacing)!important}.flex-gap-sm{gap:var(--smSpacing)!important}.flex-gap-lg{gap:var(--lgSpacing)!important}.flex-gap-xl{gap:var(--xlSpacing)!important}.flex-gap-0{gap:0px!important}.flex-gap-5{gap:5px!important}.flex-gap-10{gap:10px!important}.flex-gap-15{gap:15px!important}.flex-gap-20{gap:20px!important}.flex-gap-25{gap:25px!important}.flex-gap-30{gap:30px!important}.flex-gap-35{gap:35px!important}.flex-gap-40{gap:40px!important}.flex-gap-45{gap:45px!important}.flex-gap-50{gap:50px!important}.flex-gap-55{gap:55px!important}.flex-gap-60{gap:60px!important}.m-base{margin:var(--baseSpacing)!important}.p-base{padding:var(--baseSpacing)!important}.m-xs{margin:var(--xsSpacing)!important}.p-xs{padding:var(--xsSpacing)!important}.m-sm{margin:var(--smSpacing)!important}.p-sm{padding:var(--smSpacing)!important}.m-lg{margin:var(--lgSpacing)!important}.p-lg{padding:var(--lgSpacing)!important}.m-xl{margin:var(--xlSpacing)!important}.p-xl{padding:var(--xlSpacing)!important}.m-t-auto{margin-top:auto!important}.p-t-auto{padding-top:auto!important}.m-t-base{margin-top:var(--baseSpacing)!important}.p-t-base{padding-top:var(--baseSpacing)!important}.m-t-xs{margin-top:var(--xsSpacing)!important}.p-t-xs{padding-top:var(--xsSpacing)!important}.m-t-sm{margin-top:var(--smSpacing)!important}.p-t-sm{padding-top:var(--smSpacing)!important}.m-t-lg{margin-top:var(--lgSpacing)!important}.p-t-lg{padding-top:var(--lgSpacing)!important}.m-t-xl{margin-top:var(--xlSpacing)!important}.p-t-xl{padding-top:var(--xlSpacing)!important}.m-r-auto{margin-right:auto!important}.p-r-auto{padding-right:auto!important}.m-r-base{margin-right:var(--baseSpacing)!important}.p-r-base{padding-right:var(--baseSpacing)!important}.m-r-xs{margin-right:var(--xsSpacing)!important}.p-r-xs{padding-right:var(--xsSpacing)!important}.m-r-sm{margin-right:var(--smSpacing)!important}.p-r-sm{padding-right:var(--smSpacing)!important}.m-r-lg{margin-right:var(--lgSpacing)!important}.p-r-lg{padding-right:var(--lgSpacing)!important}.m-r-xl{margin-right:var(--xlSpacing)!important}.p-r-xl{padding-right:var(--xlSpacing)!important}.m-b-auto{margin-bottom:auto!important}.p-b-auto{padding-bottom:auto!important}.m-b-base{margin-bottom:var(--baseSpacing)!important}.p-b-base{padding-bottom:var(--baseSpacing)!important}.m-b-xs{margin-bottom:var(--xsSpacing)!important}.p-b-xs{padding-bottom:var(--xsSpacing)!important}.m-b-sm{margin-bottom:var(--smSpacing)!important}.p-b-sm{padding-bottom:var(--smSpacing)!important}.m-b-lg{margin-bottom:var(--lgSpacing)!important}.p-b-lg{padding-bottom:var(--lgSpacing)!important}.m-b-xl{margin-bottom:var(--xlSpacing)!important}.p-b-xl{padding-bottom:var(--xlSpacing)!important}.m-l-auto{margin-left:auto!important}.p-l-auto{padding-left:auto!important}.m-l-base{margin-left:var(--baseSpacing)!important}.p-l-base{padding-left:var(--baseSpacing)!important}.m-l-xs{margin-left:var(--xsSpacing)!important}.p-l-xs{padding-left:var(--xsSpacing)!important}.m-l-sm{margin-left:var(--smSpacing)!important}.p-l-sm{padding-left:var(--smSpacing)!important}.m-l-lg{margin-left:var(--lgSpacing)!important}.p-l-lg{padding-left:var(--lgSpacing)!important}.m-l-xl{margin-left:var(--xlSpacing)!important}.p-l-xl{padding-left:var(--xlSpacing)!important}.m-0{margin:0!important}.p-0{padding:0!important}.m-t-0{margin-top:0!important}.p-t-0{padding-top:0!important}.m-r-0{margin-right:0!important}.p-r-0{padding-right:0!important}.m-b-0{margin-bottom:0!important}.p-b-0{padding-bottom:0!important}.m-l-0{margin-left:0!important}.p-l-0{padding-left:0!important}.m-5{margin:5px!important}.p-5{padding:5px!important}.m-t-5{margin-top:5px!important}.p-t-5{padding-top:5px!important}.m-r-5{margin-right:5px!important}.p-r-5{padding-right:5px!important}.m-b-5{margin-bottom:5px!important}.p-b-5{padding-bottom:5px!important}.m-l-5{margin-left:5px!important}.p-l-5{padding-left:5px!important}.m-10{margin:10px!important}.p-10{padding:10px!important}.m-t-10{margin-top:10px!important}.p-t-10{padding-top:10px!important}.m-r-10{margin-right:10px!important}.p-r-10{padding-right:10px!important}.m-b-10{margin-bottom:10px!important}.p-b-10{padding-bottom:10px!important}.m-l-10{margin-left:10px!important}.p-l-10{padding-left:10px!important}.m-15{margin:15px!important}.p-15{padding:15px!important}.m-t-15{margin-top:15px!important}.p-t-15{padding-top:15px!important}.m-r-15{margin-right:15px!important}.p-r-15{padding-right:15px!important}.m-b-15{margin-bottom:15px!important}.p-b-15{padding-bottom:15px!important}.m-l-15{margin-left:15px!important}.p-l-15{padding-left:15px!important}.m-20{margin:20px!important}.p-20{padding:20px!important}.m-t-20{margin-top:20px!important}.p-t-20{padding-top:20px!important}.m-r-20{margin-right:20px!important}.p-r-20{padding-right:20px!important}.m-b-20{margin-bottom:20px!important}.p-b-20{padding-bottom:20px!important}.m-l-20{margin-left:20px!important}.p-l-20{padding-left:20px!important}.m-25{margin:25px!important}.p-25{padding:25px!important}.m-t-25{margin-top:25px!important}.p-t-25{padding-top:25px!important}.m-r-25{margin-right:25px!important}.p-r-25{padding-right:25px!important}.m-b-25{margin-bottom:25px!important}.p-b-25{padding-bottom:25px!important}.m-l-25{margin-left:25px!important}.p-l-25{padding-left:25px!important}.m-30{margin:30px!important}.p-30{padding:30px!important}.m-t-30{margin-top:30px!important}.p-t-30{padding-top:30px!important}.m-r-30{margin-right:30px!important}.p-r-30{padding-right:30px!important}.m-b-30{margin-bottom:30px!important}.p-b-30{padding-bottom:30px!important}.m-l-30{margin-left:30px!important}.p-l-30{padding-left:30px!important}.m-35{margin:35px!important}.p-35{padding:35px!important}.m-t-35{margin-top:35px!important}.p-t-35{padding-top:35px!important}.m-r-35{margin-right:35px!important}.p-r-35{padding-right:35px!important}.m-b-35{margin-bottom:35px!important}.p-b-35{padding-bottom:35px!important}.m-l-35{margin-left:35px!important}.p-l-35{padding-left:35px!important}.m-40{margin:40px!important}.p-40{padding:40px!important}.m-t-40{margin-top:40px!important}.p-t-40{padding-top:40px!important}.m-r-40{margin-right:40px!important}.p-r-40{padding-right:40px!important}.m-b-40{margin-bottom:40px!important}.p-b-40{padding-bottom:40px!important}.m-l-40{margin-left:40px!important}.p-l-40{padding-left:40px!important}.m-45{margin:45px!important}.p-45{padding:45px!important}.m-t-45{margin-top:45px!important}.p-t-45{padding-top:45px!important}.m-r-45{margin-right:45px!important}.p-r-45{padding-right:45px!important}.m-b-45{margin-bottom:45px!important}.p-b-45{padding-bottom:45px!important}.m-l-45{margin-left:45px!important}.p-l-45{padding-left:45px!important}.m-50{margin:50px!important}.p-50{padding:50px!important}.m-t-50{margin-top:50px!important}.p-t-50{padding-top:50px!important}.m-r-50{margin-right:50px!important}.p-r-50{padding-right:50px!important}.m-b-50{margin-bottom:50px!important}.p-b-50{padding-bottom:50px!important}.m-l-50{margin-left:50px!important}.p-l-50{padding-left:50px!important}.m-55{margin:55px!important}.p-55{padding:55px!important}.m-t-55{margin-top:55px!important}.p-t-55{padding-top:55px!important}.m-r-55{margin-right:55px!important}.p-r-55{padding-right:55px!important}.m-b-55{margin-bottom:55px!important}.p-b-55{padding-bottom:55px!important}.m-l-55{margin-left:55px!important}.p-l-55{padding-left:55px!important}.m-60{margin:60px!important}.p-60{padding:60px!important}.m-t-60{margin-top:60px!important}.p-t-60{padding-top:60px!important}.m-r-60{margin-right:60px!important}.p-r-60{padding-right:60px!important}.m-b-60{margin-bottom:60px!important}.p-b-60{padding-bottom:60px!important}.m-l-60{margin-left:60px!important}.p-l-60{padding-left:60px!important}.no-min-width{min-width:0!important}.wrapper{position:relative;width:var(--wrapperWidth);margin:0 auto;max-width:100%}.wrapper.wrapper-sm{width:var(--smWrapperWidth)}.wrapper.wrapper-lg{width:var(--lgWrapperWidth)}.label{--labelVPadding: 3px;--labelHPadding: 9px;display:inline-flex;align-items:center;justify-content:center;vertical-align:top;gap:5px;padding:var(--labelVPadding) var(--labelHPadding);min-height:24px;max-width:100%;text-align:center;line-height:var(--smLineHeight);font-size:var(--smFontSize);background:var(--baseAlt2Color);color:var(--txtPrimaryColor);white-space:nowrap;border-radius:15px}.label .btn:last-child{margin-right:calc(-.5 * var(--labelHPadding))}.label .btn:first-child{margin-left:calc(-.5 * var(--labelHPadding))}.label.label-sm{--labelHPadding: 5px;font-size:var(--xsFontSize);min-height:18px;line-height:1}.label.label-primary{color:var(--baseColor);background:var(--primaryColor)}.label.label-info{background:var(--infoAltColor)}.label.label-success{background:var(--successAltColor)}.label.label-danger{background:var(--dangerAltColor)}.label.label-warning{background:var(--warningAltColor)}.thumb{--thumbSize: 40px;display:inline-flex;vertical-align:top;position:relative;flex-shrink:0;align-items:center;justify-content:center;line-height:1;width:var(--thumbSize);height:var(--thumbSize);aspect-ratio:1;background:var(--baseAlt2Color);border-radius:var(--baseRadius);color:var(--txtPrimaryColor);outline-offset:-2px;outline:2px solid transparent;box-shadow:0 2px 5px 0 var(--shadowColor)}.thumb i{font-size:inherit}.thumb img{width:100%;height:100%;border-radius:inherit;overflow:hidden}.thumb.thumb-xs{--thumbSize: 24px;font-size:.85rem}.thumb.thumb-sm{--thumbSize: 32px;font-size:.92rem}.thumb.thumb-lg{--thumbSize: 60px;font-size:1.3rem}.thumb.thumb-xl{--thumbSize: 80px;font-size:1.5rem}.thumb.thumb-circle{border-radius:50%}.thumb.thumb-primary{outline-color:var(--primaryColor)}.thumb.thumb-info{outline-color:var(--infoColor)}.thumb.thumb-info-alt{outline-color:var(--infoAltColor)}.thumb.thumb-success{outline-color:var(--successColor)}.thumb.thumb-success-alt{outline-color:var(--successAltColor)}.thumb.thumb-danger{outline-color:var(--dangerColor)}.thumb.thumb-danger-alt{outline-color:var(--dangerAltColor)}.thumb.thumb-warning{outline-color:var(--warningColor)}.thumb.thumb-warning-alt{outline-color:var(--warningAltColor)}.handle.thumb:not(.thumb-active),a.thumb:not(.thumb-active){cursor:pointer;transition:opacity var(--baseAnimationSpeed),outline-color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.handle.thumb:not(.thumb-active):hover,.handle.thumb:not(.thumb-active):focus-visible,.handle.thumb:not(.thumb-active):active,a.thumb:not(.thumb-active):hover,a.thumb:not(.thumb-active):focus-visible,a.thumb:not(.thumb-active):active{opacity:.8;box-shadow:0 2px 5px 0 var(--shadowColor),0 2px 4px 1px var(--shadowColor)}.handle.thumb:not(.thumb-active):active,a.thumb:not(.thumb-active):active{transition-duration:var(--activeAnimationSpeed);transform:scale(.97)}.section-title{display:flex;align-items:center;width:100%;column-gap:10px;row-gap:5px;margin:0 0 var(--xsSpacing);font-weight:600;font-size:var(--baseFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor)}.logo{position:relative;vertical-align:top;display:inline-flex;align-items:center;gap:10px;font-size:23px;text-decoration:none;color:inherit;-webkit-user-select:none;user-select:none}.logo strong{font-weight:700}.logo .version{position:absolute;right:0;top:-5px;line-height:1;font-size:10px;font-weight:400;padding:2px 4px;border-radius:var(--baseRadius);background:var(--dangerAltColor);color:var(--txtPrimaryColor)}.logo.logo-sm{font-size:20px}.drag-handle{position:relative;display:inline-flex;align-items:center;justify-content:center;text-align:center;flex-shrink:0;color:var(--txtDisabledColor);-webkit-user-select:none;user-select:none;cursor:pointer;transition:color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed)}.drag-handle:before{content:"";line-height:1;font-family:var(--iconFontFamily);padding-right:5px;text-shadow:5px 0px currentColor}.drag-handle:hover,.drag-handle:focus-visible{color:var(--txtHintColor)}.drag-handle:active{transition-duration:var(--activeAnimationSpeed);color:var(--txtPrimaryColor)}.loader{--loaderSize: 32px;position:relative;display:inline-flex;vertical-align:top;flex-direction:column;align-items:center;justify-content:center;row-gap:10px;margin:0;color:var(--txtDisabledColor);text-align:center;font-weight:400}.loader:before{content:"";display:inline-block;vertical-align:top;clear:both;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);font-weight:400;font-family:var(--iconFontFamily);color:inherit;text-align:center;animation:loaderShow var(--activeAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.loader.loader-primary{color:var(--primaryColor)}.loader.loader-info{color:var(--infoColor)}.loader.loader-info-alt{color:var(--infoAltColor)}.loader.loader-success{color:var(--successColor)}.loader.loader-success-alt{color:var(--successAltColor)}.loader.loader-danger{color:var(--dangerColor)}.loader.loader-danger-alt{color:var(--dangerAltColor)}.loader.loader-warning{color:var(--warningColor)}.loader.loader-warning-alt{color:var(--warningAltColor)}.loader.loader-xs{--loaderSize: 18px}.loader.loader-sm{--loaderSize: 24px}.loader.loader-lg{--loaderSize: 42px}.skeleton-loader{position:relative;height:12px;margin:5px 0;border-radius:var(--baseRadius);background:var(--baseAlt1Color);animation:fadeIn .4s}.skeleton-loader:before{content:"";width:100%;height:100%;display:block;border-radius:inherit;background:linear-gradient(90deg,var(--baseAlt1Color) 8%,var(--bodyColor) 18%,var(--baseAlt1Color) 33%);background-size:200% 100%;animation:shine 1s linear infinite}.placeholder-section{display:flex;width:100%;align-items:center;justify-content:center;text-align:center;flex-direction:column;gap:var(--smSpacing);color:var(--txtHintColor)}.placeholder-section .icon{font-size:50px;height:50px;line-height:1;opacity:.3}.placeholder-section .icon i{font-size:inherit;vertical-align:top}.list{position:relative;overflow:auto;overflow:overlay;border:1px solid var(--baseAlt2Color);border-radius:var(--baseRadius)}.list .list-item{word-break:break-word;position:relative;display:flex;align-items:center;width:100%;gap:var(--xsSpacing);outline:0;padding:10px var(--xsSpacing);min-height:50px;border-top:1px solid var(--baseAlt2Color);transition:background var(--baseAnimationSpeed)}.list .list-item:first-child{border-top:0}.list .list-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list .list-item .content,.list .list-item .form-field .help-block,.form-field .list .list-item .help-block,.list .list-item .overlay-panel .panel-content,.overlay-panel .list .list-item .panel-content,.list .list-item .panel,.list .list-item .sub-panel{display:flex;align-items:center;gap:5px;min-width:0;max-width:100%;-webkit-user-select:text;user-select:text}.list .list-item .actions{gap:10px;flex-shrink:0;display:inline-flex;align-items:center;margin:-1px -5px -1px 0}.list .list-item .actions.nonintrusive{opacity:0;transform:translate(5px);transition:transform var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed)}.list .list-item:hover,.list .list-item:focus-visible,.list .list-item:focus-within,.list .list-item:active{background:var(--bodyColor)}.list .list-item:hover .actions.nonintrusive,.list .list-item:focus-visible .actions.nonintrusive,.list .list-item:focus-within .actions.nonintrusive,.list .list-item:active .actions.nonintrusive{opacity:1;transform:translate(0)}.list .list-item.selected{background:var(--bodyColor)}.list .list-item.handle:not(.disabled){cursor:pointer;-webkit-user-select:none;user-select:none}.list .list-item.handle:not(.disabled):hover,.list .list-item.handle:not(.disabled):focus-visible{background:var(--baseAlt1Color)}.list .list-item.handle:not(.disabled):active{background:var(--baseAlt2Color)}.list .list-item.disabled:not(.selected){cursor:default;opacity:.6}.list .list-item-placeholder{color:var(--txtHintColor)}.list .list-item-btn{padding:5px;min-height:auto}.list .list-item-placeholder:hover,.list .list-item-placeholder:focus-visible,.list .list-item-placeholder:focus-within,.list .list-item-placeholder:active,.list .list-item-btn:hover,.list .list-item-btn:focus-visible,.list .list-item-btn:focus-within,.list .list-item-btn:active{background:none}.list.list-compact .list-item{gap:10px;min-height:40px}.entrance-top{animation:entranceTop var(--entranceAnimationSpeed)}.entrance-bottom{animation:entranceBottom var(--entranceAnimationSpeed)}.entrance-left{animation:entranceLeft var(--entranceAnimationSpeed)}.entrance-right{animation:entranceRight var(--entranceAnimationSpeed)}.entrance-fade{animation:fadeIn var(--entranceAnimationSpeed)}.provider-logo{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:32px;height:32px;border-radius:var(--baseRadius);background:var(--bodyColor);padding:0;gap:0}.provider-logo img{max-width:20px;max-height:20px;height:auto;flex-shrink:0}.provider-card{display:flex;align-items:center;width:100%;height:100%;gap:10px;padding:10px;border-radius:var(--baseRadius);border:1px solid var(--baseAlt1Color)}.sidebar-menu{--sidebarListItemMargin: 10px;z-index:0;display:flex;flex-direction:column;width:200px;flex-shrink:0;flex-grow:0;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);padding:calc(var(--baseSpacing) - 5px) 0 var(--smSpacing)}.sidebar-menu>*{padding:0 var(--smSpacing)}.sidebar-menu .sidebar-content{overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.sidebar-menu .sidebar-content>:first-child{margin-top:0}.sidebar-menu .sidebar-content>:last-child{margin-bottom:0}.sidebar-menu .sidebar-footer{margin-top:var(--smSpacing)}.sidebar-menu .search{display:flex;align-items:center;width:auto;column-gap:5px;margin:0 0 var(--xsSpacing);color:var(--txtHintColor);opacity:.7;transition:opacity var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.sidebar-menu .search input{border:0;background:var(--baseColor);transition:box-shadow var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.sidebar-menu .search .btn-clear{margin-right:-8px}.sidebar-menu .search:hover,.sidebar-menu .search:focus-within,.sidebar-menu .search.active{opacity:1;color:var(--txtPrimaryColor)}.sidebar-menu .search:hover input,.sidebar-menu .search:focus-within input,.sidebar-menu .search.active input{background:var(--baseAlt2Color)}.sidebar-menu .sidebar-title{display:flex;align-items:center;gap:5px;width:100%;margin:var(--baseSpacing) 0 var(--xsSpacing);font-weight:600;font-size:1rem;line-height:var(--smLineHeight);color:var(--txtHintColor)}.sidebar-menu .sidebar-title .label{font-weight:400}.sidebar-menu .sidebar-list-item{cursor:pointer;outline:0;text-decoration:none;position:relative;display:flex;width:100%;align-items:center;column-gap:10px;margin:var(--sidebarListItemMargin) 0;padding:3px 10px;font-size:var(--xlFontSize);min-height:var(--btnHeight);min-width:0;color:var(--txtHintColor);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.sidebar-menu .sidebar-list-item i{font-size:18px}.sidebar-menu .sidebar-list-item .txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-menu .sidebar-list-item:focus-visible,.sidebar-menu .sidebar-list-item:hover,.sidebar-menu .sidebar-list-item:active,.sidebar-menu .sidebar-list-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.sidebar-menu .sidebar-list-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.sidebar-menu .sidebar-content-compact .sidebar-list-item{--sidebarListItemMargin: 5px}@media screen and (max-height: 600px){.sidebar-menu{--sidebarListItemMargin: 5px}}@media screen and (max-width: 1100px){.sidebar-menu{min-width:190px}.sidebar-menu>*{padding-left:10px;padding-right:10px}}.grid{--gridGap: var(--baseSpacing);position:relative;display:flex;flex-grow:1;flex-wrap:wrap;row-gap:var(--gridGap);margin:0 calc(-.5 * var(--gridGap))}.grid.grid-center{align-items:center}.grid.grid-sm{--gridGap: var(--smSpacing)}.grid .form-field{margin-bottom:0}.grid>*{margin:0 calc(.5 * var(--gridGap))}.col-xxl-1,.col-xxl-2,.col-xxl-3,.col-xxl-4,.col-xxl-5,.col-xxl-6,.col-xxl-7,.col-xxl-8,.col-xxl-9,.col-xxl-10,.col-xxl-11,.col-xxl-12,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{position:relative;width:100%;min-height:1px}.col-auto{flex:0 0 auto;width:auto}.col-12{width:calc(100% - var(--gridGap))}.col-11{width:calc(91.6666666667% - var(--gridGap))}.col-10{width:calc(83.3333333333% - var(--gridGap))}.col-9{width:calc(75% - var(--gridGap))}.col-8{width:calc(66.6666666667% - var(--gridGap))}.col-7{width:calc(58.3333333333% - var(--gridGap))}.col-6{width:calc(50% - var(--gridGap))}.col-5{width:calc(41.6666666667% - var(--gridGap))}.col-4{width:calc(33.3333333333% - var(--gridGap))}.col-3{width:calc(25% - var(--gridGap))}.col-2{width:calc(16.6666666667% - var(--gridGap))}.col-1{width:calc(8.3333333333% - var(--gridGap))}@media (min-width: 576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-12{width:calc(100% - var(--gridGap))}.col-sm-11{width:calc(91.6666666667% - var(--gridGap))}.col-sm-10{width:calc(83.3333333333% - var(--gridGap))}.col-sm-9{width:calc(75% - var(--gridGap))}.col-sm-8{width:calc(66.6666666667% - var(--gridGap))}.col-sm-7{width:calc(58.3333333333% - var(--gridGap))}.col-sm-6{width:calc(50% - var(--gridGap))}.col-sm-5{width:calc(41.6666666667% - var(--gridGap))}.col-sm-4{width:calc(33.3333333333% - var(--gridGap))}.col-sm-3{width:calc(25% - var(--gridGap))}.col-sm-2{width:calc(16.6666666667% - var(--gridGap))}.col-sm-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-12{width:calc(100% - var(--gridGap))}.col-md-11{width:calc(91.6666666667% - var(--gridGap))}.col-md-10{width:calc(83.3333333333% - var(--gridGap))}.col-md-9{width:calc(75% - var(--gridGap))}.col-md-8{width:calc(66.6666666667% - var(--gridGap))}.col-md-7{width:calc(58.3333333333% - var(--gridGap))}.col-md-6{width:calc(50% - var(--gridGap))}.col-md-5{width:calc(41.6666666667% - var(--gridGap))}.col-md-4{width:calc(33.3333333333% - var(--gridGap))}.col-md-3{width:calc(25% - var(--gridGap))}.col-md-2{width:calc(16.6666666667% - var(--gridGap))}.col-md-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-12{width:calc(100% - var(--gridGap))}.col-lg-11{width:calc(91.6666666667% - var(--gridGap))}.col-lg-10{width:calc(83.3333333333% - var(--gridGap))}.col-lg-9{width:calc(75% - var(--gridGap))}.col-lg-8{width:calc(66.6666666667% - var(--gridGap))}.col-lg-7{width:calc(58.3333333333% - var(--gridGap))}.col-lg-6{width:calc(50% - var(--gridGap))}.col-lg-5{width:calc(41.6666666667% - var(--gridGap))}.col-lg-4{width:calc(33.3333333333% - var(--gridGap))}.col-lg-3{width:calc(25% - var(--gridGap))}.col-lg-2{width:calc(16.6666666667% - var(--gridGap))}.col-lg-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-12{width:calc(100% - var(--gridGap))}.col-xl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xl-9{width:calc(75% - var(--gridGap))}.col-xl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xl-6{width:calc(50% - var(--gridGap))}.col-xl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xl-3{width:calc(25% - var(--gridGap))}.col-xl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xl-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-12{width:calc(100% - var(--gridGap))}.col-xxl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xxl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xxl-9{width:calc(75% - var(--gridGap))}.col-xxl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xxl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xxl-6{width:calc(50% - var(--gridGap))}.col-xxl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xxl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xxl-3{width:calc(25% - var(--gridGap))}.col-xxl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xxl-1{width:calc(8.3333333333% - var(--gridGap))}}.app-tooltip{position:fixed;z-index:999999;top:0;left:0;display:inline-block;vertical-align:top;max-width:275px;padding:3px 5px;color:#fff;text-align:center;font-family:var(--baseFontFamily);font-size:var(--smFontSize);line-height:var(--smLineHeight);border-radius:var(--baseRadius);background:var(--tooltipColor);pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed),transform var(--baseAnimationSpeed);transform:translateY(1px);backface-visibility:hidden;white-space:pre-line;word-break:break-word;opacity:0;visibility:hidden}.app-tooltip.code{font-family:monospace;white-space:pre-wrap;text-align:left;min-width:150px;max-width:340px}.app-tooltip.active{transform:scale(1);opacity:1;visibility:visible}.dropdown{position:absolute;z-index:99;right:0;left:auto;top:100%;cursor:default;display:inline-block;vertical-align:top;padding:5px;margin:5px 0 0;width:auto;min-width:140px;max-width:450px;max-height:330px;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);border-radius:var(--baseRadius);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor)}.dropdown hr{margin:5px 0}.dropdown .dropdown-item{border:0;background:none;position:relative;outline:0;display:flex;align-items:center;column-gap:8px;width:100%;height:auto;min-height:0;text-align:left;padding:8px 10px;margin:0 0 5px;cursor:pointer;color:var(--txtPrimaryColor);font-weight:400;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);line-height:var(--baseLineHeight);border-radius:var(--baseRadius);text-decoration:none;word-break:break-word;-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.dropdown .dropdown-item:last-child{margin-bottom:0}.dropdown .dropdown-item.selected{background:var(--baseAlt2Color)}.dropdown .dropdown-item:focus-visible,.dropdown .dropdown-item:hover{background:var(--baseAlt1Color)}.dropdown .dropdown-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.dropdown .dropdown-item.disabled{color:var(--txtDisabledColor);background:none;pointer-events:none}.dropdown .dropdown-item.separator{cursor:default;background:none;text-transform:uppercase;padding-top:0;padding-bottom:0;margin-top:15px;color:var(--txtDisabledColor);font-weight:600;font-size:var(--smFontSize)}.dropdown.dropdown-upside{top:auto;bottom:100%;margin:0 0 5px}.dropdown.dropdown-left{right:auto;left:0}.dropdown.dropdown-center{right:auto;left:50%;transform:translate(-50%)}.dropdown.dropdown-sm{margin-top:5px;min-width:100px}.dropdown.dropdown-sm .dropdown-item{column-gap:7px;font-size:var(--smFontSize);margin:0 0 2px;padding:5px 7px}.dropdown.dropdown-sm .dropdown-item:last-child{margin-bottom:0}.dropdown.dropdown-sm.dropdown-upside{margin-top:0;margin-bottom:5px}.dropdown.dropdown-block{width:100%;min-width:130px;max-width:100%}.dropdown.dropdown-nowrap{white-space:nowrap}.toggler-container{outline:0}.overlay-panel{position:relative;z-index:1;display:flex;flex-direction:column;align-self:flex-end;margin-left:auto;background:var(--baseColor);height:100%;width:580px;max-width:100%;word-wrap:break-word;box-shadow:0 2px 5px 0 var(--shadowColor)}.overlay-panel .overlay-panel-section{position:relative;width:100%;margin:0;padding:var(--baseSpacing);transition:box-shadow var(--baseAnimationSpeed)}.overlay-panel .overlay-panel-section:empty{display:none}.overlay-panel .overlay-panel-section:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.overlay-panel .overlay-panel-section:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.overlay-panel .overlay-panel-section .btn{flex-grow:0}.overlay-panel img{max-width:100%}.overlay-panel .panel-header{position:relative;z-index:2;display:flex;flex-wrap:wrap;align-items:center;column-gap:10px;row-gap:var(--baseSpacing);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel .panel-header>*{margin-top:0;margin-bottom:0}.overlay-panel .panel-header .btn-back{margin-left:-10px}.overlay-panel .panel-header .overlay-close{z-index:3;outline:0;position:absolute;right:100%;top:20px;margin:0;display:inline-flex;align-items:center;justify-content:center;width:35px;height:35px;cursor:pointer;text-align:center;font-size:1.6rem;line-height:1;border-radius:50% 0 0 50%;color:#fff;background:var(--primaryColor);opacity:.5;transition:opacity var(--baseAnimationSpeed);-webkit-user-select:none;user-select:none}.overlay-panel .panel-header .overlay-close i{font-size:inherit}.overlay-panel .panel-header .overlay-close:hover,.overlay-panel .panel-header .overlay-close:focus-visible,.overlay-panel .panel-header .overlay-close:active{opacity:.7}.overlay-panel .panel-header .overlay-close:active{transition-duration:var(--activeAnimationSpeed);opacity:1}.overlay-panel .panel-header .btn-close{margin-right:-10px}.overlay-panel .panel-header .tabs-header{margin-bottom:-24px}.overlay-panel .panel-content{z-index:auto;flex-grow:1;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;scroll-behavior:smooth}.tox-fullscreen .overlay-panel .panel-content{z-index:9}.overlay-panel .panel-header~.panel-content{padding-top:5px}.overlay-panel .panel-footer{z-index:2;column-gap:var(--smSpacing);display:flex;align-items:center;justify-content:flex-end;border-top:1px solid var(--baseAlt2Color);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel.scrollable .panel-header{box-shadow:0 4px 5px #0000000d}.overlay-panel.scrollable .panel-footer{box-shadow:0 -4px 5px #0000000d}.overlay-panel.scrollable.scroll-top-reached .panel-header,.overlay-panel.scrollable.scroll-bottom-reached .panel-footer{box-shadow:none}.overlay-panel.overlay-panel-xl{width:850px}.overlay-panel.overlay-panel-lg{width:700px}.overlay-panel.overlay-panel-sm{width:460px}.overlay-panel.popup{height:auto;max-height:100%;align-self:center;border-radius:var(--baseRadius);margin:0 auto}.overlay-panel.popup .panel-footer{background:var(--bodyColor)}.overlay-panel.hide-content .panel-content{display:none}.overlay-panel.colored-header .panel-header{background:var(--bodyColor);border-bottom:1px solid var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header .tabs-header{border-bottom:0}.overlay-panel.colored-header .panel-header .tabs-header .tab-item{border:1px solid transparent;border-bottom:0}.overlay-panel.colored-header .panel-header .tabs-header .tab-item:hover,.overlay-panel.colored-header .panel-header .tabs-header .tab-item:focus-visible{background:var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header .tabs-header .tab-item:after{content:none;display:none}.overlay-panel.colored-header .panel-header .tabs-header .tab-item.active{background:var(--baseColor);border-color:var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header~.panel-content{padding-top:calc(var(--baseSpacing) - 5px)}.overlay-panel.compact-header .panel-header{row-gap:var(--smSpacing)}.overlay-panel.full-width-popup{width:100%}.overlay-panel.preview .panel-header{position:absolute;z-index:99;box-shadow:none}.overlay-panel.preview .panel-header .overlay-close{left:100%;right:auto;border-radius:0 50% 50% 0}.overlay-panel.preview .panel-header .overlay-close i{margin-right:5px}.overlay-panel.preview .panel-header,.overlay-panel.preview .panel-footer{padding:10px 15px}.overlay-panel.preview .panel-content{padding:0;text-align:center;display:flex;align-items:center;justify-content:center}.overlay-panel.preview img{max-width:100%;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.overlay-panel.preview object{position:absolute;z-index:1;left:0;top:0;width:100%;height:100%}.overlay-panel.preview.preview-image{width:auto;min-width:320px;min-height:300px;max-width:75%;max-height:90%}.overlay-panel.preview.preview-image img{align-self:flex-start;margin:auto}.overlay-panel.preview.preview-document,.overlay-panel.preview.preview-video{width:75%;height:90%}.overlay-panel.preview.preview-audio{min-width:320px;min-height:300px;max-width:90%;max-height:90%}@media (max-width: 900px){.overlay-panel .overlay-panel-section{padding:var(--smSpacing)}}.overlay-panel-container{display:flex;position:fixed;z-index:1000;flex-direction:row;align-items:center;top:0;left:0;width:100%;height:100%;overflow:hidden;margin:0;padding:0;outline:0}.overlay-panel-container .overlay{position:absolute;z-index:0;left:0;top:0;width:100%;height:100%;-webkit-user-select:none;user-select:none;background:var(--overlayColor)}.overlay-panel-container.padded{padding:10px}.overlay-panel-wrapper{position:relative;z-index:1000;outline:0}.alert{position:relative;display:flex;column-gap:15px;align-items:center;width:100%;min-height:50px;max-width:100%;word-break:break-word;margin:0 0 var(--baseSpacing);border-radius:var(--baseRadius);padding:12px 15px;background:var(--baseAlt1Color);color:var(--txtAltColor)}.alert .content,.alert .form-field .help-block,.form-field .alert .help-block,.alert .panel,.alert .sub-panel,.alert .overlay-panel .panel-content,.overlay-panel .alert .panel-content{flex-grow:1}.alert .icon,.alert .close{display:inline-flex;align-items:center;justify-content:center;flex-grow:0;flex-shrink:0;text-align:center}.alert .icon{align-self:stretch;font-size:1.2em;padding-right:15px;font-weight:400;border-right:1px solid rgba(0,0,0,.05);color:var(--txtHintColor)}.alert .close{display:inline-flex;margin-right:-5px;width:28px;height:28px;outline:0;cursor:pointer;text-align:center;font-size:var(--smFontSize);line-height:28px;border-radius:28px;text-decoration:none;color:inherit;opacity:.5;transition:opacity var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.alert .close:hover,.alert .close:focus{opacity:1;background:#fff3}.alert .close:active{opacity:1;background:#ffffff4d;transition-duration:var(--activeAnimationSpeed)}.alert code,.alert hr{background:#0000001a}.alert.alert-info{background:var(--infoAltColor)}.alert.alert-info .icon{color:var(--infoColor)}.alert.alert-warning{background:var(--warningAltColor)}.alert.alert-warning .icon{color:var(--warningColor)}.alert.alert-success{background:var(--successAltColor)}.alert.alert-success .icon{color:var(--successColor)}.alert.alert-danger{background:var(--dangerAltColor)}.alert.alert-danger .icon{color:var(--dangerColor)}.toasts-wrapper{position:fixed;z-index:999999;bottom:0;left:0;right:0;padding:0 var(--smSpacing);width:auto;display:block;text-align:center;pointer-events:none}.toasts-wrapper .alert{text-align:left;pointer-events:auto;width:var(--smWrapperWidth);margin:var(--baseSpacing) auto;box-shadow:0 2px 5px 0 var(--shadowColor)}@media screen and (min-width: 980px){body:not(.overlay-active):has(.app-sidebar) .toasts-wrapper{left:var(--appSidebarWidth)}body:not(.overlay-active):has(.page-sidebar) .toasts-wrapper{left:calc(var(--appSidebarWidth) + var(--pageSidebarWidth))}}button{outline:0;border:0;background:none;padding:0;text-align:left;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit}.btn{position:relative;z-index:1;display:inline-flex;vertical-align:top;align-items:center;justify-content:center;outline:0;border:0;margin:0;flex-shrink:0;cursor:pointer;padding:5px 20px;column-gap:7px;-webkit-user-select:none;user-select:none;min-width:var(--btnHeight);min-height:var(--btnHeight);text-align:center;text-decoration:none;line-height:1;font-weight:600;color:#fff;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);border-radius:var(--btnRadius);background:none;transition:color var(--baseAnimationSpeed)}.btn i{font-size:1.1428em;vertical-align:middle;display:inline-block}.btn .dropdown{-webkit-user-select:text;user-select:text}.btn:before{content:"";border-radius:inherit;position:absolute;left:0;top:0;z-index:-1;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;user-select:none;backface-visibility:hidden;background:var(--primaryColor);transition:filter var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.btn:hover:before,.btn:focus-visible:before{opacity:.9}.btn.active,.btn:active{z-index:999}.btn.active:before,.btn:active:before{opacity:.8;transition-duration:var(--activeAnimationSpeed)}.btn.btn-info:before{background:var(--infoColor)}.btn.btn-info:hover:before,.btn.btn-info:focus-visible:before{opacity:.8}.btn.btn-info:active:before{opacity:.7}.btn.btn-success:before{background:var(--successColor)}.btn.btn-success:hover:before,.btn.btn-success:focus-visible:before{opacity:.8}.btn.btn-success:active:before{opacity:.7}.btn.btn-danger:before{background:var(--dangerColor)}.btn.btn-danger:hover:before,.btn.btn-danger:focus-visible:before{opacity:.8}.btn.btn-danger:active:before{opacity:.7}.btn.btn-warning:before{background:var(--warningColor)}.btn.btn-warning:hover:before,.btn.btn-warning:focus-visible:before{opacity:.8}.btn.btn-warning:active:before{opacity:.7}.btn.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-hint:hover:before,.btn.btn-hint:focus-visible:before{opacity:.8}.btn.btn-hint:active:before{opacity:.7}.btn.btn-outline{border:2px solid currentColor;background:#fff}.btn.btn-secondary,.btn.btn-transparent,.btn.btn-outline{box-shadow:none;color:var(--txtPrimaryColor)}.btn.btn-secondary:before,.btn.btn-transparent:before,.btn.btn-outline:before{opacity:0}.btn.btn-secondary:focus-visible:before,.btn.btn-secondary:hover:before,.btn.btn-transparent:focus-visible:before,.btn.btn-transparent:hover:before,.btn.btn-outline:focus-visible:before,.btn.btn-outline:hover:before{opacity:.3}.btn.btn-secondary.active:before,.btn.btn-secondary:active:before,.btn.btn-transparent.active:before,.btn.btn-transparent:active:before,.btn.btn-outline.active:before,.btn.btn-outline:active:before{opacity:.45}.btn.btn-secondary:before,.btn.btn-transparent:before,.btn.btn-outline:before{background:var(--baseAlt3Color)}.btn.btn-secondary.btn-info,.btn.btn-transparent.btn-info,.btn.btn-outline.btn-info{color:var(--infoColor)}.btn.btn-secondary.btn-info:before,.btn.btn-transparent.btn-info:before,.btn.btn-outline.btn-info:before{opacity:0}.btn.btn-secondary.btn-info:focus-visible:before,.btn.btn-secondary.btn-info:hover:before,.btn.btn-transparent.btn-info:focus-visible:before,.btn.btn-transparent.btn-info:hover:before,.btn.btn-outline.btn-info:focus-visible:before,.btn.btn-outline.btn-info:hover:before{opacity:.15}.btn.btn-secondary.btn-info.active:before,.btn.btn-secondary.btn-info:active:before,.btn.btn-transparent.btn-info.active:before,.btn.btn-transparent.btn-info:active:before,.btn.btn-outline.btn-info.active:before,.btn.btn-outline.btn-info:active:before{opacity:.25}.btn.btn-secondary.btn-info:before,.btn.btn-transparent.btn-info:before,.btn.btn-outline.btn-info:before{background:var(--infoColor)}.btn.btn-secondary.btn-success,.btn.btn-transparent.btn-success,.btn.btn-outline.btn-success{color:var(--successColor)}.btn.btn-secondary.btn-success:before,.btn.btn-transparent.btn-success:before,.btn.btn-outline.btn-success:before{opacity:0}.btn.btn-secondary.btn-success:focus-visible:before,.btn.btn-secondary.btn-success:hover:before,.btn.btn-transparent.btn-success:focus-visible:before,.btn.btn-transparent.btn-success:hover:before,.btn.btn-outline.btn-success:focus-visible:before,.btn.btn-outline.btn-success:hover:before{opacity:.15}.btn.btn-secondary.btn-success.active:before,.btn.btn-secondary.btn-success:active:before,.btn.btn-transparent.btn-success.active:before,.btn.btn-transparent.btn-success:active:before,.btn.btn-outline.btn-success.active:before,.btn.btn-outline.btn-success:active:before{opacity:.25}.btn.btn-secondary.btn-success:before,.btn.btn-transparent.btn-success:before,.btn.btn-outline.btn-success:before{background:var(--successColor)}.btn.btn-secondary.btn-danger,.btn.btn-transparent.btn-danger,.btn.btn-outline.btn-danger{color:var(--dangerColor)}.btn.btn-secondary.btn-danger:before,.btn.btn-transparent.btn-danger:before,.btn.btn-outline.btn-danger:before{opacity:0}.btn.btn-secondary.btn-danger:focus-visible:before,.btn.btn-secondary.btn-danger:hover:before,.btn.btn-transparent.btn-danger:focus-visible:before,.btn.btn-transparent.btn-danger:hover:before,.btn.btn-outline.btn-danger:focus-visible:before,.btn.btn-outline.btn-danger:hover:before{opacity:.15}.btn.btn-secondary.btn-danger.active:before,.btn.btn-secondary.btn-danger:active:before,.btn.btn-transparent.btn-danger.active:before,.btn.btn-transparent.btn-danger:active:before,.btn.btn-outline.btn-danger.active:before,.btn.btn-outline.btn-danger:active:before{opacity:.25}.btn.btn-secondary.btn-danger:before,.btn.btn-transparent.btn-danger:before,.btn.btn-outline.btn-danger:before{background:var(--dangerColor)}.btn.btn-secondary.btn-warning,.btn.btn-transparent.btn-warning,.btn.btn-outline.btn-warning{color:var(--warningColor)}.btn.btn-secondary.btn-warning:before,.btn.btn-transparent.btn-warning:before,.btn.btn-outline.btn-warning:before{opacity:0}.btn.btn-secondary.btn-warning:focus-visible:before,.btn.btn-secondary.btn-warning:hover:before,.btn.btn-transparent.btn-warning:focus-visible:before,.btn.btn-transparent.btn-warning:hover:before,.btn.btn-outline.btn-warning:focus-visible:before,.btn.btn-outline.btn-warning:hover:before{opacity:.15}.btn.btn-secondary.btn-warning.active:before,.btn.btn-secondary.btn-warning:active:before,.btn.btn-transparent.btn-warning.active:before,.btn.btn-transparent.btn-warning:active:before,.btn.btn-outline.btn-warning.active:before,.btn.btn-outline.btn-warning:active:before{opacity:.25}.btn.btn-secondary.btn-warning:before,.btn.btn-transparent.btn-warning:before,.btn.btn-outline.btn-warning:before{background:var(--warningColor)}.btn.btn-secondary.btn-hint,.btn.btn-transparent.btn-hint,.btn.btn-outline.btn-hint{color:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint:before,.btn.btn-transparent.btn-hint:before,.btn.btn-outline.btn-hint:before{opacity:0}.btn.btn-secondary.btn-hint:focus-visible:before,.btn.btn-secondary.btn-hint:hover:before,.btn.btn-transparent.btn-hint:focus-visible:before,.btn.btn-transparent.btn-hint:hover:before,.btn.btn-outline.btn-hint:focus-visible:before,.btn.btn-outline.btn-hint:hover:before{opacity:.15}.btn.btn-secondary.btn-hint.active:before,.btn.btn-secondary.btn-hint:active:before,.btn.btn-transparent.btn-hint.active:before,.btn.btn-transparent.btn-hint:active:before,.btn.btn-outline.btn-hint.active:before,.btn.btn-outline.btn-hint:active:before{opacity:.25}.btn.btn-secondary.btn-hint:before,.btn.btn-transparent.btn-hint:before,.btn.btn-outline.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint,.btn.btn-transparent.btn-hint,.btn.btn-outline.btn-hint{color:var(--txtHintColor)}.btn.btn-secondary.btn-hint:focus-visible,.btn.btn-secondary.btn-hint:hover,.btn.btn-secondary.btn-hint:active,.btn.btn-secondary.btn-hint.active,.btn.btn-transparent.btn-hint:focus-visible,.btn.btn-transparent.btn-hint:hover,.btn.btn-transparent.btn-hint:active,.btn.btn-transparent.btn-hint.active,.btn.btn-outline.btn-hint:focus-visible,.btn.btn-outline.btn-hint:hover,.btn.btn-outline.btn-hint:active,.btn.btn-outline.btn-hint.active{color:var(--txtPrimaryColor)}.btn.btn-secondary:before{opacity:.35}.btn.btn-secondary:focus-visible:before,.btn.btn-secondary:hover:before{opacity:.5}.btn.btn-secondary.active:before,.btn.btn-secondary:active:before{opacity:.7}.btn.btn-secondary.btn-info:before{opacity:.15}.btn.btn-secondary.btn-info:focus-visible:before,.btn.btn-secondary.btn-info:hover:before{opacity:.25}.btn.btn-secondary.btn-info.active:before,.btn.btn-secondary.btn-info:active:before{opacity:.3}.btn.btn-secondary.btn-success:before{opacity:.15}.btn.btn-secondary.btn-success:focus-visible:before,.btn.btn-secondary.btn-success:hover:before{opacity:.25}.btn.btn-secondary.btn-success.active:before,.btn.btn-secondary.btn-success:active:before{opacity:.3}.btn.btn-secondary.btn-danger:before{opacity:.15}.btn.btn-secondary.btn-danger:focus-visible:before,.btn.btn-secondary.btn-danger:hover:before{opacity:.25}.btn.btn-secondary.btn-danger.active:before,.btn.btn-secondary.btn-danger:active:before{opacity:.3}.btn.btn-secondary.btn-warning:before{opacity:.15}.btn.btn-secondary.btn-warning:focus-visible:before,.btn.btn-secondary.btn-warning:hover:before{opacity:.25}.btn.btn-secondary.btn-warning.active:before,.btn.btn-secondary.btn-warning:active:before{opacity:.3}.btn.btn-secondary.btn-hint:before{opacity:.15}.btn.btn-secondary.btn-hint:focus-visible:before,.btn.btn-secondary.btn-hint:hover:before{opacity:.25}.btn.btn-secondary.btn-hint.active:before,.btn.btn-secondary.btn-hint:active:before{opacity:.3}.btn.btn-disabled,.btn[disabled]{box-shadow:none;cursor:default;background:var(--baseAlt1Color);color:var(--txtDisabledColor)!important}.btn.btn-disabled:before,.btn[disabled]:before{display:none}.btn.btn-disabled.btn-transparent,.btn[disabled].btn-transparent{background:none}.btn.btn-disabled.btn-outline,.btn[disabled].btn-outline{border-color:var(--baseAlt2Color)}.btn.txt-left{text-align:left;justify-content:flex-start}.btn.txt-right{text-align:right;justify-content:flex-end}.btn.btn-expanded{min-width:150px}.btn.btn-expanded-sm{min-width:90px}.btn.btn-expanded-lg{min-width:170px}.btn.btn-lg{column-gap:10px;font-size:var(--lgFontSize);min-height:var(--lgBtnHeight);min-width:var(--lgBtnHeight);padding-left:30px;padding-right:30px}.btn.btn-lg i{font-size:1.2666em}.btn.btn-lg.btn-expanded{min-width:240px}.btn.btn-lg.btn-expanded-sm{min-width:160px}.btn.btn-lg.btn-expanded-lg{min-width:300px}.btn.btn-sm,.btn.btn-xs{column-gap:5px;font-size:var(--smFontSize);min-height:var(--smBtnHeight);min-width:var(--smBtnHeight);padding-left:12px;padding-right:12px}.btn.btn-sm i,.btn.btn-xs i{font-size:1rem}.btn.btn-sm.btn-expanded,.btn.btn-xs.btn-expanded{min-width:100px}.btn.btn-sm.btn-expanded-sm,.btn.btn-xs.btn-expanded-sm{min-width:80px}.btn.btn-sm.btn-expanded-lg,.btn.btn-xs.btn-expanded-lg{min-width:130px}.btn.btn-xs{padding-left:7px;padding-right:7px;min-width:var(--xsBtnHeight);min-height:var(--xsBtnHeight)}.btn.btn-block{display:flex;width:100%}.btn.btn-pill{border-radius:30px}.btn.btn-circle{border-radius:50%;padding:0;gap:0}.btn.btn-circle i{font-size:1.2857rem;text-align:center;width:19px;height:19px;line-height:19px}.btn.btn-circle i:before{margin:0;display:block}.btn.btn-circle.btn-sm i{font-size:1.1rem}.btn.btn-circle.btn-xs i{font-size:1.05rem}.btn.btn-loading{--loaderSize: 24px;cursor:default;pointer-events:none}.btn.btn-loading:after{content:"";position:absolute;display:inline-block;vertical-align:top;left:50%;top:50%;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);color:inherit;text-align:center;font-weight:400;margin-left:calc(var(--loaderSize) * -.5);margin-top:calc(var(--loaderSize) * -.5);font-family:var(--iconFontFamily);animation:loaderShow var(--baseAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.btn.btn-loading>*{opacity:0;transform:scale(.9)}.btn.btn-loading.btn-sm,.btn.btn-loading.btn-xs{--loaderSize: 20px}.btn.btn-loading.btn-lg{--loaderSize: 28px}.btn.btn-prev i,.btn.btn-next i{transition:transform var(--baseAnimationSpeed)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i,.btn.btn-next:hover i,.btn.btn-next:focus-within i{transform:translate(3px)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i{transform:translate(-3px)}.btn.btn-horizontal-sticky{position:sticky;left:var(--xsSpacing);right:var(--xsSpacing)}.btns-group{display:inline-flex;align-items:center;gap:var(--xsSpacing)}.btns-group.no-gap{gap:0}.btns-group.no-gap>*{border-radius:0;min-width:0;box-shadow:-1px 0 #ffffff1a}.btns-group.no-gap>*:first-child{border-top-left-radius:var(--btnRadius);border-bottom-left-radius:var(--btnRadius);box-shadow:none}.btns-group.no-gap>*:last-child{border-top-right-radius:var(--btnRadius);border-bottom-right-radius:var(--btnRadius)}.tinymce-wrapper,.code-editor,.select .selected-container,input,select,textarea{display:block;width:100%;outline:0;border:0;margin:0;background:none;padding:5px 10px;line-height:20px;min-width:0;min-height:var(--inputHeight);background:var(--baseAlt1Color);color:var(--txtPrimaryColor);font-size:var(--baseFontSize);font-family:var(--baseFontFamily);font-weight:400;border-radius:var(--baseRadius);overflow:auto;overflow:overlay}.tinymce-wrapper::placeholder,.code-editor::placeholder,.select .selected-container::placeholder,input::placeholder,select::placeholder,textarea::placeholder{color:var(--txtDisabledColor)}@media screen and (min-width: 550px){.tinymce-wrapper:focus,.code-editor:focus,.select .selected-container:focus,input:focus,select:focus,textarea:focus,.tinymce-wrapper:focus-within,.code-editor:focus-within,.select .selected-container:focus-within,input:focus-within,select:focus-within,textarea:focus-within{scrollbar-color:var(--baseAlt3Color) transparent;scrollbar-width:thin;scroll-behavior:smooth}.tinymce-wrapper:focus::-webkit-scrollbar,.code-editor:focus::-webkit-scrollbar,.select .selected-container:focus::-webkit-scrollbar,input:focus::-webkit-scrollbar,select:focus::-webkit-scrollbar,textarea:focus::-webkit-scrollbar,.tinymce-wrapper:focus-within::-webkit-scrollbar,.code-editor:focus-within::-webkit-scrollbar,.select .selected-container:focus-within::-webkit-scrollbar,input:focus-within::-webkit-scrollbar,select:focus-within::-webkit-scrollbar,textarea:focus-within::-webkit-scrollbar{width:8px;height:8px;border-radius:var(--baseRadius)}.tinymce-wrapper:focus::-webkit-scrollbar-track,.code-editor:focus::-webkit-scrollbar-track,.select .selected-container:focus::-webkit-scrollbar-track,input:focus::-webkit-scrollbar-track,select:focus::-webkit-scrollbar-track,textarea:focus::-webkit-scrollbar-track,.tinymce-wrapper:focus-within::-webkit-scrollbar-track,.code-editor:focus-within::-webkit-scrollbar-track,.select .selected-container:focus-within::-webkit-scrollbar-track,input:focus-within::-webkit-scrollbar-track,select:focus-within::-webkit-scrollbar-track,textarea:focus-within::-webkit-scrollbar-track{background:transparent;border-radius:var(--baseRadius)}.tinymce-wrapper:focus::-webkit-scrollbar-thumb,.code-editor:focus::-webkit-scrollbar-thumb,.select .selected-container:focus::-webkit-scrollbar-thumb,input:focus::-webkit-scrollbar-thumb,select:focus::-webkit-scrollbar-thumb,textarea:focus::-webkit-scrollbar-thumb,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb,.code-editor:focus-within::-webkit-scrollbar-thumb,.select .selected-container:focus-within::-webkit-scrollbar-thumb,input:focus-within::-webkit-scrollbar-thumb,select:focus-within::-webkit-scrollbar-thumb,textarea:focus-within::-webkit-scrollbar-thumb{background-color:var(--baseAlt3Color);border-radius:15px;border:2px solid transparent;background-clip:padding-box}.tinymce-wrapper:focus::-webkit-scrollbar-thumb:hover,.code-editor:focus::-webkit-scrollbar-thumb:hover,.select .selected-container:focus::-webkit-scrollbar-thumb:hover,input:focus::-webkit-scrollbar-thumb:hover,select:focus::-webkit-scrollbar-thumb:hover,textarea:focus::-webkit-scrollbar-thumb:hover,.tinymce-wrapper:focus::-webkit-scrollbar-thumb:active,.code-editor:focus::-webkit-scrollbar-thumb:active,.select .selected-container:focus::-webkit-scrollbar-thumb:active,input:focus::-webkit-scrollbar-thumb:active,select:focus::-webkit-scrollbar-thumb:active,textarea:focus::-webkit-scrollbar-thumb:active,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb:hover,.code-editor:focus-within::-webkit-scrollbar-thumb:hover,.select .selected-container:focus-within::-webkit-scrollbar-thumb:hover,input:focus-within::-webkit-scrollbar-thumb:hover,select:focus-within::-webkit-scrollbar-thumb:hover,textarea:focus-within::-webkit-scrollbar-thumb:hover,.tinymce-wrapper:focus-within::-webkit-scrollbar-thumb:active,.code-editor:focus-within::-webkit-scrollbar-thumb:active,.select .selected-container:focus-within::-webkit-scrollbar-thumb:active,input:focus-within::-webkit-scrollbar-thumb:active,select:focus-within::-webkit-scrollbar-thumb:active,textarea:focus-within::-webkit-scrollbar-thumb:active{background-color:var(--baseAlt4Color)}}[readonly].tinymce-wrapper,[readonly].code-editor,.select [readonly].selected-container,input[readonly],select[readonly],textarea[readonly],.readonly.tinymce-wrapper,.readonly.code-editor,.select .readonly.selected-container,input.readonly,select.readonly,textarea.readonly{cursor:default;color:var(--txtHintColor)}[disabled].tinymce-wrapper,[disabled].code-editor,.select [disabled].selected-container,input[disabled],select[disabled],textarea[disabled],.disabled.tinymce-wrapper,.disabled.code-editor,.select .disabled.selected-container,input.disabled,select.disabled,textarea.disabled{cursor:default;color:var(--txtDisabledColor)}.txt-mono.tinymce-wrapper,.txt-mono.code-editor,.select .txt-mono.selected-container,input.txt-mono,select.txt-mono,textarea.txt-mono{line-height:var(--smLineHeight)}.code.tinymce-wrapper,.code.code-editor,.select .code.selected-container,input.code,select.code,textarea.code{font-size:15px;line-height:1.379rem;font-family:var(--monospaceFontFamily)}input{height:var(--inputHeight)}input:-webkit-autofill{-webkit-text-fill-color:var(--txtPrimaryColor);-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt1Color)}.form-field:focus-within input:-webkit-autofill,input:-webkit-autofill:focus{-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt2Color)}input[type=file]{padding:9px}input[type=checkbox],input[type=radio]{width:auto;height:auto;display:inline}input[type=number]{-moz-appearance:textfield;-webkit-appearance:textfield;appearance:textfield}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none}textarea{min-height:80px;resize:vertical}select{padding-left:8px}.form-field{--hPadding: 15px;position:relative;display:block;width:100%;margin-bottom:var(--baseSpacing)}.form-field .tinymce-wrapper,.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea{z-index:0;padding-left:var(--hPadding);padding-right:var(--hPadding)}.form-field select{padding-left:8px}.form-field label{display:flex;width:100%;column-gap:5px;align-items:center;-webkit-user-select:none;user-select:none;font-weight:600;font-size:var(--smFontSize);letter-spacing:.1px;color:var(--txtHintColor);line-height:1;padding-top:12px;padding-bottom:3px;padding-left:var(--hPadding);padding-right:var(--hPadding);border:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.form-field label~.tinymce-wrapper,.form-field label~.code-editor,.form-field .select label~.selected-container,.select .form-field label~.selected-container,.form-field label~input,.form-field label~select,.form-field label~textarea{border-top:0;padding-top:2px;padding-bottom:8px;border-top-left-radius:0;border-top-right-radius:0}.form-field label i{font-size:.96rem;margin-bottom:-1px}.form-field label i:before{margin:0}.form-field .tinymce-wrapper,.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea,.form-field label{background:var(--baseAlt1Color);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.form-field:focus-within .tinymce-wrapper,.form-field:focus-within .code-editor,.form-field:focus-within .select .selected-container,.select .form-field:focus-within .selected-container,.form-field:focus-within input,.form-field:focus-within select,.form-field:focus-within textarea,.form-field:focus-within label{background:var(--baseAlt2Color)}.form-field:focus-within label{color:var(--txtPrimaryColor)}.form-field .form-field-addon{position:absolute;display:inline-flex;align-items:center;z-index:1;top:0;right:var(--hPadding);min-height:var(--inputHeight);color:var(--txtHintColor)}.form-field .form-field-addon .btn{margin-right:-5px}.form-field .form-field-addon:not(.prefix)~.tinymce-wrapper,.form-field .form-field-addon:not(.prefix)~.code-editor,.form-field .select .form-field-addon:not(.prefix)~.selected-container,.select .form-field .form-field-addon:not(.prefix)~.selected-container,.form-field .form-field-addon:not(.prefix)~input,.form-field .form-field-addon:not(.prefix)~select,.form-field .form-field-addon:not(.prefix)~textarea{padding-right:45px}.form-field .form-field-addon.prefix{right:auto;left:var(--hPadding)}.form-field .form-field-addon.prefix~.tinymce-wrapper,.form-field .form-field-addon.prefix~.code-editor,.form-field .select .form-field-addon.prefix~.selected-container,.select .form-field .form-field-addon.prefix~.selected-container,.form-field .form-field-addon.prefix~input,.form-field .form-field-addon.prefix~select,.form-field .form-field-addon.prefix~textarea{padding-left:45px}.form-field label~.form-field-addon{min-height:calc(26px + var(--inputHeight))}.form-field .help-block{position:relative;margin-top:8px;font-size:var(--smFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor);word-break:break-word}.form-field .help-block pre{white-space:pre-wrap}.form-field .help-block-error{color:var(--dangerColor)}.form-field.error>label,.form-field.invalid>label{color:var(--dangerColor)}.form-field.invalid label,.form-field.invalid .tinymce-wrapper,.form-field.invalid .code-editor,.form-field.invalid .select .selected-container,.select .form-field.invalid .selected-container,.form-field.invalid input,.form-field.invalid select,.form-field.invalid textarea{background:var(--dangerAltColor)}.form-field.required:not(.form-field-toggle)>label:after{content:"*";color:var(--dangerColor);margin-top:-2px;margin-left:-2px}.form-field.readonly label,.form-field.readonly .tinymce-wrapper,.form-field.readonly .code-editor,.form-field.readonly .select .selected-container,.select .form-field.readonly .selected-container,.form-field.readonly input,.form-field.readonly select,.form-field.readonly textarea,.form-field.disabled label,.form-field.disabled .tinymce-wrapper,.form-field.disabled .code-editor,.form-field.disabled .select .selected-container,.select .form-field.disabled .selected-container,.form-field.disabled input,.form-field.disabled select,.form-field.disabled textarea{background:var(--baseAlt1Color)}.form-field.readonly>label,.form-field.disabled>label{color:var(--txtHintColor)}.form-field.readonly.required>label:after,.form-field.disabled.required>label:after{opacity:.5}.form-field.disabled label,.form-field.disabled .tinymce-wrapper,.form-field.disabled .code-editor,.form-field.disabled .select .selected-container,.select .form-field.disabled .selected-container,.form-field.disabled input,.form-field.disabled select,.form-field.disabled textarea{box-shadow:inset 0 0 0 var(--btnHeight) #ffffff73}.form-field.disabled>label{color:var(--txtDisabledColor)}.form-field input[type=radio],.form-field input[type=checkbox]{position:absolute;z-index:-1;left:0;width:0;height:0;min-height:0;min-width:0;border:0;background:none;-webkit-user-select:none;user-select:none;pointer-events:none;box-shadow:none;opacity:0}.form-field input[type=radio]~label,.form-field input[type=checkbox]~label{border:0;margin:0;outline:0;background:none;display:inline-flex;vertical-align:top;align-items:center;width:auto;column-gap:5px;-webkit-user-select:none;user-select:none;padding:0 0 0 27px;line-height:20px;min-height:20px;font-weight:400;font-size:var(--baseFontSize);text-transform:none;color:var(--txtPrimaryColor)}.form-field input[type=radio]~label:before,.form-field input[type=checkbox]~label:before{content:"";display:inline-block;vertical-align:top;position:absolute;z-index:0;left:0;top:0;width:20px;height:20px;line-height:16px;font-family:var(--iconFontFamily);font-size:1.2rem;text-align:center;color:var(--baseColor);cursor:pointer;background:var(--baseColor);border-radius:var(--baseRadius);border:2px solid var(--baseAlt3Color);transition:transform var(--baseAnimationSpeed),border-color var(--baseAnimationSpeed),color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.form-field input[type=radio]~label:active:before,.form-field input[type=checkbox]~label:active:before{transform:scale(.9)}.form-field input[type=radio]:focus~label:before,.form-field input[type=radio]~label:hover:before,.form-field input[type=checkbox]:focus~label:before,.form-field input[type=checkbox]~label:hover:before{border-color:var(--baseAlt4Color)}.form-field input[type=radio]:checked~label:before,.form-field input[type=checkbox]:checked~label:before{content:"";box-shadow:none;mix-blend-mode:unset;background:var(--successColor);border-color:var(--successColor)}.form-field input[type=radio]:disabled~label,.form-field input[type=checkbox]:disabled~label{pointer-events:none;cursor:not-allowed;color:var(--txtDisabledColor)}.form-field input[type=radio]:disabled~label:before,.form-field input[type=checkbox]:disabled~label:before{opacity:.5}.form-field input[type=radio]~label:before{border-radius:50%;font-size:1rem}.form-field .form-field-block{position:relative;margin:0 0 var(--xsSpacing)}.form-field .form-field-block:last-child{margin-bottom:0}.form-field.form-field-toggle input[type=radio]~label,.form-field.form-field-toggle input[type=checkbox]~label{position:relative}.form-field.form-field-toggle input[type=radio]~label:before,.form-field.form-field-toggle input[type=checkbox]~label:before{content:"";border:0;box-shadow:none;background:var(--baseAlt3Color);transition:background var(--activeAnimationSpeed)}.form-field.form-field-toggle input[type=radio]~label:after,.form-field.form-field-toggle input[type=checkbox]~label:after{content:"";position:absolute;z-index:1;cursor:pointer;background:var(--baseColor);transition:left var(--activeAnimationSpeed),transform var(--activeAnimationSpeed),background var(--activeAnimationSpeed);box-shadow:0 2px 5px 0 var(--shadowColor)}.form-field.form-field-toggle input[type=radio]~label:active:before,.form-field.form-field-toggle input[type=checkbox]~label:active:before{transform:none}.form-field.form-field-toggle input[type=radio]~label:active:after,.form-field.form-field-toggle input[type=checkbox]~label:active:after{transform:scale(.9)}.form-field.form-field-toggle input[type=radio]:focus-visible~label:before,.form-field.form-field-toggle input[type=checkbox]:focus-visible~label:before{box-shadow:0 0 0 2px var(--baseAlt2Color)}.form-field.form-field-toggle input[type=radio]~label:hover:before,.form-field.form-field-toggle input[type=checkbox]~label:hover:before{background:var(--baseAlt4Color)}.form-field.form-field-toggle input[type=radio]:checked~label:before,.form-field.form-field-toggle input[type=checkbox]:checked~label:before{background:var(--successColor)}.form-field.form-field-toggle input[type=radio]:checked~label:after,.form-field.form-field-toggle input[type=checkbox]:checked~label:after{background:var(--baseColor)}.form-field.form-field-toggle input[type=radio]~label,.form-field.form-field-toggle input[type=checkbox]~label{min-height:24px;padding-left:47px}.form-field.form-field-toggle input[type=radio]~label:empty,.form-field.form-field-toggle input[type=checkbox]~label:empty{padding-left:40px}.form-field.form-field-toggle input[type=radio]~label:before,.form-field.form-field-toggle input[type=checkbox]~label:before{width:40px;height:24px;border-radius:24px}.form-field.form-field-toggle input[type=radio]~label:after,.form-field.form-field-toggle input[type=checkbox]~label:after{top:4px;left:4px;width:16px;height:16px;border-radius:16px}.form-field.form-field-toggle input[type=radio]:checked~label:after,.form-field.form-field-toggle input[type=checkbox]:checked~label:after{left:20px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label{min-height:20px;padding-left:39px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:empty,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:empty{padding-left:32px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:before,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:before{width:32px;height:20px;border-radius:20px}.form-field.form-field-toggle.form-field-sm input[type=radio]~label:after,.form-field.form-field-toggle.form-field-sm input[type=checkbox]~label:after{top:4px;left:4px;width:12px;height:12px;border-radius:12px}.form-field.form-field-toggle.form-field-sm input[type=radio]:checked~label:after,.form-field.form-field-toggle.form-field-sm input[type=checkbox]:checked~label:after{left:16px}.form-field-group{display:flex;width:100%;align-items:center}.form-field-group>.form-field{flex-grow:1;border-left:1px solid var(--baseAlt2Color)}.form-field-group>.form-field:first-child{border-left:0}.form-field-group>.form-field:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.form-field-group>.form-field:not(:first-child)>label{border-top-left-radius:0}.form-field-group>.form-field:not(:first-child)>.tinymce-wrapper,.form-field-group>.form-field:not(:first-child)>.code-editor,.select .form-field-group>.form-field:not(:first-child)>.selected-container,.form-field-group>.form-field:not(:first-child)>input,.form-field-group>.form-field:not(:first-child)>select,.form-field-group>.form-field:not(:first-child)>textarea,.form-field-group>.form-field:not(:first-child)>.select .selected-container{border-bottom-left-radius:0}.form-field-group>.form-field:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.form-field-group>.form-field:not(:last-child)>label{border-top-right-radius:0}.form-field-group>.form-field:not(:last-child)>.tinymce-wrapper,.form-field-group>.form-field:not(:last-child)>.code-editor,.select .form-field-group>.form-field:not(:last-child)>.selected-container,.form-field-group>.form-field:not(:last-child)>input,.form-field-group>.form-field:not(:last-child)>select,.form-field-group>.form-field:not(:last-child)>textarea,.form-field-group>.form-field:not(:last-child)>.select .selected-container{border-bottom-right-radius:0}.form-field-group .form-field.col-12{width:100%}.form-field-group .form-field.col-11{width:91.6666666667%}.form-field-group .form-field.col-10{width:83.3333333333%}.form-field-group .form-field.col-9{width:75%}.form-field-group .form-field.col-8{width:66.6666666667%}.form-field-group .form-field.col-7{width:58.3333333333%}.form-field-group .form-field.col-6{width:50%}.form-field-group .form-field.col-5{width:41.6666666667%}.form-field-group .form-field.col-4{width:33.3333333333%}.form-field-group .form-field.col-3{width:25%}.form-field-group .form-field.col-2{width:16.6666666667%}.form-field-group .form-field.col-1{width:8.3333333333%}.select{position:relative;display:block;outline:0}.select .option{-webkit-user-select:none;user-select:none;column-gap:5px}.select .option .icon{min-width:20px;text-align:center;line-height:inherit}.select .option .icon i{vertical-align:middle;line-height:inherit}.select .txt-placeholder{color:var(--txtHintColor)}label~.select .selected-container{border-top:0}.select .selected-container{position:relative;display:flex;flex-wrap:wrap;width:100%;align-items:center;padding-top:0;padding-bottom:0;padding-right:35px!important;-webkit-user-select:none;user-select:none}.select .selected-container:after{content:"";position:absolute;right:5px;top:50%;width:20px;height:20px;line-height:20px;text-align:center;margin-top:-10px;display:inline-block;vertical-align:top;font-size:1rem;font-family:var(--iconFontFamily);align-self:flex-end;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed)}.select .selected-container:active,.select .selected-container.active{border-bottom-left-radius:0;border-bottom-right-radius:0}.select .selected-container:active:after,.select .selected-container.active:after{color:var(--txtPrimaryColor);transform:rotate(180deg)}.select .selected-container .option{display:flex;width:100%;align-items:center;max-width:100%;-webkit-user-select:text;user-select:text}.select .selected-container .clear{margin-left:auto;cursor:pointer;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed)}.select .selected-container .clear i{display:inline-block;vertical-align:middle;line-height:1}.select .selected-container .clear:hover{color:var(--txtPrimaryColor)}.select.multiple .selected-container{display:flex;align-items:center;padding-left:2px;row-gap:3px;column-gap:4px}.select.multiple .selected-container .txt-placeholder{margin-left:5px}.select.multiple .selected-container .option{display:inline-flex;width:auto;padding:3px 5px;line-height:1;border-radius:var(--baseRadius);background:var(--baseColor)}.select:not(.multiple) .selected-container .label{margin-left:-2px}.select:not(.multiple) .selected-container .option .txt{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;max-width:100%;line-height:normal}.select:not(.disabled) .selected-container:hover{cursor:pointer}.select.readonly,.select.disabled{color:var(--txtHintColor);pointer-events:none}.select.readonly .txt-placeholder,.select.disabled .txt-placeholder,.select.readonly .selected-container,.select.disabled .selected-container{color:inherit}.select.readonly .selected-container .link-hint,.select.disabled .selected-container .link-hint{pointer-events:auto}.select.readonly .selected-container *:not(.link-hint),.select.disabled .selected-container *:not(.link-hint){color:inherit!important}.select.readonly .selected-container:after,.select.readonly .selected-container .clear,.select.disabled .selected-container:after,.select.disabled .selected-container .clear{display:none}.select.readonly .selected-container:hover,.select.disabled .selected-container:hover{cursor:inherit}.select.disabled{color:var(--txtDisabledColor)}.select .txt-missing{color:var(--txtHintColor);padding:5px 12px;margin:0}.select .options-dropdown{max-height:none;border:0;overflow:auto;border-top-left-radius:0;border-top-right-radius:0;margin-top:-2px;box-shadow:0 2px 5px 0 var(--shadowColor),inset 0 0 0 2px var(--baseAlt2Color)}.select .options-dropdown .input-group:focus-within{box-shadow:none}.select .options-dropdown .form-field.options-search{margin:0 0 5px;padding:0 0 2px;color:var(--txtHintColor);border-bottom:1px solid var(--baseAlt2Color)}.select .options-dropdown .form-field.options-search .input-group{border-radius:0;padding:0 0 0 10px;margin:0;background:none;column-gap:0;border:0}.select .options-dropdown .form-field.options-search input{border:0;padding-left:9px;padding-right:9px;background:none}.select .options-dropdown .options-list{overflow:auto;max-height:240px;width:auto;margin-left:0;margin-right:-5px;padding-right:5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty){margin:5px -5px -5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty) .btn-block{border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}label~.select .selected-container{padding-bottom:4px;border-top-left-radius:0;border-top-right-radius:0}label~.select.multiple .selected-container{padding-top:3px;padding-bottom:3px;padding-left:10px}.select.block-options.multiple .selected-container .option{width:100%;box-shadow:0 2px 5px 0 var(--shadowColor)}.select.upside .selected-container.active{border-radius:0 0 var(--baseRadius) var(--baseRadius)}.select.upside .options-dropdown{border-radius:var(--baseRadius) var(--baseRadius) 0 0;margin:0}.field-type-select .options-dropdown{padding:2px 1px 1px 2px}.field-type-select .options-dropdown .form-field.options-search{margin:0}.field-type-select .options-dropdown .options-list{max-height:490px;display:flex;flex-direction:row;flex-wrap:wrap;width:100%;padding:0}.field-type-select .options-dropdown .dropdown-item{width:50%;margin:0;padding-left:12px;border-radius:0;border-bottom:1px solid var(--baseAlt2Color);border-right:1px solid var(--baseAlt2Color)}.field-type-select .options-dropdown .dropdown-item.selected{background:var(--baseAlt1Color)}.form-field-list{border-radius:var(--baseRadius);transition:box-shadow var(--baseAnimationSpeed)}.form-field-list label{padding-bottom:10px}.form-field-list .list{background:var(--baseAlt1Color);border:0;border-radius:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius);transition:background var(--baseAnimationSpeed)}.form-field-list .list .list-item{border-top:1px solid var(--baseAlt2Color)}.form-field-list .list .list-item:hover,.form-field-list .list .list-item:focus,.form-field-list .list .list-item:focus-within,.form-field-list .list .list-item:focus-visible,.form-field-list .list .list-item:active{background:none}.form-field-list .list .list-item.selected{background:var(--baseAlt2Color)}.form-field-list .list .list-item.handle:not(.disabled):hover,.form-field-list .list .list-item.handle:not(.disabled):focus-visible{background:var(--baseAlt2Color)}.form-field-list .list .list-item.handle:not(.disabled):active{background:var(--baseAlt3Color)}.form-field-list .list .list-item.dragging{z-index:9;box-shadow:inset 0 0 0 1px var(--baseAlt3Color)}.form-field-list .list .list-item.dragover{background:var(--baseAlt2Color)}.form-field-list:focus-within .list,.form-field-list:focus-within label{background:var(--baseAlt1Color)}.form-field-list.dragover:not(:has(.dragging)){box-shadow:0 0 0 2px var(--warningColor)}.code-editor{display:flex;flex-direction:column;width:100%}.form-field label~.code-editor{padding-bottom:6px;padding-top:4px}.code-editor .cm-editor{flex-grow:1;border:0!important;outline:none!important}.code-editor .cm-editor .cm-line{padding-left:0;padding-right:0}.code-editor .cm-editor .cm-tooltip-autocomplete{box-shadow:0 2px 5px 0 var(--shadowColor);border-radius:var(--baseRadius);background:var(--baseColor);border:0;z-index:9999;padding:0 3px;font-size:.92rem}.code-editor .cm-editor .cm-tooltip-autocomplete ul{margin:0;border-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul li[aria-selected]{background:var(--infoColor)}.code-editor .cm-editor .cm-scroller{flex-grow:1;outline:0!important;font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.code-editor .cm-editor .cm-cursorLayer .cm-cursor{margin-left:0!important}.code-editor .cm-editor .cm-placeholder{color:var(--txtDisabledColor);font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.code-editor .cm-editor .cm-selectionMatch{background:var(--infoAltColor)}.code-editor .cm-editor.cm-focused .cm-matchingBracket{background-color:#328c821a}.code-editor .ͼf{color:var(--dangerColor)}.tinymce-wrapper{min-height:277px}.tinymce-wrapper .tox-tinymce{border-radius:var(--baseRadius);border:0}.form-field label~.tinymce-wrapper{position:relative;z-index:auto;padding:5px 2px 2px}.form-field label~.tinymce-wrapper:before{content:"";position:absolute;z-index:-1;top:5px;left:2px;right:2px;bottom:2px;background:#fff;border-radius:var(--baseRadius)}body .tox .tox-dialog{border:0;border-radius:var(--baseRadius)}body .tox .tox-dialog-wrap__backdrop{background:var(--overlayColor)}body .tox .tox-tbtn{height:30px}body .tox .tox-tbtn svg{transform:scale(.85)}body .tox .tox-collection__item-checkmark,body .tox .tox-collection__item-icon{width:22px;height:22px;transform:scale(.85)}body .tox .tox-tbtn:not(.tox-tbtn--select){width:30px}body .tox .tox-button,body .tox .tox-button--secondary{font-size:var(--smFontSize)}body .tox .tox-toolbar-overlord{box-shadow:0 2px 5px 0 var(--shadowColor)}body .tox .tox-listboxfield .tox-listbox--select,body .tox .tox-textarea,body .tox .tox-textfield,body .tox .tox-toolbar-textfield{padding:3px 5px}body .tox-swatch:not(.tox-swatch--remove):not(.tox-collection__item--enabled) svg{display:none}body .tox .tox-textarea-wrap{display:flex;flex:1}body.tox-fullscreen .overlay-panel-section{overflow:hidden}.main-menu{--menuItemSize: 45px;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;row-gap:var(--smSpacing);font-size:var(--xlFontSize);color:var(--txtPrimaryColor)}.main-menu i{font-size:24px;line-height:1}.main-menu .menu-item{position:relative;outline:0;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;text-align:center;justify-content:center;-webkit-user-select:none;user-select:none;color:inherit;min-width:var(--menuItemSize);min-height:var(--menuItemSize);border:2px solid transparent;border-radius:var(--lgRadius);transition:background var(--baseAnimationSpeed),border var(--baseAnimationSpeed)}.main-menu .menu-item:focus-visible,.main-menu .menu-item:hover{background:var(--baseAlt1Color)}.main-menu .menu-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.main-menu .menu-item.active,.main-menu .menu-item.current-route{background:var(--baseColor);border-color:var(--primaryColor)}.app-sidebar{position:relative;z-index:1;display:flex;flex-grow:0;flex-shrink:0;flex-direction:column;align-items:center;width:var(--appSidebarWidth);padding:var(--smSpacing) 0px var(--smSpacing);background:var(--baseColor);border-right:1px solid var(--baseAlt2Color)}.app-sidebar .main-menu{flex-grow:1;justify-content:flex-start;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;margin-top:34px;margin-bottom:var(--baseSpacing)}.app-layout{display:flex;width:100%;height:100vh}.app-layout .app-body{flex-grow:1;min-width:0;height:100%;display:flex;align-items:stretch}.app-layout .app-sidebar~.app-body{min-width:650px}.page-sidebar{--sidebarListItemMargin: 10px;position:relative;z-index:0;display:flex;flex-direction:column;width:var(--pageSidebarWidth);min-width:var(--pageSidebarWidth);max-width:400px;flex-shrink:0;flex-grow:0;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);padding:calc(var(--baseSpacing) - 5px) 0 var(--smSpacing);border-right:1px solid var(--baseAlt2Color)}.page-sidebar>*{padding:0 var(--xsSpacing)}.page-sidebar .sidebar-content{overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.page-sidebar .sidebar-content>:first-child{margin-top:0}.page-sidebar .sidebar-content>:last-child{margin-bottom:0}.page-sidebar .sidebar-footer{margin-top:var(--smSpacing)}.page-sidebar .search{display:flex;align-items:center;width:auto;column-gap:5px;margin:0 0 var(--xsSpacing);color:var(--txtHintColor);opacity:.7;transition:opacity var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .search input{border:0;background:var(--baseColor);transition:box-shadow var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.page-sidebar .search .btn-clear{margin-right:-8px}.page-sidebar .search:hover,.page-sidebar .search:focus-within,.page-sidebar .search.active{opacity:1;color:var(--txtPrimaryColor)}.page-sidebar .search:hover input,.page-sidebar .search:focus-within input,.page-sidebar .search.active input{background:var(--baseAlt2Color)}.page-sidebar .sidebar-title{display:flex;align-items:center;gap:5px;width:100%;margin:var(--baseSpacing) 5px var(--xsSpacing);font-weight:600;font-size:1rem;line-height:var(--smLineHeight);color:var(--txtHintColor)}.page-sidebar .sidebar-title .label{font-weight:400}.page-sidebar .sidebar-list-item{cursor:pointer;outline:0;text-decoration:none;position:relative;display:flex;width:100%;align-items:center;column-gap:10px;margin:var(--sidebarListItemMargin) 0;padding:3px 10px;font-size:var(--xlFontSize);min-height:var(--btnHeight);min-width:0;color:var(--txtHintColor);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .sidebar-list-item i{font-size:18px}.page-sidebar .sidebar-list-item .txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.page-sidebar .sidebar-list-item:focus-visible,.page-sidebar .sidebar-list-item:hover,.page-sidebar .sidebar-list-item:active,.page-sidebar .sidebar-list-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.page-sidebar .sidebar-list-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.page-sidebar .sidebar-content-compact .sidebar-list-item{--sidebarListItemMargin: 5px}@media screen and (max-height: 600px){.page-sidebar{--sidebarListItemMargin: 5px}}@media screen and (max-width: 1100px){.page-sidebar{min-width:200px}.page-sidebar>*{padding-left:10px;padding-right:10px}}.page-header{display:flex;flex-shrink:0;align-items:center;width:100%;min-height:var(--btnHeight);gap:var(--xsSpacing);margin:0 0 var(--baseSpacing)}.page-header .btns-group{margin-left:auto;justify-content:end}@media screen and (max-width: 1050px){.page-header{flex-wrap:wrap}.page-header .btns-group{width:100%}.page-header .btns-group .btn{flex-grow:1;flex-basis:0}}.page-header-wrapper{background:var(--baseColor);width:auto;margin-top:calc(-1 * (var(--baseSpacing) - 5px));margin-left:calc(-1 * var(--baseSpacing));margin-right:calc(-1 * var(--baseSpacing));margin-bottom:var(--baseSpacing);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);border-bottom:1px solid var(--baseAlt2Color)}.breadcrumbs{display:flex;align-items:center;gap:30px;color:var(--txtDisabledColor)}.breadcrumbs .breadcrumb-item{position:relative;margin:0;line-height:1;font-weight:400}.breadcrumbs .breadcrumb-item:after{content:"/";position:absolute;right:-20px;top:0;width:10px;text-align:center;pointer-events:none;opacity:.4}.breadcrumbs .breadcrumb-item:last-child{word-break:break-word;color:var(--txtPrimaryColor)}.breadcrumbs .breadcrumb-item:last-child:after{content:none;display:none}.breadcrumbs a{text-decoration:none;color:inherit;transition:color var(--baseAnimationSpeed)}.breadcrumbs a:hover{color:var(--txtPrimaryColor)}.page-content{position:relative;z-index:0;display:block;width:100%;flex-grow:1;padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing) var(--smSpacing)}.page-footer{display:flex;gap:5px;align-items:center;justify-content:right;padding:0px var(--baseSpacing) var(--smSpacing);color:var(--txtDisabledColor);font-size:var(--xsFontSize);line-height:var(--smLineHeight)}.page-footer i{font-size:1.2em}.page-footer a{color:inherit;text-decoration:none;transition:color var(--baseAnimationSpeed)}.page-footer a:focus-visible,.page-footer a:hover,.page-footer a:active{color:var(--txtPrimaryColor)}.page-wrapper{display:flex;flex-direction:column;flex-grow:1;width:100%;overflow-x:hidden;overflow-y:auto;scroll-behavior:smooth;scrollbar-gutter:stable}.overlay-active .page-wrapper{overflow-y:hidden}.page-wrapper.full-page{scrollbar-gutter:auto;background:var(--baseColor)}.page-wrapper.center-content .page-content{display:flex;align-items:center}.page-wrapper.flex-content{scrollbar-gutter:auto}.page-wrapper.flex-content .page-content{display:flex;min-height:0;flex-direction:column}@keyframes tabChange{0%{opacity:.7}to{opacity:1}}.tabs-header{display:flex;align-items:stretch;justify-content:flex-start;column-gap:10px;width:100%;min-height:50px;-webkit-user-select:none;user-select:none;margin:0 0 var(--baseSpacing);border-bottom:2px solid var(--baseAlt2Color)}.tabs-header .tab-item{position:relative;outline:0;border:0;background:none;display:inline-flex;align-items:center;justify-content:center;min-width:70px;gap:5px;padding:10px;margin:0;font-size:var(--lgFontSize);line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);color:var(--txtHintColor);text-align:center;text-decoration:none;cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.tabs-header .tab-item:after{content:"";position:absolute;display:block;left:0;bottom:-2px;width:100%;height:2px;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);background:var(--primaryColor);transform:rotateY(90deg);transition:transform .2s}.tabs-header .tab-item .txt,.tabs-header .tab-item i{display:inline-block;vertical-align:top}.tabs-header .tab-item:hover,.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{color:var(--txtPrimaryColor)}.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.tabs-header .tab-item.active{color:var(--txtPrimaryColor)}.tabs-header .tab-item.active:after{transform:rotateY(0)}.tabs-header .tab-item.disabled{pointer-events:none;color:var(--txtDisabledColor)}.tabs-header .tab-item.disabled:after{display:none}.tabs-header.right{justify-content:flex-end}.tabs-header.center{justify-content:center}.tabs-header.stretched .tab-item{flex-grow:1;flex-basis:0}.tabs-header.compact{min-height:30px;margin-bottom:var(--smSpacing)}.tabs-header.combined{border:0;margin-bottom:-2px}.tabs-header.combined .tab-item:after{content:none;display:none}.tabs-header.combined .tab-item.active{background:var(--baseAlt1Color)}.tabs-content{position:relative}.tabs-content>.tab-item{width:100%;display:none}.tabs-content>.tab-item.active{display:block;opacity:0;animation:tabChange .2s forwards}.tabs-content>.tab-item>:first-child{margin-top:0}.tabs-content>.tab-item>:last-child{margin-bottom:0}.tabs-content.no-animations>.tab-item.active{opacity:1;animation:none}.tabs{position:relative}.accordion{outline:0;position:relative;border-radius:var(--baseRadius);background:var(--baseColor);border:1px solid var(--baseAlt2Color);transition:border-radius var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed),margin var(--baseAnimationSpeed)}.accordion .accordion-header{outline:0;position:relative;display:flex;min-height:52px;align-items:center;row-gap:10px;column-gap:var(--smSpacing);padding:12px 20px 10px;width:100%;-webkit-user-select:none;user-select:none;color:var(--txtPrimaryColor);border-radius:inherit;transition:border-radius var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.accordion .accordion-header .icon{width:18px;text-align:center}.accordion .accordion-header .icon i{display:inline-block;vertical-align:top;font-size:1.1rem}.accordion .accordion-header.interactive{padding-right:50px;cursor:pointer}.accordion .accordion-header.interactive:after{content:"";position:absolute;right:15px;top:50%;margin-top:-12.5px;width:25px;height:25px;line-height:25px;color:var(--txtHintColor);font-family:var(--iconFontFamily);font-size:1.3em;text-align:center;transition:color var(--baseAnimationSpeed)}.accordion .accordion-header:hover:after,.accordion .accordion-header.focus:after,.accordion .accordion-header:focus-visible:after{color:var(--txtPrimaryColor)}.accordion .accordion-header:active{transition-duration:var(--activeAnimationSpeed)}.accordion .accordion-content{padding:20px}.accordion:hover,.accordion:focus-visible,.accordion.active{z-index:9}.accordion:hover .accordion-header.interactive,.accordion:focus-visible .accordion-header.interactive,.accordion.active .accordion-header.interactive{background:var(--baseAlt1Color)}.accordion.drag-over .accordion-header{background:var(--bodyColor)}.accordion.active{box-shadow:0 2px 5px 0 var(--shadowColor)}.accordion.active .accordion-header{position:relative;top:0;z-index:9;box-shadow:0 0 0 1px var(--baseAlt2Color);border-bottom-left-radius:0;border-bottom-right-radius:0;background:var(--bodyColor)}.accordion.active .accordion-header.interactive{background:var(--bodyColor)}.accordion.active .accordion-header.interactive:after{color:inherit;content:""}.accordion.disabled{z-index:0;border-color:var(--baseAlt1Color)}.accordion.disabled .accordion-header{color:var(--txtDisabledColor)}.accordions .accordion{border-radius:0;margin:-1px 0 0}.accordions .accordion:has(+.accordion.active){border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.accordions>.accordion.active,.accordions>.accordion-wrapper>.accordion.active{margin:var(--smSpacing) 0;border-radius:var(--baseRadius)}.accordions>.accordion.active+.accordion,.accordions>.accordion-wrapper>.accordion.active+.accordion{border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions>.accordion:first-child,.accordions>.accordion-wrapper:first-child>.accordion{margin-top:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions>.accordion:last-child,.accordions>.accordion-wrapper:last-child>.accordion{margin-bottom:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}table{--entranceAnimationSpeed: .3s;border-collapse:separate;min-width:100%;transition:opacity var(--baseAnimationSpeed)}table .form-field{margin:0;line-height:1;text-align:left}table td,table th{outline:0;vertical-align:middle;position:relative;text-align:left;padding:10px;border-bottom:1px solid var(--baseAlt2Color)}table td:first-child,table th:first-child{padding-left:20px}table td:last-child,table th:last-child{padding-right:20px}table th{color:var(--txtHintColor);font-weight:600;font-size:1rem;-webkit-user-select:none;user-select:none;height:50px;line-height:var(--smLineHeight)}table th i{font-size:inherit}table td{height:56px;word-break:break-word}table .min-width{width:1%!important;white-space:nowrap}table .nowrap{white-space:nowrap}table .col-sort{cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);padding-right:30px;transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}table .col-sort:after{content:"";position:absolute;right:10px;top:50%;margin-top:-12.5px;line-height:25px;height:25px;font-family:var(--iconFontFamily);font-weight:400;color:var(--txtHintColor);opacity:0;transition:color var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed)}table .col-sort.sort-desc:after{content:""}table .col-sort.sort-asc:after{content:""}table .col-sort.sort-active:after{opacity:1}table .col-sort:hover,table .col-sort:focus-visible{background:var(--baseAlt1Color)}table .col-sort:hover:after,table .col-sort:focus-visible:after{opacity:1}table .col-sort:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}table .col-sort.col-sort-disabled{cursor:default;background:none}table .col-sort.col-sort-disabled:after{display:none}table .col-header-content{display:inline-flex;align-items:center;flex-wrap:nowrap;gap:5px}table .col-header-content .txt{max-width:140px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table td.col-field-username,table .col-field-created,table .col-field-updated,table .col-type-action{width:1%!important;white-space:nowrap}table .col-type-action{white-space:nowrap;text-align:right;color:var(--txtHintColor)}table .col-type-action i{display:inline-block;vertical-align:top;transition:transform var(--baseAnimationSpeed)}table td.col-type-json{font-family:monospace;font-size:var(--smFontSize);line-height:var(--smLineHeight);max-width:300px}table .col-type-text{max-width:300px}table .col-type-editor{min-width:300px}table .col-type-select{min-width:150px}table .col-type-email{min-width:120px;white-space:nowrap}table .col-type-file{min-width:100px}table .col-type-number{white-space:nowrap}table td.col-field-id{width:175px;white-space:nowrap}table tr{outline:0;background:var(--bodyColor);transition:background var(--baseAnimationSpeed)}table tr.row-handle{cursor:pointer;-webkit-user-select:none;user-select:none}table tr.row-handle:focus-visible,table tr.row-handle:hover,table tr.row-handle:active{background:var(--baseAlt1Color)}table tr.row-handle:focus-visible .action-col,table tr.row-handle:hover .action-col,table tr.row-handle:active .action-col{color:var(--txtPrimaryColor)}table tr.row-handle:focus-visible .action-col i,table tr.row-handle:hover .action-col i,table tr.row-handle:active .action-col i{transform:translate(3px)}table tr.row-handle:active{transition-duration:var(--activeAnimationSpeed)}table.table-border{border:1px solid var(--baseAlt2Color);border-radius:var(--baseRadius)}table.table-border tr{background:var(--baseColor)}table.table-border td,table.table-border th{height:45px}table.table-border th{background:var(--baseAlt1Color)}table.table-border>:last-child>:last-child th,table.table-border>:last-child>:last-child td{border-bottom:0}table.table-border>tr:first-child>:first-child,table.table-border>:first-child>tr:first-child>:first-child{border-top-left-radius:var(--baseRadius)}table.table-border>tr:first-child>:last-child,table.table-border>:first-child>tr:first-child>:last-child{border-top-right-radius:var(--baseRadius)}table.table-border>tr:last-child>:first-child,table.table-border>:last-child>tr:last-child>:first-child{border-bottom-left-radius:var(--baseRadius)}table.table-border>tr:last-child>:last-child,table.table-border>:last-child>tr:last-child>:last-child{border-bottom-right-radius:var(--baseRadius)}table.table-compact td,table.table-compact th{height:auto}table.table-animate tr{animation:entranceTop var(--entranceAnimationSpeed)}table.table-loading{pointer-events:none;opacity:.7}.table-wrapper{width:auto;padding:0;max-height:100%;max-width:calc(100% + 2 * var(--baseSpacing));margin-left:calc(var(--baseSpacing) * -1);margin-right:calc(var(--baseSpacing) * -1);border-bottom:1px solid var(--baseAlt2Color)}.table-wrapper .bulk-select-col{min-width:70px}.table-wrapper td,.table-wrapper th{position:relative}.table-wrapper td:first-child,.table-wrapper th:first-child{padding-left:calc(var(--baseSpacing) + 3px)}.table-wrapper td:last-child,.table-wrapper th:last-child{padding-right:calc(var(--baseSpacing) + 3px)}.table-wrapper thead{position:sticky;top:0;z-index:100;transition:box-shadow var(--baseAnimationSpeed)}.table-wrapper tbody{position:relative;z-index:0}.table-wrapper tbody tr:last-child td,.table-wrapper tbody tr:last-child th{border-bottom:0}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{position:sticky;z-index:99;transition:box-shadow var(--baseAnimationSpeed)}.table-wrapper .bulk-select-col{left:0}.table-wrapper .col-type-action{right:0}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{background:inherit}.table-wrapper th.bulk-select-col,.table-wrapper th.col-type-action{background:var(--bodyColor)}.table-wrapper.h-scroll .bulk-select-col{box-shadow:3px 0 5px 0 var(--shadowColor)}.table-wrapper.h-scroll .col-type-action{box-shadow:-3px 0 5px 0 var(--shadowColor)}.table-wrapper.h-scroll.h-scroll-start .bulk-select-col,.table-wrapper.h-scroll.h-scroll-end .col-type-action{box-shadow:none}.table-wrapper.v-scroll:not(.v-scroll-start) thead{box-shadow:0 2px 5px 0 var(--shadowColor)}.searchbar{--searchHeight: 44px;outline:0;display:flex;align-items:center;width:100%;min-height:var(--searchHeight);padding:5px 7px;margin:0;white-space:nowrap;color:var(--txtHintColor);background:var(--baseAlt1Color);border-radius:var(--btnHeight);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.searchbar>:first-child{border-top-left-radius:var(--btnHeight);border-bottom-left-radius:var(--btnHeight)}.searchbar>:last-child{border-top-right-radius:var(--btnHeight);border-bottom-right-radius:var(--btnHeight)}.searchbar .btn{border-radius:var(--btnHeight)}.searchbar .code-editor,.searchbar input,.searchbar input:focus{font-size:var(--baseFontSize);font-family:var(--monospaceFontFamily);border:0;background:none;min-height:0;height:100%;max-height:100px;padding-top:0;padding-bottom:0}.searchbar .cm-editor{flex-grow:0;margin-top:auto;margin-bottom:auto}.searchbar label>i{line-height:inherit}.searchbar .search-options{flex-shrink:0;width:90px}.searchbar .search-options .selected-container{border-radius:inherit;background:none;padding-right:25px!important}.searchbar .search-options:not(:focus-within) .selected-container{color:var(--txtHintColor)}.searchbar:focus-within{color:var(--txtPrimaryColor);background:var(--baseAlt2Color)}.bulkbar{position:absolute;bottom:var(--baseSpacing);left:50%;z-index:101;gap:10px;display:flex;justify-content:center;align-items:center;width:var(--smWrapperWidth);max-width:100%;margin-bottom:10px;padding:10px var(--smSpacing);border-radius:var(--btnHeight);background:var(--baseColor);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor);transform:translate(-50%)}.flatpickr-calendar{opacity:0;display:none;text-align:center;visibility:hidden;padding:0;animation:none;direction:ltr;border:0;font-size:1rem;line-height:24px;position:absolute;width:298px;box-sizing:border-box;-webkit-user-select:none;user-select:none;color:var(--txtPrimaryColor);background:var(--baseColor);border-radius:var(--baseRadius);box-shadow:0 2px 5px 0 var(--shadowColor),0 0 0 1px var(--baseAlt2Color)}.flatpickr-calendar input,.flatpickr-calendar select{box-shadow:none;min-height:0;height:var(--smBtnHeight);padding-top:3px;padding-bottom:3px;background:none;border-radius:var(--baseRadius);border:1px solid var(--baseAlt1Color)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:0;width:100%}.flatpickr-calendar.static{position:absolute;top:100%;margin-top:2px;margin-bottom:10px;width:100%}.flatpickr-calendar.static .flatpickr-days{width:100%}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none!important;box-shadow:none!important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color);box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid var(--baseAlt2Color)}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:"";height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowTop:after{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:after{border-top-color:var(--baseColor)}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative}.flatpickr-months{display:flex;align-items:center;padding:5px 0}.flatpickr-months .flatpickr-month{display:flex;align-items:center;justify-content:center;background:transparent;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor);line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{display:flex;align-items:center;text-decoration:none;cursor:pointer;height:34px;padding:5px 12px;z-index:3;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{left:0}.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{right:0}.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover,.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:var(--txtHintColor)}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto;border-radius:var(--baseRadius)}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.15);box-sizing:border-box}.numInputWrapper span:hover{background:#0000001a}.numInputWrapper span:active{background:#0003}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:#00000080}.numInputWrapper:hover{background:var(--baseAlt1Color)}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{line-height:inherit;color:inherit;width:85%;padding:1px 0;line-height:1;display:flex;gap:10px;align-items:center;justify-content:center;text-align:center}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:var(--baseAlt1Color)}.flatpickr-current-month .numInputWrapper{display:inline-flex;align-items:center;justify-content:center;width:62px}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-current-month input.cur-year{background:transparent;box-sizing:border-box;color:inherit;cursor:text;margin:0;display:inline-block;font-size:inherit;font-family:inherit;line-height:inherit;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{color:var(--txtDisabledColor);background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;line-height:inherit;outline:none;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:var(--baseAlt1Color)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{display:block;flex:1;margin:0;cursor:default;line-height:1;background:transparent;color:var(--txtHintColor);text-align:center;font-weight:bolder;font-size:var(--smFontSize)}.dayContainer,.flatpickr-weeks{padding:1px 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:100%;box-sizing:border-box;display:inline-block;display:flex;flex-wrap:wrap;transform:translateZ(0);opacity:1;gap:2px}.dayContainer+.dayContainer{-webkit-box-shadow:-1px 0 0 var(--baseAlt2Color);box-shadow:-1px 0 0 var(--baseAlt2Color)}.flatpickr-day{background:none;border:1px solid transparent;border-radius:var(--baseRadius);box-sizing:border-box;color:var(--txtPrimaryColor);cursor:pointer;font-weight:400;width:calc(14.2857143% - 2px);flex-basis:calc(14.2857143% - 2px);height:39px;display:inline-flex;align-items:center;justify-content:center;position:relative;text-align:center;flex-direction:column}.flatpickr-day.weekend,.flatpickr-day:nth-child(7n+6),.flatpickr-day:nth-child(7n+7){color:var(--dangerColor)}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:var(--baseAlt2Color);border-color:var(--baseAlt2Color)}.flatpickr-day.today{border-color:var(--baseColor)}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:var(--primaryColor);background:var(--primaryColor);color:var(--baseColor)}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:var(--primaryColor);-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:var(--primaryColor)}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 var(--primaryColor);box-shadow:-10px 0 0 var(--primaryColor)}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;box-shadow:-5px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:var(--txtDisabledColor);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:var(--txtDisabledColor);background:var(--baseAlt2Color)}.flatpickr-day.week.selected{border-radius:0;box-shadow:-5px 0 0 var(--primaryColor),5px 0 0 var(--primaryColor)}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 var(--baseAlt2Color);box-shadow:1px 0 0 var(--baseAlt2Color)}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:var(--txtHintColor);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:flex;box-sizing:border-box;overflow:hidden;padding:5px}.flatpickr-rContainer{display:inline-block;padding:0;width:100%;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;box-shadow:none;border:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:var(--txtPrimaryColor);font-size:14px;position:relative;box-sizing:border-box;background:var(--baseColor);-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:var(--txtPrimaryColor);font-weight:700;width:2%;-webkit-user-select:none;user-select:none;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:var(--baseAlt1Color)}.flatpickr-input[readonly]{cursor:pointer}@keyframes fpFadeInDown{0%{opacity:0;transform:translate3d(0,10px,0)}to{opacity:1;transform:translateZ(0)}}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .prevMonthDay{visibility:hidden}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .nextMonthDay,.flatpickr-inline-container .flatpickr-input{display:none}.flatpickr-inline-container .flatpickr-calendar{margin:0;box-shadow:none;border:1px solid var(--baseAlt2Color)}.docs-sidebar{--itemsSpacing: 10px;--itemsHeight: 40px;position:relative;min-width:180px;max-width:300px;height:100%;flex-shrink:0;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;background:var(--bodyColor);padding:var(--smSpacing) var(--xsSpacing);border-right:1px solid var(--baseAlt1Color)}.docs-sidebar .sidebar-content{display:block;width:100%}.docs-sidebar .sidebar-item{position:relative;outline:0;cursor:pointer;text-decoration:none;display:flex;width:100%;gap:10px;align-items:center;text-align:right;justify-content:start;padding:5px 15px;margin:0 0 var(--itemsSpacing) 0;font-size:var(--lgFontSize);min-height:var(--itemsHeight);border-radius:var(--baseRadius);-webkit-user-select:none;user-select:none;color:var(--txtHintColor);transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.docs-sidebar .sidebar-item:last-child{margin-bottom:0}.docs-sidebar .sidebar-item:focus-visible,.docs-sidebar .sidebar-item:hover,.docs-sidebar .sidebar-item:active,.docs-sidebar .sidebar-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.docs-sidebar .sidebar-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.docs-sidebar.compact .sidebar-item{--itemsSpacing: 7px}.docs-content{width:100%;display:block;padding:calc(var(--baseSpacing) - 3px) var(--baseSpacing);overflow:auto}.docs-content-wrapper{display:flex;width:100%;height:100%}.docs-panel{width:960px;height:100%}.docs-panel .overlay-panel-section.panel-header{padding:0;border:0;box-shadow:none}.docs-panel .overlay-panel-section.panel-content{padding:0!important}.docs-panel .overlay-panel-section.panel-footer{display:none}@media screen and (max-width: 1000px){.docs-panel .overlay-panel-section.panel-footer{display:flex}}.schema-field-header{position:relative;display:flex;width:100%;min-height:42px;gap:5px;padding:0 5px;align-items:center;justify-content:stretch;background:var(--baseAlt1Color);border-radius:var(--baseRadius);transition:border-radius var(--baseAnimationSpeed)}.schema-field-header .form-field{margin:0}.schema-field-header .form-field .form-field-addon.prefix{left:10px}.schema-field-header .form-field .form-field-addon.prefix~input,.schema-field-header .form-field .form-field-addon.prefix~select,.schema-field-header .form-field .form-field-addon.prefix~textarea,.schema-field-header .form-field .select .form-field-addon.prefix~.selected-container,.select .schema-field-header .form-field .form-field-addon.prefix~.selected-container,.schema-field-header .form-field .form-field-addon.prefix~.code-editor,.schema-field-header .form-field .form-field-addon.prefix~.tinymce-wrapper{padding-left:37px}.schema-field-header .options-trigger{padding:2px;margin:0 3px}.schema-field-header .options-trigger i{transition:transform var(--baseAnimationSpeed)}.schema-field-header .separator{flex-shrink:0;width:1px;height:42px;background:#0000000d}.schema-field-header .drag-handle-wrapper{position:absolute;top:0;left:auto;right:100%;height:100%;display:flex;align-items:center}.schema-field-header .drag-handle{padding:0 5px;transform:translate(5px);opacity:0;visibility:hidden}.schema-field-header .form-field-single-multiple-select{width:100px;flex-shrink:0}.schema-field-header .form-field-single-multiple-select .dropdown{min-width:0}.schema-field-header .field-labels{position:absolute;z-index:1;right:0;top:0;gap:2px;display:inline-flex;align-items:center;transition:opacity var(--baseAnimationSpeed)}.schema-field-header .field-labels .label{min-height:0;font-size:inherit;padding:0 2px;font-size:.7rem;line-height:.75rem;border-radius:var(--baseRadius)}.schema-field-header .field-labels~.inline-error-icon{margin-top:4px}.schema-field-header .field-labels~.inline-error-icon i{font-size:1rem}.schema-field-header .form-field:focus-within .field-labels{opacity:.2}.schema-field-options{background:#fff;padding:var(--xsSpacing);border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius);border-top:2px solid transparent;transition:border-color var(--baseAnimationSpeed)}.schema-field-options-footer{display:flex;flex-wrap:wrap;align-items:center;width:100%;min-width:0;gap:var(--baseSpacing)}.schema-field-options-footer .form-field{margin:0;width:auto}.schema-field{position:relative;margin:0 0 var(--xsSpacing);border-radius:var(--baseRadius);background:var(--baseAlt1Color);transition:border var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed);border:2px solid var(--baseAlt1Color)}.schema-field:not(.deleted):hover .drag-handle{transform:translate(0);opacity:1;visibility:visible}.dragover .schema-field,.schema-field.dragover{opacity:.5}.schema-field.expanded{box-shadow:0 2px 5px 0 var(--shadowColor);border-color:var(--baseAlt2Color)}.schema-field.expanded .schema-field-header{border-bottom-left-radius:0;border-bottom-right-radius:0}.schema-field.expanded .schema-field-header .options-trigger i{transform:rotate(-60deg)}.schema-field.expanded .schema-field-options{border-top-color:var(--baseAlt2Color)}.schema-field.deleted .schema-field-header{background:var(--bodyColor)}.schema-field.deleted .markers,.schema-field.deleted .separator{opacity:.5}.schema-field.deleted input,.schema-field.deleted select,.schema-field.deleted textarea,.schema-field.deleted .select .selected-container,.select .schema-field.deleted .selected-container,.schema-field.deleted .code-editor,.schema-field.deleted .tinymce-wrapper{background:none;box-shadow:none}.file-picker-sidebar{flex-shrink:0;width:180px;text-align:right;max-height:100%;overflow:auto}.file-picker-sidebar .sidebar-item{outline:0;cursor:pointer;text-decoration:none;display:flex;width:100%;align-items:center;text-align:left;gap:10px;font-weight:600;padding:5px 10px;margin:0 0 10px;color:var(--txtHintColor);min-height:var(--btnHeight);border-radius:var(--baseRadius);word-break:break-word;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.file-picker-sidebar .sidebar-item:last-child{margin-bottom:0}.file-picker-sidebar .sidebar-item:hover,.file-picker-sidebar .sidebar-item:focus-visible,.file-picker-sidebar .sidebar-item:active,.file-picker-sidebar .sidebar-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.file-picker-sidebar .sidebar-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.files-list{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--xsSpacing);flex-grow:1;min-height:0;max-height:100%;overflow:auto;scrollbar-gutter:stable}.files-list .list-item{cursor:pointer;outline:0;transition:box-shadow var(--baseAnimationSpeed)}.file-picker-size-select{width:170px;margin:0}.file-picker-size-select .selected-container{min-height:var(--btnHeight)}.file-picker-content{position:relative;display:flex;flex-direction:column;width:100%;flex-grow:1;min-width:0;min-height:0;height:100%}.file-picker-content .thumb{--thumbSize: 14.6%}.file-picker{display:flex;height:420px;max-height:100%;align-items:stretch;gap:var(--baseSpacing)}.overlay-panel.file-picker-popup{width:930px}.export-list{display:flex;flex-direction:column;gap:15px;width:220px;min-height:0;flex-shrink:0;overflow:auto;padding:var(--xsSpacing);background:var(--baseAlt1Color);border-radius:var(--baseRadius)}.export-list .list-item{margin:0;width:100%}.export-list .form-field{margin:0}.export-list .form-field label{width:100%;display:block!important;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.export-preview{position:relative;flex-grow:1;border-radius:var(--baseRadius);overflow:hidden}.export-preview .copy-schema{position:absolute;right:15px;top:10px}.export-preview .code-wrapper{height:100%;width:100%;padding:var(--xsSpacing);overflow:auto;background:var(--baseAlt1Color);font-family:var(--monospaceFontFamily)}.export-panel{display:flex;width:100%;height:550px;align-items:stretch}.export-panel>*{border-radius:0;border-left:1px solid var(--baseAlt2Color)}.export-panel>:first-child{border-top-left-radius:var(--baseRadius);border-bottom-left-radius:var(--baseRadius);border-left:0}.export-panel>:last-child{border-top-right-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.panel-wrapper.svelte-lxxzfu{animation:slideIn .2s}@keyframes svelte-1bvelc2-refresh{to{transform:rotate(180deg)}}.btn.refreshing.svelte-1bvelc2 i.svelte-1bvelc2{animation:svelte-1bvelc2-refresh .15s ease-out}.scroller.svelte-3a0gfs{width:auto;min-height:0;overflow:auto}.scroller-wrapper.svelte-3a0gfs{position:relative;min-height:0}.scroller-wrapper .columns-dropdown{top:40px;z-index:101;max-height:340px}.log-level-label.svelte-ha6hme{min-width:75px;font-weight:600;font-size:var(--xsFontSize)}.log-level-label.svelte-ha6hme:before{content:"";width:5px;height:5px;border-radius:5px;background:var(--baseAlt4Color)}.log-level-label.level--8.svelte-ha6hme:before{background:var(--primaryColor)}.log-level-label.level-0.svelte-ha6hme:before{background:var(--infoColor)}.log-level-label.level-4.svelte-ha6hme:before{background:var(--warningColor)}.log-level-label.level-8.svelte-ha6hme:before{background:var(--dangerColor)}.bulkbar.svelte-91v05h{position:sticky;margin-top:var(--smSpacing);bottom:var(--baseSpacing)}.col-field-level.svelte-91v05h{min-width:100px}.col-field-message.svelte-91v05h{min-width:600px}.chart-wrapper.svelte-12c378i.svelte-12c378i{position:relative;display:block;width:100%;height:170px}.chart-wrapper.loading.svelte-12c378i .chart-canvas.svelte-12c378i{pointer-events:none;opacity:.5}.chart-loader.svelte-12c378i.svelte-12c378i{position:absolute;z-index:999;top:50%;left:50%;transform:translate(-50%,-50%)}.total-logs.svelte-12c378i.svelte-12c378i{position:absolute;right:0;top:-50px;font-size:var(--smFontSize);color:var(--txtHintColor)}code.svelte-s3jkbp.svelte-s3jkbp{display:block;width:100%;padding:10px 15px;white-space:pre-wrap;word-break:break-word}.code-wrapper.svelte-s3jkbp.svelte-s3jkbp{display:block;width:100%}.prism-light.svelte-s3jkbp code.svelte-s3jkbp{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.log-error-label.svelte-144j2mz{white-space:normal}.dragline.svelte-y9un12{position:relative;z-index:101;left:0;top:0;height:100%;width:5px;padding:0;margin:0 -3px 0 -1px;background:none;cursor:ew-resize;box-sizing:content-box;-webkit-user-select:none;user-select:none;transition:box-shadow var(--activeAnimationSpeed);box-shadow:inset 1px 0 0 0 var(--baseAlt2Color)}.dragline.svelte-y9un12:hover,.dragline.dragging.svelte-y9un12{box-shadow:inset 3px 0 0 0 var(--baseAlt2Color)}.indexes-list.svelte-167lbwu{display:flex;flex-wrap:wrap;width:100%;gap:10px}.label.svelte-167lbwu{overflow:hidden;min-width:50px}.field-types-btn.active.svelte-1gz9b6p{border-bottom-left-radius:0;border-bottom-right-radius:0}.field-types-dropdown{display:flex;flex-wrap:wrap;width:100%;max-width:none;padding:10px;margin-top:2px;border:0;box-shadow:0 0 0 2px var(--primaryColor);border-top-left-radius:0;border-top-right-radius:0}.field-types-dropdown .dropdown-item.svelte-1gz9b6p{width:25%}.form-field-file-max-select{width:100px;flex-shrink:0}.draggable.svelte-28orm4{-webkit-user-select:none;user-select:none;outline:0;min-width:0}.lock-toggle.svelte-1akuazq.svelte-1akuazq{position:absolute;right:0;top:0;min-width:135px;padding:10px;border-top-left-radius:0;border-bottom-right-radius:0;background:#35476817}.rule-field .code-editor .cm-placeholder{font-family:var(--baseFontFamily)}.input-wrapper.svelte-1akuazq.svelte-1akuazq{position:relative}.unlock-overlay.svelte-1akuazq.svelte-1akuazq{--hoverAnimationSpeed:.2s;position:absolute;z-index:1;left:0;top:0;width:100%;height:100%;display:flex;padding:20px;gap:10px;align-items:center;justify-content:end;text-align:center;border-radius:var(--baseRadius);background:#fff3;outline:0;cursor:pointer;text-decoration:none;color:var(--successColor);border:2px solid var(--baseAlt1Color);transition:border-color var(--baseAnimationSpeed)}.unlock-overlay.svelte-1akuazq i.svelte-1akuazq{font-size:inherit}.unlock-overlay.svelte-1akuazq .icon.svelte-1akuazq{color:var(--successColor);font-size:1.15rem;line-height:1;font-weight:400;transition:transform var(--hoverAnimationSpeed)}.unlock-overlay.svelte-1akuazq .txt.svelte-1akuazq{opacity:0;font-size:var(--xsFontSize);font-weight:600;line-height:var(--smLineHeight);transform:translate(5px);transition:transform var(--hoverAnimationSpeed),opacity var(--hoverAnimationSpeed)}.unlock-overlay.svelte-1akuazq.svelte-1akuazq:hover,.unlock-overlay.svelte-1akuazq.svelte-1akuazq:focus-visible,.unlock-overlay.svelte-1akuazq.svelte-1akuazq:active{border-color:var(--baseAlt3Color)}.unlock-overlay.svelte-1akuazq:hover .icon.svelte-1akuazq,.unlock-overlay.svelte-1akuazq:focus-visible .icon.svelte-1akuazq,.unlock-overlay.svelte-1akuazq:active .icon.svelte-1akuazq{transform:scale(1.1)}.unlock-overlay.svelte-1akuazq:hover .txt.svelte-1akuazq,.unlock-overlay.svelte-1akuazq:focus-visible .txt.svelte-1akuazq,.unlock-overlay.svelte-1akuazq:active .txt.svelte-1akuazq{opacity:1;transform:scale(1)}.unlock-overlay.svelte-1akuazq.svelte-1akuazq:active{transition-duration:var(--activeAnimationSpeed);border-color:var(--baseAlt3Color)}.changes-list.svelte-xqpcsf.svelte-xqpcsf{word-break:break-word;line-height:var(--smLineHeight)}.changes-list.svelte-xqpcsf li.svelte-xqpcsf{margin-top:10px;margin-bottom:10px}.upsert-panel-title.svelte-12y0yzb{display:inline-flex;align-items:center;min-height:var(--smBtnHeight)}.tabs-content.svelte-12y0yzb:focus-within{z-index:9}.pin-collection.svelte-1u3ag8h.svelte-1u3ag8h{margin:0 -5px 0 -15px;opacity:0;transition:opacity var(--baseAnimationSpeed)}.pin-collection.svelte-1u3ag8h i.svelte-1u3ag8h{font-size:inherit}a.svelte-1u3ag8h:hover .pin-collection.svelte-1u3ag8h{opacity:.4}a.svelte-1u3ag8h:hover .pin-collection.svelte-1u3ag8h:hover{opacity:1}.secret.svelte-1md8247{font-family:monospace;font-weight:400;-webkit-user-select:all;user-select:all}.email-visibility-addon.svelte-1751a4d~input.svelte-1751a4d{padding-right:100px}textarea.svelte-1x1pbts{resize:none;padding-top:4px!important;padding-bottom:5px!important;min-height:var(--inputHeight);height:var(--inputHeight)}.clear-btn.svelte-11df51y{margin-top:20px}.json-state.svelte-p6ecb8{position:absolute;right:10px}.record-info.svelte-q8liga{display:inline-flex;vertical-align:top;align-items:center;max-width:100%;min-width:0;gap:5px}.record-info.svelte-q8liga .thumb{box-shadow:none}.picker-list.svelte-1u8jhky{max-height:380px}.selected-list.svelte-1u8jhky{display:flex;flex-wrap:wrap;align-items:center;gap:10px;max-height:220px;overflow:auto}.relations-list.svelte-1ynw0pc{max-height:300px;overflow:auto;overflow:overlay}.panel-title.svelte-qc5ngu{line-height:var(--smBtnHeight)}.datetime.svelte-5pjd03{display:inline-block;vertical-align:top;white-space:nowrap;line-height:var(--smLineHeight)}.time.svelte-5pjd03{font-size:var(--smFontSize);color:var(--txtHintColor)}.fallback-block.svelte-jdf51v{max-height:100px;overflow:auto}.col-field.svelte-1nt58f7{max-width:1px}.collections-diff-table.svelte-lmkr38.svelte-lmkr38{color:var(--txtHintColor);border:2px solid var(--primaryColor)}.collections-diff-table.svelte-lmkr38 tr.svelte-lmkr38{background:none}.collections-diff-table.svelte-lmkr38 th.svelte-lmkr38,.collections-diff-table.svelte-lmkr38 td.svelte-lmkr38{height:auto;padding:2px 15px;border-bottom:1px solid rgba(0,0,0,.07)}.collections-diff-table.svelte-lmkr38 th.svelte-lmkr38{height:35px;padding:4px 15px;color:var(--txtPrimaryColor)}.collections-diff-table.svelte-lmkr38 thead tr.svelte-lmkr38{background:var(--primaryColor)}.collections-diff-table.svelte-lmkr38 thead tr th.svelte-lmkr38{color:var(--baseColor);background:none}.collections-diff-table.svelte-lmkr38 .label.svelte-lmkr38{font-weight:400}.collections-diff-table.svelte-lmkr38 .changed-none-col.svelte-lmkr38{color:var(--txtDisabledColor);background:var(--baseAlt1Color)}.collections-diff-table.svelte-lmkr38 .changed-old-col.svelte-lmkr38{color:var(--txtPrimaryColor);background:var(--dangerAltColor)}.collections-diff-table.svelte-lmkr38 .changed-new-col.svelte-lmkr38{color:var(--txtPrimaryColor);background:var(--successAltColor)}.collections-diff-table.svelte-lmkr38 .field-key-col.svelte-lmkr38{padding-left:30px}.list-label.svelte-1jx20fl{min-width:65px}.popup-title.svelte-1fcgldh{max-width:80%}.list-content.svelte-1ulbkf5.svelte-1ulbkf5{overflow:auto;max-height:342px}.list-content.svelte-1ulbkf5 .list-item.svelte-1ulbkf5{min-height:49px}.backup-name.svelte-1ulbkf5.svelte-1ulbkf5{max-width:300px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis} diff --git a/ui/dist/fonts/remixicon/remixicon.woff2 b/ui/dist/fonts/remixicon/remixicon.woff2 index 7ee9f229..5e9be8ce 100644 Binary files a/ui/dist/fonts/remixicon/remixicon.woff2 and b/ui/dist/fonts/remixicon/remixicon.woff2 differ diff --git a/ui/dist/images/avatars/avatar0.svg b/ui/dist/images/avatars/avatar0.svg deleted file mode 100644 index c950e612..00000000 --- a/ui/dist/images/avatars/avatar0.svg +++ /dev/null @@ -1 +0,0 @@ -Mary Roebling diff --git a/ui/dist/images/avatars/avatar1.svg b/ui/dist/images/avatars/avatar1.svg deleted file mode 100644 index 42936504..00000000 --- a/ui/dist/images/avatars/avatar1.svg +++ /dev/null @@ -1 +0,0 @@ -Nellie Bly diff --git a/ui/dist/images/avatars/avatar2.svg b/ui/dist/images/avatars/avatar2.svg deleted file mode 100644 index b9847d2b..00000000 --- a/ui/dist/images/avatars/avatar2.svg +++ /dev/null @@ -1 +0,0 @@ -Elizabeth Peratrovich diff --git a/ui/dist/images/avatars/avatar3.svg b/ui/dist/images/avatars/avatar3.svg deleted file mode 100644 index a59f1d79..00000000 --- a/ui/dist/images/avatars/avatar3.svg +++ /dev/null @@ -1 +0,0 @@ -Amelia Boynton diff --git a/ui/dist/images/avatars/avatar4.svg b/ui/dist/images/avatars/avatar4.svg deleted file mode 100644 index 8722ef34..00000000 --- a/ui/dist/images/avatars/avatar4.svg +++ /dev/null @@ -1 +0,0 @@ -Victoria Woodhull diff --git a/ui/dist/images/avatars/avatar5.svg b/ui/dist/images/avatars/avatar5.svg deleted file mode 100644 index 5147a3ff..00000000 --- a/ui/dist/images/avatars/avatar5.svg +++ /dev/null @@ -1 +0,0 @@ -Chien-Shiung diff --git a/ui/dist/images/avatars/avatar6.svg b/ui/dist/images/avatars/avatar6.svg deleted file mode 100644 index d0252d7a..00000000 --- a/ui/dist/images/avatars/avatar6.svg +++ /dev/null @@ -1 +0,0 @@ -Hetty Green diff --git a/ui/dist/images/avatars/avatar7.svg b/ui/dist/images/avatars/avatar7.svg deleted file mode 100644 index fa3350f7..00000000 --- a/ui/dist/images/avatars/avatar7.svg +++ /dev/null @@ -1 +0,0 @@ -Elizabeth Peratrovich diff --git a/ui/dist/images/avatars/avatar8.svg b/ui/dist/images/avatars/avatar8.svg deleted file mode 100644 index fa36d56b..00000000 --- a/ui/dist/images/avatars/avatar8.svg +++ /dev/null @@ -1 +0,0 @@ -Jane Johnston diff --git a/ui/dist/images/avatars/avatar9.svg b/ui/dist/images/avatars/avatar9.svg deleted file mode 100644 index 35a648f0..00000000 --- a/ui/dist/images/avatars/avatar9.svg +++ /dev/null @@ -1 +0,0 @@ -Virginia Apgar diff --git a/ui/dist/images/favicon/android-chrome-192x192.png b/ui/dist/images/favicon/android-chrome-192x192.png deleted file mode 100644 index 699777f3..00000000 Binary files a/ui/dist/images/favicon/android-chrome-192x192.png and /dev/null differ diff --git a/ui/dist/images/favicon/android-chrome-512x512.png b/ui/dist/images/favicon/android-chrome-512x512.png deleted file mode 100644 index 5e6b696e..00000000 Binary files a/ui/dist/images/favicon/android-chrome-512x512.png and /dev/null differ diff --git a/ui/dist/images/favicon/browserconfig.xml b/ui/dist/images/favicon/browserconfig.xml deleted file mode 100644 index 1b34ba8b..00000000 --- a/ui/dist/images/favicon/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #ffffff - - - diff --git a/ui/dist/images/favicon/favicon-16x16.png b/ui/dist/images/favicon/favicon-16x16.png deleted file mode 100644 index 9ab99725..00000000 Binary files a/ui/dist/images/favicon/favicon-16x16.png and /dev/null differ diff --git a/ui/dist/images/favicon/favicon-32x32.png b/ui/dist/images/favicon/favicon-32x32.png deleted file mode 100644 index e6520daa..00000000 Binary files a/ui/dist/images/favicon/favicon-32x32.png and /dev/null differ diff --git a/ui/dist/images/favicon/favicon.ico b/ui/dist/images/favicon/favicon.ico deleted file mode 100644 index a43854eb..00000000 Binary files a/ui/dist/images/favicon/favicon.ico and /dev/null differ diff --git a/ui/dist/images/favicon/favicon.png b/ui/dist/images/favicon/favicon.png new file mode 100644 index 00000000..96ffccdf Binary files /dev/null and b/ui/dist/images/favicon/favicon.png differ diff --git a/ui/dist/images/favicon/favicon_prod.png b/ui/dist/images/favicon/favicon_prod.png new file mode 100644 index 00000000..c39f9990 Binary files /dev/null and b/ui/dist/images/favicon/favicon_prod.png differ diff --git a/ui/dist/images/favicon/mstile-144x144.png b/ui/dist/images/favicon/mstile-144x144.png deleted file mode 100644 index 31f31a28..00000000 Binary files a/ui/dist/images/favicon/mstile-144x144.png and /dev/null differ diff --git a/ui/dist/images/favicon/mstile-150x150.png b/ui/dist/images/favicon/mstile-150x150.png deleted file mode 100644 index 5f2ea4e1..00000000 Binary files a/ui/dist/images/favicon/mstile-150x150.png and /dev/null differ diff --git a/ui/dist/images/favicon/mstile-310x150.png b/ui/dist/images/favicon/mstile-310x150.png deleted file mode 100644 index 6c0b2660..00000000 Binary files a/ui/dist/images/favicon/mstile-310x150.png and /dev/null differ diff --git a/ui/dist/images/favicon/mstile-310x310.png b/ui/dist/images/favicon/mstile-310x310.png deleted file mode 100644 index 6dc4705d..00000000 Binary files a/ui/dist/images/favicon/mstile-310x310.png and /dev/null differ diff --git a/ui/dist/images/favicon/mstile-70x70.png b/ui/dist/images/favicon/mstile-70x70.png deleted file mode 100644 index d59d662b..00000000 Binary files a/ui/dist/images/favicon/mstile-70x70.png and /dev/null differ diff --git a/ui/dist/images/favicon/site.webmanifest b/ui/dist/images/favicon/site.webmanifest deleted file mode 100644 index c9d27bd2..00000000 --- a/ui/dist/images/favicon/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/_/images/favicon/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/_/images/favicon/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/ui/dist/index.html b/ui/dist/index.html index 09cefd20..2a72d27f 100644 --- a/ui/dist/index.html +++ b/ui/dist/index.html @@ -11,14 +11,8 @@ PocketBase - - - - - - - + @@ -47,8 +41,8 @@ window.Prism = window.Prism || {}; window.Prism.manual = true; - - + +
    diff --git a/ui/embed.go b/ui/embed.go index 5d1cb45f..5aadee26 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -1,14 +1,13 @@ -// Package ui handles the PocketBase Admin frontend embedding. +// Package ui handles the PocketBase Superuser frontend embedding. package ui import ( "embed" - - "github.com/labstack/echo/v5" + "io/fs" ) //go:embed all:dist var distDir embed.FS // DistDirFS contains the embedded dist directory files (without the "dist" prefix) -var DistDirFS = echo.MustSubFS(distDir, "dist") +var DistDirFS, _ = fs.Sub(distDir, "dist") diff --git a/ui/index.html b/ui/index.html index a4ac7167..8475f153 100644 --- a/ui/index.html +++ b/ui/index.html @@ -11,14 +11,8 @@ PocketBase - - - - - - - + diff --git a/ui/package-lock.json b/ui/package-lock.json index 2f9c4b68..cda188b0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,10 +1,10 @@ { - "name": "pocketbase-admin", + "name": "pocketbase-superuser-ui", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pocketbase-admin", + "name": "pocketbase-superuser-ui", "devDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -20,8 +20,9 @@ "@sveltejs/vite-plugin-svelte": "^3.0.1", "chart.js": "^4.0.0", "chartjs-adapter-luxon": "^1.2.0", + "chartjs-plugin-zoom": "^2.0.1", "luxon": "^3.4.4", - "pocketbase": "^0.20.0", + "pocketbase": "^0.22.0-rc", "sass": "^1.45.0", "svelte": "^4.0.0", "svelte-flatpickr": "^3.3.3", @@ -31,9 +32,8 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -43,10 +43,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz", - "integrity": "sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==", + "version": "6.18.0", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -61,10 +60,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", - "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "version": "6.6.1", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -73,23 +71,21 @@ } }, "node_modules/@codemirror/lang-css": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz", - "integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==", + "version": "6.3.0", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", - "@lezer/css": "^1.0.0" + "@lezer/css": "^1.1.7" } }, "node_modules/@codemirror/lang-html": { "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", - "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -104,9 +100,8 @@ }, "node_modules/@codemirror/lang-javascript": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", - "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -119,19 +114,17 @@ }, "node_modules/@codemirror/lang-json": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "node_modules/@codemirror/lang-sql": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.7.0.tgz", - "integrity": "sha512-KMXp6rtyPYz6RaElvkh/77ClEAoQoHRPZo0zutRRialeFs/B/X8YaUJBCnAV2zqyeJPLZ4hgo48mG8TKoNXfZA==", + "version": "6.7.1", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -143,9 +136,8 @@ }, "node_modules/@codemirror/language": { "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", - "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -156,19 +148,17 @@ } }, "node_modules/@codemirror/legacy-modes": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.0.tgz", - "integrity": "sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==", + "version": "6.4.1", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" } }, "node_modules/@codemirror/lint": { "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz", - "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -177,9 +167,8 @@ }, "node_modules/@codemirror/search": { "version": "6.5.6", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", - "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -188,285 +177,26 @@ }, "node_modules/@codemirror/state": { "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@codemirror/view": { - "version": "6.28.4", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.4.tgz", - "integrity": "sha512-QScv95fiviSQ/CaVGflxAvvvDy/9wi0RFyDl4LkHHWiMr/UPebyuTspmYSeN5Nx6eujcPYwsQzA6ZIZucKZVHQ==", + "version": "6.33.0", "dev": true, + "license": "MIT", "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -475,107 +205,10 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -587,33 +220,29 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -621,21 +250,18 @@ }, "node_modules/@kurkle/color": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@lezer/common": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", - "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@lezer/css": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz", - "integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==", + "version": "1.1.9", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -643,19 +269,17 @@ } }, "node_modules/@lezer/highlight": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", - "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "version": "1.2.1", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, "node_modules/@lezer/html": { "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", - "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -664,9 +288,8 @@ }, "node_modules/@lezer/javascript": { "version": "1.4.17", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz", - "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -675,9 +298,8 @@ }, "node_modules/@lezer/json": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", - "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -685,227 +307,41 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", - "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "version": "1.4.2", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", - "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", + "version": "4.22.5", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", - "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", + "version": "4.22.5", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", - "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "version": "3.1.2", "dev": true, + "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "debug": "^4.3.4", @@ -925,9 +361,8 @@ }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -941,16 +376,14 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "dev": true, + "license": "MIT" }, "node_modules/acorn": { "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -960,9 +393,8 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -973,27 +405,24 @@ }, "node_modules/aria-query": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" } }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -1003,9 +432,8 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1014,10 +442,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", - "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "version": "4.4.4", "dev": true, + "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1027,19 +454,28 @@ }, "node_modules/chartjs-adapter-luxon": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", - "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", "dev": true, + "license": "MIT", "peerDependencies": { "chart.js": ">=3.0.0", "luxon": ">=1.0.0" } }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1061,9 +497,8 @@ }, "node_modules/code-red": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", @@ -1074,15 +509,13 @@ }, "node_modules/crelt": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/css-tree": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, + "license": "MIT", "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -1092,12 +525,11 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1110,28 +542,25 @@ }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1166,18 +595,16 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1187,29 +614,13 @@ }, "node_modules/flatpickr": { "version": "4.6.13", - "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", - "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "license": "MIT" }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1217,17 +628,23 @@ "node": ">= 6" } }, + "node_modules/hammerjs": { + "version": "2.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true + "version": "4.3.7", + "dev": true, + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1237,18 +654,16 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1258,71 +673,61 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-reference": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*" } }, "node_modules/kleur": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/locate-character": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/mdn-data": { "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -1330,6 +735,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1339,18 +745,16 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/periscopic": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -1358,16 +762,14 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.0", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1376,15 +778,14 @@ } }, "node_modules/pocketbase": { - "version": "0.20.3", - "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.20.3.tgz", - "integrity": "sha512-qembHhE7HumDBZpxWgFIbhJPeaCoUIdwhW59xF/VlMR79pDTYz/LaQ4q89y7GczKo4X9actFgFN8hs4dTl0spQ==", - "dev": true + "version": "0.22.0-rc", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.22.0-rc.tgz", + "integrity": "sha512-DSCCWdyPgKGVyhVSufMHlSxsjYvwDzinyyfGtOIiBvarbQsHj1YhAQC7ZOEFw7q9AJ/IB+SHM0EmI2Zfg1aQow==", + "dev": true, + "license": "MIT" }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.45", "dev": true, "funding": [ { @@ -1400,6 +801,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -1411,9 +813,8 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1423,20 +824,18 @@ }, "node_modules/regexparam": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", - "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/rollup": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", - "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", + "version": "4.22.5", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1446,30 +845,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.1", - "@rollup/rollup-android-arm64": "4.18.1", - "@rollup/rollup-darwin-arm64": "4.18.1", - "@rollup/rollup-darwin-x64": "4.18.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", - "@rollup/rollup-linux-arm-musleabihf": "4.18.1", - "@rollup/rollup-linux-arm64-gnu": "4.18.1", - "@rollup/rollup-linux-arm64-musl": "4.18.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", - "@rollup/rollup-linux-riscv64-gnu": "4.18.1", - "@rollup/rollup-linux-s390x-gnu": "4.18.1", - "@rollup/rollup-linux-x64-gnu": "4.18.1", - "@rollup/rollup-linux-x64-musl": "4.18.1", - "@rollup/rollup-win32-arm64-msvc": "4.18.1", - "@rollup/rollup-win32-ia32-msvc": "4.18.1", - "@rollup/rollup-win32-x64-msvc": "4.18.1", + "@rollup/rollup-android-arm-eabi": "4.22.5", + "@rollup/rollup-android-arm64": "4.22.5", + "@rollup/rollup-darwin-arm64": "4.22.5", + "@rollup/rollup-darwin-x64": "4.22.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", + "@rollup/rollup-linux-arm-musleabihf": "4.22.5", + "@rollup/rollup-linux-arm64-gnu": "4.22.5", + "@rollup/rollup-linux-arm64-musl": "4.22.5", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", + "@rollup/rollup-linux-riscv64-gnu": "4.22.5", + "@rollup/rollup-linux-s390x-gnu": "4.22.5", + "@rollup/rollup-linux-x64-gnu": "4.22.5", + "@rollup/rollup-linux-x64-musl": "4.22.5", + "@rollup/rollup-win32-arm64-msvc": "4.22.5", + "@rollup/rollup-win32-ia32-msvc": "4.22.5", + "@rollup/rollup-win32-x64-msvc": "4.22.5", "fsevents": "~2.3.2" } }, "node_modules/sass": { - "version": "1.77.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "version": "1.78.0", "dev": true, + "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -1483,25 +881,22 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/style-mod": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "4.2.19", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -1524,9 +919,8 @@ }, "node_modules/svelte-flatpickr": { "version": "3.3.4", - "resolved": "https://registry.npmjs.org/svelte-flatpickr/-/svelte-flatpickr-3.3.4.tgz", - "integrity": "sha512-i+QqJRs8zPRKsxv8r2GIk1fsb8cI3ozn3/aHXtViAoNKLy0j4PV7OSWavgEZC1wlAa34qi2hMkUh+vg6qt2DRA==", "dev": true, + "license": "MIT", "dependencies": { "flatpickr": "^4.5.2" }, @@ -1536,9 +930,8 @@ }, "node_modules/svelte-hmr": { "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", "dev": true, + "license": "ISC", "engines": { "node": "^12.20 || ^14.13.1 || >= 16" }, @@ -1548,9 +941,8 @@ }, "node_modules/svelte-spa-router": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz", - "integrity": "sha512-2JkmUQ2f9jRluijL58LtdQBIpynSbem2eBGp4zXdi7aDY1znbR6yjw0KsonD0aq2QLwf4Yx4tBJQjxIjgjXHKg==", "dev": true, + "license": "MIT", "dependencies": { "regexparam": "2.0.2" }, @@ -1560,9 +952,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -1571,14 +962,13 @@ } }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.4.8", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -1597,6 +987,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -1614,6 +1005,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1627,9 +1021,8 @@ }, "node_modules/vitefu": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", "dev": true, + "license": "MIT", "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, @@ -1641,9 +1034,8 @@ }, "node_modules/w3c-keyname": { "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true + "dev": true, + "license": "MIT" } } } diff --git a/ui/package.json b/ui/package.json index b700cfcd..1b914faa 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,5 +1,5 @@ { - "name": "pocketbase-admin", + "name": "pocketbase-superuser-ui", "private": true, "scripts": { "dev": "vite", @@ -17,8 +17,8 @@ "@codemirror/commands": "^6.0.0", "@codemirror/lang-html": "^6.1.0", "@codemirror/lang-javascript": "^6.0.2", - "@codemirror/lang-sql": "^6.4.0", "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-sql": "^6.4.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.0.0", "@codemirror/search": "^6.0.0", @@ -27,8 +27,9 @@ "@sveltejs/vite-plugin-svelte": "^3.0.1", "chart.js": "^4.0.0", "chartjs-adapter-luxon": "^1.2.0", + "chartjs-plugin-zoom": "^2.0.1", "luxon": "^3.4.4", - "pocketbase": "^0.20.0", + "pocketbase": "^0.22.0-rc", "sass": "^1.45.0", "svelte": "^4.0.0", "svelte-flatpickr": "^3.3.3", diff --git a/ui/public/fonts/remixicon/remixicon.woff2 b/ui/public/fonts/remixicon/remixicon.woff2 index 7ee9f229..5e9be8ce 100644 Binary files a/ui/public/fonts/remixicon/remixicon.woff2 and b/ui/public/fonts/remixicon/remixicon.woff2 differ diff --git a/ui/public/images/avatars/avatar0.svg b/ui/public/images/avatars/avatar0.svg deleted file mode 100644 index c950e612..00000000 --- a/ui/public/images/avatars/avatar0.svg +++ /dev/null @@ -1 +0,0 @@ -Mary Roebling diff --git a/ui/public/images/avatars/avatar1.svg b/ui/public/images/avatars/avatar1.svg deleted file mode 100644 index 42936504..00000000 --- a/ui/public/images/avatars/avatar1.svg +++ /dev/null @@ -1 +0,0 @@ -Nellie Bly diff --git a/ui/public/images/avatars/avatar2.svg b/ui/public/images/avatars/avatar2.svg deleted file mode 100644 index b9847d2b..00000000 --- a/ui/public/images/avatars/avatar2.svg +++ /dev/null @@ -1 +0,0 @@ -Elizabeth Peratrovich diff --git a/ui/public/images/avatars/avatar3.svg b/ui/public/images/avatars/avatar3.svg deleted file mode 100644 index a59f1d79..00000000 --- a/ui/public/images/avatars/avatar3.svg +++ /dev/null @@ -1 +0,0 @@ -Amelia Boynton diff --git a/ui/public/images/avatars/avatar4.svg b/ui/public/images/avatars/avatar4.svg deleted file mode 100644 index 8722ef34..00000000 --- a/ui/public/images/avatars/avatar4.svg +++ /dev/null @@ -1 +0,0 @@ -Victoria Woodhull diff --git a/ui/public/images/avatars/avatar5.svg b/ui/public/images/avatars/avatar5.svg deleted file mode 100644 index 5147a3ff..00000000 --- a/ui/public/images/avatars/avatar5.svg +++ /dev/null @@ -1 +0,0 @@ -Chien-Shiung diff --git a/ui/public/images/avatars/avatar6.svg b/ui/public/images/avatars/avatar6.svg deleted file mode 100644 index d0252d7a..00000000 --- a/ui/public/images/avatars/avatar6.svg +++ /dev/null @@ -1 +0,0 @@ -Hetty Green diff --git a/ui/public/images/avatars/avatar7.svg b/ui/public/images/avatars/avatar7.svg deleted file mode 100644 index fa3350f7..00000000 --- a/ui/public/images/avatars/avatar7.svg +++ /dev/null @@ -1 +0,0 @@ -Elizabeth Peratrovich diff --git a/ui/public/images/avatars/avatar8.svg b/ui/public/images/avatars/avatar8.svg deleted file mode 100644 index fa36d56b..00000000 --- a/ui/public/images/avatars/avatar8.svg +++ /dev/null @@ -1 +0,0 @@ -Jane Johnston diff --git a/ui/public/images/avatars/avatar9.svg b/ui/public/images/avatars/avatar9.svg deleted file mode 100644 index 35a648f0..00000000 --- a/ui/public/images/avatars/avatar9.svg +++ /dev/null @@ -1 +0,0 @@ -Virginia Apgar diff --git a/ui/public/images/favicon/android-chrome-192x192.png b/ui/public/images/favicon/android-chrome-192x192.png deleted file mode 100644 index 699777f3..00000000 Binary files a/ui/public/images/favicon/android-chrome-192x192.png and /dev/null differ diff --git a/ui/public/images/favicon/android-chrome-512x512.png b/ui/public/images/favicon/android-chrome-512x512.png deleted file mode 100644 index 5e6b696e..00000000 Binary files a/ui/public/images/favicon/android-chrome-512x512.png and /dev/null differ diff --git a/ui/public/images/favicon/browserconfig.xml b/ui/public/images/favicon/browserconfig.xml deleted file mode 100644 index 1b34ba8b..00000000 --- a/ui/public/images/favicon/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #ffffff - - - diff --git a/ui/public/images/favicon/favicon-16x16.png b/ui/public/images/favicon/favicon-16x16.png deleted file mode 100644 index 9ab99725..00000000 Binary files a/ui/public/images/favicon/favicon-16x16.png and /dev/null differ diff --git a/ui/public/images/favicon/favicon-32x32.png b/ui/public/images/favicon/favicon-32x32.png deleted file mode 100644 index e6520daa..00000000 Binary files a/ui/public/images/favicon/favicon-32x32.png and /dev/null differ diff --git a/ui/public/images/favicon/favicon.ico b/ui/public/images/favicon/favicon.ico deleted file mode 100644 index a43854eb..00000000 Binary files a/ui/public/images/favicon/favicon.ico and /dev/null differ diff --git a/ui/public/images/favicon/favicon.png b/ui/public/images/favicon/favicon.png new file mode 100644 index 00000000..96ffccdf Binary files /dev/null and b/ui/public/images/favicon/favicon.png differ diff --git a/ui/public/images/favicon/favicon_prod.png b/ui/public/images/favicon/favicon_prod.png new file mode 100644 index 00000000..c39f9990 Binary files /dev/null and b/ui/public/images/favicon/favicon_prod.png differ diff --git a/ui/public/images/favicon/mstile-144x144.png b/ui/public/images/favicon/mstile-144x144.png deleted file mode 100644 index 31f31a28..00000000 Binary files a/ui/public/images/favicon/mstile-144x144.png and /dev/null differ diff --git a/ui/public/images/favicon/mstile-150x150.png b/ui/public/images/favicon/mstile-150x150.png deleted file mode 100644 index 5f2ea4e1..00000000 Binary files a/ui/public/images/favicon/mstile-150x150.png and /dev/null differ diff --git a/ui/public/images/favicon/mstile-310x150.png b/ui/public/images/favicon/mstile-310x150.png deleted file mode 100644 index 6c0b2660..00000000 Binary files a/ui/public/images/favicon/mstile-310x150.png and /dev/null differ diff --git a/ui/public/images/favicon/mstile-310x310.png b/ui/public/images/favicon/mstile-310x310.png deleted file mode 100644 index 6dc4705d..00000000 Binary files a/ui/public/images/favicon/mstile-310x310.png and /dev/null differ diff --git a/ui/public/images/favicon/mstile-70x70.png b/ui/public/images/favicon/mstile-70x70.png deleted file mode 100644 index d59d662b..00000000 Binary files a/ui/public/images/favicon/mstile-70x70.png and /dev/null differ diff --git a/ui/public/images/favicon/site.webmanifest b/ui/public/images/favicon/site.webmanifest deleted file mode 100644 index c9d27bd2..00000000 --- a/ui/public/images/favicon/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/_/images/favicon/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/_/images/favicon/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 66b6fe61..998f6489 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,20 +1,20 @@ - - { - if (hasChanges && confirmClose) { - confirm("You have unsaved changes. Do you really want to close the panel?", () => { - confirmClose = false; - hide(); - }); - return false; - } - return true; - }} - on:hide - on:show -> - -

    - {isNew ? "New admin" : "Edit admin"} -

    -
    - -
    - {#if !isNew} - - -
    - -
    - -
    - {/if} - -
    -

    Avatar

    -
    - {#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as index} - - {/each} -
    -
    - - - - - - - {#if !isNew} - - - - - {/if} - - {#if isNew || changePasswordToggle} -
    -
    -
    - - - -
    - -
    -
    -
    -
    - - - - -
    -
    -
    - {/if} - - - - {#if !isNew} -
    - -

    Multi-factor authentication (MFA) requires the user to authenticate with any 2 different auth + methods (otp, identity/password, oauth2) before issuing an auth token. + (Learn more) .